@graphenedata/cli 0.0.15 → 0.0.16

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 (117) hide show
  1. package/README.md +138 -0
  2. package/dist/cli/bigQuery-I3F46SC6.js +75 -0
  3. package/dist/cli/bigQuery-I3F46SC6.js.map +7 -0
  4. package/dist/cli/chunk-OVWODUTJ.js +12849 -0
  5. package/dist/cli/chunk-OVWODUTJ.js.map +7 -0
  6. package/dist/cli/chunk-QAXEOZ43.js +53 -0
  7. package/dist/cli/chunk-QAXEOZ43.js.map +7 -0
  8. package/dist/cli/cli.js +234 -11197
  9. package/dist/cli/clickhouse-ZN5AN2UL.js +64 -0
  10. package/dist/cli/clickhouse-ZN5AN2UL.js.map +7 -0
  11. package/dist/cli/duckdb-IYBIO5KJ.js +87 -0
  12. package/dist/cli/duckdb-IYBIO5KJ.js.map +7 -0
  13. package/dist/cli/serve2-TNN5EROW.js +447 -0
  14. package/dist/cli/serve2-TNN5EROW.js.map +7 -0
  15. package/dist/cli/snowflake-MOQB5GA4.js +128 -0
  16. package/dist/cli/snowflake-MOQB5GA4.js.map +7 -0
  17. package/dist/index.d.ts +63 -0
  18. package/dist/lang/index.d.ts +63 -0
  19. package/dist/skills/graphene/SKILL.md +150 -96
  20. package/dist/skills/graphene/references/big-value.md +6 -41
  21. package/dist/skills/graphene/references/date-range.md +64 -0
  22. package/dist/skills/graphene/references/dropdown.md +3 -4
  23. package/dist/skills/graphene/references/echarts.md +162 -0
  24. package/dist/skills/graphene/references/gsql.md +55 -25
  25. package/dist/skills/graphene/references/model-gsql.md +72 -0
  26. package/dist/skills/graphene/references/table.md +13 -14
  27. package/dist/skills/graphene/references/text-input.md +2 -1
  28. package/dist/ui/app.css +239 -340
  29. package/dist/ui/component-utilities/dataShaping.ts +484 -0
  30. package/dist/ui/component-utilities/dataSummary.ts +57 -0
  31. package/dist/ui/component-utilities/enrich.ts +763 -0
  32. package/dist/ui/component-utilities/format.ts +177 -0
  33. package/dist/ui/component-utilities/inputUtils.ts +44 -8
  34. package/dist/ui/component-utilities/theme.ts +200 -0
  35. package/dist/ui/component-utilities/themeStores.ts +21 -8
  36. package/dist/ui/component-utilities/types.ts +70 -0
  37. package/dist/ui/components/AreaChart.svelte +57 -105
  38. package/dist/ui/components/BarChart.svelte +71 -129
  39. package/dist/ui/components/BigValue.svelte +24 -40
  40. package/dist/ui/components/Column.svelte +10 -18
  41. package/dist/ui/components/DateRange.svelte +54 -21
  42. package/dist/ui/components/Dropdown.svelte +47 -26
  43. package/dist/ui/components/DropdownOption.svelte +1 -2
  44. package/dist/ui/components/ECharts.svelte +181 -67
  45. package/dist/ui/components/InlineDelta.svelte +50 -31
  46. package/dist/ui/components/LineChart.svelte +54 -125
  47. package/dist/ui/components/PieChart.svelte +27 -37
  48. package/dist/ui/components/QueryLoad.svelte +77 -45
  49. package/dist/ui/components/Row.svelte +2 -1
  50. package/dist/ui/components/ScatterPlot.svelte +52 -0
  51. package/dist/ui/components/Skeleton.svelte +32 -0
  52. package/dist/ui/components/Table.svelte +3 -2
  53. package/dist/ui/components/TableGroupRow.svelte +28 -36
  54. package/dist/ui/components/TableHarness.svelte +32 -0
  55. package/dist/ui/components/TableHeader.svelte +34 -59
  56. package/dist/ui/components/TableRow.svelte +14 -38
  57. package/dist/ui/components/TableSubtotalRow.svelte +18 -21
  58. package/dist/ui/components/TableTotalRow.svelte +27 -37
  59. package/dist/ui/components/TextInput.svelte +13 -12
  60. package/dist/ui/components/Value.svelte +25 -0
  61. package/dist/ui/components/_Table.svelte +72 -70
  62. package/dist/ui/internal/ChartGallery.svelte +527 -0
  63. package/dist/ui/internal/ErrorDisplay.svelte +22 -97
  64. package/dist/ui/internal/LocalApp.svelte +80 -17
  65. package/dist/ui/internal/PageNavGroup.svelte +269 -0
  66. package/dist/ui/internal/Sidebar.svelte +178 -0
  67. package/dist/ui/internal/SidebarToggle.svelte +47 -0
  68. package/dist/ui/internal/StyleGallery.svelte +244 -0
  69. package/dist/ui/internal/clientCache.ts +2 -2
  70. package/dist/ui/internal/pageInputs.svelte.js +292 -0
  71. package/dist/ui/internal/queryEngine.ts +102 -117
  72. package/dist/ui/internal/runSocket.ts +32 -12
  73. package/dist/ui/internal/sidebar.svelte.js +18 -0
  74. package/dist/ui/internal/telemetry.ts +51 -16
  75. package/dist/ui/internal/types.d.ts +7 -0
  76. package/dist/ui/web.js +28 -11
  77. package/package.json +36 -38
  78. package/dist/skills/graphene/references/area-chart.md +0 -95
  79. package/dist/skills/graphene/references/bar-chart.md +0 -112
  80. package/dist/skills/graphene/references/line-chart.md +0 -108
  81. package/dist/skills/graphene/references/pie-chart.md +0 -29
  82. package/dist/skills/graphene/references/value-formats.md +0 -104
  83. package/dist/ui/component-utilities/autoFormatting.js +0 -280
  84. package/dist/ui/component-utilities/builtInFormats.js +0 -481
  85. package/dist/ui/component-utilities/chartContext.js +0 -12
  86. package/dist/ui/component-utilities/chartWindowDebug.js +0 -21
  87. package/dist/ui/component-utilities/checkInputs.js +0 -84
  88. package/dist/ui/component-utilities/convert.js +0 -15
  89. package/dist/ui/component-utilities/dateParsing.js +0 -56
  90. package/dist/ui/component-utilities/dropdownContext.ts +0 -1
  91. package/dist/ui/component-utilities/echarts.js +0 -252
  92. package/dist/ui/component-utilities/echartsThemes.js +0 -443
  93. package/dist/ui/component-utilities/formatTitle.js +0 -24
  94. package/dist/ui/component-utilities/formatting.js +0 -241
  95. package/dist/ui/component-utilities/getColumnExtents.js +0 -79
  96. package/dist/ui/component-utilities/getColumnSummary.js +0 -62
  97. package/dist/ui/component-utilities/getCompletedData.js +0 -122
  98. package/dist/ui/component-utilities/getDistinctCount.js +0 -7
  99. package/dist/ui/component-utilities/getDistinctValues.js +0 -15
  100. package/dist/ui/component-utilities/getSeriesConfig.js +0 -231
  101. package/dist/ui/component-utilities/getSortedData.js +0 -9
  102. package/dist/ui/component-utilities/getStackPercentages.js +0 -45
  103. package/dist/ui/component-utilities/getStackedData.js +0 -19
  104. package/dist/ui/component-utilities/getYAxisIndex.js +0 -15
  105. package/dist/ui/component-utilities/globalContexts.js +0 -1
  106. package/dist/ui/component-utilities/helpers/getCompletedData.helpers.js +0 -119
  107. package/dist/ui/component-utilities/replaceNulls.js +0 -16
  108. package/dist/ui/component-utilities/tableUtils.ts +0 -107
  109. package/dist/ui/component-utilities/tidyWithTypes.js +0 -9
  110. package/dist/ui/components/Area.svelte +0 -214
  111. package/dist/ui/components/Bar.svelte +0 -347
  112. package/dist/ui/components/Chart.svelte +0 -995
  113. package/dist/ui/components/Line.svelte +0 -227
  114. package/dist/ui/internal/NavSidebar.svelte +0 -396
  115. package/dist/ui/internal/theme.ts +0 -60
  116. package/dist/ui/public/inter-latin-ext.woff2 +0 -0
  117. package/dist/ui/public/inter-latin.woff2 +0 -0
@@ -0,0 +1,484 @@
1
+ import type {Field, NormalConfig, SeriesWithGroupingHint} from './types.ts'
2
+
3
+ // Fill sparse grouped data so each split series has a value for each x bucket.
4
+ //
5
+ // This only applies to split templates (`encode.splitBy`).
6
+ // We do not attempt to fabricate x values here; we only ensure a full Cartesian
7
+ // product of existing x values and split values.
8
+ //
9
+ // Missing-value behavior by chart type (Evidence defaults):
10
+ // - line (no area): null -> visible line gaps
11
+ // - area (line + areaStyle):
12
+ // - stacked area: 0 -> continuous stacked area baseline
13
+ // - unstacked area: null -> visible gaps like line charts
14
+ // - bar: 0 -> missing category bars render as zero-height bars
15
+ export function applyMissingPointDefaults(config: NormalConfig, rows: Record<string, any>[]) {
16
+ let series = config.series
17
+ if (series.length === 0 || rows.length === 0) return
18
+
19
+ let groups = new Map<string, {xField: string; splitFields: string[]; fills: Map<string, any>}>()
20
+
21
+ for (let entry of series) {
22
+ let splitFields = getSplitFields(entry)
23
+ let xField = getSeriesXField(entry)
24
+ let yField = getSeriesYField(entry)
25
+ if (splitFields.length === 0 || !xField || !yField) continue
26
+
27
+ let key = `${xField}::${splitFields.join('::')}`
28
+ if (!groups.has(key)) groups.set(key, {xField, splitFields, fills: new Map()})
29
+
30
+ // This line is where chart-specific missing-value behavior is chosen.
31
+ // See getMissingFillValueForSeries() below for the type mapping.
32
+ let fillValue = getMissingFillValueForSeries(entry)
33
+ let fills = groups.get(key)!.fills
34
+
35
+ // If multiple templates target the same y field, prefer zero over null.
36
+ // (bar/area should win over plain line when mixed configs exist.)
37
+ if (!fills.has(yField) || fillValue === 0) fills.set(yField, fillValue)
38
+ }
39
+
40
+ for (let group of groups.values()) {
41
+ let xValues = distinctValues(rows, group.xField)
42
+ let splitValues = group.splitFields.map(field => distinctValues(rows, field))
43
+ if (xValues.length === 0 || splitValues.some(values => values.length === 0)) continue
44
+
45
+ let existing = new Set<string>()
46
+ for (let row of rows) {
47
+ existing.add(compositeKey([row?.[group.xField], ...group.splitFields.map(field => row?.[field])]))
48
+ }
49
+
50
+ for (let xValue of xValues) {
51
+ for (let splitCombination of cartesianValues(splitValues)) {
52
+ if (existing.has(compositeKey([xValue, ...splitCombination]))) continue
53
+
54
+ let row: Record<string, any> = {[group.xField]: xValue}
55
+ group.splitFields.forEach((field, index) => {
56
+ row[field] = splitCombination[index]
57
+ })
58
+ for (let [yField, fillValue] of group.fills.entries()) row[yField] = fillValue
59
+ rows.push(row)
60
+ }
61
+ }
62
+ }
63
+ }
64
+
65
+ // Evidence stacked100 behavior: compute percentages per x-domain and rewrite series to synthetic pct fields.
66
+ export function applyStackPercentage(config: NormalConfig, rows: Record<string, any>[], fields: Field[]) {
67
+ let series = config.series
68
+ if (series.length === 0 || rows.length === 0) return
69
+
70
+ let groupIndex = 0
71
+
72
+ for (let entry of series) {
73
+ let xField = getSeriesXField(entry)
74
+ let yField = getSeriesYField(entry)
75
+ if (entry?.stackPercentage !== true || !entry?.stack || !xField || !yField || entry?.datasetId != null) continue
76
+
77
+ let stackGroup = series.filter(candidate => {
78
+ return candidate?.stack === entry.stack && getSeriesXField(candidate) === xField && getSeriesYField(candidate)
79
+ })
80
+ if (stackGroup[0] !== entry) continue
81
+
82
+ let yFields = Array.from(new Set(stackGroup.map(candidate => getSeriesYField(candidate)).filter(Boolean))) as string[]
83
+ let pctFieldByY = Object.fromEntries(yFields.map((y, index) => [y, `__graphene_stack_pct_${groupIndex}_${index}`])) as Record<string, string>
84
+
85
+ let totalsByX = new Map<string, number>()
86
+ for (let row of rows) {
87
+ let xKey = valueKey(row?.[xField])
88
+ let rowTotal = yFields.reduce((sum, y) => sum + (Number(row?.[y]) || 0), 0)
89
+ totalsByX.set(xKey, (totalsByX.get(xKey) ?? 0) + rowTotal)
90
+ }
91
+
92
+ for (let row of rows) {
93
+ let xKey = valueKey(row?.[xField])
94
+ let total = totalsByX.get(xKey) ?? 0
95
+ for (let y of yFields) row[pctFieldByY[y]] = total <= 0 ? 0 : (Number(row?.[y]) || 0) / total
96
+ }
97
+
98
+ for (let y of yFields) ensureField(fields, pctFieldByY[y], {metadata: {ratio: true}})
99
+
100
+ for (let candidate of stackGroup) {
101
+ let y = getSeriesYField(candidate)
102
+ if (!y) continue
103
+ candidate.encode = {...candidate.encode, y: pctFieldByY[y]}
104
+ delete candidate.stackPercentage
105
+ }
106
+
107
+ groupIndex++
108
+ }
109
+ }
110
+
111
+ // Sort rows with either an explicit `encode.sort` rule or our built-in defaults.
112
+ // Explicit sort format: "column" or "column asc|desc".
113
+ export function applySorting(config: NormalConfig, rows: Record<string, any>[], fields: Field[]) {
114
+ let series = config.series
115
+ if (series.length === 0) return
116
+
117
+ // Explicit sort always wins and only applies to categorical axes.
118
+ let explicitSort = resolveExplicitSort(series, fields)
119
+ removeSortHints(series)
120
+ if (rows.length === 0) return
121
+
122
+ let categoryField = [...config.xAxis, ...config.yAxis].find(axis => axis?.type === 'category')?.field?.name
123
+ if (explicitSort) {
124
+ if (!categoryField) throw new Error('sort is only supported when the chart has a categorical axis')
125
+ sortCategoriesByField(rows, categoryField, explicitSort.field, explicitSort.direction, fields)
126
+ return
127
+ }
128
+
129
+ let primaryXField = config.xAxis[0]?.field
130
+ if (!primaryXField) return
131
+
132
+ let timeOrdinal = String(primaryXField.metadata?.timeOrdinal || '').toLowerCase()
133
+ if (timeOrdinal) {
134
+ sortRowsByXTimeOrdinal(rows, primaryXField.name, timeOrdinal)
135
+ return
136
+ }
137
+
138
+ // time/value x fields keep natural ascending order
139
+ if (primaryXField.type === 'date' || primaryXField.type === 'timestamp' || primaryXField.type === 'number') {
140
+ sortRowsByXAscending(rows, primaryXField.name, primaryXField.type === 'number' ? 'number' : 'date')
141
+ return
142
+ }
143
+
144
+ if (primaryXField.type !== 'string') return
145
+
146
+ let primarySeries = series.filter(entry => getSeriesXField(entry) === primaryXField.name && getSeriesYField(entry))
147
+ if (primarySeries.length === 0) return
148
+
149
+ let hasStackedBars = primarySeries.some(entry => entry?.type === 'bar' && (!!entry?.stack || getSplitFields(entry).length === 2))
150
+ if (hasStackedBars) {
151
+ let yFields = Array.from(new Set(primarySeries.map(entry => getSeriesYField(entry)).filter(Boolean))) as string[]
152
+ sortCategoriesByValue(rows, primaryXField.name, row => yFields.reduce((sum, y) => sum + (Number(row?.[y]) || 0), 0), 'desc')
153
+ return
154
+ }
155
+
156
+ let firstY = getSeriesYField(primarySeries[0])
157
+ if (!firstY) return
158
+ sortCategoriesByValue(rows, primaryXField.name, row => Number(row?.[firstY]) || 0, 'desc')
159
+ }
160
+
161
+ // Materialize dataset-backed bar series into explicit point arrays so later enrichments can mutate points.
162
+ // This is needed to round the corners of bars, which can only be done with point-level item styles.
163
+ export function inlineDataIntoSeries(config: NormalConfig, rows: Record<string, any>[]) {
164
+ let horizontal = isHorizontalBar(config)
165
+ let datasetsById = new Map<string, Record<string, any>>()
166
+ for (let dataset of config.dataset) {
167
+ if (!dataset?.id) continue
168
+ datasetsById.set(String(dataset.id), dataset as Record<string, any>)
169
+ }
170
+
171
+ let memo = new Map<string, Record<string, any>[] | null>()
172
+ let datasetRows = (datasetId?: string): Record<string, any>[] | null => {
173
+ if (!datasetId) return rows
174
+ if (memo.has(datasetId)) return memo.get(datasetId) ?? null
175
+
176
+ let dataset = datasetsById.get(datasetId)
177
+ if (!dataset) return null
178
+
179
+ if (Array.isArray(dataset.source)) {
180
+ memo.set(datasetId, dataset.source as Record<string, any>[])
181
+ return dataset.source as Record<string, any>[]
182
+ }
183
+
184
+ let parentId = dataset.fromDatasetId != null ? String(dataset.fromDatasetId) : undefined
185
+ if (!parentId) return null
186
+
187
+ let parentRows = datasetRows(parentId)
188
+ if (!parentRows) return null
189
+
190
+ let transform = dataset.transform as Record<string, any> | undefined
191
+ if (transform?.type !== 'filter') return null
192
+
193
+ let filterConfig = transform.config as Record<string, any> | undefined
194
+ let filterField = filterConfig?.dimension
195
+ if (typeof filterField !== 'string' || !filterConfig || !Object.prototype.hasOwnProperty.call(filterConfig, '=')) return null
196
+
197
+ let filtered = parentRows.filter(row => row?.[filterField] === filterConfig['='])
198
+ memo.set(datasetId, filtered)
199
+ return filtered
200
+ }
201
+
202
+ for (let series of config.series) {
203
+ if (series?.type !== 'bar' || !series?.stack || series?.data != null) continue
204
+
205
+ let xField = getSeriesXField(series)
206
+ let yField = getSeriesYField(series)
207
+ let categoryField = horizontal ? yField : xField
208
+ if (!xField || !yField || !categoryField) continue
209
+
210
+ let seriesRows = datasetRows(series.datasetId)
211
+ if (!seriesRows) continue
212
+
213
+ let rowByCategory = new Map<string, Record<string, any>>()
214
+ for (let row of seriesRows) {
215
+ let key = valueKey(row?.[categoryField])
216
+ if (!rowByCategory.has(key)) rowByCategory.set(key, row)
217
+ }
218
+
219
+ let categories = distinctValues(rows, categoryField)
220
+ series.data = categories.map(categoryValue => {
221
+ let sourceRow = rowByCategory.get(valueKey(categoryValue))!
222
+ return {...sourceRow, value: [sourceRow[xField], sourceRow[yField]]}
223
+ })
224
+ delete series.datasetId
225
+ }
226
+ }
227
+
228
+ function sortRowsByXAscending(rows: Record<string, any>[], xField: string, xType: 'date' | 'number') {
229
+ let indexed = rows.map((row, index) => ({row, index}))
230
+ indexed.sort((a, b) => {
231
+ let aValue = sortableValue(a.row?.[xField], xType)
232
+ let bValue = sortableValue(b.row?.[xField], xType)
233
+ if (aValue < bValue) return -1
234
+ if (aValue > bValue) return 1
235
+ return a.index - b.index
236
+ })
237
+ for (let i = 0; i < indexed.length; i++) rows[i] = indexed[i].row
238
+ }
239
+
240
+ function sortRowsByXTimeOrdinal(rows: Record<string, any>[], xField: string, timeOrdinal: string) {
241
+ let indexed = rows.map((row, index) => ({row, index}))
242
+ indexed.sort((a, b) => {
243
+ let aValue = ordinalSortValue(a.row?.[xField], timeOrdinal)
244
+ let bValue = ordinalSortValue(b.row?.[xField], timeOrdinal)
245
+ if (aValue < bValue) return -1
246
+ if (aValue > bValue) return 1
247
+ return a.index - b.index
248
+ })
249
+ for (let i = 0; i < indexed.length; i++) rows[i] = indexed[i].row
250
+ }
251
+
252
+ // Aggregate one numeric value per category, then order categories by that value.
253
+ function sortCategoriesByValue(rows: Record<string, any>[], categoryField: string, metricForRow: (row: Record<string, any>) => number, direction: 'asc' | 'desc') {
254
+ let metricByCategory = new Map<string, number>()
255
+
256
+ for (let row of rows) {
257
+ let key = valueKey(row?.[categoryField])
258
+ metricByCategory.set(key, (metricByCategory.get(key) ?? 0) + metricForRow(row))
259
+ }
260
+
261
+ let orderedCategoryKeys = Array.from(metricByCategory.keys())
262
+ orderedCategoryKeys.sort((left, right) => {
263
+ let leftMetric = metricByCategory.get(left) ?? 0
264
+ let rightMetric = metricByCategory.get(right) ?? 0
265
+ return direction === 'asc' ? leftMetric - rightMetric : rightMetric - leftMetric
266
+ })
267
+
268
+ sortRowsByCategoryOrder(rows, categoryField, orderedCategoryKeys)
269
+ }
270
+
271
+ // Sort categories by a specific field.
272
+ // Numeric fields are summed per category; non-numeric fields use first value seen.
273
+ function sortCategoriesByField(rows: Record<string, any>[], categoryField: string, sortField: string, direction: 'asc' | 'desc', fields: Field[]) {
274
+ let sortType = inferFieldType(fields, sortField)
275
+
276
+ if (sortType === 'number') {
277
+ sortCategoriesByValue(rows, categoryField, row => Number(row?.[sortField]) || 0, direction)
278
+ return
279
+ }
280
+
281
+ let sortValueByCategory = new Map<string, unknown>()
282
+ for (let row of rows) {
283
+ let key = valueKey(row?.[categoryField])
284
+ if (sortValueByCategory.has(key)) continue
285
+ sortValueByCategory.set(key, row?.[sortField])
286
+ }
287
+
288
+ let orderedCategoryKeys = distinctValues(rows, categoryField).map(value => valueKey(value))
289
+ orderedCategoryKeys.sort((left, right) => {
290
+ let leftValue = sortValueByCategory.get(left)
291
+ let rightValue = sortValueByCategory.get(right)
292
+
293
+ if (sortType === 'date') {
294
+ let leftDate = sortableValue(leftValue, 'date')
295
+ let rightDate = sortableValue(rightValue, 'date')
296
+ return direction === 'asc' ? leftDate - rightDate : rightDate - leftDate
297
+ }
298
+
299
+ let comparison = String(leftValue ?? '').localeCompare(String(rightValue ?? ''), undefined, {numeric: true})
300
+ return direction === 'asc' ? comparison : -comparison
301
+ })
302
+
303
+ sortRowsByCategoryOrder(rows, categoryField, orderedCategoryKeys)
304
+ }
305
+
306
+ // Apply a category order while preserving original row order within each category.
307
+ function sortRowsByCategoryOrder(rows: Record<string, any>[], categoryField: string, orderedCategoryKeys: string[]) {
308
+ let positionByCategory = new Map<string, number>(orderedCategoryKeys.map((key, index) => [key, index]))
309
+ let indexed = rows.map((row, index) => ({row, index}))
310
+
311
+ indexed.sort((left, right) => {
312
+ let leftPos = positionByCategory.get(valueKey(left.row?.[categoryField])) ?? Number.MAX_SAFE_INTEGER
313
+ let rightPos = positionByCategory.get(valueKey(right.row?.[categoryField])) ?? Number.MAX_SAFE_INTEGER
314
+ if (leftPos !== rightPos) return leftPos - rightPos
315
+ return left.index - right.index
316
+ })
317
+
318
+ for (let i = 0; i < indexed.length; i++) rows[i] = indexed[i].row
319
+ }
320
+
321
+ function ensureField(fields: Field[], name: string, options?: Partial<Field>) {
322
+ if (fields.some(field => field.name === name)) return
323
+ fields.push({name, type: 'number', ...options})
324
+ }
325
+
326
+ // Default missing datapoint handling differs by chart type.
327
+ // - bar: missing grouped points become 0
328
+ // - area: stacked -> 0, unstacked -> null (gap)
329
+ // - line: missing grouped points become null (shows a gap unless connectNulls is enabled)
330
+ function getMissingFillValueForSeries(series: SeriesWithGroupingHint) {
331
+ if (series?.type === 'bar') return 0
332
+
333
+ let isArea = series?.type === 'line' && series?.areaStyle != null
334
+ if (isArea && series?.stack) return 0
335
+
336
+ return null
337
+ }
338
+
339
+ function inferFieldType(fields: Field[], fieldName: string) {
340
+ let field = fields.find(entry => entry.name === fieldName)
341
+ if (!field) return 'string'
342
+ if (typeof field.type !== 'string') throw new Error(`Field ${fieldName} has unsupported non-scalar type: array`)
343
+ if (field.type === 'date' || field.type === 'timestamp') return 'date'
344
+ if (field.type === 'number') return 'number'
345
+ return 'string'
346
+ }
347
+
348
+ function resolveExplicitSort(series: SeriesWithGroupingHint[], fields: Field[]) {
349
+ let specs = Array.from(
350
+ new Set(
351
+ series
352
+ .map(entry => entry?.encode?.sort)
353
+ .filter(value => typeof value === 'string')
354
+ .map(value => String(value)),
355
+ ),
356
+ )
357
+ if (specs.length === 0) return undefined
358
+ if (specs.length > 1) throw new Error('sort must be the same across all series')
359
+
360
+ let parsed = parseSortSpec(specs[0])
361
+ if (!fields.some(field => field.name === parsed.field))
362
+ throw new Error(`${parsed.field} is not a column in the dataset. sort should contain one column name and optionally a direction (asc or desc).`)
363
+ return parsed
364
+ }
365
+
366
+ // encode.sort is a Graphene-only hint. Remove it once parsed so ECharts does not
367
+ // treat the sort column as another encoded dimension in tooltips.
368
+ function removeSortHints(series: SeriesWithGroupingHint[]) {
369
+ for (let entry of series) {
370
+ if (!entry?.encode || entry.encode.sort == null) continue
371
+ entry.encode = {...entry.encode}
372
+ delete entry.encode.sort
373
+ }
374
+ }
375
+
376
+ function parseSortSpec(sort: string): {field: string; direction: 'asc' | 'desc'} {
377
+ let parts = sort.trim().split(/\s+/).filter(Boolean)
378
+ if (parts.length === 0 || parts.length > 2) throw new Error('sort should contain one column name and optionally a direction (asc or desc).')
379
+
380
+ let field = parts[0]
381
+ let direction = parts[1]?.toLowerCase()
382
+ if (!field) throw new Error('sort should contain one column name and optionally a direction (asc or desc).')
383
+ if (!direction) return {field, direction: 'asc'}
384
+ if (direction !== 'asc' && direction !== 'desc') throw new Error('sort should contain one column name and optionally a direction (asc or desc).')
385
+ return {field, direction}
386
+ }
387
+
388
+ function getSplitFields(series: SeriesWithGroupingHint) {
389
+ let splitBy = series?.encode?.splitBy
390
+ if (typeof splitBy === 'string') return [splitBy]
391
+ if (!Array.isArray(splitBy)) return []
392
+ return splitBy
393
+ .filter(value => typeof value === 'string')
394
+ .map(value => value.trim())
395
+ .filter(Boolean)
396
+ }
397
+
398
+ function isHorizontalBar(config: NormalConfig) {
399
+ let xAxis = config.xAxis[0]
400
+ let yAxis = config.yAxis[0]
401
+ let hasBarSeries = config.series.some(series => series?.type === 'bar')
402
+ return Boolean(hasBarSeries && xAxis?.type === 'value' && yAxis?.type === 'category')
403
+ }
404
+
405
+ function getSeriesXField(series?: SeriesWithGroupingHint) {
406
+ return getEncodeField(series?.encode?.x)
407
+ }
408
+
409
+ function getSeriesYField(series?: SeriesWithGroupingHint) {
410
+ return getEncodeField(series?.encode?.y) ?? getEncodeField(series?.encode?.value)
411
+ }
412
+
413
+ function getEncodeField(value: unknown): string | undefined {
414
+ if (typeof value === 'string') return value
415
+ if (Array.isArray(value)) return value.find(entry => typeof entry === 'string')
416
+ return undefined
417
+ }
418
+
419
+ function distinctValues(rows: Record<string, any>[], field: string) {
420
+ let values: unknown[] = []
421
+ let seen = new Set<string>()
422
+ for (let row of rows) {
423
+ let value = row?.[field]
424
+ let key = valueKey(value)
425
+ if (seen.has(key)) continue
426
+ seen.add(key)
427
+ values.push(value)
428
+ }
429
+ return values
430
+ }
431
+
432
+ function sortableValue(value: unknown, type: 'date' | 'number') {
433
+ if (type === 'number') {
434
+ let parsed = Number(value)
435
+ return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY
436
+ }
437
+ let timestamp = value instanceof Date ? value.getTime() : Date.parse(String(value ?? ''))
438
+ return Number.isFinite(timestamp) ? timestamp : Number.POSITIVE_INFINITY
439
+ }
440
+
441
+ function ordinalSortValue(value: unknown, timeOrdinal: string) {
442
+ let numeric = Number(value)
443
+ if (!Number.isFinite(numeric)) return Number.POSITIVE_INFINITY
444
+
445
+ if (timeOrdinal === 'dow_1m') return numeric >= 1 && numeric <= 7 ? numeric : Number.POSITIVE_INFINITY
446
+
447
+ if (timeOrdinal === 'dow_1s') {
448
+ if (numeric < 1 || numeric > 7) return Number.POSITIVE_INFINITY
449
+ return numeric === 1 ? 7 : numeric - 1
450
+ }
451
+
452
+ if (timeOrdinal === 'dow_0s') {
453
+ if (numeric < 0 || numeric > 6) return Number.POSITIVE_INFINITY
454
+ return numeric === 0 ? 7 : numeric
455
+ }
456
+
457
+ if (timeOrdinal === 'month_of_year') return numeric >= 1 && numeric <= 12 ? numeric : Number.POSITIVE_INFINITY
458
+ if (timeOrdinal === 'quarter_of_year') return numeric >= 1 && numeric <= 4 ? numeric : Number.POSITIVE_INFINITY
459
+
460
+ return numeric
461
+ }
462
+
463
+ function cartesianValues(valueLists: unknown[][]) {
464
+ if (valueLists.length === 0) return [[]] as unknown[][]
465
+
466
+ return valueLists.reduce<unknown[][]>(
467
+ (acc, values) => {
468
+ let next: unknown[][] = []
469
+ for (let prefix of acc) {
470
+ for (let value of values) next.push([...prefix, value])
471
+ }
472
+ return next
473
+ },
474
+ [[]],
475
+ )
476
+ }
477
+
478
+ function compositeKey(values: unknown[]) {
479
+ return values.map(value => valueKey(value)).join('|')
480
+ }
481
+
482
+ function valueKey(value: unknown) {
483
+ return JSON.stringify(value ?? null)
484
+ }
@@ -0,0 +1,57 @@
1
+ import type {Field} from './types.ts'
2
+
3
+ export type SummaryMetric = 'min' | 'max' | 'median' | 'mean' | 'sum' | 'count' | 'countDistinct'
4
+
5
+ export type ColumnUnitSummary = Partial<Record<SummaryMetric, number>>
6
+
7
+ /**
8
+ * Summarize a field using only the requested metrics.
9
+ */
10
+ export function summarizeColumn(rows: Record<string, unknown>[], field: Field, metrics: SummaryMetric[] = []): ColumnUnitSummary {
11
+ if (!Array.isArray(rows) || rows.length === 0 || metrics.length === 0 || !field?.name) return {}
12
+
13
+ let requested = new Set(metrics)
14
+ let result: ColumnUnitSummary = {}
15
+ let values = rows.map(row => row?.[field.name])
16
+
17
+ if (requested.has('count')) result.count = rows.length
18
+
19
+ if (requested.has('countDistinct')) {
20
+ let distinct = new Set(values.filter(value => value !== undefined && value !== null).map(value => String(value)))
21
+ result.countDistinct = distinct.size
22
+ }
23
+
24
+ let needsNumeric = ['min', 'max', 'median', 'mean', 'sum'].some(metric => requested.has(metric as SummaryMetric))
25
+ let isNumeric = String(field.type || '').toLowerCase() === 'number'
26
+ if (!isNumeric || !needsNumeric) return result
27
+
28
+ let numericValues = values.map(value => (typeof value === 'number' ? value : Number(value))).filter(value => Number.isFinite(value))
29
+ if (!numericValues.length) return result
30
+
31
+ if (requested.has('sum') || requested.has('mean')) {
32
+ let total = 0
33
+ for (let value of numericValues) total += value
34
+ if (requested.has('sum')) result.sum = total
35
+ if (requested.has('mean')) result.mean = total / numericValues.length
36
+ }
37
+
38
+ if (requested.has('min')) {
39
+ let min = numericValues[0]
40
+ for (let value of numericValues) if (value < min) min = value
41
+ result.min = min
42
+ }
43
+
44
+ if (requested.has('max')) {
45
+ let max = numericValues[0]
46
+ for (let value of numericValues) if (value > max) max = value
47
+ result.max = max
48
+ }
49
+
50
+ if (requested.has('median')) {
51
+ let sorted = [...numericValues].sort((a, b) => a - b)
52
+ let midpoint = Math.floor(sorted.length / 2)
53
+ result.median = sorted.length % 2 ? sorted[midpoint] : (sorted[midpoint - 1] + sorted[midpoint]) / 2
54
+ }
55
+
56
+ return result
57
+ }