@rokkit/chart 1.0.0-next.151 → 1.0.0-next.155

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 (86) hide show
  1. package/dist/PlotState.svelte.d.ts +26 -0
  2. package/dist/index.d.ts +6 -1
  3. package/dist/lib/brewing/BoxBrewer.svelte.d.ts +3 -5
  4. package/dist/lib/brewing/QuartileBrewer.svelte.d.ts +9 -0
  5. package/dist/lib/brewing/ViolinBrewer.svelte.d.ts +3 -4
  6. package/dist/lib/brewing/colors.d.ts +10 -1
  7. package/dist/lib/brewing/marks/points.d.ts +17 -2
  8. package/dist/lib/keyboard-nav.d.ts +15 -0
  9. package/dist/lib/plot/preset.d.ts +1 -1
  10. package/dist/lib/preset.d.ts +30 -0
  11. package/package.json +2 -1
  12. package/src/AnimatedPlot.svelte +375 -207
  13. package/src/Chart.svelte +81 -84
  14. package/src/ChartProvider.svelte +10 -0
  15. package/src/FacetPlot/Panel.svelte +30 -16
  16. package/src/FacetPlot.svelte +100 -76
  17. package/src/Plot/Area.svelte +26 -19
  18. package/src/Plot/Axis.svelte +81 -59
  19. package/src/Plot/Bar.svelte +47 -89
  20. package/src/Plot/Grid.svelte +23 -19
  21. package/src/Plot/Legend.svelte +213 -147
  22. package/src/Plot/Line.svelte +31 -21
  23. package/src/Plot/Point.svelte +35 -22
  24. package/src/Plot/Root.svelte +46 -91
  25. package/src/Plot/Timeline.svelte +82 -82
  26. package/src/Plot/Tooltip.svelte +68 -62
  27. package/src/Plot.svelte +290 -174
  28. package/src/PlotState.svelte.js +338 -265
  29. package/src/Sparkline.svelte +95 -56
  30. package/src/charts/AreaChart.svelte +22 -20
  31. package/src/charts/BarChart.svelte +23 -21
  32. package/src/charts/BoxPlot.svelte +15 -15
  33. package/src/charts/BubbleChart.svelte +17 -17
  34. package/src/charts/LineChart.svelte +20 -20
  35. package/src/charts/PieChart.svelte +30 -20
  36. package/src/charts/ScatterPlot.svelte +20 -19
  37. package/src/charts/ViolinPlot.svelte +15 -15
  38. package/src/crossfilter/CrossFilter.svelte +33 -29
  39. package/src/crossfilter/FilterBar.svelte +17 -25
  40. package/src/crossfilter/FilterHistogram.svelte +290 -0
  41. package/src/crossfilter/FilterSlider.svelte +69 -65
  42. package/src/crossfilter/createCrossFilter.svelte.js +94 -90
  43. package/src/geoms/Arc.svelte +114 -69
  44. package/src/geoms/Area.svelte +67 -39
  45. package/src/geoms/Bar.svelte +184 -126
  46. package/src/geoms/Box.svelte +101 -91
  47. package/src/geoms/LabelPill.svelte +11 -11
  48. package/src/geoms/Line.svelte +110 -86
  49. package/src/geoms/Point.svelte +130 -90
  50. package/src/geoms/Violin.svelte +51 -41
  51. package/src/geoms/lib/areas.js +122 -99
  52. package/src/geoms/lib/bars.js +195 -144
  53. package/src/index.js +21 -14
  54. package/src/lib/brewing/BoxBrewer.svelte.js +8 -50
  55. package/src/lib/brewing/CartesianBrewer.svelte.js +11 -7
  56. package/src/lib/brewing/PieBrewer.svelte.js +5 -5
  57. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  58. package/src/lib/brewing/ViolinBrewer.svelte.js +8 -49
  59. package/src/lib/brewing/brewer.svelte.js +242 -195
  60. package/src/lib/brewing/colors.js +34 -5
  61. package/src/lib/brewing/marks/arcs.js +28 -28
  62. package/src/lib/brewing/marks/areas.js +54 -41
  63. package/src/lib/brewing/marks/bars.js +34 -34
  64. package/src/lib/brewing/marks/boxes.js +51 -51
  65. package/src/lib/brewing/marks/lines.js +37 -30
  66. package/src/lib/brewing/marks/points.js +74 -26
  67. package/src/lib/brewing/marks/violins.js +57 -57
  68. package/src/lib/brewing/patterns.js +25 -11
  69. package/src/lib/brewing/scales.js +17 -17
  70. package/src/lib/brewing/stats.js +37 -29
  71. package/src/lib/brewing/symbols.js +1 -1
  72. package/src/lib/chart.js +2 -1
  73. package/src/lib/keyboard-nav.js +37 -0
  74. package/src/lib/plot/crossfilter.js +5 -5
  75. package/src/lib/plot/facet.js +30 -30
  76. package/src/lib/plot/frames.js +30 -29
  77. package/src/lib/plot/helpers.js +4 -4
  78. package/src/lib/plot/preset.js +48 -34
  79. package/src/lib/plot/scales.js +64 -39
  80. package/src/lib/plot/stat.js +47 -47
  81. package/src/lib/preset.js +41 -0
  82. package/src/patterns/DefinePatterns.svelte +24 -24
  83. package/src/patterns/README.md +3 -0
  84. package/src/patterns/patterns.js +328 -176
  85. package/src/patterns/scale.js +61 -32
  86. package/src/spec/chart-spec.js +64 -21
@@ -2,55 +2,80 @@ import { scaleBand, scaleLinear } from 'd3-scale'
2
2
  import { extent } from 'd3-array'
3
3
 
4
4
  export function inferFieldType(data, field) {
5
- const values = data.map((d) => d[field]).filter((v) => v !== null && v !== undefined)
6
- if (values.length === 0) return 'band'
7
- const isNumeric = values.every((v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== ''))
8
- return isNumeric ? 'continuous' : 'band'
5
+ const values = data.map((d) => d[field]).filter((v) => v !== null && v !== undefined)
6
+ if (values.length === 0) return 'band'
7
+ const isNumeric = values.every(
8
+ (v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
9
+ )
10
+ return isNumeric ? 'continuous' : 'band'
9
11
  }
10
12
 
11
13
  export function inferOrientation(xType, yType) {
12
- if (xType === 'band' && yType === 'continuous') return 'vertical'
13
- if (yType === 'band' && xType === 'continuous') return 'horizontal'
14
- return 'none'
14
+ if (xType === 'band' && yType === 'continuous') return 'vertical'
15
+ if (yType === 'band' && xType === 'continuous') return 'horizontal'
16
+ return 'none'
15
17
  }
16
18
 
17
19
  export function buildUnifiedXScale(datasets, field, width, opts = {}) {
18
- const allValues = datasets.flatMap((d) => d.map((r) => r[field]))
19
- const isNumeric = allValues.every((v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== ''))
20
-
21
- // opts.band forces scaleBand even for numeric data (e.g. bar charts with year on X).
22
- if (opts.domain) {
23
- const domainIsNumeric = opts.domain.every((v) => typeof v === 'number')
24
- if (!opts.band && (domainIsNumeric || isNumeric)) {
25
- return scaleLinear().domain(opts.domain).range([0, width]).nice()
26
- }
27
- return scaleBand().domain(opts.domain).range([0, width]).padding(opts.padding ?? 0.2)
28
- }
29
-
30
- if (isNumeric && !opts.band) {
31
- const numericValues = allValues.map(Number)
32
- const [minVal, maxVal] = extent(numericValues)
33
- const domainMin = (opts.includeZero ?? false) ? 0 : (minVal ?? 0)
34
- return scaleLinear().domain([domainMin, maxVal ?? 0]).range([0, width]).nice()
35
- }
36
-
37
- const domain = [...new Set(allValues)].filter((v) => v !== null && v !== undefined)
38
- return scaleBand().domain(domain).range([0, width]).padding(opts.padding ?? 0.2)
20
+ const allValues = datasets.flatMap((d) => d.map((r) => r[field]))
21
+ const isNumeric = allValues.every(
22
+ (v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
23
+ )
24
+
25
+ // opts.band forces scaleBand even for numeric data (e.g. bar charts with year on X).
26
+ if (opts.domain) {
27
+ const domainIsNumeric = opts.domain.every((v) => typeof v === 'number')
28
+ if (!opts.band && (domainIsNumeric || isNumeric)) {
29
+ return scaleLinear().domain(opts.domain).range([0, width]).nice()
30
+ }
31
+ return scaleBand()
32
+ .domain(opts.domain)
33
+ .range([0, width])
34
+ .padding(opts.padding ?? 0.2)
35
+ }
36
+
37
+ if (isNumeric && !opts.band) {
38
+ const numericValues = allValues.map(Number)
39
+ const [minVal, maxVal] = extent(numericValues)
40
+ const domainMin = (opts.includeZero ?? false) ? 0 : (minVal ?? 0)
41
+ return scaleLinear()
42
+ .domain([domainMin, maxVal ?? 0])
43
+ .range([0, width])
44
+ .nice()
45
+ }
46
+
47
+ const domain = [...new Set(allValues)].filter((v) => v !== null && v !== undefined)
48
+ return scaleBand()
49
+ .domain(domain)
50
+ .range([0, width])
51
+ .padding(opts.padding ?? 0.2)
39
52
  }
40
53
 
41
54
  export function buildUnifiedYScale(datasets, field, height, opts = {}) {
42
- if (opts.domain) {
43
- return scaleLinear().domain(opts.domain).range([height, 0]).nice()
44
- }
45
- const allValues = datasets.flatMap((d) => d.map((r) => Number(r[field]))).filter((v) => !isNaN(v))
46
- const [minVal, maxVal] = extent(allValues)
47
- const domainMin = (opts.includeZero ?? true) ? 0 : (minVal ?? 0)
48
- return scaleLinear().domain([domainMin, maxVal ?? 0]).range([height, 0]).nice()
55
+ if (opts.domain) {
56
+ return scaleLinear().domain(opts.domain).range([height, 0]).nice()
57
+ }
58
+ const rawValues = datasets.flatMap((d) => d.map((r) => r[field])).filter((v) => v !== null && v !== undefined)
59
+ const isNumeric = rawValues.length > 0 && rawValues.every(
60
+ (v) => typeof v === 'number' || (!isNaN(Number(v)) && String(v).trim() !== '')
61
+ )
62
+ if (!isNumeric) {
63
+ // Categorical y-axis (e.g. horizontal bar chart) — use scaleBand
64
+ const domain = [...new Set(rawValues.map(String))]
65
+ return scaleBand().domain(domain).range([height, 0]).padding(0.2)
66
+ }
67
+ const numericValues = rawValues.map(Number)
68
+ const [minVal, maxVal] = extent(numericValues)
69
+ const domainMin = (opts.includeZero ?? true) ? 0 : (minVal ?? 0)
70
+ return scaleLinear()
71
+ .domain([domainMin, maxVal ?? 0])
72
+ .range([height, 0])
73
+ .nice()
49
74
  }
50
75
 
51
76
  export function inferColorScaleType(data, field, spec = {}) {
52
- if (spec.colorScale) return spec.colorScale
53
- if (spec.colorMidpoint !== undefined) return 'diverging'
54
- const type = inferFieldType(data, field)
55
- return type === 'continuous' ? 'sequential' : 'categorical'
77
+ if (spec.colorScale) return spec.colorScale
78
+ if (spec.colorMidpoint !== undefined) return 'diverging'
79
+ const type = inferFieldType(data, field)
80
+ return type === 'continuous' ? 'sequential' : 'categorical'
56
81
  }
@@ -2,12 +2,12 @@ import { sum, mean, min, max, median } from 'd3-array'
2
2
  import { applyAggregate, applyBoxStat } from '../brewing/stats.js'
3
3
 
4
4
  const BUILT_IN_STATS = {
5
- sum,
6
- mean,
7
- min,
8
- max,
9
- count: (values) => values.length,
10
- median
5
+ sum,
6
+ mean,
7
+ min,
8
+ max,
9
+ count: (values) => values.length,
10
+ median
11
11
  }
12
12
 
13
13
  /**
@@ -19,14 +19,14 @@ const BUILT_IN_STATS = {
19
19
  * @returns {Function}
20
20
  */
21
21
  export function resolveStat(name, helpers = {}) {
22
- if (name === 'identity') return (data) => data
23
- if (BUILT_IN_STATS[name]) return BUILT_IN_STATS[name]
24
- if (helpers?.stats?.[name]) return helpers.stats[name]
25
- // eslint-disable-next-line no-console
26
- console.warn(
27
- `[Plot] Unknown stat "${name}" — falling back to identity. Add it to helpers.stats to suppress this warning.`
28
- )
29
- return (data) => data
22
+ if (name === 'identity') return (data) => data
23
+ if (BUILT_IN_STATS[name]) return BUILT_IN_STATS[name]
24
+ if (helpers?.stats?.[name]) return helpers.stats[name]
25
+ // eslint-disable-next-line no-console
26
+ console.warn(
27
+ `[Plot] Unknown stat "${name}" — falling back to identity. Add it to helpers.stats to suppress this warning.`
28
+ )
29
+ return (data) => data
30
30
  }
31
31
 
32
32
  /**
@@ -39,16 +39,16 @@ export function resolveStat(name, helpers = {}) {
39
39
  * @returns {string[]}
40
40
  */
41
41
  export function inferGroupByFields(channels, valueFields) {
42
- const seen = new Set()
43
- const result = []
44
- for (const [key, field] of Object.entries(channels)) {
45
- if (!field) continue
46
- if (valueFields.includes(key) || valueFields.includes(field)) continue
47
- if (seen.has(field)) continue
48
- seen.add(field)
49
- result.push(field)
50
- }
51
- return result
42
+ const seen = new Set()
43
+ const result = []
44
+ for (const [key, field] of Object.entries(channels)) {
45
+ if (!field) continue
46
+ if (valueFields.includes(key) || valueFields.includes(field)) continue
47
+ if (seen.has(field)) continue
48
+ seen.add(field)
49
+ result.push(field)
50
+ }
51
+ return result
52
52
  }
53
53
 
54
54
  /**
@@ -61,32 +61,32 @@ export function inferGroupByFields(channels, valueFields) {
61
61
  * @returns {Object[]}
62
62
  */
63
63
  export function applyGeomStat(data, geomConfig, helpers = {}) {
64
- const { stat = 'identity', channels = {} } = geomConfig
65
- if (stat === 'identity') return data
66
- if (stat === 'boxplot') return applyBoxStat(data, channels)
64
+ const { stat = 'identity', channels = {} } = geomConfig
65
+ if (stat === 'identity') return data
66
+ if (stat === 'boxplot') return applyBoxStat(data, channels)
67
67
 
68
- const statFn = resolveStat(stat, helpers)
68
+ const statFn = resolveStat(stat, helpers)
69
69
 
70
- const VALUE_CHANNEL_KEYS = ['y', 'size', 'theta']
71
- const groupByFields = inferGroupByFields(channels, VALUE_CHANNEL_KEYS)
72
- const primaryKey = VALUE_CHANNEL_KEYS.find((k) => channels[k])
73
- if (!primaryKey) return data
70
+ const VALUE_CHANNEL_KEYS = ['y', 'size', 'theta']
71
+ const groupByFields = inferGroupByFields(channels, VALUE_CHANNEL_KEYS)
72
+ const primaryKey = VALUE_CHANNEL_KEYS.find((k) => channels[k])
73
+ if (!primaryKey) return data
74
74
 
75
- let result = applyAggregate(data, {
76
- by: groupByFields,
77
- value: channels[primaryKey],
78
- stat: statFn
79
- })
75
+ let result = applyAggregate(data, {
76
+ by: groupByFields,
77
+ value: channels[primaryKey],
78
+ stat: statFn
79
+ })
80
80
 
81
- for (const key of VALUE_CHANNEL_KEYS.filter((k) => k !== primaryKey && channels[k])) {
82
- const extra = applyAggregate(data, { by: groupByFields, value: channels[key], stat: statFn })
83
- const index = new Map(extra.map((r) => [groupByFields.map((f) => r[f]).join('|'), r]))
84
- result = result.map((r) => {
85
- const mapKey = groupByFields.map((f) => r[f]).join('|')
86
- const extraRow = index.get(mapKey)
87
- return extraRow ? { ...r, [channels[key]]: extraRow[channels[key]] } : r
88
- })
89
- }
81
+ for (const key of VALUE_CHANNEL_KEYS.filter((k) => k !== primaryKey && channels[k])) {
82
+ const extra = applyAggregate(data, { by: groupByFields, value: channels[key], stat: statFn })
83
+ const index = new Map(extra.map((r) => [groupByFields.map((f) => r[f]).join('|'), r]))
84
+ result = result.map((r) => {
85
+ const mapKey = groupByFields.map((f) => r[f]).join('|')
86
+ const extraRow = index.get(mapKey)
87
+ return extraRow ? { ...r, [channels[key]]: extraRow[channels[key]] } : r
88
+ })
89
+ }
90
90
 
91
- return result
91
+ return result
92
92
  }
@@ -0,0 +1,41 @@
1
+ // packages/chart/src/lib/preset.js
2
+
3
+ export const defaultPreset = {
4
+ colors: ['blue', 'emerald', 'rose', 'amber', 'violet', 'sky', 'pink', 'teal',
5
+ 'orange', 'indigo', 'lime', 'cyan', 'gold', 'lavender'],
6
+ shades: {
7
+ light: { fill: '300', stroke: '700' },
8
+ dark: { fill: '500', stroke: '200' }
9
+ },
10
+ opacity: {
11
+ area: 0.6,
12
+ box: 0.5,
13
+ violin: 0.5,
14
+ point: 0.8
15
+ },
16
+ patterns: ['diagonal', 'dots', 'triangles', 'hatch', 'lattice', 'swell',
17
+ 'checkerboard', 'waves', 'petals', 'brick', 'diamonds', 'tile',
18
+ 'scales', 'circles', 'pip', 'rings', 'chevrons', 'shards',
19
+ 'wedge', 'argyle', 'shell'],
20
+ symbols: ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
21
+ }
22
+
23
+ /**
24
+ * Creates a chart preset by deep-merging overrides with the default preset.
25
+ * All fields are optional. `opacity` is merged key-by-key so partial overrides work.
26
+ * @param {Partial<typeof defaultPreset>} [overrides]
27
+ * @returns {typeof defaultPreset}
28
+ */
29
+ export function createChartPreset(overrides = {}) {
30
+ return {
31
+ ...defaultPreset,
32
+ ...overrides,
33
+ shades: overrides.shades
34
+ ? {
35
+ light: { ...defaultPreset.shades.light, ...overrides.shades.light },
36
+ dark: { ...defaultPreset.shades.dark, ...overrides.shades.dark }
37
+ }
38
+ : defaultPreset.shades,
39
+ opacity: { ...defaultPreset.opacity, ...overrides.opacity }
40
+ }
41
+ }
@@ -1,32 +1,32 @@
1
1
  <script>
2
- import { getContext } from 'svelte'
3
- import { toPatternId } from '../lib/brewing/patterns.js'
4
- import { PATTERNS } from './patterns.js'
5
- import PatternDef from './PatternDef.svelte'
2
+ import { getContext } from 'svelte'
3
+ import { toPatternId } from '../lib/brewing/patterns.js'
4
+ import { PATTERNS } from './patterns.js'
5
+ import PatternDef from './PatternDef.svelte'
6
6
 
7
- /** @type {{ patterns?: Record<string, import('./patterns.js').PatternMark[]> }} */
8
- let { patterns = PATTERNS } = $props()
7
+ /** @type {{ patterns?: Record<string, import('./patterns.js').PatternMark[]> }} */
8
+ let { patterns = PATTERNS } = $props()
9
9
 
10
- const state = getContext('plot-state')
10
+ const state = getContext('plot-state')
11
11
 
12
- const patternDefs = $derived.by(() => {
13
- const defs = []
14
- for (const [key, patternName] of (state.patterns ?? new Map()).entries()) {
15
- const colorEntry = state.colors?.get(key) ?? { stroke: '#444' }
16
- defs.push({
17
- id: toPatternId(String(key)),
18
- marks: patterns[patternName] ?? [],
19
- stroke: colorEntry.stroke ?? '#444'
20
- })
21
- }
22
- return defs
23
- })
12
+ const patternDefs = $derived.by(() => {
13
+ const defs = []
14
+ for (const [key, patternName] of (state.patterns ?? new Map()).entries()) {
15
+ const colorEntry = state.colors?.get(key) ?? { stroke: '#444' }
16
+ defs.push({
17
+ id: toPatternId(String(key)),
18
+ marks: patterns[patternName] ?? [],
19
+ stroke: colorEntry.stroke ?? '#444'
20
+ })
21
+ }
22
+ return defs
23
+ })
24
24
  </script>
25
25
 
26
26
  {#if patternDefs.length > 0}
27
- <defs data-plot-pattern-defs>
28
- {#each patternDefs as def (def.id)}
29
- <PatternDef id={def.id} marks={def.marks} stroke={def.stroke} />
30
- {/each}
31
- </defs>
27
+ <defs data-plot-pattern-defs>
28
+ {#each patternDefs as def (def.id)}
29
+ <PatternDef id={def.id} marks={def.marks} stroke={def.stroke} />
30
+ {/each}
31
+ </defs>
32
32
  {/if}
@@ -0,0 +1,3 @@
1
+ # Patterns
2
+
3
+ These patterns are inspired from https://superdesigner.co/tools/svg-backgrounds