@graphenedata/cli 0.0.12 → 0.0.14

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 (46) hide show
  1. package/dist/cli/cli.js +8591 -1214
  2. package/dist/docs/base.md +98 -0
  3. package/dist/docs/cli.md +22 -0
  4. package/dist/docs/graphene.md +10 -10
  5. package/dist/ui/component-utilities/echarts.js +2 -3
  6. package/dist/ui/component-utilities/formatting.js +3 -11
  7. package/dist/ui/component-utilities/getSeriesConfig.js +2 -1
  8. package/dist/ui/components/Area.svelte +188 -151
  9. package/dist/ui/components/AreaChart.svelte +43 -79
  10. package/dist/ui/components/Bar.svelte +273 -255
  11. package/dist/ui/components/BarChart.svelte +58 -112
  12. package/dist/ui/components/BigValue.svelte +13 -7
  13. package/dist/ui/components/Chart.svelte +280 -317
  14. package/dist/ui/components/Column.svelte +102 -113
  15. package/dist/ui/components/DateRange.svelte +37 -27
  16. package/dist/ui/components/Dropdown.svelte +77 -57
  17. package/dist/ui/components/DropdownOption.svelte +10 -7
  18. package/dist/ui/components/ECharts.svelte +23 -16
  19. package/dist/ui/components/ErrorChart.svelte +85 -21
  20. package/dist/ui/components/GrapheneQuery.svelte +7 -3
  21. package/dist/ui/components/InlineDelta.svelte +53 -34
  22. package/dist/ui/components/Line.svelte +192 -178
  23. package/dist/ui/components/LineChart.svelte +53 -96
  24. package/dist/ui/components/PieChart.svelte +26 -15
  25. package/dist/ui/components/QueryLoad.svelte +15 -10
  26. package/dist/ui/components/SortIcon.svelte +5 -1
  27. package/dist/ui/components/Table.svelte +15 -9
  28. package/dist/ui/components/TableCell.svelte +30 -17
  29. package/dist/ui/components/TableGroupRow.svelte +26 -19
  30. package/dist/ui/components/TableGroupToggle.svelte +9 -6
  31. package/dist/ui/components/TableHeader.svelte +37 -27
  32. package/dist/ui/components/TableRow.svelte +30 -20
  33. package/dist/ui/components/TableSubtotalRow.svelte +16 -9
  34. package/dist/ui/components/TableTotalRow.svelte +18 -11
  35. package/dist/ui/components/TextInput.svelte +23 -20
  36. package/dist/ui/components/_Table.svelte +303 -260
  37. package/dist/ui/internal/LocalApp.svelte +40 -0
  38. package/dist/ui/internal/NavSidebar.svelte +27 -30
  39. package/dist/ui/internal/PageError.svelte +23 -0
  40. package/dist/ui/internal/checkSocket.ts +48 -0
  41. package/dist/ui/internal/queryEngine.ts +9 -2
  42. package/dist/ui/internal/telemetry.ts +1 -0
  43. package/dist/ui/web.js +5 -55
  44. package/package.json +9 -10
  45. package/cli.ts +0 -156
  46. package/dist/ui/internal/NavSidebarHMR.svelte +0 -8
@@ -0,0 +1,98 @@
1
+ Graphene is a framework for doing data analysis as code.
2
+
3
+ Schema definitions and semantic models are done in `.gsql`, dashboards in `.md`.
4
+
5
+ ## GSQL
6
+ GSQL extends ansi sql with a few key features:
7
+ `table` statements define existing tables, adding joins and dimensions/measures
8
+ `table X as (select ...)` define a table as the result of a query
9
+ `extend` add measures and joins to an existing table, usually used with `table X as (select ...)`
10
+ Implicit joins: `from orders select status, user.name` will automatically join users on to orders
11
+ Measure expansion: `from users select id, orders.revenue` automatically expands to `select users.id, sum(orders.amount) ...`
12
+ - NEVER `sum(revenue)` or `group by revenue`
13
+ - OK: `floor(revenue)`, `revenue / cost`
14
+ `group by all` is implied, and does not need to be put in gsql
15
+
16
+ A few common features are not supported in gsql, and must be avoided: window functions, subqueries, CTEs.
17
+
18
+ ```sql
19
+ table orders (
20
+ id BIGINT
21
+ user_id BIGINT
22
+ amount FLOAT
23
+ status STRING
24
+ join one users on user_id = users.id -- many orders per user
25
+ is_complete: status = 'Complete' -- dimension (scalar expression)
26
+ revenue: sum(amount) -- measure (agg expression)
27
+ avg_order: revenue / count(*) -- measures can compose
28
+ )
29
+ table users (
30
+ id BIGINT
31
+ name VARCHAR
32
+ join many orders on id = orders.user_id
33
+ )
34
+ ```
35
+
36
+ ## Dashboards
37
+
38
+ `.md` files with GSQL in code fences, viz components reference query names.
39
+
40
+ ````md
41
+ ```gsql monthly_sales
42
+ select date_trunc(created_at, month) as month, revenue from orders
43
+ ```
44
+ <LineChart data="monthly_sales" x="month" y="revenue" />
45
+ <BigValue data="monthly_sales" value="revenue" />
46
+ ````
47
+
48
+ `data` can be query name or table name. Attributes accept columns, expressions, or GSQL.
49
+
50
+ ## Components
51
+
52
+ BarChart: data, x, y, y2, series, title, subtitle, xFmt, yFmt, y2Fmt, colorPalette, type, swapXY, labels, labelFmt, sort, legend, yMin, yMax
53
+ LineChart: data, x, y, y2, series, title, subtitle, xFmt, yFmt, colorPalette, labels, sort, legend, handleMissing, markers, markerShape, lineType, lineWidth
54
+ AreaChart: data, x, y, series, title, subtitle, xFmt, yFmt, colorPalette, type, labels, sort, legend, handleMissing, fillOpacity, line
55
+ PieChart: data, category, value, title, subtitle
56
+ BigValue: data, value, title, fmt, comparison, comparisonFmt, comparisonTitle, downIsGood, sparkline, sparklineType, sparklineColor
57
+ Table: data, rows, title, subtitle, groupBy, groupType, subtotals, totalRow, search, sort, link, rowShading, rowNumbers, compact, headerColor
58
+ Column: id, title, fmt, align, wrap, contentType, totalAgg, redNegatives
59
+ Dropdown: name, data, value, label, defaultValue, multiple, title
60
+ TextInput: name, title, placeholder
61
+ Row: layout container, distributes children horizontally
62
+
63
+ series: column whose values become separate lines/bars (series=country plots one line per country)
64
+ type: stacked (default), grouped, stacked100
65
+ y2: secondary y-axis, y2SeriesType sets its chart type (line/bar/scatter)
66
+ swapXY: horizontal bars
67
+ handleMissing: gap (default), connect, zero
68
+ colorPalette: comma-separated "#hex1, #hex2", applied to series in order
69
+ seriesOrder: control series order "Val1, Val2", pairs with colorPalette
70
+ labels: show value labels on chart
71
+ markers: show dots on line points; markerShape: circle, emptyCircle, rect, triangle, diamond
72
+ lineType: solid, dashed, dotted
73
+ downIsGood: green for negative (for comparison/delta)
74
+ sparkline: date column for mini trend; sparklineType: line, area, bar
75
+ groupBy: group table rows; groupType: accordion (collapsible) or section (merged)
76
+ totalRow: sum row at bottom; subtotals: sum row per group
77
+ link: make table rows clickable, value is URL column
78
+
79
+ Column contentType:
80
+ - delta: arrows+colors. deltaSymbol, downIsGood, chip, neutralMin/Max
81
+ - colorscale: background gradient. colorScale, colorMin/Mid/Max
82
+ - bar: in-cell bar. barColor, negativeBarColor
83
+ - link: clickable. linkLabel, openInNewTab
84
+ - image: show image. height, width
85
+ - sparkline/sparkarea/sparkbar: mini chart from array. sparkX, sparkY, sparkColor
86
+
87
+ Column totalAgg: sum (default), mean, weightedMean, median, min, max, count, countDistinct
88
+
89
+ Inputs: reference as $name in queries. `<Dropdown name=status .../>` -> `where status = $status`
90
+
91
+ ## Formatting
92
+
93
+ `fmt` attr accepts Excel codes or built-ins:
94
+ Numbers: num0-num4, num0k, num0m, num0b
95
+ Currency: usd, usd0, usd1k, usd2m (also eur, gbp, etc)
96
+ Percent: pct, pct0, pct1, pct2
97
+ Dates: shortdate, longdate, mdy, dmy, mmm, yyyy
98
+ Excel: "$#,##0.00", "0.0%", "m/d/yy"
@@ -0,0 +1,22 @@
1
+ ## CLI
2
+
3
+ When using Graphene from the CLI, the main command would be:
4
+
5
+ `npm exec graphene check`
6
+
7
+ Use the appropriate package manager if this project doesn't use npm. `check` takes several options
8
+
9
+ ```bash
10
+ npm exec graphene check # Syntax check entire project
11
+ npm exec graphene check [mdPath] # Check specific markdown file, and take a screenshot
12
+ npm exec graphene check [mdPath] -c [chartTitle] # Check and get a screenshot for one specific chart
13
+ ```
14
+
15
+ `npm exec graphene run "<sql>"` can be used to run sql directly, without creating a md file.
16
+ `npm exec graphene compile "<sql>"` shows the compiled, dialect-specific SQL.
17
+
18
+ ### Best Practices
19
+
20
+ Start simple - Get basic query working, then add complexity.
21
+ Use check often - Catches syntax errors and shows visual output.
22
+ Leverage models - Use modeled joins and stored expressions rather than raw SQL.
@@ -109,7 +109,7 @@ GSQL is comprised of four primary statements:
109
109
 
110
110
  ```sql
111
111
  table orders (
112
- id BIGINT primary_key
112
+ id BIGINT
113
113
  user_id BIGINT
114
114
  created_at DATETIME
115
115
  status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
@@ -126,7 +126,7 @@ table orders (
126
126
  )
127
127
 
128
128
  table users (
129
- id BIGINT primary_key
129
+ id BIGINT
130
130
  name VARCHAR
131
131
  email VARCHAR
132
132
  age INTEGER
@@ -140,7 +140,7 @@ We can break down a table statement into three parts: [base columns](#base-colum
140
140
 
141
141
  ### Base columns (required)
142
142
 
143
- The base column set is simply a reflection of the underlying database table's schema. Similar to `create table` statements in regular SQL DDL, you list each column's name and data type. One column must be designated as the primary key.
143
+ The base column set is simply a reflection of the underlying database table's schema. Similar to `create table` statements in regular SQL DDL, you list each column's name and data type.
144
144
 
145
145
  ### Join relationships
146
146
 
@@ -231,7 +231,7 @@ table orders (
231
231
  )
232
232
 
233
233
  table users (
234
- id BIGINT primary_key,
234
+ id BIGINT,
235
235
  name VARCHAR,
236
236
  ...
237
237
  )
@@ -270,7 +270,7 @@ table users (
270
270
  )
271
271
 
272
272
  table countries (
273
- code VARCHAR primary_key
273
+ code VARCHAR
274
274
  name VARCHAR
275
275
  currency VARCHAR
276
276
  free_shipping BOOLEAN
@@ -300,7 +300,7 @@ Again, using the orders table from before:
300
300
 
301
301
  ```sql
302
302
  table orders (
303
- id BIGINT primary_key
303
+ id BIGINT
304
304
  user_id BIGINT
305
305
  created_at DATETIME
306
306
  status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
@@ -385,7 +385,7 @@ Here's an example of a fan-out:
385
385
 
386
386
  ```sql
387
387
  table orders (
388
- id BIGINT primary_key
388
+ id BIGINT
389
389
  customer_name VARCHAR
390
390
  amt_with_shipping FLOAT
391
391
 
@@ -393,7 +393,7 @@ table orders (
393
393
  )
394
394
 
395
395
  table order_items (
396
- id BIGINT primary_key
396
+ id BIGINT
397
397
  order_id BIGINT
398
398
  product VARCHAR
399
399
  price FLOAT
@@ -583,7 +583,7 @@ You can turn the output of any `select` statement into a table with `table foo a
583
583
 
584
584
  ```sql
585
585
  table orders (
586
- id BIGINT primary_key
586
+ id BIGINT
587
587
  user_id BIGINT
588
588
  created_at DATETIME
589
589
  status STRING -- One of 'Processing', 'Shipped', 'Complete', 'Cancelled', 'Returned'
@@ -600,7 +600,7 @@ table orders (
600
600
  )
601
601
 
602
602
  table users (
603
- id BIGINT primary_key
603
+ id BIGINT
604
604
  name VARCHAR
605
605
  email VARCHAR
606
606
  age INTEGER
@@ -4,7 +4,7 @@ import * as chartWindowDebug from './chartWindowDebug'
4
4
 
5
5
  /**
6
6
  * @typedef {import("echarts").EChartsOption & {
7
- * dispatch?: ReturnType<typeof import("svelte").createEventDispatcher>;
7
+ * onclick?: (params: any) => void;
8
8
  * showAllXAxisLabels?: boolean;
9
9
  * theme: 'light' | 'dark';
10
10
  * }
@@ -148,9 +148,8 @@ const echartsAction = (node, options) => {
148
148
  applySeriesOptions()
149
149
 
150
150
  // Click event handler:
151
- let dispatch = options.dispatch
152
151
  chart.on('click', function (params) {
153
- dispatch('click', params)
152
+ options.onclick?.(params)
154
153
  })
155
154
 
156
155
  // watching parent element is necessary for charts within `Fullscreen` components
@@ -241,18 +241,10 @@ function maybeExtractFormatTag (columnName) {
241
241
  * Formats a value to whichever format is passed in
242
242
  * @param {*} value the value to be formatted
243
243
  * @param {string} format string containing an Excel-style format code, or a format name matching a built-in or custom format
244
+ * @param {'number' | 'date' | 'boolean' | 'string'} [valueType] the known column type
244
245
  * @returns a formatted value
245
246
  */
246
- export function fmt (value, format) {
247
- let formatObj = getFormatObjectFromString(format)
248
- let valueType = valueTypeFromSample(value)
249
- formatObj.valueType = valueType
247
+ export function fmt (value, format, valueType = undefined) {
248
+ let formatObj = getFormatObjectFromString(format, valueType)
250
249
  return formatValue(value, formatObj)
251
250
  }
252
-
253
- function valueTypeFromSample (value) {
254
- if (typeof value === 'number') return 'number'
255
- if (typeof value === 'boolean') return 'boolean'
256
- if (value instanceof Date) return 'date'
257
- return 'string'
258
- }
@@ -227,8 +227,9 @@ export default function getSeriesConfig (
227
227
 
228
228
  // format series config:
229
229
  if (seriesLabelFmt) {
230
+ let seriesType = series ? columnSummary[series]?.type : undefined
230
231
  seriesConfig.forEach((item) => {
231
- item.name = fmt(item.name, seriesLabelFmt)
232
+ item.name = fmt(item.name, seriesLabelFmt, seriesType)
232
233
  })
233
234
  }
234
235
 
@@ -1,177 +1,214 @@
1
- <script>
2
- import {beforeUpdate, getContext} from 'svelte'
1
+ <script lang="ts">
2
+ import {getContext} from 'svelte'
3
+ import type {Writable} from 'svelte/store'
3
4
  import {propKey, configKey} from '../component-utilities/chartContext.js'
4
5
  import getSeriesConfig from '../component-utilities/getSeriesConfig.js'
5
6
  import formatTitle from '../component-utilities/formatTitle.js'
6
7
  import replaceNulls from '../component-utilities/replaceNulls.js'
7
8
  import getCompletedData from '../component-utilities/getCompletedData.js'
8
- import {
9
- formatValue,
10
- getFormatObjectFromString,
11
- } from '../component-utilities/formatting.js'
9
+ import {formatValue, getFormatObjectFromString} from '../component-utilities/formatting.js'
12
10
  import {getThemeStores} from '../component-utilities/themeStores'
13
11
  import {parseCommaList} from '../component-utilities/inputUtils.ts'
14
12
 
15
- const {resolveColor} = getThemeStores()
16
- const props = getContext(propKey)
17
- const config = getContext(configKey)
18
-
19
- export let y = undefined
20
- const ySet = !!y
21
- export let series = undefined
22
- const seriesSet = !!series
23
- export let options = undefined
24
- export let name = undefined
25
- export let type = 'stacked'
26
- export let fillColor = undefined
27
- $: fillColorStore = resolveColor(fillColor)
28
- export let lineColor = undefined
29
- $: lineColorStore = resolveColor(lineColor)
30
- export let fillOpacity = undefined
31
- export let line = true
32
- $: line = line === 'true' || line === true
33
- export let markers = false
34
- $: markers = markers === 'true' || markers === true
35
- export let markerShape = 'circle'
36
- export let markerSize = 8
37
- export let handleMissing = 'gap'
38
- export let step = false
39
- $: step = step === 'true' || step === true
40
- export let stepPosition = 'end'
41
- export let labels = false
42
- $: labels = labels === 'true' || labels === true
43
- export let labelSize = 11
44
- export let labelPosition = 'top'
45
- export let labelColor = undefined
46
- $: labelColorStore = resolveColor(labelColor)
47
- export let labelFmt = undefined
48
- export let showAllLabels = false
49
- export let seriesOrder = undefined
50
- export let seriesLabelFmt = undefined
51
-
52
- let labelFormat
53
- let data
54
- let x
55
- let swapXY
56
- let yFormat
57
- let xType
58
- let xMismatch
59
- let columnSummary
60
- let seriesConfig
61
- let chartOverrides
62
- let defaultLabelPosition
63
- let resolvedY
64
- let stackName
65
-
66
- if (labelFmt) labelFormat = getFormatObjectFromString(labelFmt)
67
-
68
- $: data = $props.data
69
- $: x = $props.x
70
- $: swapXY = $props.swapXY
71
- $: yFormat = $props.yFormat
72
- $: xType = $props.xType
73
- $: xMismatch = $props.xMismatch
74
- $: columnSummary = $props.columnSummary
75
- $: series = seriesSet ? series : $props.series
76
- $: resolvedY = ySet ? parseCommaList(y) : $props.y
77
- $: seriesOrder = parseCommaList(seriesOrder)
78
-
79
- $: {
80
- if (!series && (!Array.isArray(resolvedY) || resolvedY.length === 1)) {
81
- stackName = undefined
82
- let col = Array.isArray(resolvedY) ? resolvedY[0] : resolvedY
83
- if (columnSummary?.[col]) name = name ?? formatTitle(col, columnSummary[col].title)
84
- } else {
85
- stackName = 'area'
86
- data = getCompletedData(data, x, resolvedY, series, false, xType === 'value')
87
- data = replaceNulls(data, resolvedY)
88
- xType = xType === 'value' ? 'category' : xType
89
- }
13
+ interface Props {
14
+ y?: any, series?: any, options?: any, name?: any, type?: string, fillColor?: any, lineColor?: any
15
+ fillOpacity?: any, line?: boolean | string, markers?: boolean | string, markerShape?: string
16
+ markerSize?: number, handleMissing?: string, step?: boolean | string, stepPosition?: string
17
+ labels?: boolean | string, labelSize?: number, labelPosition?: string, labelColor?: any
18
+ labelFmt?: any, showAllLabels?: boolean | string, seriesOrder?: any, seriesLabelFmt?: any
90
19
  }
91
20
 
92
- $: if (handleMissing === 'zero') data = replaceNulls(data, resolvedY)
21
+ const {resolveColor} = getThemeStores()
22
+ const chartProps: Writable<any> = getContext(propKey)
23
+ const config: Writable<any> = getContext(configKey)
24
+
25
+ let {
26
+ y = undefined, series = undefined, options = undefined, name = undefined, type = 'stacked',
27
+ fillColor = undefined, lineColor = undefined, fillOpacity = undefined, line = true, markers = false,
28
+ markerShape = 'circle', markerSize = 8, handleMissing = 'gap', step = false, stepPosition = 'end',
29
+ labels = false, labelSize = 11, labelPosition = 'top', labelColor = undefined, labelFmt = undefined,
30
+ showAllLabels = false, seriesOrder = undefined, seriesLabelFmt = undefined,
31
+ }: Props = $props()
32
+
33
+ // Use $derived for values that depend on props
34
+ let ySet = $derived(y ? true : false)
35
+ let seriesSet = $derived(series ? true : false)
36
+
37
+ let fillColorStore = $derived(resolveColor(fillColor))
38
+ let lineColorStore = $derived(resolveColor(lineColor))
39
+ let lineBool = $derived(line === 'true' || line === true)
40
+ let markersBool = $derived(markers === 'true' || markers === true)
41
+ let stepBool = $derived(step === 'true' || step === true)
42
+ let labelsBool = $derived(labels === 'true' || labels === true)
43
+ let labelColorStore = $derived(resolveColor(labelColor))
44
+
45
+ // Format objects derived from props
46
+ let labelFormat = $derived(labelFmt ? getFormatObjectFromString(labelFmt) : undefined)
93
47
 
94
48
  const labelPositions = {above: 'top', below: 'bottom', middle: 'inside'}
95
49
  const swapXYLabelPositions = {above: 'right', below: 'left', middle: 'inside'}
96
50
 
97
- $: {
98
- defaultLabelPosition = swapXY ? 'right' : 'top'
99
- labelPosition = (swapXY ? swapXYLabelPositions[labelPosition] : labelPositions[labelPosition]) ?? defaultLabelPosition
100
- }
51
+ // Derive values from chartProps store instead of using $effect to assign
52
+ let data = $derived($chartProps.data)
53
+ let x = $derived($chartProps.x)
54
+ let swapXY = $derived($chartProps.swapXY)
55
+ let yFormat = $derived($chartProps.yFormat)
56
+ let baseXType = $derived($chartProps.xType)
57
+ let xMismatch = $derived($chartProps.xMismatch)
58
+ let columnSummary = $derived($chartProps.columnSummary)
59
+ let resolvedSeries = $derived(seriesSet ? series : $chartProps.series)
60
+ let resolvedY = $derived(ySet ? parseCommaList(y) : $chartProps.y)
61
+ let resolvedSeriesOrder = $derived(parseCommaList(seriesOrder))
101
62
 
102
- $: baseConfig = {
103
- type: 'line',
104
- stack: stackName,
105
- areaStyle: {color: $fillColorStore, opacity: fillOpacity},
106
- connectNulls: handleMissing === 'connect',
107
- lineStyle: {width: line ? 1 : 0, color: $lineColorStore},
108
- label: {
109
- show: labels,
110
- formatter: (params) =>
111
- params.value[swapXY ? 0 : 1] === 0
112
- ? ''
113
- : formatValue(params.value[swapXY ? 0 : 1], labelFormat ?? yFormat),
114
- fontSize: labelSize,
115
- color: $labelColorStore,
116
- position: labelPosition,
117
- padding: 3,
118
- },
119
- labelLayout: {hideOverlap: showAllLabels ? false : true},
120
- emphasis: {focus: 'series'},
121
- showSymbol: labels || markers,
122
- symbol: markerShape,
123
- symbolSize: labels && !markers ? 0 : markerSize,
124
- step: step ? stepPosition : false,
125
- }
63
+ // Compute all the derived state in one $derived.by block to avoid read/write conflicts
64
+ let computedState = $derived.by(() => {
65
+ let isSingleSeries = !resolvedSeries && (!Array.isArray(resolvedY) || resolvedY.length === 1)
66
+ let computedData = data
67
+ let computedXType = baseXType
68
+ let computedName = name
69
+ let computedStackName: string | undefined = undefined
70
+ let computedDefaultLabelPosition = swapXY ? 'right' : 'top'
126
71
 
127
- $: seriesConfig = getSeriesConfig(
128
- data,
129
- x,
130
- resolvedY,
131
- series,
132
- swapXY,
133
- baseConfig,
134
- name,
135
- xMismatch,
136
- columnSummary,
137
- seriesOrder,
138
- undefined,
139
- undefined,
140
- undefined,
141
- seriesLabelFmt,
142
- )
72
+ if (!data || !columnSummary) {
73
+ return {
74
+ data: computedData,
75
+ xType: computedXType,
76
+ name: computedName,
77
+ stackName: computedStackName,
78
+ defaultLabelPosition: computedDefaultLabelPosition,
79
+ }
80
+ }
81
+
82
+ if (isSingleSeries) {
83
+ // Single Series
84
+ let col = Array.isArray(resolvedY) ? resolvedY[0] : resolvedY
85
+ if (col && columnSummary[col]) {
86
+ computedName = computedName ?? formatTitle(col, columnSummary[col].title)
87
+ }
88
+ } else {
89
+ // Multi Series
90
+ computedStackName = 'area'
91
+ computedData = getCompletedData(computedData, x, resolvedY, resolvedSeries, false, computedXType === 'value')
92
+ computedData = replaceNulls(computedData, resolvedY)
93
+ computedXType = computedXType === 'value' ? 'category' : computedXType
94
+ }
95
+
96
+ // Handle missing values
97
+ if (handleMissing === 'zero') {
98
+ computedData = replaceNulls(computedData, resolvedY)
99
+ }
143
100
 
144
- $: config.update((value) => {
145
- value.series.push(...seriesConfig)
146
- value.legend.data.push(...seriesConfig.map((entry) => entry.name.toString()))
147
- return value
101
+ return {
102
+ data: computedData,
103
+ xType: computedXType,
104
+ name: computedName,
105
+ stackName: computedStackName,
106
+ defaultLabelPosition: computedDefaultLabelPosition,
107
+ }
148
108
  })
149
109
 
150
- $: if (options) {
151
- config.update((value) => ({...value, ...options}))
152
- }
110
+ // Extract computed values for use in template and other derived values
111
+ let processedData = $derived(computedState.data)
112
+ let xType = $derived(computedState.xType)
113
+ let resolvedName = $derived(computedState.name)
114
+ let resolvedStackName = $derived(computedState.stackName)
115
+ let defaultLabelPosition = $derived(computedState.defaultLabelPosition)
116
+
117
+ let resolvedLabelPosition = $derived(
118
+ (swapXY ? swapXYLabelPositions[labelPosition] : labelPositions[labelPosition]) ?? defaultLabelPosition,
119
+ )
153
120
 
154
- $: chartOverrides = {
121
+ let chartOverrides = $derived({
155
122
  yAxis: {boundaryGap: ['0%', '1%']},
156
123
  xAxis: {boundaryGap: ['4%', '4%'], type: xType},
157
- }
124
+ })
158
125
 
159
- beforeUpdate(() => {
160
- config.update((value) => {
161
- value.tooltip = {...value.tooltip, order: 'seriesDesc'}
162
- if (swapXY) {
163
- value.yAxis = {...value.yAxis, ...chartOverrides.xAxis}
164
- value.xAxis = {...value.xAxis, ...chartOverrides.yAxis}
165
- } else {
166
- value.yAxis[0] = {...value.yAxis[0], ...chartOverrides.yAxis}
167
- value.xAxis = {...value.xAxis, ...chartOverrides.xAxis}
168
- }
169
- if (type === 'stacked100') {
170
- if (swapXY) value.xAxis = {...value.xAxis, max: 1}
171
- else value.yAxis[0] = {...value.yAxis[0], max: 1}
172
- }
173
- if (labels) value.axisPointer = {triggerEmphasis: false}
174
- return value
126
+ $effect(() => {
127
+ // Don't run until we have data
128
+ if (!processedData || !columnSummary) return
129
+
130
+ let baseConfig = {
131
+ type: 'line',
132
+ stack: resolvedStackName,
133
+ areaStyle: {color: $fillColorStore, opacity: fillOpacity},
134
+ connectNulls: handleMissing === 'connect',
135
+ lineStyle: {width: lineBool ? 1 : 0, color: $lineColorStore},
136
+ label: {
137
+ show: labelsBool,
138
+ formatter: (params: any) =>
139
+ params.value[swapXY ? 0 : 1] === 0
140
+ ? ''
141
+ : formatValue(params.value[swapXY ? 0 : 1], labelFormat ?? yFormat),
142
+ fontSize: labelSize,
143
+ color: $labelColorStore,
144
+ position: resolvedLabelPosition,
145
+ padding: 3,
146
+ },
147
+ labelLayout: {hideOverlap: showAllLabels ? false : true},
148
+ emphasis: {focus: 'series'},
149
+ showSymbol: labelsBool || markersBool,
150
+ symbol: markerShape,
151
+ symbolSize: labelsBool && !markersBool ? 0 : markerSize,
152
+ step: stepBool ? stepPosition : false,
153
+ }
154
+
155
+ let seriesConfig = getSeriesConfig(
156
+ processedData,
157
+ x,
158
+ resolvedY,
159
+ resolvedSeries,
160
+ swapXY,
161
+ baseConfig,
162
+ resolvedName,
163
+ xMismatch,
164
+ columnSummary,
165
+ resolvedSeriesOrder,
166
+ undefined,
167
+ undefined,
168
+ undefined,
169
+ seriesLabelFmt,
170
+ )
171
+
172
+ config.update((d: any) => {
173
+ // Guard against incomplete config state
174
+ if (!d.series) d.series = []
175
+ if (!d.legend) d.legend = {data: []}
176
+ if (!d.legend.data) d.legend.data = []
177
+
178
+ d.series.push(...seriesConfig)
179
+ d.legend.data.push(...seriesConfig.map((entry: any) => entry.name.toString()))
180
+ return d
175
181
  })
176
182
  })
183
+
184
+ // Use $effect.pre() instead of beforeUpdate for runes mode
185
+ $effect.pre(() => {
186
+ if (options) {
187
+ config.update((d: any) => ({...d, ...options}))
188
+ }
189
+
190
+ if (chartOverrides) {
191
+ config.update((d: any) => {
192
+ // Guard against incomplete config state
193
+ if (!d.yAxis || !Array.isArray(d.yAxis)) return d
194
+
195
+ d.tooltip = {...d.tooltip, order: 'seriesDesc'}
196
+ if (swapXY) {
197
+ d.yAxis = {...d.yAxis, ...chartOverrides.xAxis}
198
+ d.xAxis = {...d.xAxis, ...chartOverrides.yAxis}
199
+ } else {
200
+ if (d.yAxis[0]) {
201
+ d.yAxis[0] = {...d.yAxis[0], ...chartOverrides.yAxis}
202
+ }
203
+ d.xAxis = {...d.xAxis, ...chartOverrides.xAxis}
204
+ }
205
+ if (type === 'stacked100') {
206
+ if (swapXY) d.xAxis = {...d.xAxis, max: 1}
207
+ else if (d.yAxis[0]) d.yAxis[0] = {...d.yAxis[0], max: 1}
208
+ }
209
+ if (labelsBool) d.axisPointer = {triggerEmphasis: false}
210
+ return d
211
+ })
212
+ }
213
+ })
177
214
  </script>