@duckcodeailabs/dql-compiler 0.1.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.
- package/LICENSE +123 -0
- package/dist/charts/area.d.ts +5 -0
- package/dist/charts/area.d.ts.map +1 -0
- package/dist/charts/area.js +30 -0
- package/dist/charts/area.js.map +1 -0
- package/dist/charts/bar.d.ts +5 -0
- package/dist/charts/bar.d.ts.map +1 -0
- package/dist/charts/bar.js +37 -0
- package/dist/charts/bar.js.map +1 -0
- package/dist/charts/boxplot.d.ts +5 -0
- package/dist/charts/boxplot.d.ts.map +1 -0
- package/dist/charts/boxplot.js +36 -0
- package/dist/charts/boxplot.js.map +1 -0
- package/dist/charts/chart-registry.d.ts +24 -0
- package/dist/charts/chart-registry.d.ts.map +1 -0
- package/dist/charts/chart-registry.js +14 -0
- package/dist/charts/chart-registry.js.map +1 -0
- package/dist/charts/combo.d.ts +5 -0
- package/dist/charts/combo.d.ts.map +1 -0
- package/dist/charts/combo.js +54 -0
- package/dist/charts/combo.js.map +1 -0
- package/dist/charts/funnel.d.ts +5 -0
- package/dist/charts/funnel.d.ts.map +1 -0
- package/dist/charts/funnel.js +36 -0
- package/dist/charts/funnel.js.map +1 -0
- package/dist/charts/gauge.d.ts +5 -0
- package/dist/charts/gauge.d.ts.map +1 -0
- package/dist/charts/gauge.js +53 -0
- package/dist/charts/gauge.js.map +1 -0
- package/dist/charts/geo.d.ts +5 -0
- package/dist/charts/geo.d.ts.map +1 -0
- package/dist/charts/geo.js +48 -0
- package/dist/charts/geo.js.map +1 -0
- package/dist/charts/grouped-bar.d.ts +5 -0
- package/dist/charts/grouped-bar.d.ts.map +1 -0
- package/dist/charts/grouped-bar.js +37 -0
- package/dist/charts/grouped-bar.js.map +1 -0
- package/dist/charts/heatmap.d.ts +5 -0
- package/dist/charts/heatmap.d.ts.map +1 -0
- package/dist/charts/heatmap.js +31 -0
- package/dist/charts/heatmap.js.map +1 -0
- package/dist/charts/histogram.d.ts +5 -0
- package/dist/charts/histogram.d.ts.map +1 -0
- package/dist/charts/histogram.js +32 -0
- package/dist/charts/histogram.js.map +1 -0
- package/dist/charts/index.d.ts +25 -0
- package/dist/charts/index.d.ts.map +1 -0
- package/dist/charts/index.js +66 -0
- package/dist/charts/index.js.map +1 -0
- package/dist/charts/kpi.d.ts +14 -0
- package/dist/charts/kpi.d.ts.map +1 -0
- package/dist/charts/kpi.js +71 -0
- package/dist/charts/kpi.js.map +1 -0
- package/dist/charts/line.d.ts +5 -0
- package/dist/charts/line.d.ts.map +1 -0
- package/dist/charts/line.js +38 -0
- package/dist/charts/line.js.map +1 -0
- package/dist/charts/pie.d.ts +5 -0
- package/dist/charts/pie.d.ts.map +1 -0
- package/dist/charts/pie.js +25 -0
- package/dist/charts/pie.js.map +1 -0
- package/dist/charts/sankey.d.ts +10 -0
- package/dist/charts/sankey.d.ts.map +1 -0
- package/dist/charts/sankey.js +120 -0
- package/dist/charts/sankey.js.map +1 -0
- package/dist/charts/scatter.d.ts +5 -0
- package/dist/charts/scatter.d.ts.map +1 -0
- package/dist/charts/scatter.js +34 -0
- package/dist/charts/scatter.js.map +1 -0
- package/dist/charts/shared.d.ts +11 -0
- package/dist/charts/shared.d.ts.map +1 -0
- package/dist/charts/shared.js +52 -0
- package/dist/charts/shared.js.map +1 -0
- package/dist/charts/small-multiples.d.ts +5 -0
- package/dist/charts/small-multiples.d.ts.map +1 -0
- package/dist/charts/small-multiples.js +59 -0
- package/dist/charts/small-multiples.js.map +1 -0
- package/dist/charts/sparkline.d.ts +5 -0
- package/dist/charts/sparkline.d.ts.map +1 -0
- package/dist/charts/sparkline.js +41 -0
- package/dist/charts/sparkline.js.map +1 -0
- package/dist/charts/stacked-bar.d.ts +5 -0
- package/dist/charts/stacked-bar.d.ts.map +1 -0
- package/dist/charts/stacked-bar.js +34 -0
- package/dist/charts/stacked-bar.js.map +1 -0
- package/dist/charts/table.d.ts +14 -0
- package/dist/charts/table.d.ts.map +1 -0
- package/dist/charts/table.js +58 -0
- package/dist/charts/table.js.map +1 -0
- package/dist/charts/treemap.d.ts +9 -0
- package/dist/charts/treemap.d.ts.map +1 -0
- package/dist/charts/treemap.js +60 -0
- package/dist/charts/treemap.js.map +1 -0
- package/dist/charts/waterfall.d.ts +5 -0
- package/dist/charts/waterfall.d.ts.map +1 -0
- package/dist/charts/waterfall.js +36 -0
- package/dist/charts/waterfall.js.map +1 -0
- package/dist/codegen/bundle.d.ts +34 -0
- package/dist/codegen/bundle.d.ts.map +1 -0
- package/dist/codegen/bundle.js +20 -0
- package/dist/codegen/bundle.js.map +1 -0
- package/dist/codegen/html-emitter.d.ts +10 -0
- package/dist/codegen/html-emitter.d.ts.map +1 -0
- package/dist/codegen/html-emitter.js +1082 -0
- package/dist/codegen/html-emitter.js.map +1 -0
- package/dist/codegen/index.d.ts +6 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +6 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/react-emitter.d.ts +42 -0
- package/dist/codegen/react-emitter.d.ts.map +1 -0
- package/dist/codegen/react-emitter.js +135 -0
- package/dist/codegen/react-emitter.js.map +1 -0
- package/dist/codegen/runtime-emitter.d.ts +2 -0
- package/dist/codegen/runtime-emitter.d.ts.map +1 -0
- package/dist/codegen/runtime-emitter.js +1807 -0
- package/dist/codegen/runtime-emitter.js.map +1 -0
- package/dist/codegen/vega-lite-emitter.d.ts +23 -0
- package/dist/codegen/vega-lite-emitter.d.ts.map +1 -0
- package/dist/codegen/vega-lite-emitter.js +92 -0
- package/dist/codegen/vega-lite-emitter.js.map +1 -0
- package/dist/compiler.d.ts +17 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +106 -0
- package/dist/compiler.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/ir/index.d.ts +3 -0
- package/dist/ir/index.d.ts.map +1 -0
- package/dist/ir/index.js +2 -0
- package/dist/ir/index.js.map +1 -0
- package/dist/ir/ir-nodes.d.ts +169 -0
- package/dist/ir/ir-nodes.d.ts.map +1 -0
- package/dist/ir/ir-nodes.js +2 -0
- package/dist/ir/ir-nodes.js.map +1 -0
- package/dist/ir/lowering.d.ts +9 -0
- package/dist/ir/lowering.d.ts.map +1 -0
- package/dist/ir/lowering.js +980 -0
- package/dist/ir/lowering.js.map +1 -0
- package/dist/modules/index.d.ts +4 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +3 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/resolver.d.ts +21 -0
- package/dist/modules/resolver.d.ts.map +1 -0
- package/dist/modules/resolver.js +112 -0
- package/dist/modules/resolver.js.map +1 -0
- package/dist/modules/use-expander.d.ts +11 -0
- package/dist/modules/use-expander.d.ts.map +1 -0
- package/dist/modules/use-expander.js +42 -0
- package/dist/modules/use-expander.js.map +1 -0
- package/dist/themes/colorful.d.ts +3 -0
- package/dist/themes/colorful.d.ts.map +1 -0
- package/dist/themes/colorful.js +14 -0
- package/dist/themes/colorful.js.map +1 -0
- package/dist/themes/corporate.d.ts +3 -0
- package/dist/themes/corporate.d.ts.map +1 -0
- package/dist/themes/corporate.js +14 -0
- package/dist/themes/corporate.js.map +1 -0
- package/dist/themes/dark.d.ts +3 -0
- package/dist/themes/dark.d.ts.map +1 -0
- package/dist/themes/dark.js +14 -0
- package/dist/themes/dark.js.map +1 -0
- package/dist/themes/index.d.ts +11 -0
- package/dist/themes/index.d.ts.map +1 -0
- package/dist/themes/index.js +29 -0
- package/dist/themes/index.js.map +1 -0
- package/dist/themes/light.d.ts +3 -0
- package/dist/themes/light.d.ts.map +1 -0
- package/dist/themes/light.js +14 -0
- package/dist/themes/light.js.map +1 -0
- package/dist/themes/minimal.d.ts +3 -0
- package/dist/themes/minimal.d.ts.map +1 -0
- package/dist/themes/minimal.js +14 -0
- package/dist/themes/minimal.js.map +1 -0
- package/dist/themes/theme-types.d.ts +14 -0
- package/dist/themes/theme-types.d.ts.map +1 -0
- package/dist/themes/theme-types.js +2 -0
- package/dist/themes/theme-types.js.map +1 -0
- package/package.json +43 -0
|
@@ -0,0 +1,1807 @@
|
|
|
1
|
+
export function emitRuntimeJS() {
|
|
2
|
+
return `
|
|
3
|
+
// DQL Runtime - Auto-generated
|
|
4
|
+
(function() {
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
// Store Vega view instances for signal listening
|
|
8
|
+
var vegaViews = {};
|
|
9
|
+
|
|
10
|
+
// Current filter state
|
|
11
|
+
var filterState = {};
|
|
12
|
+
var crossFilterState = {};
|
|
13
|
+
var drillState = { stack: [] };
|
|
14
|
+
var chartColumns = {};
|
|
15
|
+
var restoredState = false;
|
|
16
|
+
var suppressStateSync = false;
|
|
17
|
+
var paramActionState = {};
|
|
18
|
+
var VIEW_PRESET_STORAGE_PREFIX = 'dql:view-presets:';
|
|
19
|
+
var queryCache = {};
|
|
20
|
+
|
|
21
|
+
function computeVariables() {
|
|
22
|
+
var vars = {};
|
|
23
|
+
|
|
24
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
25
|
+
|
|
26
|
+
// Prefer reading current filter UI values so variables always exist
|
|
27
|
+
// (e.g. dropdown "All" option uses empty string).
|
|
28
|
+
if (DQL_CONFIG.filters) {
|
|
29
|
+
for (var fi = 0; fi < DQL_CONFIG.filters.length; fi++) {
|
|
30
|
+
var f = DQL_CONFIG.filters[fi];
|
|
31
|
+
if (!f || !f.id || !f.param) continue;
|
|
32
|
+
|
|
33
|
+
var el = document.getElementById(f.id);
|
|
34
|
+
if (el && el.tagName === 'SELECT' && el.multiple) {
|
|
35
|
+
var selected = [];
|
|
36
|
+
try {
|
|
37
|
+
for (var si = 0; si < el.selectedOptions.length; si++) {
|
|
38
|
+
selected.push(el.selectedOptions[si].value);
|
|
39
|
+
}
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
vars[f.param] = selected;
|
|
42
|
+
} else if (el && 'value' in el) {
|
|
43
|
+
vars[f.param] = el.value;
|
|
44
|
+
} else if (filterState && filterState[f.param] !== undefined) {
|
|
45
|
+
vars[f.param] = filterState[f.param];
|
|
46
|
+
} else if (f.defaultValue !== undefined && vars[f.param] === undefined) {
|
|
47
|
+
vars[f.param] = f.defaultValue;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Merge filter state (fallback)
|
|
53
|
+
try {
|
|
54
|
+
Object.keys(filterState || {}).forEach(function(k) {
|
|
55
|
+
if (vars[k] === undefined) vars[k] = filterState[k];
|
|
56
|
+
});
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
|
|
59
|
+
// Merge dashboard params (URL/default-driven)
|
|
60
|
+
if (DQL_CONFIG.params) {
|
|
61
|
+
for (var i = 0; i < DQL_CONFIG.params.length; i++) {
|
|
62
|
+
var p = DQL_CONFIG.params[i];
|
|
63
|
+
if (vars[p.name] === undefined) {
|
|
64
|
+
vars[p.name] = urlParams.get(p.name) || p.defaultValue;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Apply parameter actions from chart interactions (highest precedence).
|
|
70
|
+
try {
|
|
71
|
+
Object.keys(paramActionState || {}).forEach(function(key) {
|
|
72
|
+
vars[key] = paramActionState[key];
|
|
73
|
+
});
|
|
74
|
+
} catch (_) {}
|
|
75
|
+
|
|
76
|
+
// Merge URL params (lowest precedence; ignore breadcrumb/system params)
|
|
77
|
+
urlParams.forEach(function(value, key) {
|
|
78
|
+
if (key === '_from' || key === '_fromTitle' || key === 'dqlState') return;
|
|
79
|
+
if (vars[key] === undefined) vars[key] = value;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return vars;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getPresetStorageKey() {
|
|
86
|
+
return VIEW_PRESET_STORAGE_PREFIX + (DQL_CONFIG.title || 'dashboard');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function getChartConfig(chartId) {
|
|
90
|
+
if (!DQL_CONFIG || !Array.isArray(DQL_CONFIG.charts)) return null;
|
|
91
|
+
for (var i = 0; i < DQL_CONFIG.charts.length; i++) {
|
|
92
|
+
var chart = DQL_CONFIG.charts[i];
|
|
93
|
+
if (chart && chart.id === chartId) return chart;
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getChartCacheTTL(chartId) {
|
|
99
|
+
var chart = getChartConfig(chartId);
|
|
100
|
+
if (!chart || chart.cacheTTL == null) return 0;
|
|
101
|
+
var ttl = Number(chart.cacheTTL);
|
|
102
|
+
return Number.isFinite(ttl) && ttl > 0 ? ttl : 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getChartMaterializeRefresh(chartId) {
|
|
106
|
+
var chart = getChartConfig(chartId);
|
|
107
|
+
if (!chart || !chart.materializeRefresh) return null;
|
|
108
|
+
return String(chart.materializeRefresh);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildQueryCacheKey(sql, params, connection, variables) {
|
|
112
|
+
var role = DQL_CONFIG && DQL_CONFIG.userRole ? String(DQL_CONFIG.userRole) : '';
|
|
113
|
+
var userId = DQL_CONFIG && DQL_CONFIG.userId ? String(DQL_CONFIG.userId) : '';
|
|
114
|
+
var conn = connection ? String(connection) : '';
|
|
115
|
+
return JSON.stringify({
|
|
116
|
+
role: role,
|
|
117
|
+
userId: userId,
|
|
118
|
+
connection: conn,
|
|
119
|
+
sql: sql,
|
|
120
|
+
params: params || [],
|
|
121
|
+
variables: variables || {},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function isNonDeterministicSQL(sql) {
|
|
126
|
+
var text = String(sql || '');
|
|
127
|
+
if (!text) return false;
|
|
128
|
+
return /\\b(now|random|rand)\\s*\\(/i.test(text)
|
|
129
|
+
|| /\\b(current_timestamp|current_date|current_time)\\b/i.test(text);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function isCacheableSQL(sql) {
|
|
133
|
+
var allowNondeterministic = DQL_CONFIG.features && DQL_CONFIG.features.cacheAllowNondeterministic === true;
|
|
134
|
+
if (allowNondeterministic) return true;
|
|
135
|
+
return !isNonDeterministicSQL(sql);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function mergePredicateState() {
|
|
139
|
+
var merged = {};
|
|
140
|
+
|
|
141
|
+
// Drill stack predicates (hierarchical AND semantics)
|
|
142
|
+
if (drillState && Array.isArray(drillState.stack)) {
|
|
143
|
+
for (var i = 0; i < drillState.stack.length; i++) {
|
|
144
|
+
var frame = drillState.stack[i];
|
|
145
|
+
var predicates = frame && frame.predicates ? frame.predicates : {};
|
|
146
|
+
var drillKeys = Object.keys(predicates);
|
|
147
|
+
for (var dk = 0; dk < drillKeys.length; dk++) {
|
|
148
|
+
var drillKey = drillKeys[dk];
|
|
149
|
+
merged[drillKey] = predicates[drillKey];
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Cross-filters merge as OR lists per field across source charts.
|
|
155
|
+
var sourceIds = Object.keys(crossFilterState || {});
|
|
156
|
+
for (var s = 0; s < sourceIds.length; s++) {
|
|
157
|
+
var sourceFilters = crossFilterState[sourceIds[s]] || {};
|
|
158
|
+
var fields = Object.keys(sourceFilters);
|
|
159
|
+
for (var f = 0; f < fields.length; f++) {
|
|
160
|
+
var field = fields[f];
|
|
161
|
+
var values = Array.isArray(sourceFilters[field]) ? sourceFilters[field] : [];
|
|
162
|
+
if (!merged[field]) merged[field] = [];
|
|
163
|
+
for (var v = 0; v < values.length; v++) {
|
|
164
|
+
if (merged[field].indexOf(values[v]) < 0) {
|
|
165
|
+
merged[field].push(values[v]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return merged;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function sanitizePredicateFieldName(field) {
|
|
175
|
+
if (!field) return null;
|
|
176
|
+
var raw = String(field);
|
|
177
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(raw)) return null;
|
|
178
|
+
return raw;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function applyPredicateFiltersToQuery(chartId, baseSQL, baseParams, baseVariables) {
|
|
182
|
+
var mergedPredicates = mergePredicateState();
|
|
183
|
+
var fields = Object.keys(mergedPredicates);
|
|
184
|
+
if (fields.length === 0) {
|
|
185
|
+
return { sql: baseSQL, params: baseParams || [], variables: baseVariables || {} };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
var knownColumns = chartColumns[chartId];
|
|
189
|
+
if (!Array.isArray(knownColumns) || knownColumns.length === 0) {
|
|
190
|
+
return { sql: baseSQL, params: baseParams || [], variables: baseVariables || {} };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
var columnSet = {};
|
|
194
|
+
for (var c = 0; c < knownColumns.length; c++) columnSet[String(knownColumns[c])] = true;
|
|
195
|
+
|
|
196
|
+
var nextParams = (baseParams || []).slice();
|
|
197
|
+
var nextVariables = {};
|
|
198
|
+
var baseVarKeys = Object.keys(baseVariables || {});
|
|
199
|
+
for (var bv = 0; bv < baseVarKeys.length; bv++) {
|
|
200
|
+
var bk = baseVarKeys[bv];
|
|
201
|
+
nextVariables[bk] = baseVariables[bk];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
var maxPosition = 0;
|
|
205
|
+
for (var p = 0; p < nextParams.length; p++) {
|
|
206
|
+
var pos = Number(nextParams[p].position);
|
|
207
|
+
if (Number.isFinite(pos) && pos > maxPosition) maxPosition = pos;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
var whereClauses = [];
|
|
211
|
+
for (var i = 0; i < fields.length; i++) {
|
|
212
|
+
var rawField = fields[i];
|
|
213
|
+
var safeField = sanitizePredicateFieldName(rawField);
|
|
214
|
+
if (!safeField || !columnSet[safeField]) continue;
|
|
215
|
+
|
|
216
|
+
var values = Array.isArray(mergedPredicates[rawField]) ? mergedPredicates[rawField] : [];
|
|
217
|
+
var cleanedValues = [];
|
|
218
|
+
for (var v = 0; v < values.length; v++) {
|
|
219
|
+
if (values[v] === null || values[v] === undefined || values[v] === '') continue;
|
|
220
|
+
if (cleanedValues.indexOf(values[v]) < 0) cleanedValues.push(values[v]);
|
|
221
|
+
}
|
|
222
|
+
if (cleanedValues.length === 0) continue;
|
|
223
|
+
|
|
224
|
+
if (cleanedValues.length === 1) {
|
|
225
|
+
maxPosition++;
|
|
226
|
+
var singleName = '__dql_pred_' + safeField + '_' + maxPosition;
|
|
227
|
+
nextParams.push({ name: singleName, position: maxPosition });
|
|
228
|
+
nextVariables[singleName] = cleanedValues[0];
|
|
229
|
+
whereClauses.push('"' + safeField + '" = $' + maxPosition);
|
|
230
|
+
} else {
|
|
231
|
+
var inPlaceholders = [];
|
|
232
|
+
for (var cv = 0; cv < cleanedValues.length; cv++) {
|
|
233
|
+
maxPosition++;
|
|
234
|
+
var inName = '__dql_pred_' + safeField + '_' + maxPosition;
|
|
235
|
+
nextParams.push({ name: inName, position: maxPosition });
|
|
236
|
+
nextVariables[inName] = cleanedValues[cv];
|
|
237
|
+
inPlaceholders.push('$' + maxPosition);
|
|
238
|
+
}
|
|
239
|
+
whereClauses.push('"' + safeField + '" IN (' + inPlaceholders.join(', ') + ')');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (whereClauses.length === 0) {
|
|
244
|
+
return { sql: baseSQL, params: nextParams, variables: nextVariables };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return {
|
|
248
|
+
sql: 'SELECT * FROM (' + baseSQL + ') __dql_state WHERE ' + whereClauses.join(' AND '),
|
|
249
|
+
params: nextParams,
|
|
250
|
+
variables: nextVariables,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function captureStateSnapshot() {
|
|
255
|
+
return {
|
|
256
|
+
filters: filterState || {},
|
|
257
|
+
crossFilters: crossFilterState || {},
|
|
258
|
+
drillStack: drillState && Array.isArray(drillState.stack) ? drillState.stack : [],
|
|
259
|
+
params: paramActionState || {},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function updateURLState() {
|
|
264
|
+
if (suppressStateSync) return;
|
|
265
|
+
try {
|
|
266
|
+
var snapshot = captureStateSnapshot();
|
|
267
|
+
var encoded = encodeURIComponent(JSON.stringify(snapshot));
|
|
268
|
+
var nextURL = new URL(window.location.href);
|
|
269
|
+
nextURL.searchParams.set('dqlState', encoded);
|
|
270
|
+
history.replaceState(null, '', nextURL.pathname + '?' + nextURL.searchParams.toString());
|
|
271
|
+
} catch (_) {
|
|
272
|
+
// noop
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function restoreURLState() {
|
|
277
|
+
if (restoredState) return;
|
|
278
|
+
restoredState = true;
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
var params = new URLSearchParams(window.location.search);
|
|
282
|
+
var encoded = params.get('dqlState');
|
|
283
|
+
if (!encoded) return;
|
|
284
|
+
var parsed = JSON.parse(decodeURIComponent(encoded));
|
|
285
|
+
if (parsed && typeof parsed === 'object') {
|
|
286
|
+
if (parsed.filters && typeof parsed.filters === 'object') filterState = parsed.filters;
|
|
287
|
+
if (parsed.crossFilters && typeof parsed.crossFilters === 'object') crossFilterState = parsed.crossFilters;
|
|
288
|
+
if (parsed.params && typeof parsed.params === 'object') paramActionState = parsed.params;
|
|
289
|
+
if (Array.isArray(parsed.drillStack)) {
|
|
290
|
+
drillState.stack = parsed.drillStack.filter(function(frame) {
|
|
291
|
+
return frame && typeof frame === 'object' && frame.predicates;
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
} catch (_) {
|
|
296
|
+
// noop
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function clearHierarchyExpandArtifacts(chartId) {
|
|
301
|
+
var selector = chartId
|
|
302
|
+
? '.dql-hierarchy-expand-detail[data-chart-id="' + chartId + '"]'
|
|
303
|
+
: '.dql-hierarchy-expand-detail';
|
|
304
|
+
var nodes = document.querySelectorAll(selector);
|
|
305
|
+
for (var i = 0; i < nodes.length; i++) {
|
|
306
|
+
var node = nodes[i];
|
|
307
|
+
if (node && node.parentElement) node.parentElement.removeChild(node);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updateInlineDrillControls() {
|
|
312
|
+
var controls = document.querySelectorAll('.dql-inline-drill-up');
|
|
313
|
+
var activeCharts = {};
|
|
314
|
+
if (drillState && Array.isArray(drillState.stack)) {
|
|
315
|
+
for (var i = 0; i < drillState.stack.length; i++) {
|
|
316
|
+
var frame = drillState.stack[i];
|
|
317
|
+
if (!frame || frame.kind !== 'hierarchy' || frame.mode === 'modal') continue;
|
|
318
|
+
if (frame.chartId) activeCharts[frame.chartId] = true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
for (var c = 0; c < controls.length; c++) {
|
|
323
|
+
var control = controls[c];
|
|
324
|
+
var chartId = control.getAttribute('data-chart-id');
|
|
325
|
+
if (chartId && activeCharts[chartId]) {
|
|
326
|
+
control.style.display = 'inline-flex';
|
|
327
|
+
} else {
|
|
328
|
+
control.style.display = 'none';
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function updateDrillHeader() {
|
|
334
|
+
var titleEl = document.getElementById('dql-drill-title');
|
|
335
|
+
var stackEl = document.getElementById('dql-drill-stack');
|
|
336
|
+
var upBtn = document.getElementById('dql-drill-up');
|
|
337
|
+
if (!titleEl) {
|
|
338
|
+
if (!drillState.stack || drillState.stack.length === 0) {
|
|
339
|
+
clearHierarchyExpandArtifacts();
|
|
340
|
+
}
|
|
341
|
+
updateInlineDrillControls();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!drillState.stack || drillState.stack.length === 0) {
|
|
346
|
+
titleEl.textContent = 'Detail View';
|
|
347
|
+
if (stackEl) stackEl.textContent = '';
|
|
348
|
+
if (upBtn) upBtn.setAttribute('disabled', 'true');
|
|
349
|
+
clearHierarchyExpandArtifacts();
|
|
350
|
+
updateInlineDrillControls();
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
var labels = drillState.stack.map(function(frame) {
|
|
355
|
+
return frame.label || frame.chartId || 'detail';
|
|
356
|
+
});
|
|
357
|
+
titleEl.textContent = 'Detail View - ' + labels[labels.length - 1];
|
|
358
|
+
if (stackEl) stackEl.textContent = labels.join(' > ');
|
|
359
|
+
if (upBtn) upBtn.removeAttribute('disabled');
|
|
360
|
+
updateInlineDrillControls();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildPredicateMapFromDatum(datum) {
|
|
364
|
+
var map = {};
|
|
365
|
+
if (!datum || typeof datum !== 'object') return map;
|
|
366
|
+
var keys = Object.keys(datum);
|
|
367
|
+
for (var i = 0; i < keys.length; i++) {
|
|
368
|
+
var key = keys[i];
|
|
369
|
+
if (key === '_vgsid_' || key.indexOf('__') === 0) continue;
|
|
370
|
+
var safeKey = sanitizePredicateFieldName(key);
|
|
371
|
+
if (!safeKey) continue;
|
|
372
|
+
var value = datum[key];
|
|
373
|
+
if (value === null || value === undefined || value === '') continue;
|
|
374
|
+
map[safeKey] = [value];
|
|
375
|
+
}
|
|
376
|
+
return map;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
async function fetchQueryData(chartId, sql, params, connection, variables) {
|
|
380
|
+
try {
|
|
381
|
+
var runtimeCacheEnabled = !DQL_CONFIG.features || DQL_CONFIG.features.runtimeCacheEnabled !== false;
|
|
382
|
+
var cacheTTL = runtimeCacheEnabled ? getChartCacheTTL(chartId) : 0;
|
|
383
|
+
var canUseCache = cacheTTL > 0 && isCacheableSQL(sql);
|
|
384
|
+
var cacheKey = null;
|
|
385
|
+
if (canUseCache) {
|
|
386
|
+
cacheKey = buildQueryCacheKey(sql, params, connection, variables);
|
|
387
|
+
var cached = queryCache[cacheKey];
|
|
388
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
389
|
+
return cached.result;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
var payload = { sql: sql, sqlParams: params || [], variables: variables || {} };
|
|
394
|
+
if (connection) payload.connectionId = connection;
|
|
395
|
+
var materializationEnabled = !DQL_CONFIG.features || DQL_CONFIG.features.materializationEnabled !== false;
|
|
396
|
+
var materializeRefresh = materializationEnabled ? getChartMaterializeRefresh(chartId) : null;
|
|
397
|
+
if (materializeRefresh) {
|
|
398
|
+
payload.materialize = {
|
|
399
|
+
chartId: chartId,
|
|
400
|
+
refreshPolicy: materializeRefresh,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const response = await fetch(DQL_CONFIG.apiEndpoint, {
|
|
404
|
+
method: 'POST',
|
|
405
|
+
headers: { 'Content-Type': 'application/json' },
|
|
406
|
+
body: JSON.stringify(payload),
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
if (!response.ok) {
|
|
410
|
+
throw new Error('Query failed: ' + response.statusText);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
var parsed = await response.json();
|
|
414
|
+
if (canUseCache && cacheKey) {
|
|
415
|
+
queryCache[cacheKey] = {
|
|
416
|
+
result: parsed,
|
|
417
|
+
expiresAt: Date.now() + (cacheTTL * 1000),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
return parsed;
|
|
421
|
+
} catch (error) {
|
|
422
|
+
console.error('DQL query error for ' + chartId + ':', error);
|
|
423
|
+
return { columns: [], rows: [], error: error.message };
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function renderVegaLiteChart(chartId, spec, data) {
|
|
428
|
+
var container = document.getElementById(chartId);
|
|
429
|
+
if (!container) {
|
|
430
|
+
console.error('[DQL] Container not found for ' + chartId);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Check if Vega libraries loaded
|
|
435
|
+
if (typeof vegaEmbed === 'undefined') {
|
|
436
|
+
container.innerHTML = '<div class="dql-error">Vega-Lite library failed to load. Check your internet connection.</div>';
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (!data || data.length === 0) {
|
|
441
|
+
container.innerHTML = '<div class="dql-loading">No data returned</div>';
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
// Deep clone spec to avoid mutating the original
|
|
447
|
+
var renderSpec = JSON.parse(JSON.stringify(spec));
|
|
448
|
+
renderSpec.data = { values: data };
|
|
449
|
+
|
|
450
|
+
// Replace "container" width with actual pixel width
|
|
451
|
+
if (renderSpec.width === 'container' || !renderSpec.width) {
|
|
452
|
+
var containerWidth = container.clientWidth - 32; // account for padding
|
|
453
|
+
renderSpec.width = Math.max(containerWidth, 200);
|
|
454
|
+
}
|
|
455
|
+
if (!renderSpec.height) {
|
|
456
|
+
renderSpec.height = 300;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
var result = await vegaEmbed('#' + chartId, renderSpec, {
|
|
460
|
+
actions: false,
|
|
461
|
+
renderer: 'svg',
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
// Store the view for signal handling
|
|
465
|
+
vegaViews[chartId] = result.view;
|
|
466
|
+
|
|
467
|
+
// Setup interaction handlers if configured
|
|
468
|
+
var interaction = DQL_CONFIG.interactions && DQL_CONFIG.interactions[chartId];
|
|
469
|
+
if (interaction) {
|
|
470
|
+
setupChartInteractions(chartId, result.view, interaction, data);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
console.log('[DQL] Rendered ' + chartId + ' with ' + data.length + ' rows');
|
|
474
|
+
} catch (error) {
|
|
475
|
+
console.error('[DQL] Chart render error for ' + chartId + ':', error);
|
|
476
|
+
container.innerHTML = '<div class="dql-error">Chart error: ' + (error.message || String(error)) + '</div>';
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function renderKPICard(chartId, data) {
|
|
481
|
+
var container = document.getElementById(chartId);
|
|
482
|
+
if (!container || !data || data.length === 0) return;
|
|
483
|
+
|
|
484
|
+
var row = data[0];
|
|
485
|
+
var keys = Object.keys(row);
|
|
486
|
+
var html = '<div style="display:flex;flex-wrap:wrap;gap:24px;padding:8px;">';
|
|
487
|
+
|
|
488
|
+
for (var i = 0; i < keys.length; i++) {
|
|
489
|
+
var label = keys[i].replace(/_/g, ' ');
|
|
490
|
+
var value = row[keys[i]];
|
|
491
|
+
if (typeof value === 'number') {
|
|
492
|
+
value = value.toLocaleString();
|
|
493
|
+
}
|
|
494
|
+
html += '<div style="flex:1;min-width:120px;text-align:center;">' +
|
|
495
|
+
'<div style="font-size:32px;font-weight:700;color:#4C78A8;">' + escapeHTMLRuntime(String(value)) + '</div>' +
|
|
496
|
+
'<div style="font-size:12px;text-transform:uppercase;letter-spacing:0.5px;margin-top:4px;opacity:0.7;">' + escapeHTMLRuntime(label) + '</div>' +
|
|
497
|
+
'</div>';
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
html += '</div>';
|
|
501
|
+
container.innerHTML = html;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Table pagination state per chart
|
|
505
|
+
var tablePages = {};
|
|
506
|
+
|
|
507
|
+
function renderTable(chartId, data) {
|
|
508
|
+
var container = document.getElementById(chartId);
|
|
509
|
+
if (!container || !data || data.length === 0) {
|
|
510
|
+
if (container) container.innerHTML = '<div class="dql-loading">No data</div>';
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
var colNames = Object.keys(data[0]);
|
|
515
|
+
var pageSize = 25;
|
|
516
|
+
var currentPage = tablePages[chartId] || 0;
|
|
517
|
+
var totalPages = Math.ceil(data.length / pageSize);
|
|
518
|
+
if (currentPage >= totalPages) currentPage = totalPages - 1;
|
|
519
|
+
var startRow = currentPage * pageSize;
|
|
520
|
+
var endRow = Math.min(startRow + pageSize, data.length);
|
|
521
|
+
|
|
522
|
+
// CSV export button
|
|
523
|
+
var html = '<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">';
|
|
524
|
+
html += '<button class="dql-export-csv" data-chart="' + chartId + '" style="padding:4px 12px;font-size:12px;cursor:pointer;border:1px solid var(--dql-border, #ddd);border-radius:4px;background:var(--dql-card-bg, #fff);color:var(--dql-fg, #333);">Export CSV</button>';
|
|
525
|
+
html += '</div>';
|
|
526
|
+
|
|
527
|
+
html += '<table class="dql-drill-table" style="width:100%"><thead><tr>';
|
|
528
|
+
for (var i = 0; i < colNames.length; i++) {
|
|
529
|
+
var label = colNames[i].replace(/_/g, ' ');
|
|
530
|
+
html += '<th style="cursor:pointer;" data-col="' + colNames[i] + '">' + escapeHTMLRuntime(label) + '</th>';
|
|
531
|
+
}
|
|
532
|
+
html += '</tr></thead><tbody>';
|
|
533
|
+
|
|
534
|
+
for (var r = startRow; r < endRow; r++) {
|
|
535
|
+
html += '<tr>';
|
|
536
|
+
for (var c = 0; c < colNames.length; c++) {
|
|
537
|
+
var val = data[r][colNames[c]];
|
|
538
|
+
if (val === null || val === undefined) val = '';
|
|
539
|
+
else if (typeof val === 'number') val = val.toLocaleString();
|
|
540
|
+
else val = String(val);
|
|
541
|
+
html += '<td>' + escapeHTMLRuntime(val) + '</td>';
|
|
542
|
+
}
|
|
543
|
+
html += '</tr>';
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
html += '</tbody></table>';
|
|
547
|
+
|
|
548
|
+
// Pagination controls
|
|
549
|
+
if (totalPages > 1) {
|
|
550
|
+
html += '<div style="display:flex;align-items:center;justify-content:space-between;margin-top:12px;font-size:13px;">';
|
|
551
|
+
html += '<span style="opacity:0.7;">Showing ' + (startRow + 1) + '–' + endRow + ' of ' + data.length + ' rows</span>';
|
|
552
|
+
html += '<div>';
|
|
553
|
+
html += '<button class="dql-page-btn" data-chart="' + chartId + '" data-page="' + (currentPage - 1) + '"' + (currentPage === 0 ? ' disabled' : '') + ' style="padding:4px 10px;margin-right:4px;cursor:pointer;border:1px solid var(--dql-border, #ddd);border-radius:4px;background:var(--dql-card-bg, #fff);color:var(--dql-fg, #333);">Prev</button>';
|
|
554
|
+
html += '<span style="margin:0 8px;">Page ' + (currentPage + 1) + ' / ' + totalPages + '</span>';
|
|
555
|
+
html += '<button class="dql-page-btn" data-chart="' + chartId + '" data-page="' + (currentPage + 1) + '"' + (currentPage >= totalPages - 1 ? ' disabled' : '') + ' style="padding:4px 10px;margin-left:4px;cursor:pointer;border:1px solid var(--dql-border, #ddd);border-radius:4px;background:var(--dql-card-bg, #fff);color:var(--dql-fg, #333);">Next</button>';
|
|
556
|
+
html += '</div></div>';
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
container.innerHTML = html;
|
|
560
|
+
|
|
561
|
+
// Store data reference for pagination and export
|
|
562
|
+
container._dqlTableData = data;
|
|
563
|
+
|
|
564
|
+
// Bind pagination buttons
|
|
565
|
+
var pageBtns = container.querySelectorAll('.dql-page-btn');
|
|
566
|
+
for (var p = 0; p < pageBtns.length; p++) {
|
|
567
|
+
pageBtns[p].addEventListener('click', function(e) {
|
|
568
|
+
var btn = e.currentTarget;
|
|
569
|
+
var cid = btn.getAttribute('data-chart');
|
|
570
|
+
var page = parseInt(btn.getAttribute('data-page'), 10);
|
|
571
|
+
if (isNaN(page) || page < 0) return;
|
|
572
|
+
tablePages[cid] = page;
|
|
573
|
+
var el = document.getElementById(cid);
|
|
574
|
+
if (el && el._dqlTableData) renderTable(cid, el._dqlTableData);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Bind CSV export button
|
|
579
|
+
var exportBtn = container.querySelector('.dql-export-csv');
|
|
580
|
+
if (exportBtn) {
|
|
581
|
+
exportBtn.addEventListener('click', function() {
|
|
582
|
+
exportTableCSV(chartId, colNames, data);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function exportTableCSV(chartId, colNames, data) {
|
|
588
|
+
var csv = colNames.join(',') + '\\n';
|
|
589
|
+
for (var r = 0; r < data.length; r++) {
|
|
590
|
+
var row = [];
|
|
591
|
+
for (var c = 0; c < colNames.length; c++) {
|
|
592
|
+
var val = data[r][colNames[c]];
|
|
593
|
+
if (val === null || val === undefined) val = '';
|
|
594
|
+
else val = String(val);
|
|
595
|
+
// Escape quotes and wrap in quotes if contains comma/quote/newline
|
|
596
|
+
if (val.indexOf(',') >= 0 || val.indexOf('"') >= 0 || val.indexOf('\\n') >= 0) {
|
|
597
|
+
val = '"' + val.replace(/"/g, '""') + '"';
|
|
598
|
+
}
|
|
599
|
+
row.push(val);
|
|
600
|
+
}
|
|
601
|
+
csv += row.join(',') + '\\n';
|
|
602
|
+
}
|
|
603
|
+
var blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
|
604
|
+
var url = URL.createObjectURL(blob);
|
|
605
|
+
var a = document.createElement('a');
|
|
606
|
+
a.href = url;
|
|
607
|
+
a.download = chartId + '.csv';
|
|
608
|
+
a.click();
|
|
609
|
+
URL.revokeObjectURL(url);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
// ---- Interaction Handlers ----
|
|
613
|
+
|
|
614
|
+
function setupChartInteractions(chartId, view, interaction, data) {
|
|
615
|
+
// Listen for click signals from Vega-Lite selection
|
|
616
|
+
view.addEventListener('click', function(event, item) {
|
|
617
|
+
if (!item || !item.datum) return;
|
|
618
|
+
|
|
619
|
+
var datum = item.datum;
|
|
620
|
+
var chartConfig = getChartConfig(chartId);
|
|
621
|
+
var hasHierarchyDrill = chartConfig && chartConfig.drillConfig;
|
|
622
|
+
|
|
623
|
+
if (interaction.onClick) {
|
|
624
|
+
handleOnClick(interaction.onClick, datum, chartId);
|
|
625
|
+
} else if (hasHierarchyDrill || interaction.drillDown) {
|
|
626
|
+
// Default click behavior for drill-capable charts.
|
|
627
|
+
handleOnClick('drill_down', datum, chartId);
|
|
628
|
+
} else if (interaction.linkTo) {
|
|
629
|
+
handleOnClick('link', datum, chartId);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Filter-by: listen for selection changes to filter other charts
|
|
634
|
+
if (interaction.filterBy) {
|
|
635
|
+
setupFilterByInteraction(chartId, view, interaction.filterBy, data);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function setupFilterByInteraction(chartId, view, filterBy, data) {
|
|
640
|
+
var fields = Array.isArray(filterBy) ? filterBy : [filterBy];
|
|
641
|
+
|
|
642
|
+
// Listen for the dql_filter selection signal
|
|
643
|
+
view.addSignalListener('dql_filter', function(name, value) {
|
|
644
|
+
var selectedValues = {};
|
|
645
|
+
if (value && value.vlPoint && value.vlPoint.or) {
|
|
646
|
+
for (var i = 0; i < value.vlPoint.or.length; i++) {
|
|
647
|
+
var point = value.vlPoint.or[i];
|
|
648
|
+
for (var j = 0; j < fields.length; j++) {
|
|
649
|
+
var field = fields[j];
|
|
650
|
+
if (point[field] !== undefined && point[field] !== null && point[field] !== '') {
|
|
651
|
+
if (!selectedValues[field]) selectedValues[field] = [];
|
|
652
|
+
if (selectedValues[field].indexOf(point[field]) < 0) {
|
|
653
|
+
selectedValues[field].push(point[field]);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
applyCrossChartFilter(chartId, selectedValues);
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function applyCrossChartFilter(sourceChartId, selectedValues) {
|
|
665
|
+
var keys = Object.keys(selectedValues || {});
|
|
666
|
+
if (keys.length === 0) {
|
|
667
|
+
delete crossFilterState[sourceChartId];
|
|
668
|
+
} else {
|
|
669
|
+
crossFilterState[sourceChartId] = selectedValues;
|
|
670
|
+
}
|
|
671
|
+
updateURLState();
|
|
672
|
+
refreshDashboardWithFilters();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function sanitizeDrillMode(mode) {
|
|
676
|
+
if (mode === 'replace' || mode === 'expand' || mode === 'modal') return mode;
|
|
677
|
+
return 'modal';
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function toRollupSQL(rollup, metricField) {
|
|
681
|
+
if (!metricField) return 'COUNT(*)';
|
|
682
|
+
switch (rollup) {
|
|
683
|
+
case 'count':
|
|
684
|
+
return 'COUNT(' + metricField + ')';
|
|
685
|
+
case 'count_distinct':
|
|
686
|
+
return 'COUNT(DISTINCT ' + metricField + ')';
|
|
687
|
+
case 'avg':
|
|
688
|
+
return 'AVG(' + metricField + ')';
|
|
689
|
+
case 'min':
|
|
690
|
+
return 'MIN(' + metricField + ')';
|
|
691
|
+
case 'max':
|
|
692
|
+
return 'MAX(' + metricField + ')';
|
|
693
|
+
case 'none':
|
|
694
|
+
return metricField;
|
|
695
|
+
case 'sum':
|
|
696
|
+
default:
|
|
697
|
+
return 'SUM(' + metricField + ')';
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
function buildHierarchyDrillRequest(chartId, datum) {
|
|
702
|
+
var chartConfig = getChartConfig(chartId);
|
|
703
|
+
if (!chartConfig || !chartConfig.drillConfig) return null;
|
|
704
|
+
|
|
705
|
+
var drillConfig = chartConfig.drillConfig;
|
|
706
|
+
var levels = Array.isArray(drillConfig.levels) ? drillConfig.levels : [];
|
|
707
|
+
if (levels.length === 0) return null;
|
|
708
|
+
|
|
709
|
+
var sameChartFrames = (drillState.stack || []).filter(function(frame) {
|
|
710
|
+
return frame && frame.kind === 'hierarchy' && frame.chartId === chartId;
|
|
711
|
+
});
|
|
712
|
+
var topFrame = sameChartFrames.length > 0 ? sameChartFrames[sameChartFrames.length - 1] : null;
|
|
713
|
+
|
|
714
|
+
var baseIndex = 0;
|
|
715
|
+
if (topFrame && Number.isFinite(topFrame.hierarchyLevelIndex)) {
|
|
716
|
+
baseIndex = topFrame.hierarchyLevelIndex + 1;
|
|
717
|
+
} else if (chartConfig.config && chartConfig.config.x) {
|
|
718
|
+
for (var li = 0; li < levels.length; li++) {
|
|
719
|
+
if (levels[li].dimension === chartConfig.config.x) {
|
|
720
|
+
baseIndex = li + 1;
|
|
721
|
+
break;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (baseIndex >= levels.length) return null;
|
|
727
|
+
var nextLevel = levels[baseIndex];
|
|
728
|
+
|
|
729
|
+
var predicates = {};
|
|
730
|
+
for (var i = 0; i < sameChartFrames.length; i++) {
|
|
731
|
+
var framePredicates = sameChartFrames[i].predicates || {};
|
|
732
|
+
var fields = Object.keys(framePredicates);
|
|
733
|
+
for (var f = 0; f < fields.length; f++) {
|
|
734
|
+
predicates[fields[f]] = framePredicates[fields[f]];
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
var previousLevel = levels[Math.max(0, baseIndex - 1)];
|
|
739
|
+
var currentField = previousLevel ? previousLevel.dimension : (chartConfig.config && chartConfig.config.x);
|
|
740
|
+
if (currentField && datum && datum[currentField] !== undefined) {
|
|
741
|
+
predicates[currentField] = [datum[currentField]];
|
|
742
|
+
} else {
|
|
743
|
+
var datumPredicates = buildPredicateMapFromDatum(datum || {});
|
|
744
|
+
var keys = Object.keys(datumPredicates);
|
|
745
|
+
for (var dk = 0; dk < keys.length; dk++) {
|
|
746
|
+
predicates[keys[dk]] = datumPredicates[keys[dk]];
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var metricField = chartConfig.config && chartConfig.config.y ? chartConfig.config.y : null;
|
|
751
|
+
var rollup = drillConfig.rollup || 'sum';
|
|
752
|
+
|
|
753
|
+
return {
|
|
754
|
+
hierarchy: drillConfig.hierarchy,
|
|
755
|
+
path: drillConfig.path || null,
|
|
756
|
+
mode: sanitizeDrillMode(drillConfig.mode || 'modal'),
|
|
757
|
+
levelIndex: baseIndex,
|
|
758
|
+
nextDimension: nextLevel.dimension,
|
|
759
|
+
metricField: metricField,
|
|
760
|
+
rollup: rollup,
|
|
761
|
+
rollupSQL: toRollupSQL(rollup, metricField),
|
|
762
|
+
baseSQL: chartConfig.sql,
|
|
763
|
+
baseParams: chartConfig.params || [],
|
|
764
|
+
predicates: predicates,
|
|
765
|
+
connectionId: chartConfig.connection || undefined,
|
|
766
|
+
variables: computeVariables(),
|
|
767
|
+
};
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
async function fetchHierarchyDrill(chartId, request) {
|
|
771
|
+
try {
|
|
772
|
+
var response = await fetch(DQL_CONFIG.apiEndpoint.replace('/query', '/drill/hierarchy'), {
|
|
773
|
+
method: 'POST',
|
|
774
|
+
headers: { 'Content-Type': 'application/json' },
|
|
775
|
+
body: JSON.stringify({
|
|
776
|
+
chartId: chartId,
|
|
777
|
+
baseSQL: request.baseSQL,
|
|
778
|
+
baseParams: request.baseParams || [],
|
|
779
|
+
variables: request.variables || {},
|
|
780
|
+
predicates: request.predicates || {},
|
|
781
|
+
nextDimension: request.nextDimension,
|
|
782
|
+
metricField: request.metricField,
|
|
783
|
+
rollup: request.rollup,
|
|
784
|
+
connectionId: request.connectionId,
|
|
785
|
+
}),
|
|
786
|
+
});
|
|
787
|
+
if (!response.ok) {
|
|
788
|
+
throw new Error('Hierarchy drill query failed: ' + response.statusText);
|
|
789
|
+
}
|
|
790
|
+
return await response.json();
|
|
791
|
+
} catch (error) {
|
|
792
|
+
console.error('DQL hierarchy drill error for ' + chartId + ':', error);
|
|
793
|
+
return { columns: [], rows: [], error: error.message };
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
function openDrillModalWithResult(title, result) {
|
|
798
|
+
var modal = document.getElementById('dql-drill-modal');
|
|
799
|
+
var titleEl = document.getElementById('dql-drill-title');
|
|
800
|
+
var bodyEl = document.getElementById('dql-drill-body');
|
|
801
|
+
if (!modal || !titleEl || !bodyEl) return;
|
|
802
|
+
|
|
803
|
+
titleEl.textContent = title;
|
|
804
|
+
modal.classList.add('active');
|
|
805
|
+
if (result.error) {
|
|
806
|
+
bodyEl.innerHTML = '<div class="dql-error">' + escapeHTMLRuntime(result.error) + '</div>';
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (!result.rows || result.rows.length === 0) {
|
|
810
|
+
bodyEl.innerHTML = '<div class="dql-loading">No data found</div>';
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
bodyEl.innerHTML = renderDrillTable(result.columns, result.rows);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
async function renderHierarchyInPlace(chartId, result, nextDimension, metricField, mode) {
|
|
817
|
+
var container = document.getElementById(chartId);
|
|
818
|
+
if (!container) return;
|
|
819
|
+
|
|
820
|
+
if (result.error) {
|
|
821
|
+
container.innerHTML = '<div class="dql-error">' + escapeHTMLRuntime(result.error) + '</div>';
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
var baseSpec = DQL_CONFIG.vegaLiteSpecs && DQL_CONFIG.vegaLiteSpecs[chartId];
|
|
826
|
+
if (!baseSpec) {
|
|
827
|
+
container.innerHTML = renderDrillTable(result.columns, result.rows || []);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
var spec = JSON.parse(JSON.stringify(baseSpec));
|
|
832
|
+
spec.data = { values: result.rows || [] };
|
|
833
|
+
if (spec.encoding && spec.encoding.x) {
|
|
834
|
+
spec.encoding.x.field = nextDimension;
|
|
835
|
+
if (spec.encoding.x.axis && typeof spec.encoding.x.axis === 'object') {
|
|
836
|
+
spec.encoding.x.axis.title = nextDimension;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (metricField && spec.encoding && spec.encoding.y) {
|
|
840
|
+
spec.encoding.y.field = metricField;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
try {
|
|
844
|
+
var rendered = await vegaEmbed('#' + chartId, spec, { actions: false, renderer: 'svg' });
|
|
845
|
+
vegaViews[chartId] = rendered.view;
|
|
846
|
+
if (mode === 'expand') {
|
|
847
|
+
var detailId = chartId + '-hierarchy-expand';
|
|
848
|
+
var detailEl = document.getElementById(detailId);
|
|
849
|
+
if (!detailEl) {
|
|
850
|
+
detailEl = document.createElement('div');
|
|
851
|
+
detailEl.id = detailId;
|
|
852
|
+
detailEl.className = 'dql-hierarchy-expand-detail';
|
|
853
|
+
detailEl.setAttribute('data-chart-id', chartId);
|
|
854
|
+
detailEl.style.marginTop = '12px';
|
|
855
|
+
container.parentElement.appendChild(detailEl);
|
|
856
|
+
}
|
|
857
|
+
detailEl.innerHTML = renderDrillTable(result.columns, result.rows || []);
|
|
858
|
+
} else {
|
|
859
|
+
clearHierarchyExpandArtifacts(chartId);
|
|
860
|
+
}
|
|
861
|
+
} catch (error) {
|
|
862
|
+
container.innerHTML = '<div class="dql-error">Chart error: ' + escapeHTMLRuntime(error.message || String(error)) + '</div>';
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function handleHierarchyDrill(chartId, datum, interaction) {
|
|
867
|
+
var request = buildHierarchyDrillRequest(chartId, datum);
|
|
868
|
+
if (!request) {
|
|
869
|
+
if (interaction && interaction.drillDown) {
|
|
870
|
+
await handleDrillDown(chartId, interaction.drillDown, datum);
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
return false;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
var cleanDatum = {};
|
|
877
|
+
var datumKeys = Object.keys(datum || {});
|
|
878
|
+
for (var i = 0; i < datumKeys.length; i++) {
|
|
879
|
+
var key = datumKeys[i];
|
|
880
|
+
if (key !== '_vgsid_' && key.indexOf('__') !== 0) cleanDatum[key] = datum[key];
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
var result = await fetchHierarchyDrill(chartId, request);
|
|
884
|
+
if (result.error && interaction && interaction.drillDown) {
|
|
885
|
+
// Fallback to legacy drill SQL when hierarchy planning cannot execute.
|
|
886
|
+
await handleDrillDown(chartId, interaction.drillDown, datum);
|
|
887
|
+
return true;
|
|
888
|
+
}
|
|
889
|
+
if (result.error) return false;
|
|
890
|
+
|
|
891
|
+
drillState.stack.push({
|
|
892
|
+
kind: 'hierarchy',
|
|
893
|
+
chartId: chartId,
|
|
894
|
+
mode: request.mode,
|
|
895
|
+
hierarchyLevelIndex: request.levelIndex,
|
|
896
|
+
label: request.nextDimension + ': ' + (getClickedLabel(cleanDatum) || String(getClickedValue(cleanDatum) || chartId)),
|
|
897
|
+
predicates: request.predicates || {},
|
|
898
|
+
hierarchyRequest: request,
|
|
899
|
+
datum: cleanDatum,
|
|
900
|
+
});
|
|
901
|
+
updateDrillHeader();
|
|
902
|
+
updateURLState();
|
|
903
|
+
|
|
904
|
+
if (request.mode === 'modal') {
|
|
905
|
+
openDrillModalWithResult('Detail View - ' + request.nextDimension, result);
|
|
906
|
+
} else {
|
|
907
|
+
await renderHierarchyInPlace(chartId, result, request.nextDimension, request.metricField, request.mode);
|
|
908
|
+
}
|
|
909
|
+
refreshDashboardWithFilters();
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// ---- Drill-Down ----
|
|
914
|
+
|
|
915
|
+
async function handleDrillDown(chartId, drillDownSQL, datum) {
|
|
916
|
+
var modal = document.getElementById('dql-drill-modal');
|
|
917
|
+
var titleEl = document.getElementById('dql-drill-title');
|
|
918
|
+
var bodyEl = document.getElementById('dql-drill-body');
|
|
919
|
+
|
|
920
|
+
if (!modal || !bodyEl) return;
|
|
921
|
+
|
|
922
|
+
var cleanDatum = {};
|
|
923
|
+
var datumKeys = Object.keys(datum || {});
|
|
924
|
+
for (var dk = 0; dk < datumKeys.length; dk++) {
|
|
925
|
+
var datumKey = datumKeys[dk];
|
|
926
|
+
if (datumKey !== '_vgsid_' && datumKey.indexOf('__') !== 0) {
|
|
927
|
+
cleanDatum[datumKey] = datum[datumKey];
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
drillState.stack.push({
|
|
932
|
+
kind: 'legacy',
|
|
933
|
+
chartId: chartId,
|
|
934
|
+
mode: 'modal',
|
|
935
|
+
label: getClickedLabel(cleanDatum) || String(getClickedValue(cleanDatum) || chartId),
|
|
936
|
+
predicates: buildPredicateMapFromDatum(cleanDatum),
|
|
937
|
+
templateSQL: drillDownSQL,
|
|
938
|
+
datum: cleanDatum,
|
|
939
|
+
});
|
|
940
|
+
updateDrillHeader();
|
|
941
|
+
updateURLState();
|
|
942
|
+
|
|
943
|
+
// Show modal with loading state
|
|
944
|
+
titleEl.textContent = 'Detail View - ' + (getClickedLabel(cleanDatum) || chartId);
|
|
945
|
+
bodyEl.innerHTML = '<div class="dql-loading">Loading details...</div>';
|
|
946
|
+
modal.classList.add('active');
|
|
947
|
+
|
|
948
|
+
// Send template SQL + datum to server for safe parameterized interpolation
|
|
949
|
+
var result = await fetchDrillDown(chartId, drillDownSQL, cleanDatum);
|
|
950
|
+
|
|
951
|
+
if (result.error) {
|
|
952
|
+
bodyEl.innerHTML = '<div class="dql-error">' + escapeHTMLRuntime(result.error) + '</div>';
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (!result.rows || result.rows.length === 0) {
|
|
957
|
+
bodyEl.innerHTML = '<div class="dql-loading">No data found</div>';
|
|
958
|
+
return;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Render results as a table
|
|
962
|
+
bodyEl.innerHTML = renderDrillTable(result.columns, result.rows);
|
|
963
|
+
|
|
964
|
+
// Refresh all charts to apply query-level drill predicates.
|
|
965
|
+
refreshDashboardWithFilters();
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
async function drillUp() {
|
|
969
|
+
if (!drillState.stack || drillState.stack.length === 0) return;
|
|
970
|
+
drillState.stack.pop();
|
|
971
|
+
updateDrillHeader();
|
|
972
|
+
updateURLState();
|
|
973
|
+
|
|
974
|
+
if (!drillState.stack || drillState.stack.length === 0) {
|
|
975
|
+
var modal = document.getElementById('dql-drill-modal');
|
|
976
|
+
if (modal) modal.classList.remove('active');
|
|
977
|
+
} else {
|
|
978
|
+
var top = drillState.stack[drillState.stack.length - 1];
|
|
979
|
+
if (top.kind === 'hierarchy' && top.hierarchyRequest) {
|
|
980
|
+
var hierarchyResult = await fetchHierarchyDrill(top.chartId, top.hierarchyRequest);
|
|
981
|
+
if (top.mode === 'modal') {
|
|
982
|
+
openDrillModalWithResult('Detail View - ' + (top.label || top.chartId), hierarchyResult);
|
|
983
|
+
} else {
|
|
984
|
+
await renderHierarchyInPlace(
|
|
985
|
+
top.chartId,
|
|
986
|
+
hierarchyResult,
|
|
987
|
+
top.hierarchyRequest.nextDimension,
|
|
988
|
+
top.hierarchyRequest.metricField,
|
|
989
|
+
top.mode || 'replace',
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
} else {
|
|
993
|
+
var bodyEl = document.getElementById('dql-drill-body');
|
|
994
|
+
if (bodyEl) bodyEl.innerHTML = '<div class="dql-loading">Loading details...</div>';
|
|
995
|
+
var result = await fetchDrillDown(top.chartId, top.templateSQL, top.datum || {});
|
|
996
|
+
if (bodyEl) {
|
|
997
|
+
if (result.error) {
|
|
998
|
+
bodyEl.innerHTML = '<div class="dql-error">' + escapeHTMLRuntime(result.error) + '</div>';
|
|
999
|
+
} else {
|
|
1000
|
+
bodyEl.innerHTML = renderDrillTable(result.columns, result.rows || []);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
refreshDashboardWithFilters();
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
async function fetchDrillDown(chartId, templateSQL, datum) {
|
|
1010
|
+
try {
|
|
1011
|
+
// Clean datum: remove Vega internal fields
|
|
1012
|
+
var cleanDatum = {};
|
|
1013
|
+
var keys = Object.keys(datum);
|
|
1014
|
+
for (var i = 0; i < keys.length; i++) {
|
|
1015
|
+
var k = keys[i];
|
|
1016
|
+
if (k !== '_vgsid_' && !k.startsWith('__')) {
|
|
1017
|
+
cleanDatum[k] = datum[k];
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
var chartConfig = null;
|
|
1022
|
+
if (DQL_CONFIG && Array.isArray(DQL_CONFIG.charts)) {
|
|
1023
|
+
for (var ci = 0; ci < DQL_CONFIG.charts.length; ci++) {
|
|
1024
|
+
if (DQL_CONFIG.charts[ci].id === chartId) {
|
|
1025
|
+
chartConfig = DQL_CONFIG.charts[ci];
|
|
1026
|
+
break;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
var response = await fetch(DQL_CONFIG.apiEndpoint.replace('/query', '/drill'), {
|
|
1032
|
+
method: 'POST',
|
|
1033
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1034
|
+
body: JSON.stringify({
|
|
1035
|
+
chartId: chartId,
|
|
1036
|
+
templateSQL: templateSQL,
|
|
1037
|
+
datum: cleanDatum,
|
|
1038
|
+
connectionId: chartConfig && chartConfig.connection ? chartConfig.connection : undefined
|
|
1039
|
+
}),
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
if (!response.ok) {
|
|
1043
|
+
throw new Error('Drill-down query failed: ' + response.statusText);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return await response.json();
|
|
1047
|
+
} catch (error) {
|
|
1048
|
+
console.error('DQL drill-down error for ' + chartId + ':', error);
|
|
1049
|
+
return { columns: [], rows: [], error: error.message };
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function getClickedValue(datum) {
|
|
1054
|
+
// Return the first meaningful value from the datum
|
|
1055
|
+
var keys = Object.keys(datum).filter(function(k) {
|
|
1056
|
+
return k !== '_vgsid_' && !k.startsWith('__');
|
|
1057
|
+
});
|
|
1058
|
+
if (keys.length > 0) return datum[keys[0]];
|
|
1059
|
+
return '';
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function getClickedLabel(datum) {
|
|
1063
|
+
var keys = Object.keys(datum).filter(function(k) {
|
|
1064
|
+
return k !== '_vgsid_' && !k.startsWith('__') && typeof datum[k] === 'string';
|
|
1065
|
+
});
|
|
1066
|
+
if (keys.length > 0) return datum[keys[0]];
|
|
1067
|
+
return null;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function renderDrillTable(columns, rows) {
|
|
1071
|
+
var colNames = columns ? columns.map(function(c) { return c.name; }) : Object.keys(rows[0] || {});
|
|
1072
|
+
|
|
1073
|
+
var html = '<table class="dql-drill-table"><thead><tr>';
|
|
1074
|
+
for (var i = 0; i < colNames.length; i++) {
|
|
1075
|
+
html += '<th>' + escapeHTMLRuntime(colNames[i]) + '</th>';
|
|
1076
|
+
}
|
|
1077
|
+
html += '</tr></thead><tbody>';
|
|
1078
|
+
|
|
1079
|
+
var maxRows = Math.min(rows.length, 100);
|
|
1080
|
+
for (var r = 0; r < maxRows; r++) {
|
|
1081
|
+
html += '<tr>';
|
|
1082
|
+
for (var c = 0; c < colNames.length; c++) {
|
|
1083
|
+
var val = rows[r][colNames[c]];
|
|
1084
|
+
html += '<td>' + escapeHTMLRuntime(val === null || val === undefined ? '' : String(val)) + '</td>';
|
|
1085
|
+
}
|
|
1086
|
+
html += '</tr>';
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
html += '</tbody></table>';
|
|
1090
|
+
if (rows.length > 100) {
|
|
1091
|
+
html += '<p style="margin-top: 12px; color: #888;">Showing first 100 of ' + rows.length + ' rows</p>';
|
|
1092
|
+
}
|
|
1093
|
+
return html;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// ---- Navigation ----
|
|
1097
|
+
|
|
1098
|
+
function handleLinkTo(linkTo, datum) {
|
|
1099
|
+
// Interpolate datum values into the URL
|
|
1100
|
+
var url = interpolateDatum(linkTo, datum);
|
|
1101
|
+
|
|
1102
|
+
// Add referrer info for breadcrumb navigation
|
|
1103
|
+
var sep = url.indexOf('?') >= 0 ? '&' : '?';
|
|
1104
|
+
url += sep + '_from=' + encodeURIComponent(location.pathname) + '&_fromTitle=' + encodeURIComponent(DQL_CONFIG.title);
|
|
1105
|
+
try {
|
|
1106
|
+
var state = encodeURIComponent(JSON.stringify(captureStateSnapshot()));
|
|
1107
|
+
url += '&dqlState=' + state;
|
|
1108
|
+
} catch (_) {}
|
|
1109
|
+
|
|
1110
|
+
window.location.href = url;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
function handleOnClick(action, datum, chartId) {
|
|
1114
|
+
// Dispatch a custom event with clicked data for external integrations
|
|
1115
|
+
var event = new CustomEvent('dql:click', {
|
|
1116
|
+
detail: {
|
|
1117
|
+
chartId: chartId,
|
|
1118
|
+
action: action,
|
|
1119
|
+
datum: datum,
|
|
1120
|
+
},
|
|
1121
|
+
bubbles: true,
|
|
1122
|
+
});
|
|
1123
|
+
document.dispatchEvent(event);
|
|
1124
|
+
|
|
1125
|
+
// Built-in actions
|
|
1126
|
+
if (action === 'drill_down') {
|
|
1127
|
+
var interaction = DQL_CONFIG.interactions ? DQL_CONFIG.interactions[chartId] : null;
|
|
1128
|
+
var chartConfig = getChartConfig(chartId);
|
|
1129
|
+
if (chartConfig && chartConfig.drillConfig && DQL_CONFIG.features && DQL_CONFIG.features.hierarchyDrillEnabled !== false) {
|
|
1130
|
+
handleHierarchyDrill(chartId, datum, interaction);
|
|
1131
|
+
} else if (interaction && interaction.drillDown) {
|
|
1132
|
+
handleDrillDown(chartId, interaction.drillDown, datum);
|
|
1133
|
+
}
|
|
1134
|
+
} else if (action === 'navigate' || action === 'link') {
|
|
1135
|
+
var interaction2 = DQL_CONFIG.interactions ? DQL_CONFIG.interactions[chartId] : null;
|
|
1136
|
+
if (interaction2 && interaction2.linkTo) {
|
|
1137
|
+
handleLinkTo(interaction2.linkTo, datum);
|
|
1138
|
+
}
|
|
1139
|
+
} else if (String(action || '').indexOf('set_param:') === 0 || String(action || '').indexOf('set_params:') === 0) {
|
|
1140
|
+
applyParameterAction(action, datum);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// ---- Filter System ----
|
|
1145
|
+
|
|
1146
|
+
async function initFilters() {
|
|
1147
|
+
if (!DQL_CONFIG.filters || DQL_CONFIG.filters.length === 0) return;
|
|
1148
|
+
|
|
1149
|
+
// Load filter options from SQL queries
|
|
1150
|
+
for (var i = 0; i < DQL_CONFIG.filters.length; i++) {
|
|
1151
|
+
var filter = DQL_CONFIG.filters[i];
|
|
1152
|
+
if (filter.sql) {
|
|
1153
|
+
await loadFilterOptions(filter);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Set default values
|
|
1157
|
+
if (filter.defaultValue) {
|
|
1158
|
+
filterState[filter.param] = filter.defaultValue;
|
|
1159
|
+
setFilterUIValue(filter.id, filter.defaultValue);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// Apply state restored from URL if available.
|
|
1164
|
+
for (var j = 0; j < DQL_CONFIG.filters.length; j++) {
|
|
1165
|
+
var restoredFilter = DQL_CONFIG.filters[j];
|
|
1166
|
+
if (!restoredFilter || !restoredFilter.param) continue;
|
|
1167
|
+
if (Object.prototype.hasOwnProperty.call(filterState, restoredFilter.param)) {
|
|
1168
|
+
var restoredEl = document.getElementById(restoredFilter.id);
|
|
1169
|
+
var restoredValue = filterState[restoredFilter.param];
|
|
1170
|
+
if (restoredEl && restoredEl.tagName === 'SELECT' && restoredEl.multiple && Array.isArray(restoredValue)) {
|
|
1171
|
+
for (var ro = 0; ro < restoredEl.options.length; ro++) {
|
|
1172
|
+
restoredEl.options[ro].selected = restoredValue.indexOf(restoredEl.options[ro].value) >= 0;
|
|
1173
|
+
}
|
|
1174
|
+
} else {
|
|
1175
|
+
setFilterUIValue(restoredFilter.id, restoredValue);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Setup event handlers
|
|
1181
|
+
setupFilterEventHandlers();
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
async function loadFilterOptions(filter) {
|
|
1185
|
+
var variables = computeVariables();
|
|
1186
|
+
var result = await fetchQueryData(filter.id, filter.sql, filter.params || [], null, variables);
|
|
1187
|
+
if (result.error || !result.rows) return;
|
|
1188
|
+
|
|
1189
|
+
var selectEl = document.getElementById(filter.id);
|
|
1190
|
+
if (!selectEl || selectEl.tagName !== 'SELECT') return;
|
|
1191
|
+
|
|
1192
|
+
// Get the first column's values as options
|
|
1193
|
+
var colName = result.columns && result.columns.length > 0 ? result.columns[0].name : Object.keys(result.rows[0] || {})[0];
|
|
1194
|
+
if (!colName) return;
|
|
1195
|
+
|
|
1196
|
+
for (var i = 0; i < result.rows.length; i++) {
|
|
1197
|
+
var val = result.rows[i][colName];
|
|
1198
|
+
if (val === null || val === undefined) continue;
|
|
1199
|
+
var option = document.createElement('option');
|
|
1200
|
+
option.value = String(val);
|
|
1201
|
+
option.textContent = String(val);
|
|
1202
|
+
selectEl.appendChild(option);
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function setFilterUIValue(filterId, value) {
|
|
1207
|
+
var el = document.getElementById(filterId);
|
|
1208
|
+
if (!el) return;
|
|
1209
|
+
el.value = String(value);
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function setupFilterEventHandlers() {
|
|
1213
|
+
var applyBtn = document.getElementById('dql-filter-apply');
|
|
1214
|
+
var resetBtn = document.getElementById('dql-filter-reset');
|
|
1215
|
+
|
|
1216
|
+
if (applyBtn) {
|
|
1217
|
+
applyBtn.addEventListener('click', function() {
|
|
1218
|
+
collectFilterState();
|
|
1219
|
+
refreshDashboardWithFilters();
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
if (resetBtn) {
|
|
1224
|
+
resetBtn.addEventListener('click', function() {
|
|
1225
|
+
resetFilters();
|
|
1226
|
+
refreshDashboardWithFilters();
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Auto-apply for select/date filters on change
|
|
1231
|
+
var filterElements = document.querySelectorAll('.dql-filter-select, .dql-filter-date');
|
|
1232
|
+
filterElements.forEach(function(el) {
|
|
1233
|
+
el.addEventListener('change', function() {
|
|
1234
|
+
collectFilterState();
|
|
1235
|
+
refreshDashboardWithFilters();
|
|
1236
|
+
});
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// Debounced auto-apply for text inputs
|
|
1240
|
+
var textInputs = document.querySelectorAll('.dql-filter-input[type="text"]');
|
|
1241
|
+
var debounceTimers = {};
|
|
1242
|
+
textInputs.forEach(function(el) {
|
|
1243
|
+
el.addEventListener('input', function() {
|
|
1244
|
+
var id = el.id;
|
|
1245
|
+
if (debounceTimers[id]) clearTimeout(debounceTimers[id]);
|
|
1246
|
+
debounceTimers[id] = setTimeout(function() {
|
|
1247
|
+
collectFilterState();
|
|
1248
|
+
refreshDashboardWithFilters();
|
|
1249
|
+
}, 300);
|
|
1250
|
+
});
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function collectFilterState() {
|
|
1255
|
+
filterState = {};
|
|
1256
|
+
if (!DQL_CONFIG.filters) return;
|
|
1257
|
+
|
|
1258
|
+
for (var i = 0; i < DQL_CONFIG.filters.length; i++) {
|
|
1259
|
+
var filter = DQL_CONFIG.filters[i];
|
|
1260
|
+
var el = document.getElementById(filter.id);
|
|
1261
|
+
if (!el) continue;
|
|
1262
|
+
|
|
1263
|
+
if (el.tagName === 'SELECT' && el.multiple) {
|
|
1264
|
+
var selected = [];
|
|
1265
|
+
try {
|
|
1266
|
+
for (var s = 0; s < el.selectedOptions.length; s++) {
|
|
1267
|
+
selected.push(el.selectedOptions[s].value);
|
|
1268
|
+
}
|
|
1269
|
+
} catch (_) {}
|
|
1270
|
+
if (selected.length > 0) {
|
|
1271
|
+
filterState[filter.param] = selected;
|
|
1272
|
+
}
|
|
1273
|
+
} else {
|
|
1274
|
+
var value = el.value;
|
|
1275
|
+
if (value && value !== '') {
|
|
1276
|
+
filterState[filter.param] = value;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
updateURLState();
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
function resetFilters() {
|
|
1285
|
+
filterState = {};
|
|
1286
|
+
if (!DQL_CONFIG.filters) return;
|
|
1287
|
+
|
|
1288
|
+
for (var i = 0; i < DQL_CONFIG.filters.length; i++) {
|
|
1289
|
+
var filter = DQL_CONFIG.filters[i];
|
|
1290
|
+
var el = document.getElementById(filter.id);
|
|
1291
|
+
if (!el) continue;
|
|
1292
|
+
|
|
1293
|
+
if (filter.defaultValue) {
|
|
1294
|
+
el.value = String(filter.defaultValue);
|
|
1295
|
+
filterState[filter.param] = filter.defaultValue;
|
|
1296
|
+
} else {
|
|
1297
|
+
el.value = '';
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
updateURLState();
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
async function refreshDashboardWithFilters() {
|
|
1305
|
+
for (var i = 0; i < DQL_CONFIG.charts.length; i++) {
|
|
1306
|
+
var chart = DQL_CONFIG.charts[i];
|
|
1307
|
+
var container = document.getElementById(chart.id);
|
|
1308
|
+
|
|
1309
|
+
// Error boundary per chart during filter refresh
|
|
1310
|
+
try {
|
|
1311
|
+
var variables = computeVariables();
|
|
1312
|
+
var prepared = applyPredicateFiltersToQuery(chart.id, chart.sql, chart.params, variables);
|
|
1313
|
+
|
|
1314
|
+
if (container) {
|
|
1315
|
+
container.innerHTML = '<div class="dql-loading">Updating...</div>';
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
var result = await fetchQueryData(chart.id, prepared.sql, prepared.params, chart.connection, prepared.variables);
|
|
1319
|
+
|
|
1320
|
+
if (result.error) {
|
|
1321
|
+
if (container) {
|
|
1322
|
+
container.innerHTML = '<div class="dql-error">' + escapeHTMLRuntime(result.error) + '</div>';
|
|
1323
|
+
}
|
|
1324
|
+
continue;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
chartColumns[chart.id] = Array.isArray(result.columns)
|
|
1328
|
+
? result.columns.map(function(col) { return col && col.name ? col.name : null; }).filter(Boolean)
|
|
1329
|
+
: [];
|
|
1330
|
+
|
|
1331
|
+
var spec = DQL_CONFIG.vegaLiteSpecs[chart.id];
|
|
1332
|
+
if (spec) {
|
|
1333
|
+
await renderVegaLiteChart(chart.id, JSON.parse(JSON.stringify(spec)), result.rows);
|
|
1334
|
+
} else if (chart.type === 'kpi' || chart.type === 'metric') {
|
|
1335
|
+
renderKPICard(chart.id, result.rows);
|
|
1336
|
+
} else if (chart.type === 'table') {
|
|
1337
|
+
renderTable(chart.id, result.rows);
|
|
1338
|
+
}
|
|
1339
|
+
} catch (err) {
|
|
1340
|
+
console.error('[DQL] Error boundary caught during filter refresh for ' + chart.id + ':', err);
|
|
1341
|
+
if (container) {
|
|
1342
|
+
container.innerHTML = '<div class="dql-error">' +
|
|
1343
|
+
'<strong>Chart Error</strong><br>' +
|
|
1344
|
+
escapeHTMLRuntime(err.message || String(err)) +
|
|
1345
|
+
'</div>';
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
// ---- Breadcrumb Navigation ----
|
|
1352
|
+
|
|
1353
|
+
function initBreadcrumb() {
|
|
1354
|
+
var breadcrumb = document.getElementById('dql-breadcrumb');
|
|
1355
|
+
if (!breadcrumb) return;
|
|
1356
|
+
|
|
1357
|
+
var urlParams = new URLSearchParams(window.location.search);
|
|
1358
|
+
var fromPath = urlParams.get('_from');
|
|
1359
|
+
var fromTitle = urlParams.get('_fromTitle');
|
|
1360
|
+
|
|
1361
|
+
if (fromPath && fromTitle) {
|
|
1362
|
+
breadcrumb.innerHTML = '<a href="' + escapeHTMLRuntime(fromPath) + '">' + escapeHTMLRuntime(fromTitle) + '</a>' +
|
|
1363
|
+
'<span class="dql-breadcrumb-sep">/</span>' +
|
|
1364
|
+
'<span>' + escapeHTMLRuntime(DQL_CONFIG.title) + '</span>';
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ---- Modal ----
|
|
1369
|
+
|
|
1370
|
+
function initModal() {
|
|
1371
|
+
var modal = document.getElementById('dql-drill-modal');
|
|
1372
|
+
var closeBtn = document.getElementById('dql-drill-close');
|
|
1373
|
+
var upBtn = document.getElementById('dql-drill-up');
|
|
1374
|
+
var inlineUpButtons = document.querySelectorAll('.dql-inline-drill-up');
|
|
1375
|
+
|
|
1376
|
+
for (var bi = 0; bi < inlineUpButtons.length; bi++) {
|
|
1377
|
+
inlineUpButtons[bi].addEventListener('click', function() {
|
|
1378
|
+
drillUp();
|
|
1379
|
+
});
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
if (!modal || !closeBtn) return;
|
|
1383
|
+
|
|
1384
|
+
function closeModalAndResetDrill() {
|
|
1385
|
+
modal.classList.remove('active');
|
|
1386
|
+
if (drillState && Array.isArray(drillState.stack) && drillState.stack.length > 0) {
|
|
1387
|
+
drillState.stack = [];
|
|
1388
|
+
updateDrillHeader();
|
|
1389
|
+
updateURLState();
|
|
1390
|
+
refreshDashboardWithFilters();
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
closeBtn.addEventListener('click', function() {
|
|
1395
|
+
closeModalAndResetDrill();
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
modal.addEventListener('click', function(e) {
|
|
1399
|
+
if (e.target === modal) {
|
|
1400
|
+
closeModalAndResetDrill();
|
|
1401
|
+
}
|
|
1402
|
+
});
|
|
1403
|
+
|
|
1404
|
+
if (upBtn) {
|
|
1405
|
+
upBtn.addEventListener('click', function() {
|
|
1406
|
+
drillUp();
|
|
1407
|
+
});
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
// Close on Escape key
|
|
1411
|
+
document.addEventListener('keydown', function(e) {
|
|
1412
|
+
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
|
1413
|
+
closeModalAndResetDrill();
|
|
1414
|
+
}
|
|
1415
|
+
});
|
|
1416
|
+
|
|
1417
|
+
updateDrillHeader();
|
|
1418
|
+
updateInlineDrillControls();
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// ---- Utility ----
|
|
1422
|
+
|
|
1423
|
+
function interpolateDatum(template, datum) {
|
|
1424
|
+
return template.replace(/\{(\w+)\}/g, function(_match, key) {
|
|
1425
|
+
var val = datum[key];
|
|
1426
|
+
return val === null || val === undefined ? '' : String(val);
|
|
1427
|
+
}).replace(/\{clicked\.(\w+)\}/g, function(_match, field) {
|
|
1428
|
+
var val = datum[field];
|
|
1429
|
+
return val === null || val === undefined ? '' : String(val);
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function applyParameterAction(action, datum) {
|
|
1434
|
+
if (!action) return;
|
|
1435
|
+
var actionText = String(action);
|
|
1436
|
+
var updates = {};
|
|
1437
|
+
|
|
1438
|
+
if (actionText.indexOf('set_param:') === 0) {
|
|
1439
|
+
var parts = actionText.split(':');
|
|
1440
|
+
var paramName = (parts[1] || '').trim();
|
|
1441
|
+
var datumField = (parts[2] || '').trim();
|
|
1442
|
+
if (!paramName) return;
|
|
1443
|
+
var nextValue = datumField ? datum[datumField] : getClickedValue(datum);
|
|
1444
|
+
if (nextValue !== undefined) updates[paramName] = nextValue;
|
|
1445
|
+
} else if (actionText.indexOf('set_params:') === 0) {
|
|
1446
|
+
var pairs = actionText.slice('set_params:'.length).split(',');
|
|
1447
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
1448
|
+
var pair = pairs[i];
|
|
1449
|
+
var eq = pair.indexOf('=');
|
|
1450
|
+
if (eq <= 0) continue;
|
|
1451
|
+
var pName = pair.slice(0, eq).trim();
|
|
1452
|
+
var fieldName = pair.slice(eq + 1).trim();
|
|
1453
|
+
if (!pName || !fieldName) continue;
|
|
1454
|
+
if (datum[fieldName] !== undefined) {
|
|
1455
|
+
updates[pName] = datum[fieldName];
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
var keys = Object.keys(updates);
|
|
1461
|
+
if (keys.length === 0) return;
|
|
1462
|
+
|
|
1463
|
+
for (var k = 0; k < keys.length; k++) {
|
|
1464
|
+
var key = keys[k];
|
|
1465
|
+
paramActionState[key] = updates[key];
|
|
1466
|
+
filterState[key] = updates[key];
|
|
1467
|
+
if (DQL_CONFIG.filters) {
|
|
1468
|
+
for (var fi = 0; fi < DQL_CONFIG.filters.length; fi++) {
|
|
1469
|
+
var filter = DQL_CONFIG.filters[fi];
|
|
1470
|
+
if (filter && filter.param === key) {
|
|
1471
|
+
setFilterUIValue(filter.id, updates[key]);
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
var url = new URL(window.location.href);
|
|
1476
|
+
url.searchParams.set(key, String(updates[key]));
|
|
1477
|
+
history.replaceState(null, '', url.pathname + '?' + url.searchParams.toString());
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
updateURLState();
|
|
1481
|
+
refreshDashboardWithFilters();
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function escapeHTMLRuntime(str) {
|
|
1485
|
+
if (str === null || str === undefined) return '';
|
|
1486
|
+
return String(str)
|
|
1487
|
+
.replace(/&/g, '&')
|
|
1488
|
+
.replace(/</g, '<')
|
|
1489
|
+
.replace(/>/g, '>')
|
|
1490
|
+
.replace(/"/g, '"');
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function loadSavedPresets() {
|
|
1494
|
+
try {
|
|
1495
|
+
var raw = localStorage.getItem(getPresetStorageKey());
|
|
1496
|
+
if (!raw) return {};
|
|
1497
|
+
var parsed = JSON.parse(raw);
|
|
1498
|
+
if (!parsed || typeof parsed !== 'object') return {};
|
|
1499
|
+
return parsed;
|
|
1500
|
+
} catch (_) {
|
|
1501
|
+
return {};
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function persistPresets(presets) {
|
|
1506
|
+
try {
|
|
1507
|
+
localStorage.setItem(getPresetStorageKey(), JSON.stringify(presets || {}));
|
|
1508
|
+
} catch (_) {
|
|
1509
|
+
// noop
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
function applyStateSnapshot(snapshot) {
|
|
1514
|
+
if (!snapshot || typeof snapshot !== 'object') return;
|
|
1515
|
+
|
|
1516
|
+
suppressStateSync = true;
|
|
1517
|
+
if (snapshot.filters && typeof snapshot.filters === 'object') filterState = snapshot.filters;
|
|
1518
|
+
if (snapshot.crossFilters && typeof snapshot.crossFilters === 'object') crossFilterState = snapshot.crossFilters;
|
|
1519
|
+
if (snapshot.params && typeof snapshot.params === 'object') paramActionState = snapshot.params;
|
|
1520
|
+
if (Array.isArray(snapshot.drillStack)) {
|
|
1521
|
+
drillState.stack = snapshot.drillStack.filter(function(frame) {
|
|
1522
|
+
return frame && typeof frame === 'object' && frame.predicates;
|
|
1523
|
+
});
|
|
1524
|
+
} else {
|
|
1525
|
+
drillState.stack = [];
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
if (DQL_CONFIG.filters) {
|
|
1529
|
+
for (var i = 0; i < DQL_CONFIG.filters.length; i++) {
|
|
1530
|
+
var filter = DQL_CONFIG.filters[i];
|
|
1531
|
+
if (!filter || !filter.id || !filter.param) continue;
|
|
1532
|
+
var el = document.getElementById(filter.id);
|
|
1533
|
+
if (!el) continue;
|
|
1534
|
+
var value = Object.prototype.hasOwnProperty.call(filterState, filter.param)
|
|
1535
|
+
? filterState[filter.param]
|
|
1536
|
+
: '';
|
|
1537
|
+
if (el.tagName === 'SELECT' && el.multiple && Array.isArray(value)) {
|
|
1538
|
+
for (var o = 0; o < el.options.length; o++) {
|
|
1539
|
+
el.options[o].selected = value.indexOf(el.options[o].value) >= 0;
|
|
1540
|
+
}
|
|
1541
|
+
} else {
|
|
1542
|
+
el.value = value == null ? '' : String(value);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
suppressStateSync = false;
|
|
1548
|
+
updateDrillHeader();
|
|
1549
|
+
updateURLState();
|
|
1550
|
+
refreshDashboardWithFilters();
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
function refreshPresetDropdown(selectEl, presets) {
|
|
1554
|
+
if (!selectEl) return;
|
|
1555
|
+
var current = selectEl.value;
|
|
1556
|
+
selectEl.innerHTML = '<option value="">Views</option>';
|
|
1557
|
+
var names = Object.keys(presets || {}).sort();
|
|
1558
|
+
for (var i = 0; i < names.length; i++) {
|
|
1559
|
+
var option = document.createElement('option');
|
|
1560
|
+
option.value = names[i];
|
|
1561
|
+
option.textContent = names[i];
|
|
1562
|
+
selectEl.appendChild(option);
|
|
1563
|
+
}
|
|
1564
|
+
if (current && presets[current]) selectEl.value = current;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function initViewPresets() {
|
|
1568
|
+
var saveBtn = document.getElementById('dql-save-view');
|
|
1569
|
+
var presetSelect = document.getElementById('dql-view-presets');
|
|
1570
|
+
if (!saveBtn && !presetSelect) return;
|
|
1571
|
+
|
|
1572
|
+
var presets = loadSavedPresets();
|
|
1573
|
+
refreshPresetDropdown(presetSelect, presets);
|
|
1574
|
+
|
|
1575
|
+
if (saveBtn) {
|
|
1576
|
+
saveBtn.addEventListener('click', function() {
|
|
1577
|
+
var name = prompt('Save current view as:');
|
|
1578
|
+
if (!name) return;
|
|
1579
|
+
var cleanName = name.trim();
|
|
1580
|
+
if (!cleanName) return;
|
|
1581
|
+
presets[cleanName] = captureStateSnapshot();
|
|
1582
|
+
persistPresets(presets);
|
|
1583
|
+
refreshPresetDropdown(presetSelect, presets);
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (presetSelect) {
|
|
1588
|
+
presetSelect.addEventListener('change', function() {
|
|
1589
|
+
var selected = presetSelect.value;
|
|
1590
|
+
if (!selected || !presets[selected]) return;
|
|
1591
|
+
applyStateSnapshot(presets[selected]);
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function shouldEnableTestHooks() {
|
|
1597
|
+
try {
|
|
1598
|
+
if (globalThis.__DQL_ENABLE_TEST_HOOKS__) return true;
|
|
1599
|
+
var params = new URLSearchParams(window.location.search);
|
|
1600
|
+
return params.get('dqlTest') === '1';
|
|
1601
|
+
} catch (_) {
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
function initTestHooks() {
|
|
1607
|
+
if (!shouldEnableTestHooks()) return;
|
|
1608
|
+
|
|
1609
|
+
globalThis.__DQL_TEST_HOOKS__ = {
|
|
1610
|
+
getState: function() {
|
|
1611
|
+
return captureStateSnapshot();
|
|
1612
|
+
},
|
|
1613
|
+
getChartColumns: function() {
|
|
1614
|
+
return JSON.parse(JSON.stringify(chartColumns || {}));
|
|
1615
|
+
},
|
|
1616
|
+
setCrossFilter: function(sourceChartId, selectedValues) {
|
|
1617
|
+
if (!sourceChartId) return;
|
|
1618
|
+
if (!selectedValues || Object.keys(selectedValues).length === 0) {
|
|
1619
|
+
delete crossFilterState[sourceChartId];
|
|
1620
|
+
} else {
|
|
1621
|
+
crossFilterState[sourceChartId] = selectedValues;
|
|
1622
|
+
}
|
|
1623
|
+
updateURLState();
|
|
1624
|
+
},
|
|
1625
|
+
clearCrossFilter: function(sourceChartId) {
|
|
1626
|
+
if (!sourceChartId) return;
|
|
1627
|
+
delete crossFilterState[sourceChartId];
|
|
1628
|
+
updateURLState();
|
|
1629
|
+
},
|
|
1630
|
+
pushDrill: function(chartId, templateSQL, datum) {
|
|
1631
|
+
return handleDrillDown(chartId, templateSQL, datum || {});
|
|
1632
|
+
},
|
|
1633
|
+
drillUp: function() {
|
|
1634
|
+
return drillUp();
|
|
1635
|
+
},
|
|
1636
|
+
refresh: function() {
|
|
1637
|
+
return refreshDashboardWithFilters();
|
|
1638
|
+
},
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// ---- Dashboard Init ----
|
|
1643
|
+
|
|
1644
|
+
async function initDashboard() {
|
|
1645
|
+
// Initialize breadcrumb navigation
|
|
1646
|
+
initBreadcrumb();
|
|
1647
|
+
|
|
1648
|
+
// Initialize modal
|
|
1649
|
+
initModal();
|
|
1650
|
+
|
|
1651
|
+
// Restore bookmarkable state from URL before initializing filter UI.
|
|
1652
|
+
restoreURLState();
|
|
1653
|
+
initTestHooks();
|
|
1654
|
+
|
|
1655
|
+
// Initialize filters
|
|
1656
|
+
await initFilters();
|
|
1657
|
+
initViewPresets();
|
|
1658
|
+
|
|
1659
|
+
// Render charts with lazy loading for below-the-fold content
|
|
1660
|
+
var lazyObserver = null;
|
|
1661
|
+
if (typeof IntersectionObserver !== 'undefined') {
|
|
1662
|
+
lazyObserver = new IntersectionObserver(function(entries) {
|
|
1663
|
+
entries.forEach(function(entry) {
|
|
1664
|
+
if (entry.isIntersecting) {
|
|
1665
|
+
var chartId = entry.target.getAttribute('data-chart-id');
|
|
1666
|
+
if (chartId) {
|
|
1667
|
+
lazyObserver.unobserve(entry.target);
|
|
1668
|
+
loadAndRenderChart(chartId);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
});
|
|
1672
|
+
}, { rootMargin: '200px' });
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
for (var i = 0; i < DQL_CONFIG.charts.length; i++) {
|
|
1676
|
+
var chart = DQL_CONFIG.charts[i];
|
|
1677
|
+
var container = document.getElementById(chart.id);
|
|
1678
|
+
if (!container) continue;
|
|
1679
|
+
|
|
1680
|
+
container.innerHTML = '<div class="dql-loading">Loading...</div>';
|
|
1681
|
+
|
|
1682
|
+
// First 4 charts load eagerly; rest use lazy loading if available
|
|
1683
|
+
var wrapper = container.parentElement;
|
|
1684
|
+
if (i < 4 || !lazyObserver || !wrapper) {
|
|
1685
|
+
await loadAndRenderChart(chart.id);
|
|
1686
|
+
} else {
|
|
1687
|
+
wrapper.setAttribute('data-chart-id', chart.id);
|
|
1688
|
+
lazyObserver.observe(wrapper);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
updateDrillHeader();
|
|
1693
|
+
updateURLState();
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
async function loadAndRenderChart(chartId) {
|
|
1697
|
+
var chart = DQL_CONFIG.charts.find(function(c) { return c.id === chartId; });
|
|
1698
|
+
if (!chart) return;
|
|
1699
|
+
|
|
1700
|
+
var container = document.getElementById(chartId);
|
|
1701
|
+
|
|
1702
|
+
// Error boundary: isolate each chart so one failure doesn't break the page
|
|
1703
|
+
try {
|
|
1704
|
+
var variables = computeVariables();
|
|
1705
|
+
var prepared = applyPredicateFiltersToQuery(chartId, chart.sql, chart.params, variables);
|
|
1706
|
+
var result = await fetchQueryData(chartId, prepared.sql, prepared.params, chart.connection, prepared.variables);
|
|
1707
|
+
|
|
1708
|
+
if (result.error) {
|
|
1709
|
+
if (container) {
|
|
1710
|
+
container.innerHTML = '<div class="dql-error">' + escapeHTMLRuntime(result.error) + '</div>';
|
|
1711
|
+
}
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
chartColumns[chartId] = Array.isArray(result.columns)
|
|
1716
|
+
? result.columns.map(function(col) { return col && col.name ? col.name : null; }).filter(Boolean)
|
|
1717
|
+
: [];
|
|
1718
|
+
|
|
1719
|
+
var spec = DQL_CONFIG.vegaLiteSpecs[chartId];
|
|
1720
|
+
if (spec) {
|
|
1721
|
+
await renderVegaLiteChart(chartId, spec, result.rows);
|
|
1722
|
+
} else if (chart.type === 'kpi' || chart.type === 'metric') {
|
|
1723
|
+
renderKPICard(chartId, result.rows);
|
|
1724
|
+
} else if (chart.type === 'table') {
|
|
1725
|
+
renderTable(chartId, result.rows);
|
|
1726
|
+
}
|
|
1727
|
+
} catch (err) {
|
|
1728
|
+
console.error('[DQL] Error boundary caught for ' + chartId + ':', err);
|
|
1729
|
+
if (container) {
|
|
1730
|
+
container.innerHTML = '<div class="dql-error">' +
|
|
1731
|
+
'<strong>Chart Error</strong><br>' +
|
|
1732
|
+
escapeHTMLRuntime(err.message || String(err)) +
|
|
1733
|
+
'</div>';
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// Hot-reload support
|
|
1739
|
+
function initHotReload() {
|
|
1740
|
+
if (typeof WebSocket === 'undefined') return;
|
|
1741
|
+
|
|
1742
|
+
var protocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1743
|
+
var wsUrl = protocol + '//' + location.host + '/__dql_hmr';
|
|
1744
|
+
|
|
1745
|
+
var ws;
|
|
1746
|
+
var reconnectAttempts = 0;
|
|
1747
|
+
|
|
1748
|
+
function connect() {
|
|
1749
|
+
ws = new WebSocket(wsUrl);
|
|
1750
|
+
|
|
1751
|
+
ws.onopen = function() {
|
|
1752
|
+
console.log('[DQL] Hot-reload connected');
|
|
1753
|
+
reconnectAttempts = 0;
|
|
1754
|
+
};
|
|
1755
|
+
|
|
1756
|
+
ws.onmessage = function(event) {
|
|
1757
|
+
var msg = JSON.parse(event.data);
|
|
1758
|
+
if (msg.type === 'reload') {
|
|
1759
|
+
console.log('[DQL] Reloading dashboard...');
|
|
1760
|
+
location.reload();
|
|
1761
|
+
} else if (msg.type === 'error') {
|
|
1762
|
+
document.body.innerHTML = '<div class="dql-error" style="padding: 24px;"><h2>Compilation Error</h2><pre>' + msg.message + '</pre></div>';
|
|
1763
|
+
}
|
|
1764
|
+
};
|
|
1765
|
+
|
|
1766
|
+
ws.onclose = function() {
|
|
1767
|
+
if (reconnectAttempts < 10) {
|
|
1768
|
+
reconnectAttempts++;
|
|
1769
|
+
var delay = Math.min(1000 * Math.pow(2, reconnectAttempts), 10000);
|
|
1770
|
+
setTimeout(connect, delay);
|
|
1771
|
+
}
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
connect();
|
|
1776
|
+
}
|
|
1777
|
+
|
|
1778
|
+
// Auto-refresh support: @refresh(seconds) decorator
|
|
1779
|
+
function initAutoRefresh() {
|
|
1780
|
+
var interval = DQL_CONFIG.refreshInterval;
|
|
1781
|
+
if (!interval || interval <= 0) return;
|
|
1782
|
+
|
|
1783
|
+
var intervalMs = interval * 1000;
|
|
1784
|
+
console.log('[DQL] Auto-refresh enabled: every ' + interval + 's');
|
|
1785
|
+
|
|
1786
|
+
setInterval(function() {
|
|
1787
|
+
console.log('[DQL] Auto-refreshing dashboard...');
|
|
1788
|
+
refreshDashboardWithFilters();
|
|
1789
|
+
}, intervalMs);
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
// Initialize
|
|
1793
|
+
if (document.readyState === 'loading') {
|
|
1794
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
1795
|
+
initDashboard();
|
|
1796
|
+
initHotReload();
|
|
1797
|
+
initAutoRefresh();
|
|
1798
|
+
});
|
|
1799
|
+
} else {
|
|
1800
|
+
initDashboard();
|
|
1801
|
+
initHotReload();
|
|
1802
|
+
initAutoRefresh();
|
|
1803
|
+
}
|
|
1804
|
+
})();
|
|
1805
|
+
`;
|
|
1806
|
+
}
|
|
1807
|
+
//# sourceMappingURL=runtime-emitter.js.map
|