sqlite-hub 0.6.0 → 0.7.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.
@@ -4,9 +4,14 @@ import { renderSidebar } from "./components/sidebar.js";
4
4
  import { renderStatusBar } from "./components/statusBar.js";
5
5
  import {
6
6
  mountStructureGraph,
7
- resetPersistedStructureGraphState,
8
7
  teardownStructureGraph,
8
+ resetPersistedStructureGraphState,
9
9
  } from "./components/structureGraph.js";
10
+ import {
11
+ exportQueryChartAsPng,
12
+ mountQueryChartRenderer,
13
+ teardownQueryChartRenderer,
14
+ } from "./components/queryChartRenderer.js";
10
15
  import { renderToasts } from "./components/toast.js";
11
16
  import { renderTopNav } from "./components/topNav.js";
12
17
  import { createRouter } from "./router.js";
@@ -31,10 +36,14 @@ import {
31
36
  openQueryHistoryInEditor,
32
37
  openDeleteDataRowModal,
33
38
  openDeleteEditorRowModal,
39
+ openDeleteQueryChartModal,
34
40
  openEditConnectionModal,
41
+ openCreateQueryChartModal,
42
+ openEditQueryChartModal,
35
43
  refreshCurrentRoute,
36
44
  removeConnection,
37
45
  runQueryHistoryItem,
46
+ saveCurrentQueryChartDraft,
38
47
  selectDataRow,
39
48
  selectEditorRow,
40
49
  selectConnection,
@@ -45,8 +54,13 @@ import {
45
54
  setDataPageSize,
46
55
  setDataSearchColumn,
47
56
  setDataSearchQuery,
57
+ toggleDataTablesPanel,
48
58
  setCurrentQuery,
59
+ setChartsHeightPreset,
60
+ setEditorPanelVisibility,
49
61
  setEditorTab,
62
+ submitDeleteChartConfirmation,
63
+ setQueryHistoryPanelVisibility,
50
64
  sortDataTableByColumn,
51
65
  sortEditorResultsByColumn,
52
66
  setQueryHistorySearchInput,
@@ -55,6 +69,8 @@ import {
55
69
  saveQueryHistoryNotes,
56
70
  saveQueryHistoryTitle,
57
71
  saveCurrentTableDesignerDraft,
72
+ toggleChartsResultsPanel,
73
+ toggleChartsSqlPanel,
58
74
  queueTableDesignerCsvImport,
59
75
  showToast,
60
76
  submitCreateConnection,
@@ -66,11 +82,14 @@ import {
66
82
  submitOpenConnection,
67
83
  subscribe,
68
84
  toggleQueryHistorySavedState,
85
+ updateCurrentQueryChartDraftConfigField,
86
+ updateCurrentQueryChartDraftField,
69
87
  updateCurrentTableDesignerColumnField,
70
88
  updateCurrentTableDesignerField,
71
89
  addCurrentTableDesignerColumn,
72
90
  removeCurrentTableDesignerColumn,
73
91
  } from "./store.js";
92
+ import { renderChartsView } from "./views/charts.js";
74
93
  import { renderConnectionsView } from "./views/connections.js";
75
94
  import { renderDataView } from "./views/data.js";
76
95
  import { renderEditorView } from "./views/editor.js";
@@ -218,6 +237,8 @@ function resolveView(state) {
218
237
  return renderConnectionsView(state);
219
238
  case "overview":
220
239
  return renderOverviewView(state);
240
+ case "charts":
241
+ return renderChartsView(state);
221
242
  case "data":
222
243
  return renderDataView(state);
223
244
  case "editor":
@@ -411,7 +432,7 @@ function renderApp(state) {
411
432
  const focusedInput = captureFocusedInputState();
412
433
  const previousRoutePath = lastRenderedRoutePath;
413
434
  const { main, panel } = resolveView(state);
414
- const isLockedRoute = ["editor", "editorResults", "data", "structure", "tableDesigner"].includes(
435
+ const isLockedRoute = ["editor", "editorResults", "data", "charts", "structure", "tableDesigner"].includes(
415
436
  state.route.name
416
437
  );
417
438
  const isEnteringNewTableDesignerRoute =
@@ -426,6 +447,7 @@ function renderApp(state) {
426
447
  }
427
448
 
428
449
  teardownStructureGraph();
450
+ teardownQueryChartRenderer();
429
451
  shellRefs.topNav.innerHTML = renderTopNav(state);
430
452
  shellRefs.sidebar.innerHTML = renderSidebar(state);
431
453
  shellRefs.statusBar.innerHTML = renderStatusBar(state);
@@ -456,6 +478,10 @@ function renderApp(state) {
456
478
  console.error("Failed to mount structure graph.", error);
457
479
  });
458
480
  }
481
+
482
+ if (state.route.name === "charts") {
483
+ mountQueryChartRenderer(state);
484
+ }
459
485
  }
460
486
 
461
487
  const router = createRouter((route) => {
@@ -480,6 +506,15 @@ async function handleAction(actionNode) {
480
506
  case "open-modal":
481
507
  openModal(actionNode.dataset.modal);
482
508
  return;
509
+ case "open-create-query-chart-modal":
510
+ openCreateQueryChartModal();
511
+ return;
512
+ case "open-edit-query-chart-modal":
513
+ openEditQueryChartModal(actionNode.dataset.chartId);
514
+ return;
515
+ case "open-delete-query-chart-modal":
516
+ openDeleteQueryChartModal(actionNode.dataset.chartId);
517
+ return;
483
518
  case "edit-connection":
484
519
  openEditConnectionModal(actionNode.dataset.connectionId);
485
520
  return;
@@ -554,6 +589,16 @@ async function handleAction(actionNode) {
554
589
  await setQueryHistoryTab(actionNode.dataset.tab);
555
590
  }
556
591
  return;
592
+ case "toggle-query-history-panel":
593
+ setQueryHistoryPanelVisibility(
594
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
595
+ );
596
+ return;
597
+ case "toggle-editor-panel":
598
+ setEditorPanelVisibility(
599
+ actionNode.dataset.nextValue ? actionNode.dataset.nextValue === "true" : undefined
600
+ );
601
+ return;
557
602
  case "load-more-query-history":
558
603
  await loadMoreQueryHistory();
559
604
  return;
@@ -576,6 +621,22 @@ async function handleAction(actionNode) {
576
621
  );
577
622
  }
578
623
  return;
624
+ case "toggle-charts-sql-panel":
625
+ toggleChartsSqlPanel();
626
+ return;
627
+ case "toggle-charts-results-panel":
628
+ toggleChartsResultsPanel();
629
+ return;
630
+ case "set-charts-height-preset":
631
+ if (actionNode.dataset.preset) {
632
+ setChartsHeightPreset(actionNode.dataset.preset);
633
+ }
634
+ return;
635
+ case "export-query-chart-png":
636
+ if (actionNode.dataset.chartId && !exportQueryChartAsPng(actionNode.dataset.chartId)) {
637
+ showToast("The selected chart is not ready for PNG export.", "alert");
638
+ }
639
+ return;
579
640
  case "delete-query-history":
580
641
  if (
581
642
  actionNode.dataset.historyId &&
@@ -599,6 +660,9 @@ async function handleAction(actionNode) {
599
660
  case "export-data-csv":
600
661
  await exportCurrentDataTableCsv();
601
662
  return;
663
+ case "toggle-data-tables":
664
+ toggleDataTablesPanel();
665
+ return;
602
666
  case "select-structure-entry":
603
667
  if (actionNode.dataset.entryName) {
604
668
  await selectStructureEntry(actionNode.dataset.entryName);
@@ -794,6 +858,11 @@ document.addEventListener("input", (event) => {
794
858
 
795
859
  if (bindNode.dataset.bind === "query-history-search") {
796
860
  setQueryHistorySearchInput(bindNode.value);
861
+ return;
862
+ }
863
+
864
+ if (bindNode.dataset.bind === "query-chart-draft:name") {
865
+ updateCurrentQueryChartDraftField("name", bindNode.value);
797
866
  }
798
867
  });
799
868
 
@@ -853,6 +922,28 @@ document.addEventListener("change", (event) => {
853
922
  bindNode.dataset.field,
854
923
  bindNode.checked
855
924
  );
925
+ return;
926
+ }
927
+
928
+ if (bindNode.dataset.bind === "query-chart-draft:chartType") {
929
+ updateCurrentQueryChartDraftField("chartType", bindNode.value);
930
+ return;
931
+ }
932
+
933
+ if (bindNode.dataset.bind === "query-chart-draft:tableVisible") {
934
+ updateCurrentQueryChartDraftField("tableVisible", bindNode.checked);
935
+ return;
936
+ }
937
+
938
+ if (bindNode.dataset.bind?.startsWith("query-chart-draft-config:")) {
939
+ const field = bindNode.dataset.bind.slice("query-chart-draft-config:".length);
940
+ const value =
941
+ bindNode instanceof HTMLInputElement && bindNode.type === "checkbox"
942
+ ? bindNode.checked
943
+ : bindNode.value === ""
944
+ ? null
945
+ : bindNode.value;
946
+ updateCurrentQueryChartDraftConfigField(field, value);
856
947
  }
857
948
  });
858
949
 
@@ -938,6 +1029,12 @@ document.addEventListener("submit", async (event) => {
938
1029
  case "delete-row-confirm":
939
1030
  await submitDeleteRowConfirmation();
940
1031
  return;
1032
+ case "save-query-chart":
1033
+ await saveCurrentQueryChartDraft();
1034
+ return;
1035
+ case "delete-query-chart":
1036
+ await submitDeleteChartConfirmation();
1037
+ return;
941
1038
  case "save-data-row": {
942
1039
  const values = {};
943
1040
 
@@ -1,5 +1,10 @@
1
1
  import { escapeHtml, truncateMiddle } from "../utils/format.js";
2
2
  import { renderConnectionLogo } from "./connectionLogo.js";
3
+ import {
4
+ analyzeQueryChartResult,
5
+ getQueryChartTypeLabel,
6
+ QUERY_CHART_TYPES,
7
+ } from "../lib/queryCharts.js";
3
8
 
4
9
  function renderField({ label, name, type = "text", placeholder = "", value = "" }) {
5
10
  return `
@@ -58,6 +63,31 @@ function renderFileField({
58
63
  `;
59
64
  }
60
65
 
66
+ function renderSelectField({ label, name, value = "", options = [], bind = "" }) {
67
+ return `
68
+ <label class="block space-y-2">
69
+ <span class="text-[10px] font-mono uppercase tracking-[0.22em] text-on-surface-variant/60">
70
+ ${escapeHtml(label)}
71
+ </span>
72
+ <select
73
+ class="w-full border border-outline-variant/20 bg-surface-container-lowest px-4 py-3 text-sm text-on-surface outline-none transition-colors focus:border-primary-container"
74
+ ${bind ? `data-bind="${escapeHtml(bind)}"` : ""}
75
+ ${name ? `name="${escapeHtml(name)}"` : ""}
76
+ >
77
+ ${options
78
+ .map(
79
+ (option) => `
80
+ <option value="${escapeHtml(option.value)}" ${String(option.value) === String(value) ? "selected" : ""}>
81
+ ${escapeHtml(option.label)}
82
+ </option>
83
+ `
84
+ )
85
+ .join("")}
86
+ </select>
87
+ </label>
88
+ `;
89
+ }
90
+
61
91
  function renderError(error) {
62
92
  if (!error) {
63
93
  return "";
@@ -380,6 +410,279 @@ function renderDeleteRowConfirmForm(modal) {
380
410
  `;
381
411
  }
382
412
 
413
+ function renderChartColumnOptions(analysis, { allowEmpty = false, includeNumericHint = false } = {}) {
414
+ const options = allowEmpty ? [{ value: "", label: "None" }] : [];
415
+
416
+ return options.concat(
417
+ (analysis?.columns ?? []).map((column) => ({
418
+ value: column.name,
419
+ label: `${column.name} (${column.type}${includeNumericHint && column.type === "number" ? " numeric" : ""})`,
420
+ }))
421
+ );
422
+ }
423
+
424
+ function renderChartEditorForm(modal, state) {
425
+ const draft = modal.draft ?? {};
426
+ const analysis = state.charts.result ? analyzeQueryChartResult(state.charts.result) : null;
427
+ const chartTypeOptions = QUERY_CHART_TYPES.map((chartType) => ({
428
+ value: chartType,
429
+ label: getQueryChartTypeLabel(chartType),
430
+ }));
431
+ const columnOptions = renderChartColumnOptions(analysis);
432
+ const optionalColumnOptions = renderChartColumnOptions(analysis, { allowEmpty: true });
433
+ let chartSpecificFields = "";
434
+
435
+ if (draft.chartType === "bar") {
436
+ chartSpecificFields = `
437
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
438
+ ${renderSelectField({
439
+ label: "X Column",
440
+ value: draft.config?.x_column ?? "",
441
+ options: columnOptions,
442
+ bind: "query-chart-draft-config:x_column",
443
+ })}
444
+ ${renderSelectField({
445
+ label: "Y Column",
446
+ value: draft.config?.y_column ?? "",
447
+ options: columnOptions,
448
+ bind: "query-chart-draft-config:y_column",
449
+ })}
450
+ </div>
451
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
452
+ ${renderSelectField({
453
+ label: "Sort Direction",
454
+ value: draft.config?.sort_direction ?? "asc",
455
+ options: [
456
+ { value: "asc", label: "Ascending" },
457
+ { value: "desc", label: "Descending" },
458
+ ],
459
+ bind: "query-chart-draft-config:sort_direction",
460
+ })}
461
+ ${renderCheckboxField({
462
+ label: "Show legend",
463
+ name: "",
464
+ checked: Boolean(draft.config?.show_legend),
465
+ text: "Show legend",
466
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
467
+ ${renderCheckboxField({
468
+ label: "Show labels",
469
+ name: "",
470
+ checked: Boolean(draft.config?.show_labels),
471
+ text: "Show labels",
472
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
473
+ </div>
474
+ `;
475
+ } else if (draft.chartType === "line") {
476
+ chartSpecificFields = `
477
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
478
+ ${renderSelectField({
479
+ label: "X Column",
480
+ value: draft.config?.x_column ?? "",
481
+ options: columnOptions,
482
+ bind: "query-chart-draft-config:x_column",
483
+ })}
484
+ ${renderSelectField({
485
+ label: "Y Column",
486
+ value: draft.config?.y_column ?? "",
487
+ options: columnOptions,
488
+ bind: "query-chart-draft-config:y_column",
489
+ })}
490
+ </div>
491
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-4">
492
+ ${renderSelectField({
493
+ label: "Sort Direction",
494
+ value: draft.config?.sort_direction ?? "asc",
495
+ options: [
496
+ { value: "asc", label: "Ascending" },
497
+ { value: "desc", label: "Descending" },
498
+ ],
499
+ bind: "query-chart-draft-config:sort_direction",
500
+ })}
501
+ ${renderCheckboxField({
502
+ label: "Smooth line",
503
+ name: "",
504
+ checked: Boolean(draft.config?.smooth),
505
+ text: "Smooth line",
506
+ }).replace("<input", '<input data-bind="query-chart-draft-config:smooth"')}
507
+ ${renderCheckboxField({
508
+ label: "Show legend",
509
+ name: "",
510
+ checked: Boolean(draft.config?.show_legend),
511
+ text: "Show legend",
512
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
513
+ ${renderCheckboxField({
514
+ label: "Show labels",
515
+ name: "",
516
+ checked: Boolean(draft.config?.show_labels),
517
+ text: "Show labels",
518
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
519
+ </div>
520
+ `;
521
+ } else if (draft.chartType === "pie") {
522
+ chartSpecificFields = `
523
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
524
+ ${renderSelectField({
525
+ label: "Label Column",
526
+ value: draft.config?.label_column ?? "",
527
+ options: columnOptions,
528
+ bind: "query-chart-draft-config:label_column",
529
+ })}
530
+ ${renderSelectField({
531
+ label: "Value Column",
532
+ value: draft.config?.value_column ?? "",
533
+ options: columnOptions,
534
+ bind: "query-chart-draft-config:value_column",
535
+ })}
536
+ </div>
537
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
538
+ ${renderCheckboxField({
539
+ label: "Donut",
540
+ name: "",
541
+ checked: Boolean(draft.config?.donut),
542
+ text: "Render as donut",
543
+ }).replace("<input", '<input data-bind="query-chart-draft-config:donut"')}
544
+ ${renderCheckboxField({
545
+ label: "Show legend",
546
+ name: "",
547
+ checked: Boolean(draft.config?.show_legend),
548
+ text: "Show legend",
549
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
550
+ ${renderCheckboxField({
551
+ label: "Show labels",
552
+ name: "",
553
+ checked: Boolean(draft.config?.show_labels),
554
+ text: "Show labels",
555
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_labels"')}
556
+ </div>
557
+ `;
558
+ } else if (draft.chartType === "scatter") {
559
+ chartSpecificFields = `
560
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-2">
561
+ ${renderSelectField({
562
+ label: "X Column",
563
+ value: draft.config?.x_column ?? "",
564
+ options: columnOptions,
565
+ bind: "query-chart-draft-config:x_column",
566
+ })}
567
+ ${renderSelectField({
568
+ label: "Y Column",
569
+ value: draft.config?.y_column ?? "",
570
+ options: columnOptions,
571
+ bind: "query-chart-draft-config:y_column",
572
+ })}
573
+ </div>
574
+ <div class="grid grid-cols-1 gap-4 md:grid-cols-3">
575
+ ${renderSelectField({
576
+ label: "Size Column",
577
+ value: draft.config?.size_column ?? "",
578
+ options: optionalColumnOptions,
579
+ bind: "query-chart-draft-config:size_column",
580
+ })}
581
+ ${renderSelectField({
582
+ label: "Series Column",
583
+ value: draft.config?.series_column ?? "",
584
+ options: optionalColumnOptions,
585
+ bind: "query-chart-draft-config:series_column",
586
+ })}
587
+ ${renderCheckboxField({
588
+ label: "Show legend",
589
+ name: "",
590
+ checked: Boolean(draft.config?.show_legend),
591
+ text: "Show legend",
592
+ }).replace("<input", '<input data-bind="query-chart-draft-config:show_legend"')}
593
+ </div>
594
+ `;
595
+ }
596
+
597
+ return `
598
+ <form class="space-y-5" data-form="save-query-chart">
599
+ ${renderField({
600
+ label: "Chart Name",
601
+ name: "chartName",
602
+ value: draft.name ?? "",
603
+ }).replace("<input", '<input data-bind="query-chart-draft:name"')}
604
+ ${renderSelectField({
605
+ label: "Chart Type",
606
+ value: draft.chartType ?? "bar",
607
+ options: chartTypeOptions,
608
+ bind: "query-chart-draft:chartType",
609
+ })}
610
+ ${chartSpecificFields}
611
+ ${
612
+ analysis
613
+ ? `
614
+ <div class="border border-outline-variant/10 bg-surface-container-lowest px-4 py-4">
615
+ <div class="text-[10px] font-mono uppercase tracking-[0.16em] text-on-surface-variant/55">
616
+ Result Columns
617
+ </div>
618
+ <div class="mt-3 flex flex-wrap gap-2">
619
+ ${analysis.columns
620
+ .map(
621
+ (column) => `
622
+ <span class="border border-outline-variant/15 bg-surface-container px-2 py-1 text-[10px] font-mono uppercase tracking-[0.12em] text-on-surface-variant/70">
623
+ ${escapeHtml(column.name)} • ${escapeHtml(column.type)}
624
+ </span>
625
+ `
626
+ )
627
+ .join("")}
628
+ </div>
629
+ </div>
630
+ `
631
+ : ""
632
+ }
633
+ ${renderError(modal.error)}
634
+ <div class="flex items-center justify-end gap-3 pt-2">
635
+ <button
636
+ class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
637
+ data-action="close-modal"
638
+ type="button"
639
+ >
640
+ Cancel
641
+ </button>
642
+ <button
643
+ class="bg-primary-container px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-on-primary"
644
+ type="submit"
645
+ >
646
+ ${modal.submitting ? "Saving..." : draft.mode === "edit" ? "Save Chart" : "Create Chart"}
647
+ </button>
648
+ </div>
649
+ </form>
650
+ `;
651
+ }
652
+
653
+ function renderDeleteChartForm(modal) {
654
+ return `
655
+ <form class="space-y-5" data-form="delete-query-chart">
656
+ <div class="space-y-3">
657
+ <p class="text-sm leading-7 text-on-surface">
658
+ Delete chart <span class="font-bold text-primary-container">${escapeHtml(
659
+ modal.chartName ?? "Chart"
660
+ )}</span>?
661
+ </p>
662
+ <p class="text-sm leading-7 text-on-surface-variant/65">
663
+ The linked query-history entry stays intact. Only this chart definition is removed.
664
+ </p>
665
+ </div>
666
+ ${renderError(modal.error)}
667
+ <div class="flex items-center justify-end gap-3 pt-2">
668
+ <button
669
+ class="border border-outline-variant/20 px-4 py-3 text-xs font-bold uppercase tracking-[0.18em] text-on-surface-variant hover:bg-surface-container-highest"
670
+ data-action="close-modal"
671
+ type="button"
672
+ >
673
+ Cancel
674
+ </button>
675
+ <button
676
+ class="border border-error/25 bg-error-container/10 px-5 py-3 text-xs font-black uppercase tracking-[0.18em] text-error"
677
+ type="submit"
678
+ >
679
+ ${modal.submitting ? "Deleting..." : "Delete Chart"}
680
+ </button>
681
+ </div>
682
+ </form>
683
+ `;
684
+ }
685
+
383
686
  export function renderModal(state) {
384
687
  const modal = state.modal;
385
688
 
@@ -413,6 +716,16 @@ export function renderModal(state) {
413
716
  title: "Delete Row",
414
717
  body: renderDeleteRowConfirmForm(modal),
415
718
  },
719
+ "chart-editor": {
720
+ eyebrow: "Charts // Configure query-based ECharts panel",
721
+ title: modal.draft?.mode === "edit" ? "Edit Chart" : "New Chart",
722
+ body: renderChartEditorForm(modal, state),
723
+ },
724
+ "delete-chart": {
725
+ eyebrow: "Charts // Confirm chart deletion",
726
+ title: "Delete Chart",
727
+ body: renderDeleteChartForm(modal),
728
+ },
416
729
  };
417
730
 
418
731
  const config = contentByKind[modal.kind];
@@ -423,7 +736,7 @@ export function renderModal(state) {
423
736
 
424
737
  return `
425
738
  <div class="fixed inset-0 z-50 flex items-center justify-center bg-background/85 px-4 backdrop-blur-sm">
426
- <div class="w-full max-w-xl border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
739
+ <div class="w-full ${modal.kind === "chart-editor" ? "max-w-3xl" : "max-w-xl"} border border-outline-variant/20 bg-surface-container shadow-[0_24px_80px_rgba(0,0,0,0.45)]">
427
740
  <div class="flex items-start justify-between gap-4 border-b border-outline-variant/10 bg-surface-container-low px-6 py-5">
428
741
  <div>
429
742
  <div class="text-[10px] font-mono uppercase tracking-[0.26em] text-primary-container/70">
@@ -0,0 +1,125 @@
1
+ import {
2
+ buildBarChartOption,
3
+ buildLineChartOption,
4
+ buildPieChartOption,
5
+ buildScatterChartOption,
6
+ } from "../lib/queryChartOptions.js";
7
+ import { analyzeQueryChartResult, validateQueryChartConfig } from "../lib/queryCharts.js";
8
+
9
+ const chartInstances = new Map();
10
+ const resizeObservers = new Map();
11
+
12
+ function getEchartsRuntime() {
13
+ return window.echarts ?? null;
14
+ }
15
+
16
+ function getOptionBuilder(chartType) {
17
+ if (chartType === "bar") {
18
+ return buildBarChartOption;
19
+ }
20
+
21
+ if (chartType === "line") {
22
+ return buildLineChartOption;
23
+ }
24
+
25
+ if (chartType === "pie") {
26
+ return buildPieChartOption;
27
+ }
28
+
29
+ if (chartType === "scatter") {
30
+ return buildScatterChartOption;
31
+ }
32
+
33
+ return null;
34
+ }
35
+
36
+ function disposeChart(chartId) {
37
+ const instance = chartInstances.get(chartId);
38
+
39
+ if (instance) {
40
+ instance.dispose();
41
+ chartInstances.delete(chartId);
42
+ }
43
+
44
+ const observer = resizeObservers.get(chartId);
45
+
46
+ if (observer) {
47
+ observer.disconnect();
48
+ resizeObservers.delete(chartId);
49
+ }
50
+ }
51
+
52
+ export function teardownQueryChartRenderer() {
53
+ Array.from(chartInstances.keys()).forEach((chartId) => disposeChart(chartId));
54
+ }
55
+
56
+ export function mountQueryChartRenderer(state) {
57
+ const echartsRuntime = getEchartsRuntime();
58
+ const charts = state.charts.detail?.charts ?? [];
59
+ const result = state.charts.result;
60
+
61
+ teardownQueryChartRenderer();
62
+
63
+ if (!echartsRuntime || !result || !charts.length) {
64
+ return;
65
+ }
66
+
67
+ const analysis = analyzeQueryChartResult(result);
68
+
69
+ charts.forEach((chart) => {
70
+ const host = document.querySelector(`[data-query-chart-id="${CSS.escape(String(chart.id))}"]`);
71
+
72
+ if (!(host instanceof HTMLElement)) {
73
+ return;
74
+ }
75
+
76
+ const validation = validateQueryChartConfig(chart.chartType, chart.config, analysis);
77
+
78
+ if (!validation.valid) {
79
+ return;
80
+ }
81
+
82
+ const buildOption = getOptionBuilder(chart.chartType);
83
+
84
+ if (!buildOption) {
85
+ return;
86
+ }
87
+
88
+ const instance = echartsRuntime.init(host);
89
+
90
+ instance.setOption(buildOption(chart, result.rows ?? [], analysis), true);
91
+ chartInstances.set(String(chart.id), instance);
92
+
93
+ const resizeObserver = new ResizeObserver(() => {
94
+ instance.resize();
95
+ });
96
+
97
+ resizeObserver.observe(host);
98
+ resizeObservers.set(String(chart.id), resizeObserver);
99
+ });
100
+ }
101
+
102
+ export function exportQueryChartAsPng(chartId) {
103
+ const instance = chartInstances.get(String(chartId));
104
+
105
+ if (!instance) {
106
+ return false;
107
+ }
108
+
109
+ const link = document.createElement("a");
110
+ const chartNode = document.querySelector(
111
+ `[data-query-chart-id="${CSS.escape(String(chartId))}"]`
112
+ );
113
+ const fileName = chartNode?.dataset.chartExportName ?? `chart-${chartId}`;
114
+
115
+ link.href = instance.getDataURL({
116
+ type: "png",
117
+ pixelRatio: 2,
118
+ backgroundColor: "#131313",
119
+ });
120
+ link.download = `${fileName}.png`;
121
+ document.body.appendChild(link);
122
+ link.click();
123
+ link.remove();
124
+ return true;
125
+ }