@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.
Files changed (182) hide show
  1. package/LICENSE +123 -0
  2. package/dist/charts/area.d.ts +5 -0
  3. package/dist/charts/area.d.ts.map +1 -0
  4. package/dist/charts/area.js +30 -0
  5. package/dist/charts/area.js.map +1 -0
  6. package/dist/charts/bar.d.ts +5 -0
  7. package/dist/charts/bar.d.ts.map +1 -0
  8. package/dist/charts/bar.js +37 -0
  9. package/dist/charts/bar.js.map +1 -0
  10. package/dist/charts/boxplot.d.ts +5 -0
  11. package/dist/charts/boxplot.d.ts.map +1 -0
  12. package/dist/charts/boxplot.js +36 -0
  13. package/dist/charts/boxplot.js.map +1 -0
  14. package/dist/charts/chart-registry.d.ts +24 -0
  15. package/dist/charts/chart-registry.d.ts.map +1 -0
  16. package/dist/charts/chart-registry.js +14 -0
  17. package/dist/charts/chart-registry.js.map +1 -0
  18. package/dist/charts/combo.d.ts +5 -0
  19. package/dist/charts/combo.d.ts.map +1 -0
  20. package/dist/charts/combo.js +54 -0
  21. package/dist/charts/combo.js.map +1 -0
  22. package/dist/charts/funnel.d.ts +5 -0
  23. package/dist/charts/funnel.d.ts.map +1 -0
  24. package/dist/charts/funnel.js +36 -0
  25. package/dist/charts/funnel.js.map +1 -0
  26. package/dist/charts/gauge.d.ts +5 -0
  27. package/dist/charts/gauge.d.ts.map +1 -0
  28. package/dist/charts/gauge.js +53 -0
  29. package/dist/charts/gauge.js.map +1 -0
  30. package/dist/charts/geo.d.ts +5 -0
  31. package/dist/charts/geo.d.ts.map +1 -0
  32. package/dist/charts/geo.js +48 -0
  33. package/dist/charts/geo.js.map +1 -0
  34. package/dist/charts/grouped-bar.d.ts +5 -0
  35. package/dist/charts/grouped-bar.d.ts.map +1 -0
  36. package/dist/charts/grouped-bar.js +37 -0
  37. package/dist/charts/grouped-bar.js.map +1 -0
  38. package/dist/charts/heatmap.d.ts +5 -0
  39. package/dist/charts/heatmap.d.ts.map +1 -0
  40. package/dist/charts/heatmap.js +31 -0
  41. package/dist/charts/heatmap.js.map +1 -0
  42. package/dist/charts/histogram.d.ts +5 -0
  43. package/dist/charts/histogram.d.ts.map +1 -0
  44. package/dist/charts/histogram.js +32 -0
  45. package/dist/charts/histogram.js.map +1 -0
  46. package/dist/charts/index.d.ts +25 -0
  47. package/dist/charts/index.d.ts.map +1 -0
  48. package/dist/charts/index.js +66 -0
  49. package/dist/charts/index.js.map +1 -0
  50. package/dist/charts/kpi.d.ts +14 -0
  51. package/dist/charts/kpi.d.ts.map +1 -0
  52. package/dist/charts/kpi.js +71 -0
  53. package/dist/charts/kpi.js.map +1 -0
  54. package/dist/charts/line.d.ts +5 -0
  55. package/dist/charts/line.d.ts.map +1 -0
  56. package/dist/charts/line.js +38 -0
  57. package/dist/charts/line.js.map +1 -0
  58. package/dist/charts/pie.d.ts +5 -0
  59. package/dist/charts/pie.d.ts.map +1 -0
  60. package/dist/charts/pie.js +25 -0
  61. package/dist/charts/pie.js.map +1 -0
  62. package/dist/charts/sankey.d.ts +10 -0
  63. package/dist/charts/sankey.d.ts.map +1 -0
  64. package/dist/charts/sankey.js +120 -0
  65. package/dist/charts/sankey.js.map +1 -0
  66. package/dist/charts/scatter.d.ts +5 -0
  67. package/dist/charts/scatter.d.ts.map +1 -0
  68. package/dist/charts/scatter.js +34 -0
  69. package/dist/charts/scatter.js.map +1 -0
  70. package/dist/charts/shared.d.ts +11 -0
  71. package/dist/charts/shared.d.ts.map +1 -0
  72. package/dist/charts/shared.js +52 -0
  73. package/dist/charts/shared.js.map +1 -0
  74. package/dist/charts/small-multiples.d.ts +5 -0
  75. package/dist/charts/small-multiples.d.ts.map +1 -0
  76. package/dist/charts/small-multiples.js +59 -0
  77. package/dist/charts/small-multiples.js.map +1 -0
  78. package/dist/charts/sparkline.d.ts +5 -0
  79. package/dist/charts/sparkline.d.ts.map +1 -0
  80. package/dist/charts/sparkline.js +41 -0
  81. package/dist/charts/sparkline.js.map +1 -0
  82. package/dist/charts/stacked-bar.d.ts +5 -0
  83. package/dist/charts/stacked-bar.d.ts.map +1 -0
  84. package/dist/charts/stacked-bar.js +34 -0
  85. package/dist/charts/stacked-bar.js.map +1 -0
  86. package/dist/charts/table.d.ts +14 -0
  87. package/dist/charts/table.d.ts.map +1 -0
  88. package/dist/charts/table.js +58 -0
  89. package/dist/charts/table.js.map +1 -0
  90. package/dist/charts/treemap.d.ts +9 -0
  91. package/dist/charts/treemap.d.ts.map +1 -0
  92. package/dist/charts/treemap.js +60 -0
  93. package/dist/charts/treemap.js.map +1 -0
  94. package/dist/charts/waterfall.d.ts +5 -0
  95. package/dist/charts/waterfall.d.ts.map +1 -0
  96. package/dist/charts/waterfall.js +36 -0
  97. package/dist/charts/waterfall.js.map +1 -0
  98. package/dist/codegen/bundle.d.ts +34 -0
  99. package/dist/codegen/bundle.d.ts.map +1 -0
  100. package/dist/codegen/bundle.js +20 -0
  101. package/dist/codegen/bundle.js.map +1 -0
  102. package/dist/codegen/html-emitter.d.ts +10 -0
  103. package/dist/codegen/html-emitter.d.ts.map +1 -0
  104. package/dist/codegen/html-emitter.js +1082 -0
  105. package/dist/codegen/html-emitter.js.map +1 -0
  106. package/dist/codegen/index.d.ts +6 -0
  107. package/dist/codegen/index.d.ts.map +1 -0
  108. package/dist/codegen/index.js +6 -0
  109. package/dist/codegen/index.js.map +1 -0
  110. package/dist/codegen/react-emitter.d.ts +42 -0
  111. package/dist/codegen/react-emitter.d.ts.map +1 -0
  112. package/dist/codegen/react-emitter.js +135 -0
  113. package/dist/codegen/react-emitter.js.map +1 -0
  114. package/dist/codegen/runtime-emitter.d.ts +2 -0
  115. package/dist/codegen/runtime-emitter.d.ts.map +1 -0
  116. package/dist/codegen/runtime-emitter.js +1807 -0
  117. package/dist/codegen/runtime-emitter.js.map +1 -0
  118. package/dist/codegen/vega-lite-emitter.d.ts +23 -0
  119. package/dist/codegen/vega-lite-emitter.d.ts.map +1 -0
  120. package/dist/codegen/vega-lite-emitter.js +92 -0
  121. package/dist/codegen/vega-lite-emitter.js.map +1 -0
  122. package/dist/compiler.d.ts +17 -0
  123. package/dist/compiler.d.ts.map +1 -0
  124. package/dist/compiler.js +106 -0
  125. package/dist/compiler.js.map +1 -0
  126. package/dist/index.d.ts +7 -0
  127. package/dist/index.d.ts.map +1 -0
  128. package/dist/index.js +9 -0
  129. package/dist/index.js.map +1 -0
  130. package/dist/ir/index.d.ts +3 -0
  131. package/dist/ir/index.d.ts.map +1 -0
  132. package/dist/ir/index.js +2 -0
  133. package/dist/ir/index.js.map +1 -0
  134. package/dist/ir/ir-nodes.d.ts +169 -0
  135. package/dist/ir/ir-nodes.d.ts.map +1 -0
  136. package/dist/ir/ir-nodes.js +2 -0
  137. package/dist/ir/ir-nodes.js.map +1 -0
  138. package/dist/ir/lowering.d.ts +9 -0
  139. package/dist/ir/lowering.d.ts.map +1 -0
  140. package/dist/ir/lowering.js +980 -0
  141. package/dist/ir/lowering.js.map +1 -0
  142. package/dist/modules/index.d.ts +4 -0
  143. package/dist/modules/index.d.ts.map +1 -0
  144. package/dist/modules/index.js +3 -0
  145. package/dist/modules/index.js.map +1 -0
  146. package/dist/modules/resolver.d.ts +21 -0
  147. package/dist/modules/resolver.d.ts.map +1 -0
  148. package/dist/modules/resolver.js +112 -0
  149. package/dist/modules/resolver.js.map +1 -0
  150. package/dist/modules/use-expander.d.ts +11 -0
  151. package/dist/modules/use-expander.d.ts.map +1 -0
  152. package/dist/modules/use-expander.js +42 -0
  153. package/dist/modules/use-expander.js.map +1 -0
  154. package/dist/themes/colorful.d.ts +3 -0
  155. package/dist/themes/colorful.d.ts.map +1 -0
  156. package/dist/themes/colorful.js +14 -0
  157. package/dist/themes/colorful.js.map +1 -0
  158. package/dist/themes/corporate.d.ts +3 -0
  159. package/dist/themes/corporate.d.ts.map +1 -0
  160. package/dist/themes/corporate.js +14 -0
  161. package/dist/themes/corporate.js.map +1 -0
  162. package/dist/themes/dark.d.ts +3 -0
  163. package/dist/themes/dark.d.ts.map +1 -0
  164. package/dist/themes/dark.js +14 -0
  165. package/dist/themes/dark.js.map +1 -0
  166. package/dist/themes/index.d.ts +11 -0
  167. package/dist/themes/index.d.ts.map +1 -0
  168. package/dist/themes/index.js +29 -0
  169. package/dist/themes/index.js.map +1 -0
  170. package/dist/themes/light.d.ts +3 -0
  171. package/dist/themes/light.d.ts.map +1 -0
  172. package/dist/themes/light.js +14 -0
  173. package/dist/themes/light.js.map +1 -0
  174. package/dist/themes/minimal.d.ts +3 -0
  175. package/dist/themes/minimal.d.ts.map +1 -0
  176. package/dist/themes/minimal.js +14 -0
  177. package/dist/themes/minimal.js.map +1 -0
  178. package/dist/themes/theme-types.d.ts +14 -0
  179. package/dist/themes/theme-types.d.ts.map +1 -0
  180. package/dist/themes/theme-types.js +2 -0
  181. package/dist/themes/theme-types.js.map +1 -0
  182. 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, '&amp;')
1488
+ .replace(/</g, '&lt;')
1489
+ .replace(/>/g, '&gt;')
1490
+ .replace(/"/g, '&quot;');
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