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.
- package/README.md +107 -0
- package/bin/sqlite-hub.js +285 -4
- package/changelog.md +7 -0
- package/frontend/assets/mockups/sql_editor_croped.png +0 -0
- package/frontend/index.html +1 -0
- package/frontend/js/api.js +34 -0
- package/frontend/js/app.js +99 -2
- package/frontend/js/components/modal.js +314 -1
- package/frontend/js/components/queryChartRenderer.js +125 -0
- package/frontend/js/components/queryEditor.js +35 -6
- package/frontend/js/components/queryHistoryPanel.js +16 -5
- package/frontend/js/components/sidebar.js +1 -0
- package/frontend/js/components/tableDesignerSidebar.js +41 -38
- package/frontend/js/lib/queryChartOptions.js +283 -0
- package/frontend/js/lib/queryCharts.js +560 -0
- package/frontend/js/router.js +8 -0
- package/frontend/js/store.js +618 -0
- package/frontend/js/views/charts.js +471 -0
- package/frontend/js/views/data.js +29 -15
- package/frontend/js/views/editor.js +19 -13
- package/frontend/js/views/structure.js +81 -39
- package/frontend/styles/components.css +5 -13
- package/frontend/styles/views.css +296 -66
- package/package.json +2 -1
- package/server/routes/charts.js +153 -0
- package/server/server.js +6 -0
- package/server/services/storage/appStateStore.js +400 -1
- package/server/services/storage/queryHistoryChartUtils.js +145 -0
|
@@ -52,7 +52,11 @@ export function renderQueryEditor({
|
|
|
52
52
|
exporting = false,
|
|
53
53
|
historyLoading = false,
|
|
54
54
|
historyTotal = 0,
|
|
55
|
+
editorVisible = true,
|
|
56
|
+
historyVisible = true,
|
|
55
57
|
}) {
|
|
58
|
+
const secondaryButtonClass =
|
|
59
|
+
"toolbar-button border border-outline-variant/20 bg-surface-container px-4 py-2 text-[10px] font-bold uppercase tracking-widest text-on-surface transition-colors hover:border-primary-container hover:text-primary-container";
|
|
56
60
|
const left = `
|
|
57
61
|
<div class="flex items-center gap-2 bg-surface-container-lowest px-3 py-1">
|
|
58
62
|
<span class="material-symbols-outlined text-xs text-[#FCE300]">database</span>
|
|
@@ -68,22 +72,41 @@ export function renderQueryEditor({
|
|
|
68
72
|
|
|
69
73
|
const right = `
|
|
70
74
|
<button
|
|
71
|
-
class="
|
|
75
|
+
class="${secondaryButtonClass}"
|
|
76
|
+
data-action="toggle-editor-panel"
|
|
77
|
+
data-next-value="${editorVisible ? "false" : "true"}"
|
|
78
|
+
type="button"
|
|
79
|
+
>
|
|
80
|
+
<span class="material-symbols-outlined text-sm">${editorVisible ? "keyboard_arrow_down" : "terminal"}</span>
|
|
81
|
+
${editorVisible ? "Hide Editor" : "Show Editor"}
|
|
82
|
+
</button>
|
|
83
|
+
<button
|
|
84
|
+
class="${secondaryButtonClass}"
|
|
85
|
+
data-action="toggle-query-history-panel"
|
|
86
|
+
data-next-value="${historyVisible ? "false" : "true"}"
|
|
87
|
+
type="button"
|
|
88
|
+
>
|
|
89
|
+
<span class="material-symbols-outlined text-sm">${historyVisible ? "visibility_off" : "history"}</span>
|
|
90
|
+
${historyVisible ? "Hide History" : "Show History"}
|
|
91
|
+
</button>
|
|
92
|
+
<button
|
|
93
|
+
class="${secondaryButtonClass}"
|
|
72
94
|
data-action="clear-query"
|
|
73
95
|
type="button"
|
|
74
96
|
>
|
|
75
97
|
Clear
|
|
76
98
|
</button>
|
|
77
99
|
<button
|
|
78
|
-
class="
|
|
100
|
+
class="${secondaryButtonClass}"
|
|
79
101
|
data-action="export-query-csv"
|
|
80
102
|
type="button"
|
|
81
103
|
>
|
|
82
104
|
${exporting ? "Exporting..." : "Export CSV"}
|
|
83
105
|
</button>
|
|
84
106
|
<button
|
|
85
|
-
class="bg-primary-container px-
|
|
107
|
+
class="toolbar-button toolbar-button--primary bg-primary-container px-4 py-2 font-headline text-xs font-bold uppercase tracking-widest text-on-primary clipped-corner"
|
|
86
108
|
data-action="execute-query"
|
|
109
|
+
style="--clip-path: polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 0 100%);"
|
|
87
110
|
type="button"
|
|
88
111
|
>
|
|
89
112
|
${executing ? "RUNNING..." : "EXECUTE"}
|
|
@@ -99,9 +122,15 @@ export function renderQueryEditor({
|
|
|
99
122
|
className: "flex-wrap",
|
|
100
123
|
})}
|
|
101
124
|
</div>
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
${
|
|
126
|
+
editorVisible
|
|
127
|
+
? `
|
|
128
|
+
<div class="flex min-h-0 flex-1 flex-col">
|
|
129
|
+
${renderEditorSurface({ query })}
|
|
130
|
+
</div>
|
|
131
|
+
`
|
|
132
|
+
: ""
|
|
133
|
+
}
|
|
105
134
|
</div>
|
|
106
135
|
`;
|
|
107
136
|
}
|
|
@@ -150,11 +150,22 @@ export function renderQueryHistoryPanel({
|
|
|
150
150
|
return `
|
|
151
151
|
<aside class="query-history-panel border-l border-outline-variant/10 bg-surface-container-lowest">
|
|
152
152
|
<div class="border-b border-outline-variant/10 px-4 py-4">
|
|
153
|
-
<div class="flex items-center gap-
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
153
|
+
<div class="flex items-center justify-between gap-3">
|
|
154
|
+
<div class="flex items-center gap-2">
|
|
155
|
+
<span class="material-symbols-outlined text-[18px] text-primary-container">history</span>
|
|
156
|
+
<span class="font-headline text-xs font-black uppercase tracking-[0.18em] text-primary-container">
|
|
157
|
+
Query History
|
|
158
|
+
</span>
|
|
159
|
+
</div>
|
|
160
|
+
<button
|
|
161
|
+
class="query-history-icon-button"
|
|
162
|
+
data-action="toggle-query-history-panel"
|
|
163
|
+
data-next-value="false"
|
|
164
|
+
title="Hide query history"
|
|
165
|
+
type="button"
|
|
166
|
+
>
|
|
167
|
+
<span class="material-symbols-outlined text-[18px]">close</span>
|
|
168
|
+
</button>
|
|
158
169
|
</div>
|
|
159
170
|
<div class="mt-4">${renderQueryHistoryTabs(activeTab, total)}</div>
|
|
160
171
|
<label class="mt-4 block">
|
|
@@ -5,6 +5,7 @@ const sidebarItems = [
|
|
|
5
5
|
{ label: "Connections", href: "#/connections", key: "connections", icon: "database" },
|
|
6
6
|
{ label: "Overview", href: "#/overview", key: "overview", icon: "dashboard" },
|
|
7
7
|
{ label: "Data", href: "#/data", key: "data", icon: "table_rows" },
|
|
8
|
+
{ label: "Charts", href: "#/charts", key: "charts", icon: "bar_chart" },
|
|
8
9
|
{ label: "SQL Editor", href: "#/editor", key: "editor", icon: "terminal" },
|
|
9
10
|
{ label: "Structure", href: "#/structure", key: "structure", icon: "account_tree" },
|
|
10
11
|
{ label: "Table Designer", href: "#/table-designer", key: "tableDesigner", icon: "table_chart" },
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
import { escapeHtml, formatNumber } from
|
|
1
|
+
import { escapeHtml, formatNumber } from '../utils/format.js';
|
|
2
2
|
|
|
3
3
|
function getFilteredTables(tables, searchQuery) {
|
|
4
|
-
|
|
4
|
+
const normalizedSearch = String(searchQuery ?? '')
|
|
5
|
+
.trim()
|
|
6
|
+
.toLowerCase();
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
if (!normalizedSearch) {
|
|
9
|
+
return tables;
|
|
10
|
+
}
|
|
9
11
|
|
|
10
|
-
|
|
12
|
+
return tables.filter(table => table.name.toLowerCase().includes(normalizedSearch));
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export function renderTableDesignerSidebar(state) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const tables = state.tableDesigner.tables ?? [];
|
|
17
|
+
const filteredTables = getFilteredTables(tables, state.tableDesigner.searchQuery);
|
|
18
|
+
const isNewDraft = state.tableDesigner.draft?.mode === 'create';
|
|
17
19
|
|
|
18
|
-
|
|
20
|
+
return `
|
|
19
21
|
<aside class="table-designer-sidebar">
|
|
20
22
|
<div class="table-designer-sidebar__header">
|
|
21
23
|
<div>
|
|
22
|
-
<div class="table-designer-sidebar__eyebrow">
|
|
23
|
-
<div class="table-designer-sidebar__title">Table Designer</div>
|
|
24
|
+
<div class="table-designer-sidebar__eyebrow">Table Designer</div>
|
|
24
25
|
<div class="table-designer-sidebar__meta">
|
|
25
|
-
${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ?
|
|
26
|
+
${escapeHtml(formatNumber(tables.length))} table${tables.length === 1 ? '' : 's'}
|
|
26
27
|
</div>
|
|
27
28
|
</div>
|
|
28
29
|
<div class="table-designer-sidebar__header-actions">
|
|
@@ -58,23 +59,23 @@ export function renderTableDesignerSidebar(state) {
|
|
|
58
59
|
placeholder="Search tables..."
|
|
59
60
|
spellcheck="false"
|
|
60
61
|
type="search"
|
|
61
|
-
value="${escapeHtml(state.tableDesigner.searchQuery ??
|
|
62
|
+
value="${escapeHtml(state.tableDesigner.searchQuery ?? '')}"
|
|
62
63
|
/>
|
|
63
64
|
</label>
|
|
64
65
|
|
|
65
66
|
<div class="table-designer-sidebar__list custom-scrollbar">
|
|
66
67
|
${
|
|
67
|
-
|
|
68
|
-
|
|
68
|
+
state.tableDesigner.loading && !tables.length
|
|
69
|
+
? `
|
|
69
70
|
<div class="table-designer-sidebar__empty">
|
|
70
71
|
<span class="material-symbols-outlined mb-2 text-3xl">progress_activity</span>
|
|
71
72
|
<div>Loading SQLite schema...</div>
|
|
72
73
|
</div>
|
|
73
74
|
`
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
: isNewDraft
|
|
76
|
+
? `
|
|
76
77
|
<button
|
|
77
|
-
class="table-designer-sidebar__item is-active"
|
|
78
|
+
class="table-designer-sidebar__item is-active w-full border border-primary-container/30 bg-surface-container-high px-4 py-3 text-left transition-colors"
|
|
78
79
|
data-action="navigate"
|
|
79
80
|
data-to="/table-designer/new"
|
|
80
81
|
type="button"
|
|
@@ -83,42 +84,44 @@ export function renderTableDesignerSidebar(state) {
|
|
|
83
84
|
<div class="table-designer-sidebar__item-meta">unsaved schema</div>
|
|
84
85
|
</button>
|
|
85
86
|
`
|
|
86
|
-
|
|
87
|
+
: ''
|
|
87
88
|
}
|
|
88
89
|
${
|
|
89
|
-
|
|
90
|
-
|
|
90
|
+
!filteredTables.length && !state.tableDesigner.loading
|
|
91
|
+
? `
|
|
91
92
|
<div class="table-designer-sidebar__empty">
|
|
92
93
|
${
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
94
|
+
tables.length
|
|
95
|
+
? 'No tables match the current search.'
|
|
96
|
+
: 'No tables found. Create the first table in this database.'
|
|
96
97
|
}
|
|
97
98
|
</div>
|
|
98
99
|
`
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
100
|
+
: filteredTables
|
|
101
|
+
.map(
|
|
102
|
+
table => `
|
|
102
103
|
<button
|
|
103
|
-
class="table-designer-sidebar__item ${
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
class="table-designer-sidebar__item w-full border px-4 py-3 text-left transition-colors ${
|
|
105
|
+
!isNewDraft && table.name === state.tableDesigner.selectedTableName
|
|
106
|
+
? 'is-active border-primary-container/30 bg-surface-container-high'
|
|
107
|
+
: 'border-outline-variant/10 bg-surface-container-lowest hover:bg-surface-container-high'
|
|
107
108
|
}"
|
|
108
109
|
data-action="navigate"
|
|
109
110
|
data-to="/table-designer/${encodeURIComponent(table.name)}"
|
|
110
111
|
type="button"
|
|
111
112
|
>
|
|
112
|
-
<div class="table-designer-sidebar__item-name
|
|
113
|
-
|
|
113
|
+
<div class="table-designer-sidebar__item-name ${
|
|
114
|
+
!isNewDraft && table.name === state.tableDesigner.selectedTableName ? 'is-active' : ''
|
|
115
|
+
}">${escapeHtml(table.name)}</div>
|
|
116
|
+
<div class="mt-1 truncate text-[10px] uppercase tracking-[0.16em] text-on-surface-variant/45">
|
|
114
117
|
${escapeHtml(formatNumber(table.columnCount ?? 0))} column${
|
|
115
|
-
|
|
118
|
+
Number(table.columnCount ?? 0) === 1 ? '' : 's'
|
|
116
119
|
}
|
|
117
120
|
</div>
|
|
118
121
|
</button>
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
`,
|
|
123
|
+
)
|
|
124
|
+
.join('')
|
|
122
125
|
}
|
|
123
126
|
</div>
|
|
124
127
|
</aside>
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import {
|
|
2
|
+
coerceDatetimeChartValue,
|
|
3
|
+
coerceNumericChartValue,
|
|
4
|
+
formatTimeOfDayAxisValue,
|
|
5
|
+
formatQueryChartAxisValue,
|
|
6
|
+
getAnalysisColumn,
|
|
7
|
+
sortQueryChartRows,
|
|
8
|
+
} from "./queryCharts.js";
|
|
9
|
+
|
|
10
|
+
const CHART_PALETTE = ["#FCE300", "#2DFAFF", "#FFB4AB", "#CDC7AB", "#7DD3FC", "#86EFAC"];
|
|
11
|
+
|
|
12
|
+
function buildCommonOption() {
|
|
13
|
+
return {
|
|
14
|
+
animationDuration: 220,
|
|
15
|
+
backgroundColor: "transparent",
|
|
16
|
+
color: CHART_PALETTE,
|
|
17
|
+
textStyle: {
|
|
18
|
+
color: "#e5e2e1",
|
|
19
|
+
fontFamily: "Inter, sans-serif",
|
|
20
|
+
},
|
|
21
|
+
tooltip: {
|
|
22
|
+
backgroundColor: "#201f1f",
|
|
23
|
+
borderColor: "rgba(252, 227, 0, 0.14)",
|
|
24
|
+
textStyle: {
|
|
25
|
+
color: "#e5e2e1",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
grid: {
|
|
29
|
+
left: 48,
|
|
30
|
+
right: 24,
|
|
31
|
+
top: 36,
|
|
32
|
+
bottom: 48,
|
|
33
|
+
containLabel: true,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildAxisLabel(color = "#cdc7ab") {
|
|
39
|
+
return {
|
|
40
|
+
color,
|
|
41
|
+
fontFamily: "Roboto Mono, monospace",
|
|
42
|
+
fontSize: 11,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function buildLineLabelConfig(enabled) {
|
|
47
|
+
return enabled
|
|
48
|
+
? {
|
|
49
|
+
show: true,
|
|
50
|
+
color: "#e5e2e1",
|
|
51
|
+
fontFamily: "Roboto Mono, monospace",
|
|
52
|
+
fontSize: 10,
|
|
53
|
+
}
|
|
54
|
+
: { show: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildBarChartOption(chart, rows) {
|
|
58
|
+
const sortedRows = sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
...buildCommonOption(),
|
|
62
|
+
legend: {
|
|
63
|
+
show: chart.config.show_legend,
|
|
64
|
+
textStyle: { color: "#cdc7ab" },
|
|
65
|
+
},
|
|
66
|
+
xAxis: {
|
|
67
|
+
type: "category",
|
|
68
|
+
data: sortedRows.map((row) => formatQueryChartAxisValue(row[chart.config.x_column])),
|
|
69
|
+
axisLabel: buildAxisLabel(),
|
|
70
|
+
axisLine: { lineStyle: { color: "rgba(205, 199, 171, 0.28)" } },
|
|
71
|
+
},
|
|
72
|
+
yAxis: {
|
|
73
|
+
type: "value",
|
|
74
|
+
axisLabel: buildAxisLabel(),
|
|
75
|
+
splitLine: { lineStyle: { color: "rgba(205, 199, 171, 0.12)" } },
|
|
76
|
+
},
|
|
77
|
+
series: [
|
|
78
|
+
{
|
|
79
|
+
name: chart.name,
|
|
80
|
+
type: "bar",
|
|
81
|
+
label: buildLineLabelConfig(chart.config.show_labels),
|
|
82
|
+
emphasis: { focus: "series" },
|
|
83
|
+
data: sortedRows.map((row) => coerceNumericChartValue(row[chart.config.y_column]) ?? 0),
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildLineChartOption(chart, rows) {
|
|
90
|
+
const sortedRows = sortQueryChartRows(rows, chart.config.x_column, chart.config.sort_direction);
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
...buildCommonOption(),
|
|
94
|
+
legend: {
|
|
95
|
+
show: chart.config.show_legend,
|
|
96
|
+
textStyle: { color: "#cdc7ab" },
|
|
97
|
+
},
|
|
98
|
+
xAxis: {
|
|
99
|
+
type: "category",
|
|
100
|
+
data: sortedRows.map((row) => formatQueryChartAxisValue(row[chart.config.x_column])),
|
|
101
|
+
axisLabel: buildAxisLabel(),
|
|
102
|
+
axisLine: { lineStyle: { color: "rgba(205, 199, 171, 0.28)" } },
|
|
103
|
+
},
|
|
104
|
+
yAxis: {
|
|
105
|
+
type: "value",
|
|
106
|
+
axisLabel: buildAxisLabel(),
|
|
107
|
+
splitLine: { lineStyle: { color: "rgba(205, 199, 171, 0.12)" } },
|
|
108
|
+
},
|
|
109
|
+
series: [
|
|
110
|
+
{
|
|
111
|
+
name: chart.name,
|
|
112
|
+
type: "line",
|
|
113
|
+
smooth: chart.config.smooth,
|
|
114
|
+
showSymbol: sortedRows.length < 60,
|
|
115
|
+
symbolSize: 8,
|
|
116
|
+
lineStyle: {
|
|
117
|
+
width: 3,
|
|
118
|
+
},
|
|
119
|
+
label: buildLineLabelConfig(chart.config.show_labels),
|
|
120
|
+
data: sortedRows.map((row) => coerceNumericChartValue(row[chart.config.y_column]) ?? 0),
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function buildPieChartOption(chart, rows) {
|
|
127
|
+
return {
|
|
128
|
+
...buildCommonOption(),
|
|
129
|
+
legend: {
|
|
130
|
+
show: chart.config.show_legend,
|
|
131
|
+
orient: "vertical",
|
|
132
|
+
right: 0,
|
|
133
|
+
top: "middle",
|
|
134
|
+
textStyle: { color: "#cdc7ab" },
|
|
135
|
+
},
|
|
136
|
+
series: [
|
|
137
|
+
{
|
|
138
|
+
name: chart.name,
|
|
139
|
+
type: "pie",
|
|
140
|
+
radius: chart.config.donut ? ["42%", "70%"] : "70%",
|
|
141
|
+
center: chart.config.show_legend ? ["36%", "50%"] : ["50%", "50%"],
|
|
142
|
+
label: {
|
|
143
|
+
show: chart.config.show_labels,
|
|
144
|
+
color: "#e5e2e1",
|
|
145
|
+
formatter: "{b}: {c}",
|
|
146
|
+
fontSize: 11,
|
|
147
|
+
},
|
|
148
|
+
data: rows.map((row) => ({
|
|
149
|
+
name: formatQueryChartAxisValue(row[chart.config.label_column]),
|
|
150
|
+
value: coerceNumericChartValue(row[chart.config.value_column]) ?? 0,
|
|
151
|
+
})),
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveScatterAxisType(column) {
|
|
158
|
+
if (column?.type === "datetime") {
|
|
159
|
+
return column.datetimeKind === "time" ? "time-of-day" : "time";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return "value";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function buildScatterAxisConfig(column, columnName) {
|
|
166
|
+
const axisType = resolveScatterAxisType(column);
|
|
167
|
+
const baseConfig = {
|
|
168
|
+
axisLabel: buildAxisLabel(),
|
|
169
|
+
splitLine: { lineStyle: { color: "rgba(205, 199, 171, 0.12)" } },
|
|
170
|
+
name: columnName,
|
|
171
|
+
nameTextStyle: buildAxisLabel("#FCE300"),
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (axisType === "time") {
|
|
175
|
+
return {
|
|
176
|
+
...baseConfig,
|
|
177
|
+
type: "time",
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (axisType === "time-of-day") {
|
|
182
|
+
return {
|
|
183
|
+
...baseConfig,
|
|
184
|
+
type: "value",
|
|
185
|
+
axisLabel: {
|
|
186
|
+
...buildAxisLabel(),
|
|
187
|
+
formatter: (value) => formatTimeOfDayAxisValue(value),
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
...baseConfig,
|
|
194
|
+
type: "value",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function coerceScatterAxisValue(value, axisType) {
|
|
199
|
+
if (axisType === "time" || axisType === "time-of-day") {
|
|
200
|
+
return coerceDatetimeChartValue(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return coerceNumericChartValue(value);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function buildScatterSeries(chart, rows, analysis) {
|
|
207
|
+
const groups = new Map();
|
|
208
|
+
const xColumn = getAnalysisColumn(analysis, chart.config.x_column);
|
|
209
|
+
const yColumn = getAnalysisColumn(analysis, chart.config.y_column);
|
|
210
|
+
const xAxisType = resolveScatterAxisType(xColumn);
|
|
211
|
+
const yAxisType = resolveScatterAxisType(yColumn);
|
|
212
|
+
const sizeValues = rows
|
|
213
|
+
.map((row) => coerceNumericChartValue(row[chart.config.size_column]))
|
|
214
|
+
.filter((value) => value !== null);
|
|
215
|
+
const minSize = sizeValues.length ? Math.min(...sizeValues) : 0;
|
|
216
|
+
const maxSize = sizeValues.length ? Math.max(...sizeValues) : 0;
|
|
217
|
+
|
|
218
|
+
function resolveSymbolSize(sizeValue) {
|
|
219
|
+
if (sizeValue === null || sizeValue === undefined) {
|
|
220
|
+
return 14;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (maxSize <= minSize) {
|
|
224
|
+
return 20;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return 10 + ((sizeValue - minSize) / (maxSize - minSize)) * 24;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
rows.forEach((row) => {
|
|
231
|
+
const seriesName = chart.config.series_column
|
|
232
|
+
? formatQueryChartAxisValue(row[chart.config.series_column])
|
|
233
|
+
: chart.name;
|
|
234
|
+
const x = coerceScatterAxisValue(row[chart.config.x_column], xAxisType);
|
|
235
|
+
const y = coerceScatterAxisValue(row[chart.config.y_column], yAxisType);
|
|
236
|
+
|
|
237
|
+
if (x === null || y === null) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const point = {
|
|
242
|
+
value: [
|
|
243
|
+
x,
|
|
244
|
+
y,
|
|
245
|
+
chart.config.size_column ? coerceNumericChartValue(row[chart.config.size_column]) : null,
|
|
246
|
+
],
|
|
247
|
+
symbolSize: resolveSymbolSize(
|
|
248
|
+
chart.config.size_column ? coerceNumericChartValue(row[chart.config.size_column]) : null
|
|
249
|
+
),
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
if (!groups.has(seriesName)) {
|
|
253
|
+
groups.set(seriesName, []);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
groups.get(seriesName).push(point);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
return Array.from(groups.entries()).map(([seriesName, data]) => ({
|
|
260
|
+
name: seriesName,
|
|
261
|
+
type: "scatter",
|
|
262
|
+
data,
|
|
263
|
+
symbolSize(value, params) {
|
|
264
|
+
return params?.data?.symbolSize ?? resolveSymbolSize(value?.[2] ?? null);
|
|
265
|
+
},
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function buildScatterChartOption(chart, rows, analysis = null) {
|
|
270
|
+
const xColumn = getAnalysisColumn(analysis, chart.config.x_column);
|
|
271
|
+
const yColumn = getAnalysisColumn(analysis, chart.config.y_column);
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
...buildCommonOption(),
|
|
275
|
+
legend: {
|
|
276
|
+
show: chart.config.show_legend,
|
|
277
|
+
textStyle: { color: "#cdc7ab" },
|
|
278
|
+
},
|
|
279
|
+
xAxis: buildScatterAxisConfig(xColumn, chart.config.x_column),
|
|
280
|
+
yAxis: buildScatterAxisConfig(yColumn, chart.config.y_column),
|
|
281
|
+
series: buildScatterSeries(chart, rows, analysis),
|
|
282
|
+
};
|
|
283
|
+
}
|