Skip to content

Commit 5b25b35

Browse files
committed
feat: add OrganizationHierarchyMetric metric
2 parents e3a3330 + 18df0a4 commit 5b25b35

File tree

7 files changed

+147
-9
lines changed

7 files changed

+147
-9
lines changed

ckanext/better_stats/assets/js/bstats-stats-manager.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ckanext/better_stats/assets/js/bstats-stats-manager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,15 @@ class BetterStatsManager {
509509
const holder = this._el("div", { className: "metric-chart" });
510510
container.appendChild(holder);
511511

512+
if (chartData.tooltip?._htmlTooltip) {
513+
const template = chartData.tooltip.formatter as string;
514+
chartData.tooltip.formatter = (params: any) =>
515+
template
516+
.replace(/\{b\}/g, params.name ?? "")
517+
.replace(/\{c\}/g, params.value ?? "");
518+
delete chartData.tooltip._htmlTooltip;
519+
}
520+
512521
const chart = echarts.init(holder, this._isDark() ? "dark" : "default");
513522
chart.setOption(chartData);
514523
chart._chartOptions = chartData;

ckanext/better_stats/metrics/__init__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from ckan import plugins as p
2+
13
from ckanext.better_stats.metrics.base import MetricRegistry
24

35
from .base import MetricBase
@@ -13,6 +15,7 @@
1315
from .organization_metrics import (
1416
InactiveOrganizationsMetric,
1517
OrganizationCountMetric,
18+
OrganizationHierarchyMetric,
1619
OrganizationMembershipMetric,
1720
OrganizationOverviewMetric,
1821
OrganizationSizesMetric,
@@ -30,6 +33,7 @@
3033
"DatasetsWithoutResourcesMetric",
3134
"StaleDatasetsMetric",
3235
"OrganizationCountMetric",
36+
"OrganizationHierarchyMetric",
3337
"OrganizationMembershipMetric",
3438
"OrganizationOverviewMetric",
3539
"OrganizationSizesMetric",
@@ -61,9 +65,12 @@ def register_metrics():
6165
MetricRegistry.register("cpu", CPUMetric)
6266
MetricRegistry.register("disk_usage", DiskUsageMetric)
6367

68+
if p.plugin_loaded("hierarchy_display"):
69+
MetricRegistry.register("organization_hierarchy", OrganizationHierarchyMetric)
70+
6471

6572
def get_all_metrics() -> dict[str, type[MetricBase]]:
66-
return {
73+
metrics: dict[str, type[MetricBase]] = {
6774
"dataset_count": DatasetCountMetric,
6875
"datasets_by_org": DatasetsByOrganizationMetric,
6976
"dataset_creation_history": DatasetCreationHistoryMetric,
@@ -81,4 +88,6 @@ def get_all_metrics() -> dict[str, type[MetricBase]]:
8188
"memory": MemoryMetric,
8289
"cpu": CPUMetric,
8390
"disk_usage": DiskUsageMetric,
91+
"organization_hierarchy": OrganizationHierarchyMetric,
8492
}
93+
return metrics

ckanext/better_stats/metrics/organization_metrics.py

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,106 @@
1111
from ckanext.better_stats.metrics.base import MetricBase
1212

1313

14+
class OrganizationHierarchyMetric(MetricBase):
15+
"""Tree chart of organization parent-child relationships via ckanext-hierarchy.
16+
17+
This metric is only available if `ckanext-hierarchy` is installed and enabled.
18+
"""
19+
20+
supported_visualizations: ClassVar[list[const.VisualizationType]] = [
21+
const.VisualizationType.CHART,
22+
]
23+
default_visualization: ClassVar[const.VisualizationType] = const.VisualizationType.CHART
24+
icon: ClassVar[str] = "fa-solid fa-sitemap"
25+
supported_export_formats: ClassVar[list[str]] = ["image"]
26+
27+
def __init__(self) -> None:
28+
super().__init__(
29+
name="organization_hierarchy",
30+
title=tk._("Organization Hierarchy"),
31+
description=tk._("Tree view of organization parent-child relationships"),
32+
order=14,
33+
grid_size="full",
34+
)
35+
36+
def get_data(self) -> list[dict[str, Any]]:
37+
return tk.h.group_tree(type_="organization")
38+
39+
def _to_echarts_node(self, node: dict[str, Any]) -> dict[str, Any]:
40+
name = node["name"]
41+
url = tk.url_for("organization.read", id=name)
42+
converted: dict[str, Any] = {
43+
"name": node.get("title") or name or "Unknown",
44+
"value": (f'<a href="{url}" target="_blank" style="color:inherit;">{tk._("View")} →</a>'),
45+
}
46+
children = node.get("children", [])
47+
48+
if children:
49+
converted["children"] = [self._to_echarts_node(c) for c in children]
50+
51+
return converted
52+
53+
def get_chart_data(self) -> dict[str, Any]:
54+
roots = self.get_data()
55+
56+
if not roots:
57+
return {}
58+
59+
if len(roots) == 1:
60+
tree_data = self._to_echarts_node(roots[0])
61+
else:
62+
tree_data = {
63+
"name": tk._("Organizations"),
64+
"children": [self._to_echarts_node(r) for r in roots],
65+
}
66+
67+
return {
68+
"tooltip": {
69+
"trigger": "item",
70+
"triggerOn": "mousemove",
71+
"enterable": True,
72+
"formatter": "{b}<br/>{c}",
73+
"_htmlTooltip": True,
74+
},
75+
"series": [
76+
{
77+
"type": "tree",
78+
"data": [tree_data],
79+
"top": "5%",
80+
"left": "5%",
81+
"bottom": "5%",
82+
"right": "5%",
83+
"symbolSize": 10,
84+
"lineStyle": {"width": 1},
85+
"label": {
86+
"position": "left",
87+
"verticalAlign": "middle",
88+
"align": "right",
89+
"fontSize": 13,
90+
"width": 160,
91+
"overflow": "truncate",
92+
"ellipsis": "…",
93+
},
94+
"leaves": {
95+
"label": {
96+
"position": "right",
97+
"verticalAlign": "middle",
98+
"align": "left",
99+
"width": 160,
100+
"overflow": "truncate",
101+
"ellipsis": "…",
102+
}
103+
},
104+
"emphasis": {"focus": "descendant"},
105+
"expandAndCollapse": False,
106+
"animationDuration": 350,
107+
"animationDurationUpdate": 350,
108+
"roam": True,
109+
}
110+
],
111+
}
112+
113+
14114
class OrganizationCountMetric(MetricBase):
15115
"""Total number of active organizations with a monthly creation trend."""
16116

@@ -59,7 +159,10 @@ def get_chart_data(self) -> dict[str, Any]:
59159
)
60160
return {
61161
"tooltip": {"trigger": "axis"},
62-
"xAxis": {"type": "category", "data": [row.month.strftime("%Y-%m") for row in rows]},
162+
"xAxis": {
163+
"type": "category",
164+
"data": [row.month.strftime("%Y-%m") for row in rows],
165+
},
63166
"yAxis": {"type": "value", "minInterval": 1},
64167
"series": [{"type": "line", "data": [row.count for row in rows], "smooth": True}],
65168
}
@@ -254,7 +357,10 @@ def get_data(self) -> list[dict[str, Any]]:
254357
)
255358
return [
256359
{
257-
"organization": {"label": row.title, "url": tk.url_for("organization.read", id=row.name)},
360+
"organization": {
361+
"label": row.title,
362+
"url": tk.url_for("organization.read", id=row.name),
363+
},
258364
"datasets": row.datasets,
259365
"resources": row.resources,
260366
"members": row.members,
@@ -273,7 +379,10 @@ def get_table_data(self) -> dict[str, Any]:
273379
],
274380
"rows": [
275381
[
276-
{"text": item["organization"]["label"], "url": item["organization"]["url"]},
382+
{
383+
"text": item["organization"]["label"],
384+
"url": item["organization"]["url"],
385+
},
277386
item["datasets"],
278387
item["resources"],
279388
item["members"],
@@ -401,7 +510,13 @@ def get_chart_data(self) -> dict[str, Any]:
401510
"series": [
402511
{
403512
"type": "treemap",
404-
"data": [{"name": item["organization"] or "Unknown", "value": item["count"]} for item in data],
513+
"data": [
514+
{
515+
"name": item["organization"] or "Unknown",
516+
"value": item["count"],
517+
}
518+
for item in data
519+
],
405520
"label": {"show": True, "formatter": "{b}"},
406521
"itemStyle": {"borderColor": "#fff"},
407522
"roam": False,
@@ -414,4 +529,4 @@ def get_table_data(self) -> dict[str, Any]:
414529
return {
415530
"headers": [tk._("Organization"), tk._("Datasets")],
416531
"rows": [[{"text": item["organization"], "url": item["url"]}, item["count"]] for item in data],
417-
}
532+
}

docs/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ def render_metric(metric_name: str) -> str:
2424
markdown += f"{desc}\n\n"
2525

2626
if docstring:
27-
markdown += f"```text\n{metric.__class__.__doc__}\n```\n\n"
27+
markdown += "```\n"
28+
for line in docstring.split("\n"):
29+
markdown += f"{line.strip()}\n"
30+
markdown += "```\n\n"
2831

2932
markdown += "| | |\n"
3033
markdown += "|---|---|\n"

docs/metrics/organization.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ These metrics provide insights into organization activity and structure within t
1111
{{ render_metric('inactive_organizations') }}
1212

1313
{{ render_metric('organization_sizes') }}
14+
15+
{{ render_metric('organization_hierarchy') }}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "ckanext-better-stats"
3-
version = "0.1.0"
3+
version = "0.2.0"
44
description = "Flexible CKAN extension for defining and displaying custom metrics with support for caching, access control, and multiple output formats"
55
readme = "README.md"
66
authors = [

0 commit comments

Comments
 (0)