broadlistening-viewer 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +661 -0
- data/README.md +108 -0
- data/exe/broadlistening-viewer +20 -0
- data/js/shared/blv.css +102 -0
- data/js/shared/chart_manager.js +504 -0
- data/js/shared/colors.js +81 -0
- data/js/shared/decidim_core_shim.js +67 -0
- data/js/shared/fullscreen_modal.js +108 -0
- data/js/shared/i18n.js +37 -0
- data/js/shared/plotly_shim.js +4034 -0
- data/js/shared/scatter_chart.js +250 -0
- data/js/shared/settings_dialog.js +154 -0
- data/js/shared/toolbar.js +102 -0
- data/js/shared/treemap_chart.js +202 -0
- data/js/shared/types.js +0 -0
- data/js/shared/utils.js +43 -0
- data/lib/broadlistening/viewer/assets/app.css +2 -0
- data/lib/broadlistening/viewer/assets/broadlistening-view.js +4201 -0
- data/lib/broadlistening/viewer/assets/i18n/ja.json +46 -0
- data/lib/broadlistening/viewer/assets/shared/_visualization_body.html.erb +33 -0
- data/lib/broadlistening/viewer/assets/template.html.erb +28 -0
- data/lib/broadlistening/viewer/renderer.rb +88 -0
- data/lib/broadlistening/viewer/version.rb +7 -0
- data/lib/broadlistening/viewer.rb +20 -0
- metadata +68 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
import ScatterChart from "./scatter_chart";
|
|
2
|
+
import TreemapChart from "./treemap_chart";
|
|
3
|
+
import { CLUSTER_COLORS } from "./colors";
|
|
4
|
+
import { icon, escapeHtml } from "./decidim_core_shim";
|
|
5
|
+
import { t } from "./i18n";
|
|
6
|
+
import Toolbar, { VIEW_MODES } from "./toolbar";
|
|
7
|
+
import SettingsDialog from "./settings_dialog";
|
|
8
|
+
import FullscreenModal from "./fullscreen_modal";
|
|
9
|
+
class ChartManager {
|
|
10
|
+
constructor(container, data, options = {}) {
|
|
11
|
+
this.container = container;
|
|
12
|
+
this.data = data;
|
|
13
|
+
this.options = {
|
|
14
|
+
defaultChart: VIEW_MODES.SCATTER_ALL,
|
|
15
|
+
showToolbar: true,
|
|
16
|
+
...options
|
|
17
|
+
};
|
|
18
|
+
this.arguments = data.arguments || [];
|
|
19
|
+
this.clusters = data.clusters || [];
|
|
20
|
+
this.clusterById = new Map(this.clusters.map((c) => [c.id, c]));
|
|
21
|
+
this.childrenByParent = /* @__PURE__ */ new Map();
|
|
22
|
+
this.clustersByLevel = /* @__PURE__ */ new Map();
|
|
23
|
+
for (const cluster of this.clusters) {
|
|
24
|
+
const parentId = cluster.parent;
|
|
25
|
+
if (parentId) {
|
|
26
|
+
if (!this.childrenByParent.has(parentId)) {
|
|
27
|
+
this.childrenByParent.set(parentId, []);
|
|
28
|
+
}
|
|
29
|
+
this.childrenByParent.get(parentId).push(cluster);
|
|
30
|
+
}
|
|
31
|
+
const level = cluster.level ?? 0;
|
|
32
|
+
if (!this.clustersByLevel.has(level)) {
|
|
33
|
+
this.clustersByLevel.set(level, []);
|
|
34
|
+
}
|
|
35
|
+
this.clustersByLevel.get(level).push(cluster);
|
|
36
|
+
}
|
|
37
|
+
for (const children of this.childrenByParent.values()) {
|
|
38
|
+
children.sort((a, b) => (b.value || 0) - (a.value || 0));
|
|
39
|
+
}
|
|
40
|
+
this.clusterColorMap = /* @__PURE__ */ new Map();
|
|
41
|
+
for (const children of this.childrenByParent.values()) {
|
|
42
|
+
children.forEach((cluster, index) => {
|
|
43
|
+
this.clusterColorMap.set(cluster.id, CLUSTER_COLORS[index % CLUSTER_COLORS.length]);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
this.argumentsByClusterId = /* @__PURE__ */ new Map();
|
|
47
|
+
for (const arg of this.arguments) {
|
|
48
|
+
for (const clusterId of arg.cluster_ids || []) {
|
|
49
|
+
if (!this.argumentsByClusterId.has(clusterId)) {
|
|
50
|
+
this.argumentsByClusterId.set(clusterId, []);
|
|
51
|
+
}
|
|
52
|
+
this.argumentsByClusterId.get(clusterId).push(arg);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
this.maxLevel = Math.max(...this.clusters.map((c) => c.level || 0), 0);
|
|
56
|
+
this.hasDensityData = this.clusters.some((c) => typeof c.density_rank_percentile === "number");
|
|
57
|
+
this.viewMode = VIEW_MODES.SCATTER_ALL;
|
|
58
|
+
this.isFullscreen = false;
|
|
59
|
+
this.treemapLevel = "0";
|
|
60
|
+
this.maxDensity = 0.2;
|
|
61
|
+
this.minValue = 5;
|
|
62
|
+
this.isDenseGroupEnabled = true;
|
|
63
|
+
if (this.hasDensityData) {
|
|
64
|
+
this.updateDenseGroupEnabled();
|
|
65
|
+
}
|
|
66
|
+
this.init();
|
|
67
|
+
}
|
|
68
|
+
init() {
|
|
69
|
+
if (this.arguments.length === 0) {
|
|
70
|
+
this.container.innerHTML = `<p class="text-gray text-center py-8">${escapeHtml(t("common.no_data"))}</p>`;
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
this.createLayout();
|
|
74
|
+
this.renderChart();
|
|
75
|
+
this.clusterGridContainer = document.getElementById("cluster-grid") || void 0;
|
|
76
|
+
this.clusterOverviewSection = document.getElementById("cluster-overview-section") || void 0;
|
|
77
|
+
this.bindClusterCardEvents();
|
|
78
|
+
}
|
|
79
|
+
createLayout() {
|
|
80
|
+
this.container.innerHTML = `
|
|
81
|
+
<div class="w-full bg-white rounded-lg border border-gray-200 overflow-hidden">
|
|
82
|
+
<div data-blv="toolbar" class="flex justify-between items-center px-4 py-3 border-b border-gray-200 bg-gray-50"></div>
|
|
83
|
+
<div data-blv="breadcrumb" class="hidden px-4 py-2 bg-sky-50 border-b border-sky-200"></div>
|
|
84
|
+
<div data-blv="chart-container" class="w-full"></div>
|
|
85
|
+
</div>
|
|
86
|
+
`;
|
|
87
|
+
this.toolbarContainer = this.container.querySelector('[data-blv="toolbar"]');
|
|
88
|
+
this.breadcrumbContainer = this.container.querySelector('[data-blv="breadcrumb"]');
|
|
89
|
+
this.chartContainer = this.container.querySelector('[data-blv="chart-container"]');
|
|
90
|
+
if (this.options.showToolbar) {
|
|
91
|
+
this.renderToolbar();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
renderToolbar() {
|
|
95
|
+
this.toolbar = new Toolbar({
|
|
96
|
+
viewMode: this.viewMode,
|
|
97
|
+
hasDensityData: this.hasDensityData,
|
|
98
|
+
isDenseGroupEnabled: this.isDenseGroupEnabled,
|
|
99
|
+
showSettings: this.hasDensityData,
|
|
100
|
+
showFullscreen: true,
|
|
101
|
+
onViewModeChange: (mode) => this.switchViewMode(mode),
|
|
102
|
+
onSettingsClick: () => this.openSettingsDialog(),
|
|
103
|
+
onFullscreenClick: () => this.toggleFullscreen()
|
|
104
|
+
});
|
|
105
|
+
this.toolbarContainer.innerHTML = this.toolbar.render();
|
|
106
|
+
this.toolbar.bindEvents(this.toolbarContainer);
|
|
107
|
+
}
|
|
108
|
+
renderBreadcrumb() {
|
|
109
|
+
const isScatterMode = this.viewMode === VIEW_MODES.SCATTER_ALL || this.viewMode === VIEW_MODES.SCATTER_DENSITY;
|
|
110
|
+
if (!this.selectedClusterId || !isScatterMode) {
|
|
111
|
+
this.breadcrumbContainer.innerHTML = "";
|
|
112
|
+
this.breadcrumbContainer.style.display = "none";
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const path = this.buildClusterPath(this.selectedClusterId);
|
|
116
|
+
if (path.length === 0) {
|
|
117
|
+
this.breadcrumbContainer.innerHTML = "";
|
|
118
|
+
this.breadcrumbContainer.style.display = "none";
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
this.breadcrumbContainer.style.display = "block";
|
|
122
|
+
this.breadcrumbContainer.innerHTML = `
|
|
123
|
+
<div class="flex items-center gap-2 flex-wrap">
|
|
124
|
+
<span class="text-xs font-medium text-sky-700">${escapeHtml(t("breadcrumb.viewing"))}</span>
|
|
125
|
+
<nav class="flex items-center gap-1 flex-wrap">
|
|
126
|
+
<a class="text-sm cursor-pointer text-sky-600 underline hover:text-sky-800" data-cluster-id="">
|
|
127
|
+
${escapeHtml(t("breadcrumb.all"))}
|
|
128
|
+
</a>
|
|
129
|
+
${path.map((cluster, index) => `
|
|
130
|
+
<span class="flex items-center text-slate-400 [&_svg]:w-3.5 [&_svg]:h-3.5">${icon("arrow-right-s-line")}</span>
|
|
131
|
+
${index === path.length - 1 ? `<span class="text-sm font-semibold text-slate-800">${escapeHtml(cluster.label)}</span>` : `<a class="text-sm cursor-pointer text-sky-600 underline hover:text-sky-800" data-cluster-id="${escapeHtml(cluster.id)}">${escapeHtml(cluster.label)}</a>`}
|
|
132
|
+
`).join("")}
|
|
133
|
+
</nav>
|
|
134
|
+
</div>
|
|
135
|
+
`;
|
|
136
|
+
this.breadcrumbContainer.querySelectorAll("[data-cluster-id]").forEach((btn) => {
|
|
137
|
+
btn.addEventListener("click", (e) => {
|
|
138
|
+
const clusterId = e.currentTarget.dataset.clusterId;
|
|
139
|
+
this.navigateToCluster(clusterId || void 0);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
renderBreadcrumbInto(container) {
|
|
144
|
+
const isScatterAllMode = this.viewMode === VIEW_MODES.SCATTER_ALL;
|
|
145
|
+
if (!this.selectedClusterId || !isScatterAllMode) {
|
|
146
|
+
container.innerHTML = "";
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const path = this.buildClusterPath(this.selectedClusterId);
|
|
150
|
+
if (path.length === 0) {
|
|
151
|
+
container.innerHTML = "";
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
container.innerHTML = `
|
|
155
|
+
<div class="flex items-center gap-2 flex-wrap py-2">
|
|
156
|
+
<span class="text-xs font-medium text-sky-700">${escapeHtml(t("breadcrumb.viewing"))}</span>
|
|
157
|
+
<nav class="flex items-center gap-1 flex-wrap">
|
|
158
|
+
<a class="text-sm cursor-pointer text-sky-600 underline hover:text-sky-800" data-cluster-id="">
|
|
159
|
+
${escapeHtml(t("breadcrumb.all"))}
|
|
160
|
+
</a>
|
|
161
|
+
${path.map((cluster, index) => `
|
|
162
|
+
<span class="flex items-center text-slate-400 [&_svg]:w-3.5 [&_svg]:h-3.5">${icon("arrow-right-s-line")}</span>
|
|
163
|
+
${index === path.length - 1 ? `<span class="text-sm font-semibold text-slate-800">${escapeHtml(cluster.label)}</span>` : `<a class="text-sm cursor-pointer text-sky-600 underline hover:text-sky-800" data-cluster-id="${escapeHtml(cluster.id)}">${escapeHtml(cluster.label)}</a>`}
|
|
164
|
+
`).join("")}
|
|
165
|
+
</nav>
|
|
166
|
+
</div>
|
|
167
|
+
`;
|
|
168
|
+
container.querySelectorAll("[data-cluster-id]").forEach((btn) => {
|
|
169
|
+
btn.addEventListener("click", (e) => {
|
|
170
|
+
const clusterId = e.currentTarget.dataset.clusterId;
|
|
171
|
+
this.selectedClusterId = clusterId || void 0;
|
|
172
|
+
if (this.fullscreenModal) {
|
|
173
|
+
this.fullscreenModal.renderBreadcrumb();
|
|
174
|
+
this.fullscreenModal.renderChart();
|
|
175
|
+
}
|
|
176
|
+
this.renderClusterGrid();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
buildClusterPath(clusterId) {
|
|
181
|
+
const path = [];
|
|
182
|
+
let currentId = clusterId;
|
|
183
|
+
while (currentId && currentId !== "0") {
|
|
184
|
+
const cluster = this.clusterById.get(currentId);
|
|
185
|
+
if (cluster) {
|
|
186
|
+
path.unshift(cluster);
|
|
187
|
+
currentId = cluster.parent || void 0;
|
|
188
|
+
} else {
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return path;
|
|
193
|
+
}
|
|
194
|
+
updateToolbarState() {
|
|
195
|
+
if (this.toolbar) {
|
|
196
|
+
this.toolbar.updateState(this.toolbarContainer, {
|
|
197
|
+
viewMode: this.viewMode,
|
|
198
|
+
hasDensityData: this.hasDensityData,
|
|
199
|
+
isDenseGroupEnabled: this.isDenseGroupEnabled
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
switchViewMode(mode) {
|
|
204
|
+
if (this.viewMode === mode) return;
|
|
205
|
+
this.viewMode = mode;
|
|
206
|
+
if (mode === VIEW_MODES.TREEMAP) {
|
|
207
|
+
this.selectedClusterId = void 0;
|
|
208
|
+
this.renderClusterGrid();
|
|
209
|
+
}
|
|
210
|
+
this.updateToolbarState();
|
|
211
|
+
this.renderBreadcrumb();
|
|
212
|
+
this.renderChart();
|
|
213
|
+
}
|
|
214
|
+
navigateToCluster(clusterId) {
|
|
215
|
+
this.selectedClusterId = clusterId;
|
|
216
|
+
this.renderBreadcrumb();
|
|
217
|
+
this.renderChart();
|
|
218
|
+
this.renderClusterGrid();
|
|
219
|
+
}
|
|
220
|
+
renderClusterGrid() {
|
|
221
|
+
if (!this.clusterGridContainer) return;
|
|
222
|
+
let clustersToShow;
|
|
223
|
+
if (this.selectedClusterId) {
|
|
224
|
+
clustersToShow = this.getChildClusters(this.selectedClusterId);
|
|
225
|
+
} else {
|
|
226
|
+
clustersToShow = this.getTopLevelClusters();
|
|
227
|
+
}
|
|
228
|
+
this.clusterGridContainer.innerHTML = clustersToShow.map((cluster, index) => {
|
|
229
|
+
const color = CLUSTER_COLORS[index % CLUSTER_COLORS.length];
|
|
230
|
+
const hasChildren = this.getChildClusters(cluster.id).length > 0;
|
|
231
|
+
const cursorClass = hasChildren ? "cursor-pointer" : "";
|
|
232
|
+
const valueCount = cluster.value || 0;
|
|
233
|
+
return `
|
|
234
|
+
<div class="card p-4 hover:shadow-md transition-shadow ${escapeHtml(cursorClass)}"
|
|
235
|
+
style="border-left: 4px solid ${escapeHtml(color)};"
|
|
236
|
+
data-cluster-id="${escapeHtml(cluster.id)}"
|
|
237
|
+
${hasChildren ? `title="${escapeHtml(t("cluster.click_to_expand"))}"` : ""}>
|
|
238
|
+
<div class="flex items-center gap-3 mb-2">
|
|
239
|
+
<span class="inline-block w-3 h-3 rounded-full flex-shrink-0" style="background-color: ${escapeHtml(color)};"></span>
|
|
240
|
+
<span class="font-semibold text-sm line-clamp-2">${escapeHtml(cluster.label)}</span>
|
|
241
|
+
</div>
|
|
242
|
+
<div class="flex items-center gap-2 mb-2">
|
|
243
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium" style="background-color: ${escapeHtml(color)}20; color: ${escapeHtml(color)};">
|
|
244
|
+
${escapeHtml(t("common.opinions_count", { count: valueCount }))}
|
|
245
|
+
</span>
|
|
246
|
+
</div>
|
|
247
|
+
${cluster.takeaway ? `<p class="text-gray-2 text-sm line-clamp-3">${escapeHtml(cluster.takeaway)}</p>` : ""}
|
|
248
|
+
</div>
|
|
249
|
+
`;
|
|
250
|
+
}).join("");
|
|
251
|
+
this.renderClusterGridBreadcrumb();
|
|
252
|
+
this.bindClusterCardEvents();
|
|
253
|
+
}
|
|
254
|
+
renderClusterGridBreadcrumb() {
|
|
255
|
+
if (!this.clusterOverviewSection) return;
|
|
256
|
+
const existingBreadcrumb = this.clusterOverviewSection.querySelector('[data-blv="cluster-breadcrumb"]');
|
|
257
|
+
if (existingBreadcrumb) {
|
|
258
|
+
existingBreadcrumb.remove();
|
|
259
|
+
}
|
|
260
|
+
if (!this.selectedClusterId) return;
|
|
261
|
+
const path = this.buildClusterPath(this.selectedClusterId);
|
|
262
|
+
if (path.length === 0) return;
|
|
263
|
+
const breadcrumbEl = document.createElement("div");
|
|
264
|
+
breadcrumbEl.dataset.blv = "cluster-breadcrumb";
|
|
265
|
+
breadcrumbEl.className = "mb-4 px-4 py-3 bg-slate-50 rounded-lg border border-slate-200";
|
|
266
|
+
breadcrumbEl.innerHTML = `
|
|
267
|
+
<nav class="flex items-center flex-wrap gap-1">
|
|
268
|
+
<a class="text-sm cursor-pointer text-sky-600 underline hover:text-sky-800" data-navigate-cluster="">
|
|
269
|
+
${escapeHtml(t("breadcrumb.all"))}
|
|
270
|
+
</a>
|
|
271
|
+
${path.map((c, index) => `
|
|
272
|
+
<span class="flex items-center text-slate-400 [&_svg]:w-3.5 [&_svg]:h-3.5">${icon("arrow-right-s-line")}</span>
|
|
273
|
+
${index === path.length - 1 ? `<span class="text-sm font-semibold text-slate-800">${escapeHtml(c.label)}</span>` : `<a class="text-sm cursor-pointer text-sky-600 underline hover:text-sky-800" data-navigate-cluster="${escapeHtml(c.id)}">${escapeHtml(c.label)}</a>`}
|
|
274
|
+
`).join("")}
|
|
275
|
+
</nav>
|
|
276
|
+
`;
|
|
277
|
+
this.clusterGridContainer.parentNode.insertBefore(breadcrumbEl, this.clusterGridContainer);
|
|
278
|
+
breadcrumbEl.querySelectorAll("[data-navigate-cluster]").forEach((btn) => {
|
|
279
|
+
btn.addEventListener("click", (e) => {
|
|
280
|
+
e.preventDefault();
|
|
281
|
+
const clusterId = e.currentTarget.dataset.navigateCluster;
|
|
282
|
+
this.navigateToCluster(clusterId || void 0);
|
|
283
|
+
this.renderBreadcrumb();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
renderChart() {
|
|
288
|
+
if (this.scatterChart) {
|
|
289
|
+
this.scatterChart.destroy();
|
|
290
|
+
this.scatterChart = void 0;
|
|
291
|
+
}
|
|
292
|
+
if (this.treemapChart) {
|
|
293
|
+
this.treemapChart.destroy();
|
|
294
|
+
this.treemapChart = void 0;
|
|
295
|
+
}
|
|
296
|
+
this.chartContainer.innerHTML = '<div data-blv="chart-plot" class="w-full h-[350px] md:h-[500px]"></div>';
|
|
297
|
+
const plotContainer = this.chartContainer.querySelector('[data-blv="chart-plot"]');
|
|
298
|
+
this.renderChartInto(plotContainer);
|
|
299
|
+
}
|
|
300
|
+
renderChartInto(container) {
|
|
301
|
+
if (this.viewMode === VIEW_MODES.SCATTER_ALL) {
|
|
302
|
+
const chart = new ScatterChart(container, this.data, {
|
|
303
|
+
selectedClusterId: this.selectedClusterId,
|
|
304
|
+
targetLevel: 1,
|
|
305
|
+
clusterColorMap: this.clusterColorMap,
|
|
306
|
+
clusterById: this.clusterById,
|
|
307
|
+
childrenByParent: this.childrenByParent,
|
|
308
|
+
clustersByLevel: this.clustersByLevel,
|
|
309
|
+
argumentsByClusterId: this.argumentsByClusterId
|
|
310
|
+
});
|
|
311
|
+
chart.render();
|
|
312
|
+
if (container === this.chartContainer?.querySelector('[data-blv="chart-plot"]')) {
|
|
313
|
+
this.scatterChart = chart;
|
|
314
|
+
}
|
|
315
|
+
} else if (this.viewMode === VIEW_MODES.SCATTER_DENSITY) {
|
|
316
|
+
const { filteredClusterIds } = this.getDenseClusters();
|
|
317
|
+
const chart = new ScatterChart(container, this.data, {
|
|
318
|
+
targetLevel: this.maxLevel,
|
|
319
|
+
filteredClusterIds,
|
|
320
|
+
maxLevel: this.maxLevel,
|
|
321
|
+
clusterColorMap: this.clusterColorMap,
|
|
322
|
+
clusterById: this.clusterById,
|
|
323
|
+
childrenByParent: this.childrenByParent,
|
|
324
|
+
clustersByLevel: this.clustersByLevel,
|
|
325
|
+
argumentsByClusterId: this.argumentsByClusterId
|
|
326
|
+
});
|
|
327
|
+
chart.render();
|
|
328
|
+
if (container === this.chartContainer?.querySelector('[data-blv="chart-plot"]')) {
|
|
329
|
+
this.scatterChart = chart;
|
|
330
|
+
}
|
|
331
|
+
} else if (this.viewMode === VIEW_MODES.TREEMAP) {
|
|
332
|
+
const chart = new TreemapChart(container, this.data, {
|
|
333
|
+
level: this.treemapLevel,
|
|
334
|
+
onLevelChange: (level) => {
|
|
335
|
+
this.treemapLevel = level;
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
chart.render();
|
|
339
|
+
if (container === this.chartContainer?.querySelector('[data-blv="chart-plot"]')) {
|
|
340
|
+
this.treemapChart = chart;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
toggleFullscreen() {
|
|
345
|
+
if (this.isFullscreen) {
|
|
346
|
+
this.exitFullscreen();
|
|
347
|
+
} else {
|
|
348
|
+
this.enterFullscreen();
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
enterFullscreen() {
|
|
352
|
+
this.isFullscreen = true;
|
|
353
|
+
this.fullscreenModal = new FullscreenModal({
|
|
354
|
+
viewMode: this.viewMode,
|
|
355
|
+
hasDensityData: this.hasDensityData,
|
|
356
|
+
isDenseGroupEnabled: this.isDenseGroupEnabled,
|
|
357
|
+
onViewModeChange: (mode) => {
|
|
358
|
+
this.viewMode = mode;
|
|
359
|
+
if (mode === VIEW_MODES.TREEMAP) {
|
|
360
|
+
this.selectedClusterId = void 0;
|
|
361
|
+
}
|
|
362
|
+
this.fullscreenModal.updateToolbarState({ viewMode: mode });
|
|
363
|
+
this.fullscreenModal.renderBreadcrumb();
|
|
364
|
+
this.fullscreenModal.renderChart();
|
|
365
|
+
},
|
|
366
|
+
onClose: () => {
|
|
367
|
+
this.exitFullscreen();
|
|
368
|
+
},
|
|
369
|
+
renderChart: (container) => {
|
|
370
|
+
this.renderChartInto(container);
|
|
371
|
+
},
|
|
372
|
+
renderBreadcrumb: (container) => {
|
|
373
|
+
this.renderBreadcrumbInto(container);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
this.fullscreenModal.open();
|
|
377
|
+
}
|
|
378
|
+
exitFullscreen() {
|
|
379
|
+
this.isFullscreen = false;
|
|
380
|
+
if (this.fullscreenModal) {
|
|
381
|
+
this.fullscreenModal.close();
|
|
382
|
+
this.fullscreenModal = void 0;
|
|
383
|
+
}
|
|
384
|
+
this.updateToolbarState();
|
|
385
|
+
this.renderBreadcrumb();
|
|
386
|
+
this.renderChart();
|
|
387
|
+
this.renderClusterGrid();
|
|
388
|
+
}
|
|
389
|
+
bindClusterCardEvents() {
|
|
390
|
+
if (!this.clusterGridContainer) return;
|
|
391
|
+
if (this._clusterGridClickHandler) {
|
|
392
|
+
this.clusterGridContainer.removeEventListener("click", this._clusterGridClickHandler);
|
|
393
|
+
}
|
|
394
|
+
this._clusterGridClickHandler = (e) => {
|
|
395
|
+
const card = e.target.closest("[data-cluster-id]");
|
|
396
|
+
if (!card) return;
|
|
397
|
+
const clusterId = card.dataset.clusterId;
|
|
398
|
+
if (clusterId && this.getChildClusters(clusterId).length > 0) {
|
|
399
|
+
e.preventDefault();
|
|
400
|
+
this.handleClusterCardClick(clusterId);
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
this.clusterGridContainer.addEventListener("click", this._clusterGridClickHandler);
|
|
404
|
+
}
|
|
405
|
+
handleClusterCardClick(clusterId) {
|
|
406
|
+
const children = this.getChildClusters(clusterId);
|
|
407
|
+
if (children.length > 0) {
|
|
408
|
+
if (this.viewMode === VIEW_MODES.TREEMAP) {
|
|
409
|
+
this.viewMode = VIEW_MODES.SCATTER_ALL;
|
|
410
|
+
this.updateToolbarState();
|
|
411
|
+
}
|
|
412
|
+
this.navigateToCluster(clusterId);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
getChildClusters(parentId) {
|
|
416
|
+
return this.childrenByParent.get(parentId) || [];
|
|
417
|
+
}
|
|
418
|
+
getTopLevelClusters() {
|
|
419
|
+
return this.childrenByParent.get("0") || [];
|
|
420
|
+
}
|
|
421
|
+
getDenseClusters() {
|
|
422
|
+
if (!this.hasDensityData) {
|
|
423
|
+
return { filtered: [], filteredClusterIds: /* @__PURE__ */ new Set(), isEmpty: true };
|
|
424
|
+
}
|
|
425
|
+
const deepestLevelClusters = this.clustersByLevel.get(this.maxLevel) || [];
|
|
426
|
+
const filteredDeepestLevelClusters = deepestLevelClusters.filter((c) => (c.density_rank_percentile ?? 1) <= this.maxDensity).filter((c) => (c.value || 0) >= this.minValue);
|
|
427
|
+
const filteredClusterIds = new Set(filteredDeepestLevelClusters.map((c) => c.id));
|
|
428
|
+
const filtered = [
|
|
429
|
+
...this.clusters.filter((c) => c.level !== this.maxLevel),
|
|
430
|
+
...filteredDeepestLevelClusters
|
|
431
|
+
];
|
|
432
|
+
return {
|
|
433
|
+
filtered,
|
|
434
|
+
filteredClusterIds,
|
|
435
|
+
isEmpty: filteredDeepestLevelClusters.length === 0
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
updateDenseGroupEnabled() {
|
|
439
|
+
const { isEmpty } = this.getDenseClusters();
|
|
440
|
+
this.isDenseGroupEnabled = !isEmpty;
|
|
441
|
+
if (this.viewMode === VIEW_MODES.SCATTER_DENSITY && isEmpty) {
|
|
442
|
+
this.viewMode = VIEW_MODES.SCATTER_ALL;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
openSettingsDialog() {
|
|
446
|
+
this.settingsDialog = new SettingsDialog({
|
|
447
|
+
maxDensity: this.maxDensity,
|
|
448
|
+
minValue: this.minValue,
|
|
449
|
+
onApply: (settings) => {
|
|
450
|
+
this.maxDensity = settings.maxDensity;
|
|
451
|
+
this.minValue = settings.minValue;
|
|
452
|
+
this.updateDenseGroupEnabled();
|
|
453
|
+
this.updateToolbarState();
|
|
454
|
+
if (this.fullscreenModal) {
|
|
455
|
+
this.fullscreenModal.updateToolbarState({
|
|
456
|
+
isDenseGroupEnabled: this.isDenseGroupEnabled
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (this.viewMode === VIEW_MODES.SCATTER_DENSITY) {
|
|
460
|
+
this.renderChart();
|
|
461
|
+
if (this.fullscreenModal) {
|
|
462
|
+
this.fullscreenModal.renderChart();
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
},
|
|
466
|
+
onClose: () => {
|
|
467
|
+
this.settingsDialog = void 0;
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
this.settingsDialog.open();
|
|
471
|
+
}
|
|
472
|
+
destroy() {
|
|
473
|
+
if (this.scatterChart) {
|
|
474
|
+
this.scatterChart.destroy();
|
|
475
|
+
this.scatterChart = void 0;
|
|
476
|
+
}
|
|
477
|
+
if (this.treemapChart) {
|
|
478
|
+
this.treemapChart.destroy();
|
|
479
|
+
this.treemapChart = void 0;
|
|
480
|
+
}
|
|
481
|
+
if (this.fullscreenModal) {
|
|
482
|
+
this.fullscreenModal.close();
|
|
483
|
+
this.fullscreenModal = void 0;
|
|
484
|
+
}
|
|
485
|
+
if (this.settingsDialog) {
|
|
486
|
+
this.settingsDialog.close();
|
|
487
|
+
this.settingsDialog = void 0;
|
|
488
|
+
}
|
|
489
|
+
if (this.clusterGridContainer && this._clusterGridClickHandler) {
|
|
490
|
+
this.clusterGridContainer.removeEventListener("click", this._clusterGridClickHandler);
|
|
491
|
+
this._clusterGridClickHandler = void 0;
|
|
492
|
+
}
|
|
493
|
+
if (this.container) this.container.innerHTML = "";
|
|
494
|
+
this.toolbarContainer = void 0;
|
|
495
|
+
this.breadcrumbContainer = void 0;
|
|
496
|
+
this.chartContainer = void 0;
|
|
497
|
+
this.clusterGridContainer = void 0;
|
|
498
|
+
this.clusterOverviewSection = void 0;
|
|
499
|
+
this.toolbar = void 0;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
export {
|
|
503
|
+
ChartManager as default
|
|
504
|
+
};
|
data/js/shared/colors.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const CLUSTER_COLORS = [
|
|
2
|
+
"#7ac943",
|
|
3
|
+
// green
|
|
4
|
+
"#3fa9f5",
|
|
5
|
+
// blue
|
|
6
|
+
"#ff7997",
|
|
7
|
+
// pink
|
|
8
|
+
"#ffcc5c",
|
|
9
|
+
// yellow
|
|
10
|
+
"#845ec2",
|
|
11
|
+
// purple
|
|
12
|
+
"#00c9a7",
|
|
13
|
+
// teal
|
|
14
|
+
"#ff6f61",
|
|
15
|
+
// coral
|
|
16
|
+
"#6c5ce7",
|
|
17
|
+
// indigo
|
|
18
|
+
"#fdcb6e",
|
|
19
|
+
// gold
|
|
20
|
+
"#74b9ff",
|
|
21
|
+
// light blue
|
|
22
|
+
"#e17055",
|
|
23
|
+
// burnt orange
|
|
24
|
+
"#00b894",
|
|
25
|
+
// mint
|
|
26
|
+
"#fd79a8",
|
|
27
|
+
// hot pink
|
|
28
|
+
"#a29bfe",
|
|
29
|
+
// lavender
|
|
30
|
+
"#55efc4",
|
|
31
|
+
// aqua
|
|
32
|
+
"#fab1a0",
|
|
33
|
+
// peach
|
|
34
|
+
"#81ecec",
|
|
35
|
+
// cyan
|
|
36
|
+
"#f8b500",
|
|
37
|
+
// amber
|
|
38
|
+
"#2d98da",
|
|
39
|
+
// ocean blue
|
|
40
|
+
"#26de81",
|
|
41
|
+
// lime
|
|
42
|
+
"#fc5c65",
|
|
43
|
+
// watermelon
|
|
44
|
+
"#45aaf2",
|
|
45
|
+
// sky blue
|
|
46
|
+
"#a55eea",
|
|
47
|
+
// violet
|
|
48
|
+
"#fed330",
|
|
49
|
+
// sunshine
|
|
50
|
+
"#20bf6b",
|
|
51
|
+
// emerald
|
|
52
|
+
"#eb3b5a",
|
|
53
|
+
// strawberry
|
|
54
|
+
"#fa8231",
|
|
55
|
+
// tangerine
|
|
56
|
+
"#4b7bec",
|
|
57
|
+
// royal blue
|
|
58
|
+
"#8854d0",
|
|
59
|
+
// grape
|
|
60
|
+
"#2bcbba"
|
|
61
|
+
// seafoam
|
|
62
|
+
];
|
|
63
|
+
const INACTIVE_COLOR = "rgba(200, 200, 200, 0.3)";
|
|
64
|
+
function getClusterColor(clusterId) {
|
|
65
|
+
if (!clusterId) return INACTIVE_COLOR;
|
|
66
|
+
const match = clusterId.match(/_(\d+)$/);
|
|
67
|
+
if (match) {
|
|
68
|
+
const index = parseInt(match[1], 10);
|
|
69
|
+
return CLUSTER_COLORS[index % CLUSTER_COLORS.length];
|
|
70
|
+
}
|
|
71
|
+
return CLUSTER_COLORS[0];
|
|
72
|
+
}
|
|
73
|
+
function getColorByIndex(index) {
|
|
74
|
+
return CLUSTER_COLORS[index % CLUSTER_COLORS.length];
|
|
75
|
+
}
|
|
76
|
+
export {
|
|
77
|
+
CLUSTER_COLORS,
|
|
78
|
+
INACTIVE_COLOR,
|
|
79
|
+
getClusterColor,
|
|
80
|
+
getColorByIndex
|
|
81
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const escapeHtml = (text) => {
|
|
2
|
+
if (typeof text !== "string") return String(text ?? "");
|
|
3
|
+
const map = { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" };
|
|
4
|
+
return text.replace(/[&<>"']/g, (c) => map[c]);
|
|
5
|
+
};
|
|
6
|
+
const escapeQuotes = (text) => {
|
|
7
|
+
if (typeof text !== "string") return String(text ?? "");
|
|
8
|
+
return text.replace(/"/g, """).replace(/'/g, "'");
|
|
9
|
+
};
|
|
10
|
+
const ICONS = {
|
|
11
|
+
"bubble-chart-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M16 16C17.6569 16 19 17.3431 19 19C19 20.6569 17.6569 22 16 22C14.3431 22 13 20.6569 13 19C13 17.3431 14.3431 16 16 16ZM6 12C8.20914 12 10 13.7909 10 16C10 18.2091 8.20914 20 6 20C3.79086 20 2 18.2091 2 16C2 13.7909 3.79086 12 6 12ZM16 18C15.4477 18 15 18.4477 15 19C15 19.5523 15.4477 20 16 20C16.5523 20 17 19.5523 17 19C17 18.4477 16.5523 18 16 18ZM6 14C4.89543 14 4 14.8954 4 16C4 17.1046 4.89543 18 6 18C7.10457 18 8 17.1046 8 16C8 14.8954 7.10457 14 6 14ZM14.5 2C17.5376 2 20 4.46243 20 7.5C20 10.5376 17.5376 13 14.5 13C11.4624 13 9 10.5376 9 7.5C9 4.46243 11.4624 2 14.5 2ZM14.5 4C12.567 4 11 5.567 11 7.5C11 9.433 12.567 11 14.5 11C16.433 11 18 9.433 18 7.5C18 5.567 16.433 4 14.5 4Z"/></svg>',
|
|
12
|
+
"focus-3-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M13 1L13.001 4.06201C16.6192 4.51365 19.4869 7.38163 19.9381 11L23 11V13L19.938 13.001C19.4864 16.6189 16.6189 19.4864 13.001 19.938L13 23H11L11 19.9381C7.38163 19.4869 4.51365 16.6192 4.06201 13.001L1 13V11L4.06189 11C4.51312 7.38129 7.38129 4.51312 11 4.06189L11 1H13ZM12 6C8.68629 6 6 8.68629 6 12C6 15.3137 8.68629 18 12 18C15.3137 18 18 15.3137 18 12C18 8.68629 15.3137 6 12 6ZM12 10C13.1046 10 14 10.8954 14 12C14 13.1046 13.1046 14 12 14C10.8954 14 10 13.1046 10 12C10 10.8954 10.8954 10 12 10Z"/></svg>',
|
|
13
|
+
"layout-grid-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M21 3C21.5523 3 22 3.44772 22 4V20C22 20.5523 21.5523 21 21 21H3C2.44772 21 2 20.5523 2 20V4C2 3.44772 2.44772 3 3 3H21ZM11 13H4V19H11V13ZM20 13H13V19H20V13ZM11 5H4V11H11V5ZM20 5H13V11H20V5Z"/></svg>',
|
|
14
|
+
"settings-3-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M3.33946 17.0002C2.90721 16.2515 2.58277 15.4702 2.36133 14.6741C3.3338 14.1779 3.99972 13.1668 3.99972 12.0002C3.99972 10.8345 3.3348 9.824 2.36353 9.32741C2.81025 7.71651 3.65857 6.21627 4.86474 4.99001C5.7807 5.58416 6.98935 5.65534 7.99972 5.072C9.01009 4.48866 9.55277 3.40635 9.4962 2.31604C11.1613 1.8846 12.8847 1.90004 14.5031 2.31862C14.4475 3.40806 14.9901 4.48912 15.9997 5.072C17.0101 5.65532 18.2187 5.58416 19.1346 4.99007C19.7133 5.57986 20.2277 6.25151 20.66 7.00021C21.0922 7.7489 21.4167 8.53025 21.6381 9.32628C20.6656 9.82247 19.9997 10.8336 19.9997 12.0002C19.9997 13.166 20.6646 14.1764 21.6359 14.673C21.1892 16.2839 20.3409 17.7841 19.1347 19.0104C18.2187 18.4163 17.0101 18.3451 15.9997 18.9284C14.9893 19.5117 14.4467 20.5941 14.5032 21.6844C12.8382 22.1158 11.1148 22.1004 9.49633 21.6818C9.55191 20.5923 9.00929 19.5113 7.99972 18.9284C6.98938 18.3451 5.78079 18.4162 4.86484 19.0103C4.28617 18.4205 3.77172 17.7489 3.33946 17.0002ZM8.99972 17.1964C10.0911 17.8265 10.8749 18.8227 11.2503 19.9659C11.7486 20.0133 12.2502 20.014 12.7486 19.9675C13.1238 18.8237 13.9078 17.8268 14.9997 17.1964C16.0916 16.5659 17.347 16.3855 18.5252 16.6324C18.8146 16.224 19.0648 15.7892 19.2729 15.334C18.4706 14.4373 17.9997 13.2604 17.9997 12.0002C17.9997 10.74 18.4706 9.5632 19.2729 8.6665C19.1688 8.4405 19.0538 8.21822 18.9279 8.00021C18.802 7.78219 18.667 7.57148 18.5233 7.36842C17.3457 7.61476 16.0911 7.43414 14.9997 6.80405C13.9083 6.17395 13.1246 5.17768 12.7491 4.03455C12.2509 3.98714 11.7492 3.98646 11.2509 4.03292C10.8756 5.17671 10.0916 6.17364 8.99972 6.80405C7.9078 7.43447 6.65245 7.61494 5.47428 7.36803C5.18485 7.77641 4.93463 8.21117 4.72656 8.66637C5.52881 9.56311 5.99972 10.74 5.99972 12.0002C5.99972 13.2604 5.52883 14.4372 4.72656 15.3339C4.83067 15.5599 4.94564 15.7822 5.07152 16.0002C5.19739 16.2182 5.3324 16.4289 5.47612 16.632C6.65377 16.3857 7.90838 16.5663 8.99972 17.1964ZM11.9997 15.0002C10.3429 15.0002 8.99972 13.6571 8.99972 12.0002C8.99972 10.3434 10.3429 9.00021 11.9997 9.00021C13.6566 9.00021 14.9997 10.3434 14.9997 12.0002C14.9997 13.6571 13.6566 15.0002 11.9997 15.0002ZM11.9997 13.0002C12.552 13.0002 12.9997 12.5525 12.9997 12.0002C12.9997 11.4479 12.552 11.0002 11.9997 11.0002C11.4474 11.0002 10.9997 11.4479 10.9997 12.0002C10.9997 12.5525 11.4474 13.0002 11.9997 13.0002Z"/></svg>',
|
|
15
|
+
"fullscreen-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M8 3V5H4V9H2V3H8ZM2 21V15H4V19H8V21H2ZM22 21H16V19H20V15H22V21ZM22 9H20V5H16V3H22V9Z"/></svg>',
|
|
16
|
+
"close-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M11.9997 10.5865L16.9495 5.63672L18.3637 7.05093L13.4139 12.0007L18.3637 16.9504L16.9495 18.3646L11.9997 13.4149L7.04996 18.3646L5.63574 16.9504L10.5855 12.0007L5.63574 7.05093L7.04996 5.63672L11.9997 10.5865Z"/></svg>',
|
|
17
|
+
"arrow-right-s-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M13.1717 12.0007L8.22192 7.05093L9.63614 5.63672L16.0001 12.0007L9.63614 18.3646L8.22192 16.9504L13.1717 12.0007Z"/></svg>',
|
|
18
|
+
"arrow-left-s-line": '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="1em" height="1em"><path d="M10.8284 12.0007L15.7782 16.9504L14.364 18.3646L8 12.0007L14.364 5.63672L15.7782 7.05093L10.8284 12.0007Z"/></svg>'
|
|
19
|
+
};
|
|
20
|
+
const icon = (name) => {
|
|
21
|
+
return ICONS[name] || `<span>[${name}]</span>`;
|
|
22
|
+
};
|
|
23
|
+
class Dialogs {
|
|
24
|
+
constructor(selector, options = {}) {
|
|
25
|
+
this.element = document.querySelector(selector);
|
|
26
|
+
this.options = options;
|
|
27
|
+
if (this.element) {
|
|
28
|
+
this._setupBackdrop();
|
|
29
|
+
this._setupCloseButtons();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
open() {
|
|
33
|
+
if (!this.element) return;
|
|
34
|
+
this.element.setAttribute("aria-hidden", "false");
|
|
35
|
+
this.options.onOpen?.();
|
|
36
|
+
}
|
|
37
|
+
show() {
|
|
38
|
+
this.open();
|
|
39
|
+
}
|
|
40
|
+
close() {
|
|
41
|
+
if (!this.element) return;
|
|
42
|
+
this.element.setAttribute("aria-hidden", "true");
|
|
43
|
+
this.options.onClose?.();
|
|
44
|
+
}
|
|
45
|
+
destroy() {
|
|
46
|
+
this.element = null;
|
|
47
|
+
this.options = {};
|
|
48
|
+
}
|
|
49
|
+
_setupBackdrop() {
|
|
50
|
+
this.element.addEventListener("click", (e) => {
|
|
51
|
+
if (e.target === this.element) this.close();
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
_setupCloseButtons() {
|
|
55
|
+
const closingSelector = this.options.closingSelector;
|
|
56
|
+
if (!closingSelector) return;
|
|
57
|
+
this.element.querySelectorAll(closingSelector).forEach((btn) => {
|
|
58
|
+
btn.addEventListener("click", () => this.close());
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export {
|
|
63
|
+
Dialogs,
|
|
64
|
+
escapeHtml,
|
|
65
|
+
escapeQuotes,
|
|
66
|
+
icon
|
|
67
|
+
};
|