Add a statistics dialog with heatmap
This commit is contained in:
parent
b1ba599e99
commit
7ef79c495b
5 changed files with 446 additions and 3 deletions
|
|
@ -1,6 +1,7 @@
|
||||||
# 0.3.1
|
# 0.3.1
|
||||||
|
|
||||||
* Make it possible to add a tag from the Tag Browser
|
* Make it possible to add a tag from the Tag Browser
|
||||||
|
* Add a statistics dialog with heatmap
|
||||||
|
|
||||||
# 0.3
|
# 0.3
|
||||||
|
|
||||||
|
|
|
||||||
123
bouquin/db.py
123
bouquin/db.py
|
|
@ -1,14 +1,16 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import csv
|
import csv
|
||||||
|
import datetime as _dt
|
||||||
import hashlib
|
import hashlib
|
||||||
import html
|
import html
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from sqlcipher3 import dbapi2 as sqlite
|
from sqlcipher3 import dbapi2 as sqlite
|
||||||
from typing import List, Sequence, Tuple
|
from typing import List, Sequence, Tuple, Dict
|
||||||
|
|
||||||
|
|
||||||
from . import strings
|
from . import strings
|
||||||
|
|
@ -640,6 +642,125 @@ class DBManager:
|
||||||
).fetchall()
|
).fetchall()
|
||||||
return [(r[0], r[1]) for r in rows]
|
return [(r[0], r[1]) for r in rows]
|
||||||
|
|
||||||
|
# ---------- helpers for word counting ----------
|
||||||
|
def _strip_markdown(self, text: str) -> str:
|
||||||
|
"""
|
||||||
|
Cheap markdown-ish stripper for word counting.
|
||||||
|
We only need approximate numbers.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
# Remove fenced code blocks
|
||||||
|
text = re.sub(r"```.*?```", " ", text, flags=re.DOTALL)
|
||||||
|
# Remove inline code
|
||||||
|
text = re.sub(r"`[^`]+`", " ", text)
|
||||||
|
# [text](url) → text
|
||||||
|
text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text)
|
||||||
|
# Remove emphasis markers, headings, etc.
|
||||||
|
text = re.sub(r"[#*_>]+", " ", text)
|
||||||
|
# Strip simple HTML tags
|
||||||
|
text = re.sub(r"<[^>]+>", " ", text)
|
||||||
|
|
||||||
|
return text
|
||||||
|
|
||||||
|
def _count_words(self, text: str) -> int:
|
||||||
|
text = self._strip_markdown(text)
|
||||||
|
words = re.findall(r"\b\w+\b", text, flags=re.UNICODE)
|
||||||
|
return len(words)
|
||||||
|
|
||||||
|
def gather_stats(self):
|
||||||
|
"""Compute all the numbers the Statistics dialog needs in one place."""
|
||||||
|
|
||||||
|
# 1) pages with content (current version only)
|
||||||
|
try:
|
||||||
|
pages_with_content_list = self.dates_with_content()
|
||||||
|
except Exception:
|
||||||
|
pages_with_content_list = []
|
||||||
|
pages_with_content = len(pages_with_content_list)
|
||||||
|
|
||||||
|
cur = self.conn.cursor()
|
||||||
|
|
||||||
|
# 2 & 3) total revisions + page with most revisions + per-date counts
|
||||||
|
total_revisions = 0
|
||||||
|
page_most_revisions = None
|
||||||
|
page_most_revisions_count = 0
|
||||||
|
revisions_by_date: Dict[_dt.date, int] = {}
|
||||||
|
|
||||||
|
rows = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT date, COUNT(*) AS c
|
||||||
|
FROM versions
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date;
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
for r in rows:
|
||||||
|
date_iso = r["date"]
|
||||||
|
c = int(r["c"])
|
||||||
|
total_revisions += c
|
||||||
|
|
||||||
|
if c > page_most_revisions_count:
|
||||||
|
page_most_revisions_count = c
|
||||||
|
page_most_revisions = date_iso
|
||||||
|
|
||||||
|
try:
|
||||||
|
d = _dt.date.fromisoformat(date_iso)
|
||||||
|
revisions_by_date[d] = c
|
||||||
|
except ValueError:
|
||||||
|
# Ignore malformed dates
|
||||||
|
pass
|
||||||
|
|
||||||
|
# 4) total words + per-date words (current version only)
|
||||||
|
entries = self.get_all_entries()
|
||||||
|
total_words = 0
|
||||||
|
words_by_date: Dict[_dt.date, int] = {}
|
||||||
|
|
||||||
|
for date_iso, content in entries:
|
||||||
|
wc = self._count_words(content or "")
|
||||||
|
total_words += wc
|
||||||
|
try:
|
||||||
|
d = _dt.date.fromisoformat(date_iso)
|
||||||
|
words_by_date[d] = wc
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# tags + page with most tags
|
||||||
|
|
||||||
|
rows = cur.execute("SELECT COUNT(*) AS total_unique FROM tags;").fetchall()
|
||||||
|
unique_tags = int(rows[0]["total_unique"]) if rows else 0
|
||||||
|
|
||||||
|
rows = cur.execute(
|
||||||
|
"""
|
||||||
|
SELECT page_date, COUNT(*) AS c
|
||||||
|
FROM page_tags
|
||||||
|
GROUP BY page_date
|
||||||
|
ORDER BY c DESC, page_date ASC
|
||||||
|
LIMIT 1;
|
||||||
|
"""
|
||||||
|
).fetchall()
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
page_most_tags = rows[0]["page_date"]
|
||||||
|
page_most_tags_count = int(rows[0]["c"])
|
||||||
|
else:
|
||||||
|
page_most_tags = None
|
||||||
|
page_most_tags_count = 0
|
||||||
|
|
||||||
|
return (
|
||||||
|
pages_with_content,
|
||||||
|
total_revisions,
|
||||||
|
page_most_revisions,
|
||||||
|
page_most_revisions_count,
|
||||||
|
words_by_date,
|
||||||
|
total_words,
|
||||||
|
unique_tags,
|
||||||
|
page_most_tags,
|
||||||
|
page_most_tags_count,
|
||||||
|
revisions_by_date,
|
||||||
|
)
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if self.conn is not None:
|
if self.conn is not None:
|
||||||
self.conn.close()
|
self.conn.close()
|
||||||
|
|
|
||||||
|
|
@ -114,6 +114,7 @@
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"manage_tags": "Manage tags",
|
"manage_tags": "Manage tags",
|
||||||
|
"main_window_manage_tags_accessible_flag": "Manage &Tags",
|
||||||
"add_tag_placeholder": "Add a tag and press Enter",
|
"add_tag_placeholder": "Add a tag and press Enter",
|
||||||
"tag_browser_title": "Tag Browser",
|
"tag_browser_title": "Tag Browser",
|
||||||
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
|
"tag_browser_instructions": "Click a tag to expand and see all pages with that tag. Click a date to open it. Select a tag to edit its name, change its color, or delete it globally.",
|
||||||
|
|
@ -133,5 +134,18 @@
|
||||||
"change_color": "Change colour",
|
"change_color": "Change colour",
|
||||||
"delete_tag": "Delete tag",
|
"delete_tag": "Delete tag",
|
||||||
"delete_tag_confirm": "Are you sure you want to delete the tag '{name}'? This will remove it from all pages.",
|
"delete_tag_confirm": "Are you sure you want to delete the tag '{name}'? This will remove it from all pages.",
|
||||||
"tag_already_exists_with_that_name": "A tag already exists with that name"
|
"tag_already_exists_with_that_name": "A tag already exists with that name",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"main_window_statistics_accessible_flag": "Stat&istics",
|
||||||
|
"stats_pages_with_content": "Pages with content (current version)",
|
||||||
|
"stats_total_revisions": "Total revisions",
|
||||||
|
"stats_page_most_revisions": "Page with most revisions",
|
||||||
|
"stats_total_words": "Total words (current versions)",
|
||||||
|
"stats_unique_tags": "Unique tags",
|
||||||
|
"stats_page_most_tags": "Page with most tags",
|
||||||
|
"stats_activity_heatmap": "Activity heatmap",
|
||||||
|
"stats_heatmap_metric": "Colour by",
|
||||||
|
"stats_metric_words": "Words",
|
||||||
|
"stats_metric_revisions": "Revisions",
|
||||||
|
"stats_no_data": "No statistics available yet."
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ from .save_dialog import SaveDialog
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
||||||
from .settings_dialog import SettingsDialog
|
from .settings_dialog import SettingsDialog
|
||||||
|
from .statistics_dialog import StatisticsDialog
|
||||||
from . import strings
|
from . import strings
|
||||||
from .tags_widget import PageTagsWidget
|
from .tags_widget import PageTagsWidget
|
||||||
from .toolbar import ToolBar
|
from .toolbar import ToolBar
|
||||||
|
|
@ -209,10 +210,14 @@ class MainWindow(QMainWindow):
|
||||||
act_backup.setShortcut("Ctrl+Shift+B")
|
act_backup.setShortcut("Ctrl+Shift+B")
|
||||||
act_backup.triggered.connect(self._backup)
|
act_backup.triggered.connect(self._backup)
|
||||||
file_menu.addAction(act_backup)
|
file_menu.addAction(act_backup)
|
||||||
act_tags = QAction("&" + strings._("manage_tags"), self)
|
act_tags = QAction(strings._("main_window_manage_tags_accessible_flag"), self)
|
||||||
act_tags.setShortcut("Ctrl+T")
|
act_tags.setShortcut("Ctrl+T")
|
||||||
act_tags.triggered.connect(self.tags._open_manager)
|
act_tags.triggered.connect(self.tags._open_manager)
|
||||||
file_menu.addAction(act_tags)
|
file_menu.addAction(act_tags)
|
||||||
|
act_stats = QAction(strings._("main_window_statistics_accessible_flag"), self)
|
||||||
|
act_stats.setShortcut("Shift+Ctrl+S")
|
||||||
|
act_stats.triggered.connect(self._open_statistics)
|
||||||
|
file_menu.addAction(act_stats)
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
act_quit = QAction("&" + strings._("quit"), self)
|
act_quit = QAction("&" + strings._("quit"), self)
|
||||||
act_quit.setShortcut("Ctrl+Q")
|
act_quit.setShortcut("Ctrl+Q")
|
||||||
|
|
@ -1433,3 +1438,11 @@ class MainWindow(QMainWindow):
|
||||||
super().changeEvent(ev)
|
super().changeEvent(ev)
|
||||||
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
|
if ev.type() == QEvent.ActivationChange and self.isActiveWindow():
|
||||||
QTimer.singleShot(0, self._focus_editor_now)
|
QTimer.singleShot(0, self._focus_editor_now)
|
||||||
|
|
||||||
|
def _open_statistics(self):
|
||||||
|
# If the DB isn't ready for some reason, just do nothing
|
||||||
|
if not getattr(self, "db", None) or self.db.conn is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
dlg = StatisticsDialog(self.db, self)
|
||||||
|
dlg.exec()
|
||||||
|
|
|
||||||
294
bouquin/statistics_dialog.py
Normal file
294
bouquin/statistics_dialog.py
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import datetime as _dt
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from PySide6.QtCore import Qt, QSize
|
||||||
|
from PySide6.QtGui import QColor, QPainter, QPen, QBrush
|
||||||
|
from PySide6.QtWidgets import (
|
||||||
|
QDialog,
|
||||||
|
QVBoxLayout,
|
||||||
|
QFormLayout,
|
||||||
|
QLabel,
|
||||||
|
QGroupBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QComboBox,
|
||||||
|
QScrollArea,
|
||||||
|
QWidget,
|
||||||
|
QSizePolicy,
|
||||||
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
from .db import DBManager
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Activity heatmap ----------
|
||||||
|
|
||||||
|
|
||||||
|
class DateHeatmap(QWidget):
|
||||||
|
"""
|
||||||
|
Small calendar heatmap for activity by date.
|
||||||
|
|
||||||
|
Data is a mapping: datetime.date -> integer value.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._data: Dict[_dt.date, int] = {}
|
||||||
|
self._start: _dt.date | None = None
|
||||||
|
self._end: _dt.date | None = None
|
||||||
|
self._max_value: int = 0
|
||||||
|
|
||||||
|
self._cell = 12
|
||||||
|
self._gap = 3
|
||||||
|
self._margin_left = 10
|
||||||
|
self._margin_top = 10
|
||||||
|
self._margin_bottom = 24
|
||||||
|
self._margin_right = 10
|
||||||
|
|
||||||
|
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||||
|
|
||||||
|
def set_data(self, data: Dict[_dt.date, int]) -> None:
|
||||||
|
"""Replace dataset and recompute layout."""
|
||||||
|
self._data = {k: int(v) for k, v in (data or {}).items() if v is not None}
|
||||||
|
if not self._data:
|
||||||
|
self._start = self._end = None
|
||||||
|
self._max_value = 0
|
||||||
|
else:
|
||||||
|
earliest = min(self._data.keys())
|
||||||
|
latest = max(self._data.keys())
|
||||||
|
self._start = earliest - _dt.timedelta(days=earliest.weekday())
|
||||||
|
self._end = latest
|
||||||
|
self._max_value = max(self._data.values()) if self._data else 0
|
||||||
|
|
||||||
|
self.updateGeometry()
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
# QWidget overrides ---------------------------------------------------
|
||||||
|
|
||||||
|
def sizeHint(self) -> QSize:
|
||||||
|
if not self._start or not self._end:
|
||||||
|
height = (
|
||||||
|
self._margin_top + self._margin_bottom + 7 * (self._cell + self._gap)
|
||||||
|
)
|
||||||
|
# some default width
|
||||||
|
width = (
|
||||||
|
self._margin_left + self._margin_right + 20 * (self._cell + self._gap)
|
||||||
|
)
|
||||||
|
return QSize(width, height)
|
||||||
|
|
||||||
|
day_count = (self._end - self._start).days + 1
|
||||||
|
weeks = (day_count + 6) // 7 # ceil
|
||||||
|
|
||||||
|
width = (
|
||||||
|
self._margin_left
|
||||||
|
+ self._margin_right
|
||||||
|
+ weeks * (self._cell + self._gap)
|
||||||
|
+ self._gap
|
||||||
|
)
|
||||||
|
height = (
|
||||||
|
self._margin_top
|
||||||
|
+ self._margin_bottom
|
||||||
|
+ 7 * (self._cell + self._gap)
|
||||||
|
+ self._gap
|
||||||
|
)
|
||||||
|
return QSize(width, height)
|
||||||
|
|
||||||
|
def minimumSizeHint(self) -> QSize:
|
||||||
|
sz = self.sizeHint()
|
||||||
|
return QSize(min(300, sz.width()), sz.height())
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
super().paintEvent(event)
|
||||||
|
painter = QPainter(self)
|
||||||
|
painter.setRenderHint(QPainter.Antialiasing, True)
|
||||||
|
|
||||||
|
if not self._start or not self._end:
|
||||||
|
return
|
||||||
|
|
||||||
|
palette = self.palette()
|
||||||
|
bg_no_data = palette.base().color()
|
||||||
|
active = palette.highlight().color()
|
||||||
|
|
||||||
|
painter.setPen(QPen(Qt.NoPen))
|
||||||
|
|
||||||
|
day_count = (self._end - self._start).days + 1
|
||||||
|
weeks = (day_count + 6) // 7
|
||||||
|
|
||||||
|
for week in range(weeks):
|
||||||
|
for dow in range(7):
|
||||||
|
idx = week * 7 + dow
|
||||||
|
date = self._start + _dt.timedelta(days=idx)
|
||||||
|
if date > self._end:
|
||||||
|
value = 0
|
||||||
|
else:
|
||||||
|
value = self._data.get(date, 0)
|
||||||
|
|
||||||
|
x = self._margin_left + week * (self._cell + self._gap)
|
||||||
|
y = self._margin_top + dow * (self._cell + self._gap)
|
||||||
|
|
||||||
|
if value <= 0 or self._max_value <= 0:
|
||||||
|
color = bg_no_data
|
||||||
|
else:
|
||||||
|
ratio = max(0.1, min(1.0, value / float(self._max_value)))
|
||||||
|
color = QColor(active)
|
||||||
|
# Lighter for low values, darker for high values
|
||||||
|
lighten = 150 - int(50 * ratio) # 150 ≈ light, 100 ≈ original
|
||||||
|
color = color.lighter(lighten)
|
||||||
|
|
||||||
|
painter.fillRect(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
self._cell,
|
||||||
|
self._cell,
|
||||||
|
QBrush(color),
|
||||||
|
)
|
||||||
|
|
||||||
|
painter.setPen(palette.text().color())
|
||||||
|
fm = painter.fontMetrics()
|
||||||
|
|
||||||
|
prev_month = None
|
||||||
|
for week in range(weeks):
|
||||||
|
date = self._start + _dt.timedelta(days=week * 7)
|
||||||
|
if date > self._end:
|
||||||
|
break
|
||||||
|
|
||||||
|
if prev_month == date.month:
|
||||||
|
continue
|
||||||
|
prev_month = date.month
|
||||||
|
|
||||||
|
label = date.strftime("%b")
|
||||||
|
|
||||||
|
x_center = (
|
||||||
|
self._margin_left + week * (self._cell + self._gap) + self._cell / 2
|
||||||
|
)
|
||||||
|
y = self._margin_top + 7 * (self._cell + self._gap) + fm.ascent()
|
||||||
|
|
||||||
|
text_width = fm.horizontalAdvance(label)
|
||||||
|
painter.drawText(
|
||||||
|
int(x_center - text_width / 2),
|
||||||
|
int(y),
|
||||||
|
label,
|
||||||
|
)
|
||||||
|
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Statistics dialog itself ----------
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticsDialog(QDialog):
|
||||||
|
"""
|
||||||
|
Shows aggregate statistics and the date heatmap with a metric switcher.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, db: DBManager, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self._db = db
|
||||||
|
|
||||||
|
self.setWindowTitle(strings._("statistics"))
|
||||||
|
|
||||||
|
root = QVBoxLayout(self)
|
||||||
|
|
||||||
|
(
|
||||||
|
pages_with_content,
|
||||||
|
total_revisions,
|
||||||
|
page_most_revisions,
|
||||||
|
page_most_revisions_count,
|
||||||
|
words_by_date,
|
||||||
|
total_words,
|
||||||
|
unique_tags,
|
||||||
|
page_most_tags,
|
||||||
|
page_most_tags_count,
|
||||||
|
revisions_by_date,
|
||||||
|
) = self._gather_stats()
|
||||||
|
|
||||||
|
# --- Numeric summary at the top ----------------------------------
|
||||||
|
form = QFormLayout()
|
||||||
|
root.addLayout(form)
|
||||||
|
|
||||||
|
form.addRow(
|
||||||
|
strings._("stats_pages_with_content"),
|
||||||
|
QLabel(str(pages_with_content)),
|
||||||
|
)
|
||||||
|
form.addRow(
|
||||||
|
strings._("stats_total_revisions"),
|
||||||
|
QLabel(str(total_revisions)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if page_most_revisions:
|
||||||
|
form.addRow(
|
||||||
|
strings._("stats_page_most_revisions"),
|
||||||
|
QLabel(f"{page_most_revisions} ({page_most_revisions_count})"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form.addRow(strings._("stats_page_most_revisions"), QLabel("—"))
|
||||||
|
|
||||||
|
form.addRow(
|
||||||
|
strings._("stats_total_words"),
|
||||||
|
QLabel(str(total_words)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Unique tag names
|
||||||
|
form.addRow(
|
||||||
|
strings._("stats_unique_tags"),
|
||||||
|
QLabel(str(unique_tags)),
|
||||||
|
)
|
||||||
|
|
||||||
|
if page_most_tags:
|
||||||
|
form.addRow(
|
||||||
|
strings._("stats_page_most_tags"),
|
||||||
|
QLabel(f"{page_most_tags} ({page_most_tags_count})"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
form.addRow(strings._("stats_page_most_tags"), QLabel("—"))
|
||||||
|
|
||||||
|
# --- Heatmap with switcher ---------------------------------------
|
||||||
|
if words_by_date or revisions_by_date:
|
||||||
|
group = QGroupBox(strings._("stats_activity_heatmap"))
|
||||||
|
group_layout = QVBoxLayout(group)
|
||||||
|
|
||||||
|
# Metric selector
|
||||||
|
combo_row = QHBoxLayout()
|
||||||
|
combo_row.addWidget(QLabel(strings._("stats_heatmap_metric")))
|
||||||
|
self.metric_combo = QComboBox()
|
||||||
|
self.metric_combo.addItem(strings._("stats_metric_words"), "words")
|
||||||
|
self.metric_combo.addItem(strings._("stats_metric_revisions"), "revisions")
|
||||||
|
combo_row.addWidget(self.metric_combo)
|
||||||
|
combo_row.addStretch(1)
|
||||||
|
group_layout.addLayout(combo_row)
|
||||||
|
|
||||||
|
self._heatmap = DateHeatmap()
|
||||||
|
self._words_by_date = words_by_date
|
||||||
|
self._revisions_by_date = revisions_by_date
|
||||||
|
|
||||||
|
scroll = QScrollArea()
|
||||||
|
scroll.setWidgetResizable(True)
|
||||||
|
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
|
||||||
|
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
scroll.setWidget(self._heatmap)
|
||||||
|
group_layout.addWidget(scroll)
|
||||||
|
|
||||||
|
root.addWidget(group)
|
||||||
|
|
||||||
|
# Default to "words"
|
||||||
|
self._apply_metric("words")
|
||||||
|
self.metric_combo.currentIndexChanged.connect(self._on_metric_changed)
|
||||||
|
else:
|
||||||
|
root.addWidget(QLabel(strings._("stats_no_data")))
|
||||||
|
|
||||||
|
# ---------- internal helpers ----------
|
||||||
|
|
||||||
|
def _apply_metric(self, metric: str) -> None:
|
||||||
|
if metric == "revisions":
|
||||||
|
self._heatmap.set_data(self._revisions_by_date)
|
||||||
|
else:
|
||||||
|
self._heatmap.set_data(self._words_by_date)
|
||||||
|
|
||||||
|
def _on_metric_changed(self, index: int) -> None:
|
||||||
|
metric = self.metric_combo.currentData()
|
||||||
|
if metric:
|
||||||
|
self._apply_metric(metric)
|
||||||
|
|
||||||
|
def _gather_stats(self):
|
||||||
|
return self._db.gather_stats()
|
||||||
Loading…
Add table
Add a link
Reference in a new issue