@rokkit/chart 1.0.0-next.146 → 1.0.0-next.148

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 (157) hide show
  1. package/dist/Plot/index.d.ts +4 -0
  2. package/dist/PlotState.svelte.d.ts +47 -0
  3. package/dist/crossfilter/createCrossFilter.svelte.d.ts +15 -0
  4. package/dist/geoms/lib/areas.d.ts +52 -0
  5. package/dist/geoms/lib/bars.d.ts +3 -0
  6. package/dist/index.d.ts +38 -1
  7. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +10 -0
  8. package/dist/lib/brewing/CartesianBrewer.svelte.d.ts +8 -0
  9. package/dist/lib/brewing/PieBrewer.svelte.d.ts +8 -0
  10. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +9 -0
  11. package/dist/lib/brewing/brewer.svelte.d.ts +145 -0
  12. package/dist/lib/brewing/colors.d.ts +17 -0
  13. package/dist/lib/brewing/marks/arcs.d.ts +17 -0
  14. package/dist/lib/brewing/marks/areas.d.ts +31 -0
  15. package/dist/lib/brewing/marks/bars.d.ts +1 -0
  16. package/dist/lib/brewing/marks/boxes.d.ts +24 -0
  17. package/dist/lib/brewing/marks/lines.d.ts +24 -0
  18. package/dist/lib/brewing/marks/points.d.ts +40 -0
  19. package/dist/lib/brewing/marks/violins.d.ts +20 -0
  20. package/dist/lib/brewing/patterns.d.ts +14 -0
  21. package/dist/lib/brewing/scales.d.ts +28 -0
  22. package/dist/lib/brewing/stats.d.ts +31 -0
  23. package/dist/lib/brewing/symbols.d.ts +7 -0
  24. package/dist/lib/plot/chartProps.d.ts +177 -0
  25. package/dist/lib/plot/crossfilter.d.ts +13 -0
  26. package/dist/lib/plot/facet.d.ts +24 -0
  27. package/dist/lib/plot/frames.d.ts +45 -0
  28. package/dist/lib/plot/helpers.d.ts +3 -0
  29. package/dist/lib/plot/preset.d.ts +29 -0
  30. package/dist/lib/plot/scales.d.ts +5 -0
  31. package/dist/lib/plot/stat.d.ts +32 -0
  32. package/dist/lib/plot/types.d.ts +89 -0
  33. package/dist/lib/scales.svelte.d.ts +1 -1
  34. package/dist/lib/swatch.d.ts +12 -0
  35. package/dist/lib/utils.d.ts +1 -0
  36. package/dist/lib/xscale.d.ts +11 -0
  37. package/dist/patterns/index.d.ts +4 -9
  38. package/dist/patterns/patterns.d.ts +72 -0
  39. package/dist/patterns/scale.d.ts +30 -0
  40. package/package.json +9 -3
  41. package/src/AnimatedPlot.svelte +194 -0
  42. package/src/Chart.svelte +101 -0
  43. package/src/FacetPlot/Panel.svelte +23 -0
  44. package/src/FacetPlot.svelte +90 -0
  45. package/src/Plot/Arc.svelte +29 -0
  46. package/src/Plot/Area.svelte +25 -0
  47. package/src/Plot/Axis.svelte +62 -84
  48. package/src/Plot/Grid.svelte +20 -58
  49. package/src/Plot/Legend.svelte +160 -120
  50. package/src/Plot/Line.svelte +27 -0
  51. package/src/Plot/Point.svelte +27 -0
  52. package/src/Plot/Timeline.svelte +95 -0
  53. package/src/Plot/Tooltip.svelte +81 -0
  54. package/src/Plot/index.js +4 -0
  55. package/src/Plot.svelte +189 -0
  56. package/src/PlotState.svelte.js +278 -0
  57. package/src/Sparkline.svelte +69 -0
  58. package/src/charts/AreaChart.svelte +25 -0
  59. package/src/charts/BarChart.svelte +26 -0
  60. package/src/charts/BoxPlot.svelte +21 -0
  61. package/src/charts/BubbleChart.svelte +23 -0
  62. package/src/charts/LineChart.svelte +26 -0
  63. package/src/charts/PieChart.svelte +25 -0
  64. package/src/charts/ScatterPlot.svelte +25 -0
  65. package/src/charts/ViolinPlot.svelte +21 -0
  66. package/src/crossfilter/CrossFilter.svelte +38 -0
  67. package/src/crossfilter/FilterBar.svelte +32 -0
  68. package/src/crossfilter/FilterSlider.svelte +79 -0
  69. package/src/crossfilter/createCrossFilter.svelte.js +113 -0
  70. package/src/elements/SymbolGrid.svelte +6 -7
  71. package/src/geoms/Arc.svelte +81 -0
  72. package/src/geoms/Area.svelte +50 -0
  73. package/src/geoms/Bar.svelte +142 -0
  74. package/src/geoms/Box.svelte +101 -0
  75. package/src/geoms/LabelPill.svelte +17 -0
  76. package/src/geoms/Line.svelte +100 -0
  77. package/src/geoms/Point.svelte +100 -0
  78. package/src/geoms/Violin.svelte +44 -0
  79. package/src/geoms/lib/areas.js +131 -0
  80. package/src/geoms/lib/bars.js +172 -0
  81. package/src/index.js +52 -3
  82. package/src/lib/brewing/BoxBrewer.svelte.js +56 -0
  83. package/src/lib/brewing/CartesianBrewer.svelte.js +16 -0
  84. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  85. package/src/lib/brewing/ViolinBrewer.svelte.js +55 -0
  86. package/src/lib/brewing/brewer.svelte.js +229 -0
  87. package/src/lib/brewing/colors.js +22 -0
  88. package/src/lib/brewing/marks/arcs.js +43 -0
  89. package/src/lib/brewing/marks/areas.js +59 -0
  90. package/src/lib/brewing/marks/bars.js +49 -0
  91. package/src/lib/brewing/marks/boxes.js +75 -0
  92. package/src/lib/brewing/marks/lines.js +48 -0
  93. package/src/lib/brewing/marks/points.js +57 -0
  94. package/src/lib/brewing/marks/violins.js +90 -0
  95. package/src/lib/brewing/patterns.js +31 -0
  96. package/src/lib/brewing/scales.js +51 -0
  97. package/src/lib/brewing/scales.svelte.js +2 -26
  98. package/src/lib/brewing/stats.js +62 -0
  99. package/src/lib/brewing/symbols.js +10 -0
  100. package/src/lib/plot/chartProps.js +76 -0
  101. package/src/lib/plot/crossfilter.js +16 -0
  102. package/src/lib/plot/facet.js +58 -0
  103. package/src/lib/plot/frames.js +90 -0
  104. package/src/lib/plot/helpers.js +14 -0
  105. package/src/lib/plot/preset.js +53 -0
  106. package/src/lib/plot/scales.js +56 -0
  107. package/src/lib/plot/stat.js +92 -0
  108. package/src/lib/plot/types.js +65 -0
  109. package/src/lib/scales.svelte.js +2 -26
  110. package/src/lib/swatch.js +13 -0
  111. package/src/lib/utils.js +9 -0
  112. package/src/lib/xscale.js +31 -0
  113. package/src/patterns/DefinePatterns.svelte +32 -0
  114. package/src/patterns/PatternDef.svelte +27 -0
  115. package/src/patterns/index.js +4 -14
  116. package/src/patterns/patterns.js +208 -0
  117. package/src/patterns/scale.js +87 -0
  118. package/src/spec/chart-spec.js +29 -0
  119. package/src/symbols/Shape.svelte +1 -1
  120. package/src/symbols/constants/index.js +1 -1
  121. package/dist/old_lib/index.d.ts +0 -4
  122. package/dist/old_lib/plots.d.ts +0 -3
  123. package/dist/old_lib/swatch.d.ts +0 -285
  124. package/dist/old_lib/utils.d.ts +0 -1
  125. package/dist/patterns/paths/constants.d.ts +0 -1
  126. package/dist/template/constants.d.ts +0 -43
  127. package/dist/template/shapes/index.d.ts +0 -4
  128. package/src/old_lib/index.js +0 -4
  129. package/src/old_lib/plots.js +0 -27
  130. package/src/old_lib/swatch.js +0 -16
  131. package/src/old_lib/utils.js +0 -8
  132. package/src/patterns/Brick.svelte +0 -15
  133. package/src/patterns/Circles.svelte +0 -18
  134. package/src/patterns/CrossHatch.svelte +0 -12
  135. package/src/patterns/CurvedWave.svelte +0 -7
  136. package/src/patterns/Dots.svelte +0 -20
  137. package/src/patterns/OutlineCircles.svelte +0 -13
  138. package/src/patterns/Tile.svelte +0 -16
  139. package/src/patterns/Triangles.svelte +0 -13
  140. package/src/patterns/Waves.svelte +0 -9
  141. package/src/patterns/paths/NamedPattern.svelte +0 -9
  142. package/src/patterns/paths/constants.js +0 -4
  143. package/src/template/Texture.svelte +0 -13
  144. package/src/template/constants.js +0 -43
  145. package/src/template/shapes/Circles.svelte +0 -15
  146. package/src/template/shapes/Lines.svelte +0 -16
  147. package/src/template/shapes/Path.svelte +0 -9
  148. package/src/template/shapes/Polygons.svelte +0 -15
  149. package/src/template/shapes/index.js +0 -4
  150. /package/dist/{old_lib → lib}/brewer.d.ts +0 -0
  151. /package/dist/{old_lib → lib}/chart.d.ts +0 -0
  152. /package/dist/{old_lib → lib}/grid.d.ts +0 -0
  153. /package/dist/{old_lib → lib}/ticks.d.ts +0 -0
  154. /package/src/{old_lib → lib}/brewer.js +0 -0
  155. /package/src/{old_lib → lib}/chart.js +0 -0
  156. /package/src/{old_lib → lib}/grid.js +0 -0
  157. /package/src/{old_lib → lib}/ticks.js +0 -0
@@ -0,0 +1,189 @@
1
+ <script>
2
+ import { setContext, untrack } from 'svelte'
3
+ import { PlotState } from './PlotState.svelte.js'
4
+ import Axis from './Plot/Axis.svelte'
5
+ import Grid from './Plot/Grid.svelte'
6
+ import Legend from './Plot/Legend.svelte'
7
+ import Tooltip from './Plot/Tooltip.svelte'
8
+ import DefinePatterns from './patterns/DefinePatterns.svelte'
9
+ import Bar from './geoms/Bar.svelte'
10
+ import Line from './geoms/Line.svelte'
11
+ import Area from './geoms/Area.svelte'
12
+ import Point from './geoms/Point.svelte'
13
+ import Arc from './geoms/Arc.svelte'
14
+ import Box from './geoms/Box.svelte'
15
+ import Violin from './geoms/Violin.svelte'
16
+
17
+ /**
18
+ * @type {{
19
+ * data?: Object[],
20
+ * spec?: import('./lib/plot/types.js').PlotSpec,
21
+ * helpers?: import('./lib/plot/types.js').PlotHelpers,
22
+ * width?: number,
23
+ * height?: number,
24
+ * mode?: 'light' | 'dark',
25
+ * grid?: boolean,
26
+ * axes?: boolean,
27
+ * margin?: { top: number, right: number, bottom: number, left: number },
28
+ * legend?: boolean,
29
+ * title?: string,
30
+ * tooltip?: boolean | ((data: Record<string, unknown>) => string),
31
+ * children?: import('svelte').Snippet,
32
+ * }}
33
+ */
34
+ let {
35
+ data = [],
36
+ spec = undefined,
37
+ helpers = {},
38
+ width = 600,
39
+ height = 400,
40
+ mode = 'light',
41
+ grid = true,
42
+ axes = true,
43
+ margin = undefined,
44
+ legend = false,
45
+ title = '',
46
+ tooltip = false,
47
+ children
48
+ } = $props()
49
+
50
+ // Create PlotState with initial values and provide as context.
51
+ // untrack() suppresses "captures initial value" warnings — intentional:
52
+ // the $effect below handles all subsequent reactive updates.
53
+ const plotState = untrack(() => new PlotState({
54
+ data: spec?.data ?? data,
55
+ width: spec?.width ?? width,
56
+ height: spec?.height ?? height,
57
+ mode,
58
+ margin,
59
+ channels: spec ? { x: spec.x, y: spec.y, color: spec.color ?? spec.fill } : {},
60
+ labels: spec?.labels ?? {},
61
+ helpers,
62
+ xDomain: spec?.xDomain,
63
+ yDomain: spec?.yDomain,
64
+ colorDomain: spec?.colorDomain
65
+ }))
66
+ setContext('plot-state', plotState)
67
+
68
+ // Keep state in sync when reactive config changes
69
+ $effect(() => {
70
+ plotState.update({
71
+ data: spec?.data ?? data,
72
+ width: spec?.width ?? width,
73
+ height: spec?.height ?? height,
74
+ mode,
75
+ margin,
76
+ channels: spec ? { x: spec.x, y: spec.y, color: spec.color ?? spec.fill } : {},
77
+ labels: spec?.labels ?? {},
78
+ helpers,
79
+ xDomain: spec?.xDomain,
80
+ yDomain: spec?.yDomain,
81
+ colorDomain: spec?.colorDomain
82
+ })
83
+ })
84
+
85
+ const svgWidth = $derived(spec?.width ?? width)
86
+ const svgHeight = $derived(spec?.height ?? height)
87
+ const showGrid = $derived(spec?.grid ?? grid)
88
+ const showLegend = $derived(spec?.legend ?? legend)
89
+ const chartTitle = $derived(spec?.title ?? title)
90
+
91
+ // Geoms from spec (spec-driven API)
92
+ const specGeoms = $derived(spec?.geoms ?? [])
93
+
94
+ // Geom component resolver for spec-driven mode
95
+ const GEOM_COMPONENTS = { bar: Bar, line: Line, area: Area, point: Point, arc: Arc, box: Box, violin: Violin }
96
+
97
+ /**
98
+ * @param {string} type
99
+ */
100
+ function resolveGeomComponent(type) {
101
+ return helpers?.geoms?.[type] ?? GEOM_COMPONENTS[type]
102
+ }
103
+ </script>
104
+
105
+ <div class="plot-root" data-plot-root data-mode={mode}>
106
+ {#if chartTitle}
107
+ <div class="plot-title" data-plot-title>{chartTitle}</div>
108
+ {/if}
109
+
110
+ <svg
111
+ width={svgWidth}
112
+ height={svgHeight}
113
+ viewBox="0 0 {svgWidth} {svgHeight}"
114
+ role="img"
115
+ aria-label={chartTitle || 'Chart visualization'}
116
+ >
117
+ <!-- SVG pattern defs -->
118
+ <DefinePatterns />
119
+
120
+ <g
121
+ class="plot-canvas"
122
+ transform="translate({plotState.margin.left}, {plotState.margin.top})"
123
+ data-plot-canvas
124
+ >
125
+ <!-- Grid (behind everything) -->
126
+ {#if showGrid}
127
+ <Grid />
128
+ {/if}
129
+
130
+ <!-- Declarative children (geom components) -->
131
+ {@render children?.()}
132
+
133
+ <!-- Spec-driven geoms -->
134
+ {#each specGeoms as geomSpec}
135
+ {@const GeomComponent = resolveGeomComponent(geomSpec.type)}
136
+ {#if GeomComponent}
137
+ <GeomComponent
138
+ x={geomSpec.x ?? spec?.x}
139
+ y={geomSpec.y ?? spec?.y}
140
+ color={geomSpec.color ?? spec?.color}
141
+ fill={geomSpec.fill ?? spec?.fill}
142
+ stat={geomSpec.stat}
143
+ options={geomSpec.options}
144
+ />
145
+ {/if}
146
+ {/each}
147
+
148
+ <!-- Axes -->
149
+ {#if axes}
150
+ <Axis type="x" label={spec?.labels?.[spec?.x ?? ''] ?? ''} />
151
+ <Axis type="y" label={spec?.labels?.[spec?.y ?? ''] ?? ''} />
152
+ {/if}
153
+ </g>
154
+ </svg>
155
+
156
+ <!-- Legend (HTML, outside SVG) -->
157
+ {#if showLegend}
158
+ <Legend labels={spec?.labels ?? {}} />
159
+ {/if}
160
+
161
+ <!-- Tooltip (HTML, fixed-position overlay) -->
162
+ {#if tooltip}
163
+ <Tooltip {tooltip} />
164
+ {/if}
165
+ </div>
166
+
167
+ <style>
168
+ .plot-root {
169
+ position: relative;
170
+ width: 100%;
171
+ height: auto;
172
+ }
173
+
174
+ svg {
175
+ display: block;
176
+ overflow: visible;
177
+ }
178
+
179
+ .plot-canvas {
180
+ pointer-events: all;
181
+ }
182
+
183
+ .plot-title {
184
+ font-size: 14px;
185
+ font-weight: 600;
186
+ text-align: center;
187
+ margin-bottom: 4px;
188
+ }
189
+ </style>
@@ -0,0 +1,278 @@
1
+ import { untrack } from 'svelte'
2
+ import { applyGeomStat } from './lib/plot/stat.js'
3
+ import { inferFieldType, inferOrientation, buildUnifiedXScale, buildUnifiedYScale, inferColorScaleType } from './lib/plot/scales.js'
4
+ import { resolvePreset } from './lib/plot/preset.js'
5
+ import { resolveFormat, resolveTooltip, resolveGeom } from './lib/plot/helpers.js'
6
+ import { distinct, assignColors } from './lib/brewing/colors.js'
7
+ import { assignPatterns } from './lib/brewing/patterns.js'
8
+ import { assignSymbols } from './lib/brewing/marks/points.js'
9
+
10
+ let nextId = 0
11
+
12
+ export class PlotState {
13
+ #data = $state([])
14
+ #rawData = []
15
+ #channels = $state({})
16
+ #labels = $state({})
17
+ #helpers = $state({})
18
+ #presetName = $state(undefined)
19
+ #colorMidpoint = $state(undefined)
20
+ #colorSpec = $state(undefined)
21
+ #colorDomain = $state(undefined)
22
+ #xDomain = $state(undefined)
23
+ #yDomain = $state(undefined)
24
+ #width = $state(600)
25
+ #height = $state(400)
26
+ #margin = $state({ top: 20, right: 20, bottom: 40, left: 50 })
27
+ #marginOverride = $state(undefined)
28
+
29
+ #geoms = $state([])
30
+ #mode = $state('light')
31
+ #hovered = $state(null)
32
+
33
+ axisOrigin = $state([undefined, undefined])
34
+
35
+ #effectiveMargin = $derived(this.#marginOverride ?? this.#margin)
36
+ #innerWidth = $derived(this.#width - this.#effectiveMargin.left - this.#effectiveMargin.right)
37
+ #innerHeight = $derived(this.#height - this.#effectiveMargin.top - this.#effectiveMargin.bottom)
38
+
39
+ // Effective channels: prefer top-level channels; fall back to first geom's channels
40
+ // for the declarative API where no spec is provided.
41
+ #effectiveChannels = $derived.by(() => {
42
+ const tc = this.#channels
43
+ if (tc.x && tc.y) return tc
44
+ const firstGeom = this.#geoms[0]
45
+ if (!firstGeom) return tc
46
+ return {
47
+ x: tc.x ?? firstGeom.channels?.x,
48
+ y: tc.y ?? firstGeom.channels?.y,
49
+ color: tc.color ?? firstGeom.channels?.color,
50
+ pattern: tc.pattern ?? firstGeom.channels?.pattern,
51
+ symbol: tc.symbol ?? firstGeom.channels?.symbol,
52
+ }
53
+ })
54
+
55
+ orientation = $derived.by(() => {
56
+ const xField = this.#effectiveChannels.x
57
+ const yField = this.#effectiveChannels.y
58
+ if (!xField || !yField) return 'none'
59
+ const rawXType = inferFieldType(this.#data, xField)
60
+ const yType = inferFieldType(this.#data, yField)
61
+ // Bar geoms treat numeric X as categorical (e.g. year on X → vertical bars).
62
+ const hasBarGeom = this.#geoms.some((g) => g.type === 'bar')
63
+ const xType = (hasBarGeom && rawXType === 'continuous' && yType === 'continuous') ? 'band' : rawXType
64
+ return inferOrientation(xType, yType)
65
+ })
66
+
67
+ colorScaleType = $derived.by(() => {
68
+ const field = this.#effectiveChannels.color
69
+ if (!field) return 'categorical'
70
+ return inferColorScaleType(this.#data, field, {
71
+ colorScale: this.#colorSpec,
72
+ colorMidpoint: this.#colorMidpoint
73
+ })
74
+ })
75
+
76
+ xScale = $derived.by(() => {
77
+ const field = this.#effectiveChannels.x
78
+ if (!field) return null
79
+ const datasets = this.#geoms.length > 0
80
+ ? this.#geoms.map((g) => this.geomData(g.id))
81
+ : [this.#rawData]
82
+ const includeZero = this.orientation === 'horizontal'
83
+ // For vertical bar charts, force scaleBand even when X values are numeric (e.g. year).
84
+ // Horizontal bar charts keep X as a continuous value axis.
85
+ const hasBarGeom = this.#geoms.some((g) => g.type === 'bar')
86
+ const bandX = hasBarGeom && this.orientation !== 'horizontal'
87
+ return buildUnifiedXScale(datasets, field, this.#innerWidth, {
88
+ domain: this.#xDomain,
89
+ includeZero,
90
+ band: bandX
91
+ })
92
+ })
93
+
94
+ yScale = $derived.by(() => {
95
+ const field = this.#effectiveChannels.y
96
+ if (!field) return null
97
+ const datasets = this.#geoms.length > 0
98
+ ? this.#geoms.map((g) => this.geomData(g.id))
99
+ : [this.#rawData]
100
+ const includeZero = this.orientation === 'vertical'
101
+
102
+ // For stacked bars, the y domain must cover the per-x column *total*, not the
103
+ // per-row max — otherwise bars overflow the plot area.
104
+ // For box/violin geoms, the processed data has iqr_min/iqr_max instead of raw y values.
105
+ let yDomain = this.#yDomain
106
+ if (!yDomain) {
107
+ const boxGeom = this.#geoms.find((g) => g.type === 'box' || g.type === 'violin')
108
+ if (boxGeom) {
109
+ const boxData = this.geomData(boxGeom.id)
110
+ const mins = boxData.map((d) => d.iqr_min).filter((v) => v !== null && v !== undefined && !isNaN(v))
111
+ const maxs = boxData.map((d) => d.iqr_max).filter((v) => v !== null && v !== undefined && !isNaN(v))
112
+ if (mins.length > 0 && maxs.length > 0) {
113
+ yDomain = [Math.min(...mins), Math.max(...maxs)]
114
+ }
115
+ }
116
+ }
117
+ if (!yDomain) {
118
+ const stackGeom = this.#geoms.find((g) => g.options?.stack)
119
+ if (stackGeom) {
120
+ const xField = this.#effectiveChannels.x
121
+ const stackData = this.geomData(stackGeom.id)
122
+ if (xField && stackData.length > 0) {
123
+ // Mirror buildStackedBars/subBandFields: stack dimension is the first
124
+ // non-x field among [color, pattern]. Summing all raw rows (stat=identity)
125
+ // would overcount when multiple rows share the same (x, stack) key.
126
+ const colorField = this.#effectiveChannels.color
127
+ const patternField = this.#effectiveChannels.pattern
128
+ const stackField = [colorField, patternField].find((f) => f && f !== xField) ?? colorField
129
+ const lookup = new Map()
130
+ for (const d of stackData) {
131
+ const xVal = d[xField]
132
+ const cKey = stackField ? String(d[stackField]) : '_'
133
+ if (!lookup.has(xVal)) lookup.set(xVal, new Map())
134
+ lookup.get(xVal).set(cKey, Number(d[field]) || 0)
135
+ }
136
+ const totals = new Map()
137
+ for (const [xVal, colorMap] of lookup) {
138
+ totals.set(xVal, [...colorMap.values()].reduce((s, v) => s + v, 0))
139
+ }
140
+ const maxStack = Math.max(0, ...totals.values())
141
+ yDomain = [0, maxStack]
142
+ }
143
+ }
144
+ }
145
+
146
+ return buildUnifiedYScale(datasets, field, this.#innerHeight, {
147
+ domain: yDomain,
148
+ includeZero
149
+ })
150
+ })
151
+
152
+ // Colors: Map<colorKey, { fill, stroke }> for all distinct color field values.
153
+ // If a colorDomain is provided (e.g. from FacetPlot for cross-panel consistency),
154
+ // use it instead of deriving distinct values from the local panel data.
155
+ colors = $derived.by(() => {
156
+ const field = this.#effectiveChannels.color
157
+ const values = this.#colorDomain ?? distinct(this.#data, field)
158
+ return assignColors(values, this.#mode)
159
+ })
160
+
161
+ // Patterns: Map<patternKey, patternName> — only populated when a pattern channel is set
162
+ // and the pattern field is categorical (continuous fields can't be discretely patterned).
163
+ patterns = $derived.by(() => {
164
+ const pf = this.#effectiveChannels.pattern
165
+ if (!pf) return new Map()
166
+ if (inferFieldType(this.#data, pf) === 'continuous') return new Map()
167
+ return assignPatterns(distinct(this.#data, pf))
168
+ })
169
+
170
+ // Symbols: Map<symbolKey, shapeName> — only populated when a symbol channel is set.
171
+ symbols = $derived.by(() => {
172
+ const sf = this.#effectiveChannels.symbol
173
+ if (!sf) return new Map()
174
+ return assignSymbols(distinct(this.#data, sf))
175
+ })
176
+
177
+ // Expose effective channel fields for consumers (e.g. Legend)
178
+ colorField = $derived(this.#effectiveChannels.color)
179
+ patternField = $derived(this.#effectiveChannels.pattern)
180
+ symbolField = $derived(this.#effectiveChannels.symbol)
181
+
182
+ // Set of geom types currently registered (used by Legend to pick swatch style)
183
+ geomTypes = $derived(new Set(this.#geoms.map((g) => g.type)))
184
+
185
+ xAxisY = $derived.by(() => {
186
+ if (!this.yScale || typeof this.yScale !== 'function') return this.#innerHeight
187
+ const crossVal = this.axisOrigin[1]
188
+ if (crossVal !== undefined) return this.yScale(crossVal)
189
+ const domain = this.yScale.domain?.()
190
+ return domain ? this.yScale(domain[0]) : this.#innerHeight
191
+ })
192
+
193
+ yAxisX = $derived.by(() => {
194
+ if (!this.xScale || typeof this.xScale !== 'function') return 0
195
+ const crossVal = this.axisOrigin[0]
196
+ if (crossVal !== undefined) return this.xScale(crossVal)
197
+ const domain = this.xScale.domain?.()
198
+ return domain ? this.xScale(domain[0]) : 0
199
+ })
200
+
201
+ constructor(config = {}) {
202
+ this.#rawData = config.data ?? []
203
+ this.#data = config.data ?? []
204
+ this.#channels = config.channels ?? {}
205
+ this.#labels = config.labels ?? {}
206
+ this.#helpers = config.helpers ?? {}
207
+ this.#presetName = config.preset
208
+ this.#colorMidpoint = config.colorMidpoint
209
+ this.#colorSpec = config.colorScale
210
+ this.#colorDomain = config.colorDomain
211
+ this.#xDomain = config.xDomain
212
+ this.#yDomain = config.yDomain
213
+ this.#width = config.width ?? 600
214
+ this.#height = config.height ?? 400
215
+ this.#mode = config.mode ?? 'light'
216
+ this.#marginOverride = config.margin ?? undefined
217
+ }
218
+
219
+ update(config) {
220
+ if (config.data !== undefined) { this.#rawData = config.data; this.#data = config.data }
221
+ if (config.channels !== undefined) this.#channels = config.channels
222
+ if (config.labels !== undefined) this.#labels = config.labels
223
+ if (config.helpers !== undefined) this.#helpers = config.helpers
224
+ if (config.preset !== undefined) this.#presetName = config.preset
225
+ if (config.colorMidpoint !== undefined) this.#colorMidpoint = config.colorMidpoint
226
+ if (config.colorScale !== undefined) this.#colorSpec = config.colorScale
227
+ this.#colorDomain = config.colorDomain
228
+ this.#xDomain = config.xDomain
229
+ this.#yDomain = config.yDomain
230
+ if (config.width !== undefined) this.#width = config.width
231
+ if (config.height !== undefined) this.#height = config.height
232
+ if (config.mode !== undefined) this.#mode = config.mode
233
+ this.#marginOverride = config.margin ?? undefined
234
+ }
235
+
236
+ registerGeom(config) {
237
+ const id = `geom-${nextId++}`
238
+ this.#geoms = [...this.#geoms, { id, ...config }]
239
+ return id
240
+ }
241
+
242
+ updateGeom(id, config) {
243
+ // untrack the read of #geoms to avoid effect_update_depth_exceeded when
244
+ // called from a geom's $effect (which would otherwise track #geoms as a dependency)
245
+ this.#geoms = untrack(() => this.#geoms).map((g) => g.id === id ? { ...g, ...config } : g)
246
+ }
247
+
248
+ unregisterGeom(id) {
249
+ this.#geoms = this.#geoms.filter((g) => g.id !== id)
250
+ }
251
+
252
+ geomData(id) {
253
+ const geom = this.#geoms.find((g) => g.id === id)
254
+ if (!geom) return []
255
+ const stat = geom.stat ?? 'identity'
256
+ if (stat === 'identity') return this.#rawData
257
+ const mergedChannels = { ...this.#channels, ...geom.channels }
258
+ return applyGeomStat(this.#rawData, { stat, channels: mergedChannels }, this.#helpers)
259
+ }
260
+
261
+ label(field) {
262
+ return this.#labels?.[field] ?? field
263
+ }
264
+
265
+ format(field) { return resolveFormat(field, this.#helpers) }
266
+ tooltip() { return resolveTooltip(this.#helpers) }
267
+ geomComponent(type) { return resolveGeom(type, this.#helpers) }
268
+ preset() { return resolvePreset(this.#presetName, this.#helpers) }
269
+
270
+ get margin() { return this.#effectiveMargin }
271
+ get innerWidth() { return this.#innerWidth }
272
+ get innerHeight() { return this.#innerHeight }
273
+ get mode() { return this.#mode }
274
+ get hovered() { return this.#hovered }
275
+
276
+ setHovered(data) { this.#hovered = data }
277
+ clearHovered() { this.#hovered = null }
278
+ }
@@ -0,0 +1,69 @@
1
+ <script>
2
+ import { scaleLinear } from 'd3-scale'
3
+ import { line as d3line, area as d3area, curveCatmullRom } from 'd3-shape'
4
+
5
+ /**
6
+ * @type {number[] | object[]}
7
+ */
8
+ let {
9
+ data = [],
10
+ field = undefined,
11
+ type = 'line',
12
+ curve = 'linear',
13
+ color = 'primary',
14
+ width = 80,
15
+ height = 24,
16
+ min = undefined,
17
+ max = undefined
18
+ } = $props()
19
+
20
+ const values = $derived(
21
+ field ? data.map((d) => Number(d[field])) : data.map(Number)
22
+ )
23
+
24
+ const yMin = $derived(min ?? Math.min(...values))
25
+ const yMax = $derived(max ?? Math.max(...values))
26
+
27
+ const xScale = $derived(
28
+ scaleLinear().domain([0, values.length - 1]).range([0, width])
29
+ )
30
+ const yScale = $derived(
31
+ scaleLinear().domain([yMin, yMax]).range([height, 0])
32
+ )
33
+
34
+ const linePath = $derived.by(() => {
35
+ const gen = d3line().x((_, i) => xScale(i)).y((v) => yScale(v))
36
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
37
+ return gen(values)
38
+ })
39
+
40
+ const areaPath = $derived.by(() => {
41
+ const gen = d3area().x((_, i) => xScale(i)).y0(height).y1((v) => yScale(v))
42
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
43
+ return gen(values)
44
+ })
45
+
46
+ const barWidth = $derived(Math.max(1, width / values.length - 1))
47
+
48
+ const strokeColor = $derived(`rgb(var(--color-${color}-500, 100,116,139))`)
49
+ const fillColor = $derived(`rgba(var(--color-${color}-300), 0.25)`)
50
+ </script>
51
+
52
+ <svg {width} {height} style="overflow: visible; display: block;">
53
+ {#if type === 'line'}
54
+ <path d={linePath} fill="none" stroke={strokeColor} stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
55
+ {:else if type === 'area'}
56
+ <path d={areaPath} fill={fillColor} stroke="none" />
57
+ <path d={linePath} fill="none" stroke={strokeColor} stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" />
58
+ {:else if type === 'bar'}
59
+ {#each values as v, i}
60
+ <rect
61
+ x={xScale(i) - barWidth / 2}
62
+ y={yScale(v)}
63
+ width={barWidth}
64
+ height={height - yScale(v)}
65
+ fill={strokeColor}
66
+ />
67
+ {/each}
68
+ {/if}
69
+ </svg>
@@ -0,0 +1,25 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Area from '../geoms/Area.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').LineAreaChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ fill = undefined,
11
+ stat = 'identity',
12
+ curve = undefined,
13
+ pattern = undefined,
14
+ stack = false,
15
+ width = 600,
16
+ height = 400,
17
+ mode = 'light',
18
+ grid = true,
19
+ legend = false
20
+ } = $props()
21
+ </script>
22
+
23
+ <Plot {data} {width} {height} {mode} {grid} {legend}>
24
+ <Area {x} {y} color={fill} {pattern} {stat} options={{ curve, stack }} />
25
+ </Plot>
@@ -0,0 +1,26 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Bar from '../geoms/Bar.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').BarChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ fill = undefined, // mapped to color channel
11
+ pattern = undefined,
12
+ width = 600,
13
+ height = 400,
14
+ mode = 'light',
15
+ grid = true,
16
+ legend = false,
17
+ stat = 'identity',
18
+ stack = false,
19
+ label = false,
20
+ tooltip = false
21
+ } = $props()
22
+ </script>
23
+
24
+ <Plot {data} {width} {height} {mode} {grid} {legend} {tooltip}>
25
+ <Bar {x} {y} color={fill} {pattern} {label} {stat} options={{ stack }} />
26
+ </Plot>
@@ -0,0 +1,21 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Box from '../geoms/Box.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').BoxViolinChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ fill = undefined,
11
+ width = 600,
12
+ height = 400,
13
+ mode = 'light',
14
+ grid = true,
15
+ legend = false
16
+ } = $props()
17
+ </script>
18
+
19
+ <Plot {data} {width} {height} {mode} {grid} {legend}>
20
+ <Box {x} {y} {fill} />
21
+ </Plot>
@@ -0,0 +1,23 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Point from '../geoms/Point.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').ScatterBubbleChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ color = undefined,
11
+ symbol: _symbol = undefined,
12
+ size, // required: field name for bubble radius
13
+ width = 600,
14
+ height = 400,
15
+ mode = 'light',
16
+ grid = true,
17
+ legend = false
18
+ } = $props()
19
+ </script>
20
+
21
+ <Plot {data} {width} {height} {mode} {grid} {legend}>
22
+ <Point {x} {y} {color} {size} />
23
+ </Plot>
@@ -0,0 +1,26 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Line from '../geoms/Line.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').LineAreaChartProps} */
6
+ let {
7
+ data = [],
8
+ x = undefined,
9
+ y = undefined,
10
+ color = undefined,
11
+ stat = 'identity',
12
+ symbol = undefined,
13
+ curve = undefined, // forwarded to Line options
14
+ label = false,
15
+ tooltip = false,
16
+ width = 600,
17
+ height = 400,
18
+ mode = 'light',
19
+ grid = true,
20
+ legend = false
21
+ } = $props()
22
+ </script>
23
+
24
+ <Plot {data} {width} {height} {mode} {grid} {legend} {tooltip}>
25
+ <Line {x} {y} {color} {symbol} {label} {stat} options={{ curve }} />
26
+ </Plot>
@@ -0,0 +1,25 @@
1
+ <script>
2
+ import Plot from '../Plot.svelte'
3
+ import Arc from '../geoms/Arc.svelte'
4
+
5
+ /** @type {import('../lib/plot/chartProps.js').PieChartProps} */
6
+ let {
7
+ data = [],
8
+ label = undefined,
9
+ y = undefined,
10
+ fill = undefined,
11
+ pattern = undefined,
12
+ innerRadius = 0,
13
+ labelFn = undefined,
14
+ tooltip = false,
15
+ width = 400,
16
+ height = 400,
17
+ mode = 'light',
18
+ legend = false,
19
+ stat = 'sum'
20
+ } = $props()
21
+ </script>
22
+
23
+ <Plot {data} {width} {height} {mode} grid={false} axes={false} margin={{ top: 10, right: 10, bottom: 10, left: 10 }} {legend} {tooltip}>
24
+ <Arc theta={y} fill={label ?? fill} {pattern} {labelFn} {stat} options={{ innerRadius }} />
25
+ </Plot>