@graphenedata/cli 0.0.15 → 0.0.17
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 +174 -0
- package/dist/cli/bigQuery-OQUNH3VT.js +75 -0
- package/dist/cli/bigQuery-OQUNH3VT.js.map +7 -0
- package/dist/cli/chunk-56K2FF57.js +53 -0
- package/dist/cli/chunk-56K2FF57.js.map +7 -0
- package/dist/cli/chunk-TZTTALAV.js +12868 -0
- package/dist/cli/chunk-TZTTALAV.js.map +7 -0
- package/dist/cli/cli.js +260 -11196
- package/dist/cli/clickhouse-S3BJSKND.js +65 -0
- package/dist/cli/clickhouse-S3BJSKND.js.map +7 -0
- package/dist/cli/duckdb-TKVMONRK.js +87 -0
- package/dist/cli/duckdb-TKVMONRK.js.map +7 -0
- package/dist/cli/serve2-S2LL4D4D.js +448 -0
- package/dist/cli/serve2-S2LL4D4D.js.map +7 -0
- package/dist/cli/snowflake-3VPDEYYP.js +128 -0
- package/dist/cli/snowflake-3VPDEYYP.js.map +7 -0
- package/dist/index.d.ts +63 -0
- package/dist/lang/index.d.ts +63 -0
- package/dist/skills/graphene/SKILL.md +156 -95
- package/dist/skills/graphene/references/big-value.md +6 -41
- package/dist/skills/graphene/references/date-range.md +64 -0
- package/dist/skills/graphene/references/dropdown.md +3 -4
- package/dist/skills/graphene/references/echarts.md +162 -0
- package/dist/skills/graphene/references/gsql.md +55 -25
- package/dist/skills/graphene/references/model-gsql.md +70 -0
- package/dist/skills/graphene/references/table.md +13 -14
- package/dist/skills/graphene/references/text-input.md +2 -1
- package/dist/ui/app.css +239 -340
- package/dist/ui/component-utilities/dataShaping.ts +484 -0
- package/dist/ui/component-utilities/dataSummary.ts +57 -0
- package/dist/ui/component-utilities/enrich.ts +793 -0
- package/dist/ui/component-utilities/format.ts +177 -0
- package/dist/ui/component-utilities/inputUtils.ts +44 -8
- package/dist/ui/component-utilities/theme.ts +200 -0
- package/dist/ui/component-utilities/themeStores.ts +21 -8
- package/dist/ui/component-utilities/types.ts +70 -0
- package/dist/ui/components/AreaChart.svelte +57 -105
- package/dist/ui/components/BarChart.svelte +71 -129
- package/dist/ui/components/BigValue.svelte +24 -40
- package/dist/ui/components/Column.svelte +10 -18
- package/dist/ui/components/DateRange.svelte +54 -21
- package/dist/ui/components/Dropdown.svelte +47 -26
- package/dist/ui/components/DropdownOption.svelte +1 -2
- package/dist/ui/components/ECharts.svelte +181 -67
- package/dist/ui/components/InlineDelta.svelte +50 -31
- package/dist/ui/components/LineChart.svelte +54 -125
- package/dist/ui/components/PieChart.svelte +27 -37
- package/dist/ui/components/QueryLoad.svelte +77 -45
- package/dist/ui/components/Row.svelte +2 -1
- package/dist/ui/components/ScatterPlot.svelte +52 -0
- package/dist/ui/components/Skeleton.svelte +32 -0
- package/dist/ui/components/Table.svelte +3 -2
- package/dist/ui/components/TableGroupRow.svelte +28 -36
- package/dist/ui/components/TableHarness.svelte +32 -0
- package/dist/ui/components/TableHeader.svelte +34 -59
- package/dist/ui/components/TableRow.svelte +14 -38
- package/dist/ui/components/TableSubtotalRow.svelte +18 -21
- package/dist/ui/components/TableTotalRow.svelte +27 -37
- package/dist/ui/components/TextInput.svelte +13 -12
- package/dist/ui/components/Value.svelte +25 -0
- package/dist/ui/components/_Table.svelte +72 -70
- package/dist/ui/internal/ChartGallery.svelte +527 -0
- package/dist/ui/internal/ErrorDisplay.svelte +22 -97
- package/dist/ui/internal/LocalApp.svelte +84 -19
- package/dist/ui/internal/PageNavGroup.svelte +269 -0
- package/dist/ui/internal/Sidebar.svelte +178 -0
- package/dist/ui/internal/SidebarToggle.svelte +47 -0
- package/dist/ui/internal/StyleGallery.svelte +244 -0
- package/dist/ui/internal/clientCache.ts +2 -2
- package/dist/ui/internal/pageInputs.svelte.js +292 -0
- package/dist/ui/internal/queryEngine.ts +112 -129
- package/dist/ui/internal/runSocket.ts +31 -14
- package/dist/ui/internal/sidebar.svelte.js +18 -0
- package/dist/ui/internal/telemetry.ts +51 -16
- package/dist/ui/internal/types.d.ts +7 -0
- package/dist/ui/web.js +30 -11
- package/package.json +40 -38
- package/dist/skills/graphene/references/area-chart.md +0 -95
- package/dist/skills/graphene/references/bar-chart.md +0 -112
- package/dist/skills/graphene/references/line-chart.md +0 -108
- package/dist/skills/graphene/references/pie-chart.md +0 -29
- package/dist/skills/graphene/references/value-formats.md +0 -104
- package/dist/ui/component-utilities/autoFormatting.js +0 -280
- package/dist/ui/component-utilities/builtInFormats.js +0 -481
- package/dist/ui/component-utilities/chartContext.js +0 -12
- package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
- package/dist/ui/component-utilities/checkInputs.js +0 -84
- package/dist/ui/component-utilities/convert.js +0 -15
- package/dist/ui/component-utilities/dateParsing.js +0 -56
- package/dist/ui/component-utilities/dropdownContext.ts +0 -1
- package/dist/ui/component-utilities/echarts.js +0 -252
- package/dist/ui/component-utilities/echartsThemes.js +0 -443
- package/dist/ui/component-utilities/formatTitle.js +0 -24
- package/dist/ui/component-utilities/formatting.js +0 -241
- package/dist/ui/component-utilities/getColumnExtents.js +0 -79
- package/dist/ui/component-utilities/getColumnSummary.js +0 -62
- package/dist/ui/component-utilities/getCompletedData.js +0 -122
- package/dist/ui/component-utilities/getDistinctCount.js +0 -7
- package/dist/ui/component-utilities/getDistinctValues.js +0 -15
- package/dist/ui/component-utilities/getSeriesConfig.js +0 -231
- package/dist/ui/component-utilities/getSortedData.js +0 -9
- package/dist/ui/component-utilities/getStackPercentages.js +0 -45
- package/dist/ui/component-utilities/getStackedData.js +0 -19
- package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
- package/dist/ui/component-utilities/globalContexts.js +0 -1
- package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
- package/dist/ui/component-utilities/replaceNulls.js +0 -16
- package/dist/ui/component-utilities/tableUtils.ts +0 -107
- package/dist/ui/component-utilities/tidyWithTypes.js +0 -9
- package/dist/ui/components/Area.svelte +0 -214
- package/dist/ui/components/Bar.svelte +0 -347
- package/dist/ui/components/Chart.svelte +0 -995
- package/dist/ui/components/Line.svelte +0 -227
- package/dist/ui/internal/NavSidebar.svelte +0 -396
- package/dist/ui/internal/theme.ts +0 -60
- package/dist/ui/public/inter-latin-ext.woff2 +0 -0
- package/dist/ui/public/inter-latin.woff2 +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import {onMount} from 'svelte'
|
|
3
3
|
import {toBoolean} from '../component-utilities/inputUtils'
|
|
4
|
+
import {captureInitial, getPageInputs} from '../internal/pageInputs.svelte.js'
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
6
7
|
name: string
|
|
@@ -27,13 +28,15 @@
|
|
|
27
28
|
let mounted = false
|
|
28
29
|
let queryKey = ''
|
|
29
30
|
let queryHandler: ((res: {rows?: any[]; error?: any}) => void) | null = null
|
|
31
|
+
let pageInputs = getPageInputs()
|
|
32
|
+
let field = captureInitial(() => pageInputs.dateRange(name))
|
|
30
33
|
|
|
31
34
|
let domainStart: string | null = $state(null)
|
|
32
35
|
let domainEnd: string | null = $state(null)
|
|
33
36
|
|
|
34
37
|
let currentStart: string | null = $state(null)
|
|
35
38
|
let currentEnd: string | null = $state(null)
|
|
36
|
-
let currentPreset: string = $state(
|
|
39
|
+
let currentPreset: string | null = $state(null)
|
|
37
40
|
let touched = false
|
|
38
41
|
|
|
39
42
|
let hidePrint = $derived(toBoolean(hideDuringPrint))
|
|
@@ -46,16 +49,16 @@
|
|
|
46
49
|
|
|
47
50
|
onMount(() => {
|
|
48
51
|
mounted = true
|
|
49
|
-
currentStart = normalizeInput(start)
|
|
50
|
-
currentEnd = normalizeInput(end)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
52
|
+
currentStart = field.hasExternalValue ? field.value.start : normalizeInput(start)
|
|
53
|
+
currentEnd = field.hasExternalValue ? field.value.end : normalizeInput(end)
|
|
54
|
+
currentPreset = inferPreset(currentStart, currentEnd)
|
|
55
|
+
if (field.hasExternalValue) updateParams()
|
|
56
|
+
else if (defaultValue && presetList.includes(defaultValue)) applyPreset(defaultValue, false)
|
|
57
|
+
else updateParams()
|
|
56
58
|
refreshQuery()
|
|
57
59
|
return () => {
|
|
58
60
|
mounted = false
|
|
61
|
+
field.destroy()
|
|
59
62
|
if (queryHandler) {
|
|
60
63
|
window.$GRAPHENE?.unsubscribe?.(queryHandler)
|
|
61
64
|
queryHandler = null
|
|
@@ -67,6 +70,12 @@
|
|
|
67
70
|
refreshQuery()
|
|
68
71
|
})
|
|
69
72
|
|
|
73
|
+
$effect(() => {
|
|
74
|
+
if (currentStart === field.value.start && currentEnd === field.value.end) return
|
|
75
|
+
if (!mounted) return
|
|
76
|
+
setRange(field.value.start, field.value.end, inferPreset(field.value.start, field.value.end), {persist: false})
|
|
77
|
+
})
|
|
78
|
+
|
|
70
79
|
function refreshQuery() {
|
|
71
80
|
if (!mounted) return
|
|
72
81
|
let key = data && dates ? `${data}::${dates}` : ''
|
|
@@ -78,7 +87,7 @@
|
|
|
78
87
|
queryKey = key
|
|
79
88
|
if (!data || !dates) return
|
|
80
89
|
let handler = (res: {rows?: any[]; error?: any}) => {
|
|
81
|
-
if (res.error || !res.rows?.length) return
|
|
90
|
+
if (!res || res.error || !res.rows?.length) return
|
|
82
91
|
let values = res.rows
|
|
83
92
|
.map(row => normalizeInput(row[dates]))
|
|
84
93
|
.filter((val): val is string => !!val)
|
|
@@ -86,13 +95,15 @@
|
|
|
86
95
|
values.sort()
|
|
87
96
|
domainStart = values[0]
|
|
88
97
|
domainEnd = values[values.length - 1]
|
|
89
|
-
if (
|
|
98
|
+
if (field.hasExternalValue) {
|
|
99
|
+
currentPreset = inferPreset(currentStart, currentEnd)
|
|
100
|
+
} else if (!touched) {
|
|
90
101
|
if (defaultValue && presetList.includes(defaultValue)) {
|
|
91
102
|
applyPreset(defaultValue, false)
|
|
92
103
|
} else {
|
|
93
104
|
let startCandidate = currentStart ?? domainStart
|
|
94
105
|
let endCandidate = currentEnd ?? (domainEnd ? addDaysString(domainEnd, 1) : null)
|
|
95
|
-
setRange(startCandidate, endCandidate, currentPreset, false)
|
|
106
|
+
setRange(startCandidate, endCandidate, currentPreset, {markTouched: false, persist: true})
|
|
96
107
|
}
|
|
97
108
|
}
|
|
98
109
|
}
|
|
@@ -149,27 +160,48 @@
|
|
|
149
160
|
return copy
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
|
|
163
|
+
// We only persist start/end in URL state, so the preset label is inferred by matching
|
|
164
|
+
// the current range against the configured preset definitions.
|
|
165
|
+
function inferPreset(startValue: string | null, endValue: string | null): string | null {
|
|
166
|
+
if (!startValue && !endValue) return null
|
|
167
|
+
let baseEnd = (() => {
|
|
168
|
+
if (endValue) {
|
|
169
|
+
let parsed = new Date(endValue)
|
|
170
|
+
if (!Number.isNaN(parsed.getTime())) return addDays(parsed, -1)
|
|
171
|
+
}
|
|
172
|
+
if (domainEnd) return new Date(domainEnd)
|
|
173
|
+
return new Date()
|
|
174
|
+
})()
|
|
175
|
+
if (Number.isNaN(baseEnd.getTime())) return null
|
|
176
|
+
for (let preset of presetList) {
|
|
177
|
+
let range = computePresetRange(preset, baseEnd)
|
|
178
|
+
let presetStart = range?.start ? formatDate(range.start) : null
|
|
179
|
+
let presetEnd = range?.end ? formatDate(range.end) : null
|
|
180
|
+
if (presetStart === startValue && presetEnd === endValue) return preset
|
|
181
|
+
}
|
|
182
|
+
return null
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function setRange(startValue: string | null, endValue: string | null, preset: string | null, {markTouched = false, persist = true}: {markTouched?: boolean; persist?: boolean} = {}) {
|
|
153
186
|
currentStart = startValue
|
|
154
187
|
currentEnd = endValue
|
|
155
188
|
currentPreset = preset
|
|
156
189
|
if (markTouched) touched = true
|
|
157
|
-
updateParams()
|
|
190
|
+
if (persist) updateParams()
|
|
158
191
|
}
|
|
159
192
|
|
|
160
193
|
function updateParams() {
|
|
161
|
-
|
|
162
|
-
window.$GRAPHENE.updateParam(`${name}_end`, currentEnd)
|
|
194
|
+
field.set({start: currentStart, end: currentEnd})
|
|
163
195
|
}
|
|
164
196
|
|
|
165
197
|
function onStartChange(event: Event) {
|
|
166
198
|
let value = (event.currentTarget as HTMLInputElement).value || null
|
|
167
|
-
setRange(value, currentEnd,
|
|
199
|
+
setRange(value, currentEnd, null, {markTouched: true, persist: true})
|
|
168
200
|
}
|
|
169
201
|
|
|
170
202
|
function onEndChange(event: Event) {
|
|
171
203
|
let value = (event.currentTarget as HTMLInputElement).value || null
|
|
172
|
-
setRange(currentStart, value,
|
|
204
|
+
setRange(currentStart, value, null, {markTouched: true, persist: true})
|
|
173
205
|
}
|
|
174
206
|
|
|
175
207
|
function applyPreset(preset: string, markTouched = true) {
|
|
@@ -183,7 +215,7 @@
|
|
|
183
215
|
if (!range) return
|
|
184
216
|
let startVal = range.start ? formatDate(range.start) : null
|
|
185
217
|
let endVal = range.end ? formatDate(range.end) : null
|
|
186
|
-
setRange(startVal, endVal, preset, markTouched)
|
|
218
|
+
setRange(startVal, endVal, preset, {markTouched, persist: true})
|
|
187
219
|
}
|
|
188
220
|
|
|
189
221
|
function computePresetRange(preset: string, baseEndInclusive: Date): {start: Date | null; end: Date | null} | null {
|
|
@@ -257,7 +289,7 @@
|
|
|
257
289
|
function onPresetChange(event: Event) {
|
|
258
290
|
let value = (event.currentTarget as HTMLSelectElement).value
|
|
259
291
|
if (!value) {
|
|
260
|
-
currentPreset =
|
|
292
|
+
currentPreset = null
|
|
261
293
|
touched = true
|
|
262
294
|
return
|
|
263
295
|
}
|
|
@@ -300,6 +332,7 @@
|
|
|
300
332
|
}
|
|
301
333
|
}
|
|
302
334
|
.input-label {
|
|
335
|
+
font-family: var(--font-ui);
|
|
303
336
|
font-size: 12px;
|
|
304
337
|
font-weight: 600;
|
|
305
338
|
color: var(--input-label-color, #374151);
|
|
@@ -323,7 +356,7 @@
|
|
|
323
356
|
border: 1px solid rgba(107, 114, 128, 0.4);
|
|
324
357
|
font-size: 14px;
|
|
325
358
|
min-width: 150px;
|
|
326
|
-
font-family: var(--
|
|
359
|
+
font-family: var(--font-ui);
|
|
327
360
|
font-synthesis: none;
|
|
328
361
|
}
|
|
329
362
|
.preset-select {
|
|
@@ -332,7 +365,7 @@
|
|
|
332
365
|
border-radius: 6px;
|
|
333
366
|
border: 1px solid rgba(107, 114, 128, 0.4);
|
|
334
367
|
font-size: 13px;
|
|
335
|
-
font-family: var(--
|
|
368
|
+
font-family: var(--font-ui);
|
|
336
369
|
font-synthesis: none;
|
|
337
370
|
}
|
|
338
371
|
</style>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import {onMount, setContext, tick, type Snippet} from 'svelte'
|
|
3
|
-
import {DROPDOWN_CONTEXT} from '../component-utilities/dropdownContext'
|
|
4
3
|
import {ensureArray, toBoolean} from '../component-utilities/inputUtils'
|
|
4
|
+
import {captureInitial, getPageInputs} from '../internal/pageInputs.svelte.js'
|
|
5
5
|
|
|
6
6
|
interface Option {
|
|
7
7
|
value: any
|
|
@@ -42,6 +42,8 @@
|
|
|
42
42
|
let touched = false
|
|
43
43
|
let queryHandler: ((res: {rows?: any[]; error?: any}) => void) | null = null
|
|
44
44
|
let queryKey = ''
|
|
45
|
+
let pageInputs = getPageInputs()
|
|
46
|
+
let field = captureInitial(() => pageInputs.dropdown(name, toBoolean(multiple)))
|
|
45
47
|
|
|
46
48
|
let isOpen = $state(false)
|
|
47
49
|
let searchTerm = $state('')
|
|
@@ -59,7 +61,7 @@
|
|
|
59
61
|
syncSelection(false)
|
|
60
62
|
}
|
|
61
63
|
}
|
|
62
|
-
setContext(
|
|
64
|
+
setContext('dropdown', registerOption)
|
|
63
65
|
|
|
64
66
|
const optionKey = (val: any): string => {
|
|
65
67
|
if (val === null) return 'null'
|
|
@@ -104,6 +106,12 @@
|
|
|
104
106
|
if (isOpen) activeIndex = ensureActiveIndex(activeIndex, filteredOptions)
|
|
105
107
|
})
|
|
106
108
|
|
|
109
|
+
$effect(() => {
|
|
110
|
+
if (!sameSelection(selection, field.value)) {
|
|
111
|
+
setSelection(field.value, {persist: false})
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
|
|
107
115
|
function setupQuery() {
|
|
108
116
|
if (!mounted) return
|
|
109
117
|
let key = data ? `${data}::${value}::${resolvedLabelField || ''}` : ''
|
|
@@ -121,7 +129,7 @@
|
|
|
121
129
|
let columns = [value]
|
|
122
130
|
if (resolvedLabelField && resolvedLabelField !== value) columns.push(resolvedLabelField)
|
|
123
131
|
let handler = (res: {rows?: any[]; error?: any}) => {
|
|
124
|
-
if (res.error) return
|
|
132
|
+
if (!res || res.error) return
|
|
125
133
|
if (!res.rows) return
|
|
126
134
|
queryOptions = res.rows.map(row => ({
|
|
127
135
|
value: row[value],
|
|
@@ -273,12 +281,12 @@
|
|
|
273
281
|
let exists = selection.some(val => optionKey(val) === key)
|
|
274
282
|
if (exists) {
|
|
275
283
|
let next = selection.filter(val => optionKey(val) !== key)
|
|
276
|
-
setSelection(next, true)
|
|
284
|
+
setSelection(next, {fromUser: true, persist: true})
|
|
277
285
|
} else {
|
|
278
|
-
setSelection([...selection, opt.value], true)
|
|
286
|
+
setSelection([...selection, opt.value], {fromUser: true, persist: true})
|
|
279
287
|
}
|
|
280
288
|
} else {
|
|
281
|
-
setSelection([opt.value], true)
|
|
289
|
+
setSelection([opt.value], {fromUser: true, persist: true})
|
|
282
290
|
closeMenu(fromKeyboard)
|
|
283
291
|
}
|
|
284
292
|
}
|
|
@@ -295,8 +303,11 @@
|
|
|
295
303
|
|
|
296
304
|
onMount(() => {
|
|
297
305
|
mounted = true
|
|
298
|
-
|
|
299
|
-
|
|
306
|
+
if (field.hasExternalValue) setSelection(field.value, {persist: false})
|
|
307
|
+
else {
|
|
308
|
+
let defaults = ensureArray(defaultValue)
|
|
309
|
+
if (!hasNoDefault && defaults.length) setSelection(defaults, {persist: true})
|
|
310
|
+
}
|
|
300
311
|
syncSelection(false)
|
|
301
312
|
setupQuery()
|
|
302
313
|
if (typeof document !== 'undefined') {
|
|
@@ -305,6 +316,7 @@
|
|
|
305
316
|
}
|
|
306
317
|
return () => {
|
|
307
318
|
mounted = false
|
|
319
|
+
field.destroy()
|
|
308
320
|
if (queryHandler) {
|
|
309
321
|
window.$GRAPHENE?.unsubscribe?.(queryHandler)
|
|
310
322
|
queryHandler = null
|
|
@@ -327,15 +339,21 @@
|
|
|
327
339
|
})
|
|
328
340
|
|
|
329
341
|
function syncSelection(fromUser: boolean) {
|
|
342
|
+
// Reconcile persisted selection with the current option set, while keeping external values
|
|
343
|
+
// authoritative and only applying defaults/select-all before the user has interacted.
|
|
330
344
|
let opts = availableOptions
|
|
331
345
|
if (!opts.length) {
|
|
332
|
-
|
|
346
|
+
// Keep the bound param initialized even before options load.
|
|
347
|
+
// This prevents $param references from throwing "Missing param" on first render.
|
|
348
|
+
updateInputPayload(selection)
|
|
333
349
|
return
|
|
334
350
|
}
|
|
335
351
|
let nextSelection = selection.filter(val => valueMap.has(optionKey(val)))
|
|
336
352
|
if (!fromUser) {
|
|
337
353
|
let defaults = ensureArray(defaultValue)
|
|
338
|
-
if (
|
|
354
|
+
if (field.hasExternalValue) {
|
|
355
|
+
nextSelection = nextSelection
|
|
356
|
+
} else if (multi && selectAllDefault) {
|
|
339
357
|
nextSelection = opts.map(o => o.value)
|
|
340
358
|
} else if (!touched) {
|
|
341
359
|
if (defaults.length && !hasNoDefault) {
|
|
@@ -345,34 +363,36 @@
|
|
|
345
363
|
}
|
|
346
364
|
}
|
|
347
365
|
}
|
|
348
|
-
setSelection(nextSelection, fromUser)
|
|
366
|
+
setSelection(nextSelection, {fromUser, persist: true})
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function sameSelection(left: any[], right: any[]) {
|
|
370
|
+
let leftKeys = left.map(optionKey)
|
|
371
|
+
let rightKeys = right.map(optionKey)
|
|
372
|
+
return leftKeys.length === rightKeys.length && leftKeys.every((key, index) => key === rightKeys[index])
|
|
349
373
|
}
|
|
350
374
|
|
|
351
|
-
function setSelection(values: any[], fromUser: boolean) {
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
let changed = keys.length !== existingKeys.length || keys.some((k, idx) => k !== existingKeys[idx])
|
|
355
|
-
if (!changed) {
|
|
356
|
-
if (!fromUser) updateInputPayload(selection)
|
|
375
|
+
function setSelection(values: any[], {fromUser = false, persist = true}: {fromUser?: boolean; persist?: boolean} = {}) {
|
|
376
|
+
if (sameSelection(values, selection)) {
|
|
377
|
+
if (persist && !fromUser) updateInputPayload(selection)
|
|
357
378
|
return
|
|
358
379
|
}
|
|
359
380
|
selection = values
|
|
360
381
|
if (fromUser) touched = true
|
|
361
|
-
updateInputPayload(selection)
|
|
382
|
+
if (persist) updateInputPayload(selection)
|
|
362
383
|
}
|
|
363
384
|
|
|
364
385
|
function updateInputPayload(values: any[]) {
|
|
365
|
-
|
|
366
|
-
window.$GRAPHENE.updateParam(name, paramValue)
|
|
386
|
+
field.set(values)
|
|
367
387
|
}
|
|
368
388
|
|
|
369
389
|
function selectAll() {
|
|
370
390
|
if (!multi) return
|
|
371
|
-
setSelection(availableOptions.map(opt => opt.value), true)
|
|
391
|
+
setSelection(availableOptions.map(opt => opt.value), {fromUser: true, persist: true})
|
|
372
392
|
}
|
|
373
393
|
|
|
374
394
|
function clearSelection() {
|
|
375
|
-
setSelection([], true)
|
|
395
|
+
setSelection([], {fromUser: true, persist: true})
|
|
376
396
|
}
|
|
377
397
|
|
|
378
398
|
let elementId = $derived(`dropdown-${name}`)
|
|
@@ -554,6 +574,7 @@
|
|
|
554
574
|
}
|
|
555
575
|
}
|
|
556
576
|
.input-label {
|
|
577
|
+
font-family: var(--font-ui);
|
|
557
578
|
font-size: 12px;
|
|
558
579
|
font-weight: 600;
|
|
559
580
|
color: var(--input-label-color, #374151);
|
|
@@ -583,7 +604,7 @@
|
|
|
583
604
|
background: #ffffff;
|
|
584
605
|
color: #1f2937;
|
|
585
606
|
font-size: 14px;
|
|
586
|
-
font-family: var(--
|
|
607
|
+
font-family: var(--font-ui);
|
|
587
608
|
font-synthesis: none;
|
|
588
609
|
cursor: pointer;
|
|
589
610
|
transition: border-color 120ms ease, box-shadow 120ms ease, background 120ms ease;
|
|
@@ -660,7 +681,7 @@
|
|
|
660
681
|
padding: 6px 10px;
|
|
661
682
|
font-size: 13px;
|
|
662
683
|
line-height: 16px;
|
|
663
|
-
font-family: var(--
|
|
684
|
+
font-family: var(--font-ui);
|
|
664
685
|
font-synthesis: none;
|
|
665
686
|
background: #f9fafb;
|
|
666
687
|
color: inherit;
|
|
@@ -688,7 +709,7 @@
|
|
|
688
709
|
cursor: pointer;
|
|
689
710
|
font-size: 14px;
|
|
690
711
|
line-height: 18px;
|
|
691
|
-
font-family: var(--
|
|
712
|
+
font-family: var(--font-ui);
|
|
692
713
|
font-synthesis: none;
|
|
693
714
|
transition: background 100ms ease, color 100ms ease;
|
|
694
715
|
color: #1f2937;
|
|
@@ -755,7 +776,7 @@
|
|
|
755
776
|
background: none;
|
|
756
777
|
color: #2563eb;
|
|
757
778
|
font-size: 13px;
|
|
758
|
-
font-family: var(--
|
|
779
|
+
font-family: var(--font-ui);
|
|
759
780
|
font-synthesis: none;
|
|
760
781
|
cursor: pointer;
|
|
761
782
|
padding: 4px 0;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import {getContext, onMount} from 'svelte'
|
|
3
|
-
import {DROPDOWN_CONTEXT} from '../component-utilities/dropdownContext'
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
5
|
value: any
|
|
@@ -10,7 +9,7 @@
|
|
|
10
9
|
let {value, valueLabel = undefined}: Props = $props()
|
|
11
10
|
|
|
12
11
|
type RegisterFn = ((option: {value: any; label: string}) => (() => void) | void) | undefined
|
|
13
|
-
const register = getContext<RegisterFn>(
|
|
12
|
+
const register = getContext<RegisterFn>('dropdown')
|
|
14
13
|
|
|
15
14
|
let unregister: (() => void) | void
|
|
16
15
|
|
|
@@ -1,91 +1,205 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
2
|
+
import {init} from 'echarts'
|
|
3
|
+
import {onDestroy, onMount, untrack} from 'svelte'
|
|
4
|
+
import ErrorDisplay from '../internal/ErrorDisplay.svelte'
|
|
5
|
+
import {componentLogger, logExtraProps} from '../internal/telemetry.ts'
|
|
6
|
+
import {enrich, horizontalBarCount} from '../component-utilities/enrich.ts'
|
|
7
|
+
import type {EChartsConfig, NormalConfig, QueryResult} from '../component-utilities/types.ts'
|
|
8
|
+
import '../component-utilities/theme.ts'
|
|
9
|
+
import Skeleton from './Skeleton.svelte'
|
|
4
10
|
|
|
5
11
|
interface Props {
|
|
6
|
-
config:
|
|
12
|
+
config: EChartsConfig
|
|
13
|
+
data: string | QueryResult
|
|
7
14
|
height?: string | number
|
|
8
15
|
width?: string | number
|
|
9
|
-
data: any
|
|
10
|
-
queryID?: any
|
|
11
16
|
renderer?: 'canvas' | 'svg'
|
|
12
|
-
|
|
13
|
-
seriesOptions?: any
|
|
14
|
-
seriesColors?: any
|
|
15
|
-
connectGroup?: string
|
|
16
|
-
xAxisLabelOverflow?: 'truncate' | 'break'
|
|
17
|
-
showAllXAxisLabels?: boolean
|
|
18
|
-
swapXY?: boolean
|
|
19
|
-
chartTitle?: string
|
|
20
|
-
onclick?: (params: any) => void
|
|
17
|
+
componentId?: string
|
|
21
18
|
}
|
|
22
19
|
|
|
23
|
-
const {activeAppearance} = getThemeStores()
|
|
24
|
-
|
|
25
20
|
let {
|
|
26
|
-
config
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
21
|
+
config = {},
|
|
22
|
+
data,
|
|
23
|
+
height = undefined,
|
|
24
|
+
width = '100%',
|
|
25
|
+
renderer = 'svg',
|
|
26
|
+
componentId = undefined,
|
|
27
|
+
...extraProps
|
|
28
|
+
}: Props & Record<string, unknown> = $props()
|
|
29
|
+
|
|
30
|
+
config ||= {}
|
|
31
|
+
|
|
32
|
+
let queryFieldsForLogger = untrack(() => typeof data == 'string' ? queryFields(config) : {})
|
|
33
|
+
let chartLogger = untrack(() => componentLogger(componentId || 'ECharts', componentId ? {} : {data: typeof data == 'string' ? data : undefined, ...queryFieldsForLogger}))
|
|
34
|
+
let displayId = untrack(() => componentId || chartLogger.id)
|
|
35
|
+
untrack(() => logExtraProps(chartLogger, 'ECharts', extraProps))
|
|
36
|
+
|
|
37
|
+
// not state, because we don't want `$effect` to run when they change
|
|
38
|
+
let node: HTMLDivElement | null = null
|
|
39
|
+
let chart: any
|
|
40
|
+
let resizeObserver: ResizeObserver | null = null
|
|
41
|
+
|
|
42
|
+
// Use `raw` because data can be big, and there's little upside to making it reactive
|
|
43
|
+
let loaded = $state.raw<QueryResult | null>(null)
|
|
44
|
+
let chartError: Error | null = $state(null)
|
|
45
|
+
let mountedComponentId: string | null = $state(displayId)
|
|
46
|
+
let chartTitle: string | undefined = $state(undefined)
|
|
47
|
+
let chartSizeStyle: string = $state(calculateChartSize())
|
|
48
|
+
|
|
49
|
+
function handleResults (res: QueryResult) {
|
|
50
|
+
chartError = null
|
|
51
|
+
loaded = res
|
|
52
|
+
if (res?.error) chartLogger.error(res.error, {...res.error, componentId: displayId})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// If `data` is just a string, kick off a query to fetch the data.
|
|
56
|
+
// This maybe could be an effect, but we'd have to ensure we don't double-subscribe.
|
|
57
|
+
onMount(() => {
|
|
58
|
+
resizeObserver = new ResizeObserver(() => chart?.resize())
|
|
59
|
+
if (node) resizeObserver.observe(node)
|
|
60
|
+
|
|
61
|
+
if (typeof data == 'string') {
|
|
62
|
+
try {
|
|
63
|
+
mountedComponentId = window.$GRAPHENE.query(data, queryFieldsForLogger, handleResults, displayId)
|
|
64
|
+
} catch (error) {
|
|
65
|
+
chartError = error instanceof Error ? error : new Error(String(error))
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
loaded = data
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
onDestroy(() => {
|
|
73
|
+
resizeObserver?.disconnect()
|
|
74
|
+
resizeObserver = null
|
|
75
|
+
window.$GRAPHENE.unsubscribe(handleResults)
|
|
76
|
+
destroyChart()
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
$effect(() => {
|
|
80
|
+
if (chartError) return
|
|
81
|
+
|
|
82
|
+
if (!loaded || loaded.error || loaded.rows.length == 0) {
|
|
83
|
+
destroyChart()
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!chart) {
|
|
88
|
+
chart = init(node, 'graphene-theme', {renderer})
|
|
89
|
+
chart.on('legendselectchanged', renderChart)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
window.$GRAPHENE?.renderStart?.(`chart:${chart.id}`)
|
|
94
|
+
renderChart()
|
|
95
|
+
chartError = null
|
|
96
|
+
window.$GRAPHENE?.renderComplete?.(`chart:${chart.id}`)
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error('Chart failed to render', error)
|
|
99
|
+
chartError = error instanceof Error ? error : new Error(String(error))
|
|
100
|
+
chartLogger.error(chartError, {componentId: displayId})
|
|
101
|
+
window.$GRAPHENE?.renderComplete?.(`chart:${chart.id}`)
|
|
102
|
+
destroyChart()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Build a fresh enriched option each render so legend-driven stack rounding
|
|
107
|
+
// always reflects the currently visible series.
|
|
108
|
+
function renderChart() {
|
|
109
|
+
if (!chart || !loaded) return
|
|
110
|
+
|
|
111
|
+
// clone config, since enriching mutates the config, and mutating a prop is weird
|
|
112
|
+
// structuredClone doesn't like proxies, so use state.snapshot
|
|
113
|
+
let cloned = structuredClone($state.snapshot(config)) as EChartsConfig
|
|
114
|
+
let rows = loaded.rows
|
|
115
|
+
let fields = loaded.fields || []
|
|
116
|
+
cloned.legendSelection = chart.getOption()?.legend?.[0]?.selected
|
|
117
|
+
let enriched = enrich(cloned, rows, fields)
|
|
118
|
+
|
|
119
|
+
chartTitle = enriched.title.find(t => t?.text)?.text
|
|
120
|
+
chartSizeStyle = calculateChartSize(enriched, rows, fields)
|
|
121
|
+
chart.setOption({...enriched, animation: false, animationDuration: 0, animationDurationUpdate: 0}, true)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function destroyChart() {
|
|
125
|
+
if (!chart) return
|
|
126
|
+
chart.off('legendselectchanged', renderChart)
|
|
127
|
+
chart.dispose()
|
|
128
|
+
chart = null
|
|
38
129
|
}
|
|
130
|
+
|
|
131
|
+
function queryFields(config: EChartsConfig) {
|
|
132
|
+
let fields: Record<string, string[]> = {}
|
|
133
|
+
let series = Array.isArray(config.series) ? config.series : [config.series]
|
|
134
|
+
let entries = series.flatMap(s => Object.entries(s?.encode || {}))
|
|
135
|
+
|
|
136
|
+
for (let [attr, col] of entries) {
|
|
137
|
+
let value = queryableEncodeValue(attr, col)
|
|
138
|
+
if (!value) continue
|
|
139
|
+
fields[attr] ||= []
|
|
140
|
+
if (!fields[attr].includes(value)) fields[attr].push(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return fields
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function queryableEncodeValue(attr: string, value: unknown) {
|
|
147
|
+
if (typeof value !== 'string') return undefined
|
|
148
|
+
let trimmed = value.trim()
|
|
149
|
+
if (!trimmed) return undefined
|
|
150
|
+
|
|
151
|
+
// sort supports "column" or "column asc|desc". We only need the field in SELECT.
|
|
152
|
+
if (attr === 'sort') return trimmed.split(/\s+/)[0]
|
|
153
|
+
return trimmed
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function calculateChartSize(config?: NormalConfig, rows: Record<string, any>[] = [], fields: any[] = []) {
|
|
157
|
+
let threshold = 8 // over this many bars, start to grow
|
|
158
|
+
let resolvedHeight: string | number = height ?? '320px'
|
|
159
|
+
let barSeries = config?.series.find(s => s.type == 'bar')
|
|
160
|
+
let categoricalY = config?.yAxis[0]?.type == 'category'
|
|
161
|
+
|
|
162
|
+
if (config && barSeries && categoricalY) {
|
|
163
|
+
let distinctX = horizontalBarCount(config, rows, fields)
|
|
164
|
+
if (distinctX > threshold) resolvedHeight = 320 * Math.max(1, distinctX / threshold)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return `height:${toDim(resolvedHeight)};width:${toDim(width ?? '100%')};`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function toDim(dim: string | number) {
|
|
171
|
+
let t = typeof dim
|
|
172
|
+
if (t == 'number' || (t == 'string' && (dim as string).match(/^\d+$/))) return `${dim}px`
|
|
173
|
+
return dim
|
|
174
|
+
}
|
|
175
|
+
|
|
39
176
|
</script>
|
|
40
177
|
|
|
41
|
-
<div class="echarts-
|
|
42
|
-
{#if
|
|
43
|
-
<
|
|
44
|
-
{:else}
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
data-query-id={queryID}
|
|
49
|
-
style={`height:${toDimension(height, '240px')};width:${toDimension(width, '100%')}`}
|
|
50
|
-
use:echarts={{
|
|
51
|
-
config,
|
|
52
|
-
data,
|
|
53
|
-
echartsOptions,
|
|
54
|
-
seriesOptions,
|
|
55
|
-
onclick,
|
|
56
|
-
renderer,
|
|
57
|
-
connectGroup,
|
|
58
|
-
xAxisLabelOverflow,
|
|
59
|
-
showAllXAxisLabels,
|
|
60
|
-
swapXY,
|
|
61
|
-
seriesColors,
|
|
62
|
-
theme: $activeAppearance,
|
|
63
|
-
}}
|
|
64
|
-
></div>
|
|
178
|
+
<div class="echarts" bind:this={node} style={chartSizeStyle} data-component-id={mountedComponentId} data-chart-title={chartTitle}>
|
|
179
|
+
{#if loaded?.error || chartError}
|
|
180
|
+
<ErrorDisplay error={loaded?.error || chartError} />
|
|
181
|
+
{:else if !loaded}
|
|
182
|
+
<Skeleton />
|
|
183
|
+
{:else if loaded.rows.length == 0}
|
|
184
|
+
<div class="empty-chart" role="note">Dataset is empty - query ran successfully, but no data was returned from the database</div>
|
|
65
185
|
{/if}
|
|
66
186
|
</div>
|
|
67
187
|
|
|
68
188
|
<style>
|
|
69
|
-
.echarts
|
|
189
|
+
.echarts {
|
|
70
190
|
position: relative;
|
|
71
|
-
margin: 8px 0;
|
|
72
191
|
}
|
|
73
192
|
|
|
74
|
-
.
|
|
193
|
+
.empty-chart {
|
|
75
194
|
width: 100%;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.echarts-loading {
|
|
81
|
-
width: 100%;
|
|
82
|
-
display: flex;
|
|
83
|
-
align-items: center;
|
|
84
|
-
justify-content: center;
|
|
85
|
-
border: 1px solid rgba(209, 213, 219, 0.8);
|
|
195
|
+
height: 100%;
|
|
196
|
+
padding: 12px;
|
|
197
|
+
margin: 8px 0;
|
|
198
|
+
border: 1px dashed rgba(107, 114, 128, 0.6);
|
|
86
199
|
border-radius: 4px;
|
|
87
|
-
background: rgba(249, 250, 251, 0.6);
|
|
88
|
-
color: rgba(107, 114, 128, 0.95);
|
|
89
200
|
font-size: 12px;
|
|
201
|
+
color: rgba(75, 85, 99, 0.9);
|
|
202
|
+
text-align: center;
|
|
203
|
+
background: rgba(243, 244, 246, 0.6);
|
|
90
204
|
}
|
|
91
205
|
</style>
|