@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,75 @@
1
+ /**
2
+ * Builds box geometry for box plot charts.
3
+ * Input data rows must already contain { q1, median, q3, iqr_min, iqr_max } —
4
+ * computed by applyBoxStat before reaching this function.
5
+ *
6
+ * When `fill` differs from `x`, boxes are sub-grouped within each x-band
7
+ * (one narrower box per fill value per x category, like grouped bars).
8
+ * Box body uses the lighter fill shade; whiskers and median use the darker stroke shade.
9
+ *
10
+ * @param {Object[]} data - Pre-aggregated rows with quartile fields
11
+ * @param {{ x: string, fill?: string }} channels
12
+ * `fill` drives the box and whisker color (defaults to x-field).
13
+ * @param {import('d3-scale').ScaleBand} xScale
14
+ * @param {import('d3-scale').ScaleLinear} yScale
15
+ * @param {Map<unknown, {fill:string, stroke:string}>} colors
16
+ * @returns {Array}
17
+ */
18
+ export function buildBoxes(data, channels, xScale, yScale, colors) {
19
+ const { x: xf, fill: ff } = channels
20
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 20
21
+ const grouped = ff && ff !== xf
22
+
23
+ if (grouped) {
24
+ const fillValues = [...new Set(data.map((d) => d[ff]))]
25
+ const n = fillValues.length
26
+ const subBandWidth = bw / n
27
+ const boxWidth = subBandWidth * 0.75
28
+ const whiskerWidth = subBandWidth * 0.4
29
+
30
+ return data.map((d) => {
31
+ const fillVal = d[ff]
32
+ const subIndex = fillValues.indexOf(fillVal)
33
+ const bandStart = xScale(d[xf]) ?? 0
34
+ const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
35
+ const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
36
+
37
+ return {
38
+ data: d,
39
+ cx,
40
+ q1: yScale(d.q1),
41
+ median: yScale(d.median),
42
+ q3: yScale(d.q3),
43
+ iqr_min: yScale(d.iqr_min),
44
+ iqr_max: yScale(d.iqr_max),
45
+ width: boxWidth,
46
+ whiskerWidth,
47
+ fill: colorEntry.fill,
48
+ stroke: colorEntry.stroke
49
+ }
50
+ })
51
+ }
52
+
53
+ // Non-grouped: one box per x category
54
+ const boxWidth = bw * 0.6
55
+ const whiskerWidth = bw * 0.3
56
+
57
+ return data.map((d) => {
58
+ const fillKey = ff ? d[ff] : d[xf]
59
+ const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
60
+ const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
61
+ return {
62
+ data: d,
63
+ cx,
64
+ q1: yScale(d.q1),
65
+ median: yScale(d.median),
66
+ q3: yScale(d.q3),
67
+ iqr_min: yScale(d.iqr_min),
68
+ iqr_max: yScale(d.iqr_max),
69
+ width: boxWidth,
70
+ whiskerWidth,
71
+ fill: colorEntry.fill,
72
+ stroke: colorEntry.stroke
73
+ }
74
+ })
75
+ }
@@ -0,0 +1,48 @@
1
+ import { line, curveCatmullRom, curveStep } from 'd3-shape'
2
+
3
+ /**
4
+ * @param {Object[]} data
5
+ * @param {{ x: string, y: string, color?: string }} channels
6
+ * @param {Function} xScale
7
+ * @param {Function} yScale
8
+ * @param {Map} colors
9
+ * @param {'linear'|'smooth'|'step'} [curve]
10
+ * @returns {{ d: string, fill: string, stroke: string, points: {x:number, y:number, data:Object}[], key?: unknown }[]}
11
+ */
12
+ export function buildLines(data, channels, xScale, yScale, colors, curve) {
13
+ const { x: xf, y: yf, color: cf } = channels
14
+ const xPos = (d) => typeof xScale.bandwidth === 'function'
15
+ ? xScale(d[xf]) + xScale.bandwidth() / 2
16
+ : xScale(d[xf])
17
+ const makeGen = () => {
18
+ const gen = line().x(xPos).y((d) => yScale(d[yf]))
19
+ if (curve === 'smooth') gen.curve(curveCatmullRom)
20
+ else if (curve === 'step') gen.curve(curveStep)
21
+ return gen
22
+ }
23
+ const toPoints = (rows) => rows.map((d) => ({ x: xPos(d), y: yScale(d[yf]), data: d }))
24
+
25
+ const sortByX = (rows) => [...rows].sort((a, b) => a[xf] < b[xf] ? -1 : a[xf] > b[xf] ? 1 : 0)
26
+
27
+ if (!cf) {
28
+ const sorted = sortByX(data)
29
+ const stroke = colors?.values().next().value?.stroke ?? '#888'
30
+ return [{ d: makeGen()(sorted), fill: 'none', stroke, points: toPoints(sorted) }]
31
+ }
32
+ const groups = groupBy(data, cf)
33
+ return [...groups.entries()].map(([key, rows]) => {
34
+ const sorted = sortByX(rows)
35
+ const colorEntry = colors?.get(key) ?? { fill: 'none', stroke: '#888' }
36
+ return { d: makeGen()(sorted), fill: 'none', stroke: colorEntry.stroke, points: toPoints(sorted), key }
37
+ })
38
+ }
39
+
40
+ function groupBy(arr, field) {
41
+ const map = new Map()
42
+ for (const item of arr) {
43
+ const key = item[field]
44
+ if (!map.has(key)) map.set(key, [])
45
+ map.get(key).push(item)
46
+ }
47
+ return map
48
+ }
@@ -0,0 +1,57 @@
1
+ import { symbol, symbolCircle, symbolSquare, symbolTriangle, symbolDiamond, symbolCross, symbolStar } from 'd3-shape'
2
+
3
+ const SYMBOL_TYPES = [symbolCircle, symbolSquare, symbolTriangle, symbolDiamond, symbolCross, symbolStar]
4
+ const SYMBOL_NAMES = ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
5
+
6
+ /**
7
+ * Returns a Map assigning shape names to distinct values, cycling through available shapes.
8
+ * @param {unknown[]} values
9
+ * @returns {Map<unknown, string>}
10
+ */
11
+ export function assignSymbols(values) {
12
+ return new Map(values.map((v, i) => [v, SYMBOL_NAMES[i % SYMBOL_NAMES.length]]))
13
+ }
14
+
15
+ /**
16
+ * Builds an SVG path string for a given shape name and radius.
17
+ * @param {string} shapeName
18
+ * @param {number} r
19
+ * @returns {string}
20
+ */
21
+ export function buildSymbolPath(shapeName, r) {
22
+ const idx = SYMBOL_NAMES.indexOf(shapeName)
23
+ const type = idx >= 0 ? SYMBOL_TYPES[idx] : symbolCircle
24
+ return symbol().type(type).size(Math.PI * r * r)() ?? ''
25
+ }
26
+
27
+ /**
28
+ * Builds point geometry for scatter/bubble charts.
29
+ * @param {Object[]} data
30
+ * @param {{ x: string, y: string, color?: string, size?: string, symbol?: string }} channels
31
+ * @param {Function} xScale
32
+ * @param {Function} yScale
33
+ * @param {Map} colors
34
+ * @param {Function|null} sizeScale
35
+ * @param {Map<unknown, string>|null} symbolMap — maps symbol field value → shape name
36
+ * @param {number} defaultRadius
37
+ */
38
+ export function buildPoints(data, channels, xScale, yScale, colors, sizeScale, symbolMap, defaultRadius = 5) {
39
+ const { x: xf, y: yf, color: cf, size: sf, symbol: symf } = channels
40
+ return data.map((d) => {
41
+ const colorKey = cf ? d[cf] : null
42
+ const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#444' }
43
+ const r = sf && sizeScale ? sizeScale(d[sf]) : defaultRadius
44
+ const shapeName = symf && symbolMap ? (symbolMap.get(d[symf]) ?? 'circle') : null
45
+ const symbolPath = shapeName ? buildSymbolPath(shapeName, r) : null
46
+ return {
47
+ data: d,
48
+ cx: xScale(d[xf]),
49
+ cy: yScale(d[yf]),
50
+ r,
51
+ fill: colorEntry.fill,
52
+ stroke: colorEntry.stroke,
53
+ symbolPath,
54
+ key: colorKey
55
+ }
56
+ })
57
+ }
@@ -0,0 +1,90 @@
1
+ import { line, curveCatmullRom } from 'd3-shape'
2
+
3
+ // Relative widths at each stat anchor (fraction of max half-width)
4
+ const DENSITY_AT = { iqr_min: 0.08, q1: 0.55, median: 1.0, q3: 0.55, iqr_max: 0.08 }
5
+ const ANCHOR_ORDER = ['iqr_max', 'q3', 'median', 'q1', 'iqr_min']
6
+
7
+ /**
8
+ * Builds a closed violin shape path for each group.
9
+ * Input rows must have { q1, median, q3, iqr_min, iqr_max } from applyBoxStat.
10
+ *
11
+ * When `fill` differs from `x`, violins are sub-grouped within each x-band
12
+ * (one narrower violin per fill value per x category, like grouped bars).
13
+ * Violin body uses the lighter fill shade; outline uses the darker stroke shade.
14
+ *
15
+ * @param {Object[]} data
16
+ * @param {{ x: string, fill?: string }} channels
17
+ * `fill` drives violin interior and outline (defaults to x-field).
18
+ * @param {import('d3-scale').ScaleBand} xScale
19
+ * @param {import('d3-scale').ScaleLinear} yScale
20
+ * @param {Map} colors
21
+ * @returns {Array}
22
+ */
23
+ export function buildViolins(data, channels, xScale, yScale, colors) {
24
+ const { x: xf, fill: ff } = channels
25
+ const bw = typeof xScale.bandwidth === 'function' ? xScale.bandwidth() : 40
26
+ const grouped = ff && ff !== xf
27
+
28
+ const pathGen = line()
29
+ .x((pt) => pt.x)
30
+ .y((pt) => pt.y)
31
+ .curve(curveCatmullRom.alpha(0.5))
32
+
33
+ if (grouped) {
34
+ const fillValues = [...new Set(data.map((d) => d[ff]))]
35
+ const n = fillValues.length
36
+ const subBandWidth = bw / n
37
+ const halfMax = subBandWidth * 0.45
38
+
39
+ return data.map((d) => {
40
+ const fillVal = d[ff]
41
+ const subIndex = fillValues.indexOf(fillVal)
42
+ const bandStart = xScale(d[xf]) ?? 0
43
+ const cx = bandStart + subIndex * subBandWidth + subBandWidth / 2
44
+ const colorEntry = colors?.get(fillVal) ?? { fill: '#aaa', stroke: '#666' }
45
+
46
+ const rightPts = ANCHOR_ORDER.map((key) => ({
47
+ x: cx + halfMax * DENSITY_AT[key],
48
+ y: yScale(d[key])
49
+ }))
50
+ const leftPts = [...ANCHOR_ORDER].reverse().map((key) => ({
51
+ x: cx - halfMax * DENSITY_AT[key],
52
+ y: yScale(d[key])
53
+ }))
54
+
55
+ return {
56
+ data: d,
57
+ cx,
58
+ d: pathGen([...rightPts, ...leftPts, rightPts[0]]),
59
+ fill: colorEntry.fill,
60
+ stroke: colorEntry.stroke
61
+ }
62
+ })
63
+ }
64
+
65
+ // Non-grouped: one violin per x category
66
+ const halfMax = bw * 0.45
67
+
68
+ return data.map((d) => {
69
+ const fillKey = ff ? d[ff] : d[xf]
70
+ const colorEntry = colors?.get(fillKey) ?? { fill: '#aaa', stroke: '#666' }
71
+ const cx = (xScale(d[xf]) ?? 0) + (typeof xScale.bandwidth === 'function' ? bw / 2 : 0)
72
+
73
+ const rightPts = ANCHOR_ORDER.map((key) => ({
74
+ x: cx + halfMax * DENSITY_AT[key],
75
+ y: yScale(d[key])
76
+ }))
77
+ const leftPts = [...ANCHOR_ORDER].reverse().map((key) => ({
78
+ x: cx - halfMax * DENSITY_AT[key],
79
+ y: yScale(d[key])
80
+ }))
81
+
82
+ return {
83
+ data: d,
84
+ cx,
85
+ d: pathGen([...rightPts, ...leftPts, rightPts[0]]),
86
+ fill: colorEntry.fill,
87
+ stroke: colorEntry.stroke
88
+ }
89
+ })
90
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Converts a data key to a safe SVG element ID for pattern references.
3
+ * Spaces and non-word characters are replaced to avoid broken url(#...) refs.
4
+ * @param {unknown} key
5
+ * @returns {string}
6
+ */
7
+ export function toPatternId(key) {
8
+ return `chart-pat-${String(key).replace(/\s+/g, '-').replace(/[^\w-]/g, '_')}`
9
+ }
10
+
11
+ // Keys must match the keys in packages/chart/src/patterns/patterns.js
12
+ export const PATTERN_ORDER = [
13
+ 'diagonal',
14
+ 'dots',
15
+ 'triangles',
16
+ 'hatch',
17
+ 'lattice',
18
+ 'swell',
19
+ 'checkerboard',
20
+ 'waves',
21
+ 'petals'
22
+ ]
23
+
24
+ /**
25
+ * Assigns patterns from PATTERN_ORDER to an array of distinct values.
26
+ * @param {unknown[]} values
27
+ * @returns {Map<unknown, string>}
28
+ */
29
+ export function assignPatterns(values) {
30
+ return new Map(values.map((v, i) => [v, PATTERN_ORDER[i % PATTERN_ORDER.length]]))
31
+ }
@@ -0,0 +1,51 @@
1
+ import { scaleBand, scaleLinear, scaleSqrt } from 'd3-scale'
2
+ import { max, extent } from 'd3-array'
3
+
4
+ /**
5
+ * Builds an x scale (band for categorical, linear for numeric).
6
+ * @param {Object[]} data
7
+ * @param {string} field
8
+ * @param {number} width - inner width (pixels)
9
+ * @param {{ padding?: number }} opts
10
+ */
11
+ export function buildXScale(data, field, width, opts = {}) {
12
+ const values = [...new Set(data.map((d) => d[field]))]
13
+ const isNumeric = values.every((v) => typeof v === 'number' || (!isNaN(Number(v)) && v !== ''))
14
+ if (isNumeric) {
15
+ const [minVal, maxVal] = extent(data, (d) => Number(d[field]))
16
+ return scaleLinear().domain([minVal, maxVal]).range([0, width]).nice()
17
+ }
18
+ return scaleBand()
19
+ .domain(values)
20
+ .range([0, width])
21
+ .padding(opts.padding ?? 0.2)
22
+ }
23
+
24
+ /**
25
+ * Builds a y linear scale from 0 to max, extended by any layer overrides.
26
+ * @param {Object[]} data
27
+ * @param {string} field
28
+ * @param {number} height - inner height (pixels)
29
+ * @param {Array<{data?: Object[], y?: string}>} layers
30
+ */
31
+ export function buildYScale(data, field, height, layers = []) {
32
+ let maxVal = max(data, (d) => Number(d[field])) ?? 0
33
+ for (const layer of layers) {
34
+ if (layer.data && layer.y) {
35
+ const layerMax = max(layer.data, (d) => Number(d[layer.y])) ?? 0
36
+ if (layerMax > maxVal) maxVal = layerMax
37
+ }
38
+ }
39
+ return scaleLinear().domain([0, maxVal]).range([height, 0]).nice()
40
+ }
41
+
42
+ /**
43
+ * Builds a sqrt scale for bubble/point size.
44
+ * @param {Object[]} data
45
+ * @param {string} field
46
+ * @param {number} maxRadius
47
+ */
48
+ export function buildSizeScale(data, field, maxRadius = 20) {
49
+ const maxVal = max(data, (d) => Number(d[field])) ?? 1
50
+ return scaleSqrt().domain([0, maxVal]).range([0, maxRadius])
51
+ }
@@ -1,8 +1,9 @@
1
1
  import { SvelteSet } from 'svelte/reactivity'
2
2
  import { min, max } from 'd3-array'
3
- import { scaleBand, scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale'
3
+ import { scaleLinear, scaleOrdinal } from 'd3-scale'
4
4
  import { schemeCategory10 } from 'd3-scale-chromatic'
5
5
  import {} from './types.js'
6
+ import { buildXScale } from '../xscale.js'
6
7
 
7
8
  /**
8
9
  * @typedef {import('./types').ChartScales} ChartScales
@@ -10,31 +11,6 @@ import {} from './types.js'
10
11
  * @typedef {import('./types').ChartDimensions} ChartDimensions
11
12
  */
12
13
 
13
- /**
14
- * @param {Array} xValues
15
- * @param {Object} dimensions
16
- * @param {number} padding
17
- * @returns {import('d3-scale').ScaleContinuousNumeric|import('d3-scale').ScaleBand}
18
- */
19
- function buildXScale(xValues, dimensions, padding) {
20
- const xIsDate = xValues.some((v) => v instanceof Date)
21
- const xIsNumeric = !xIsDate && xValues.every((v) => !isNaN(parseFloat(v)))
22
-
23
- if (xIsDate) {
24
- return scaleTime()
25
- .domain([min(xValues), max(xValues)])
26
- .range([0, dimensions.innerWidth])
27
- .nice()
28
- }
29
- if (xIsNumeric) {
30
- return scaleLinear()
31
- .domain([min([0, ...xValues]), max(xValues)])
32
- .range([0, dimensions.innerWidth])
33
- .nice()
34
- }
35
- return scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
36
- }
37
-
38
14
  /**
39
15
  * @param {Array} data
40
16
  * @param {string} colorField
@@ -0,0 +1,62 @@
1
+ import { sum, mean, min, max, quantile, ascending } from 'd3-array'
2
+ import { dataset } from '@rokkit/data'
3
+
4
+ function sortedQuantile(values, p) {
5
+ return quantile([...values].sort(ascending), p)
6
+ }
7
+
8
+ /**
9
+ * Built-in reduction functions. Each receives an array of numeric values.
10
+ * @type {Record<string, (values: number[]) => number>}
11
+ */
12
+ export const STAT_FNS = {
13
+ sum,
14
+ mean,
15
+ min,
16
+ max,
17
+ count: (values) => values.length
18
+ }
19
+
20
+ /**
21
+ * Computes box plot quartile statistics grouped by x (and optionally color).
22
+ * Output rows have { q1, median, q3, iqr_min, iqr_max } replacing the raw y values.
23
+ *
24
+ * @param {Object[]} data
25
+ * @param {{ x?: string, y?: string, color?: string }} channels
26
+ * @returns {Object[]}
27
+ */
28
+ export function applyBoxStat(data, channels) {
29
+ const { x: xf, y: yf, color: cf } = channels
30
+ if (!xf || !yf) return data
31
+ const by = [xf, cf].filter(Boolean)
32
+ return dataset(data)
33
+ .groupBy(...by)
34
+ .summarize((row) => row[yf], {
35
+ q1: (v) => sortedQuantile(v, 0.25),
36
+ median: (v) => sortedQuantile(v, 0.5),
37
+ q3: (v) => sortedQuantile(v, 0.75),
38
+ iqr_min: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q1 - 1.5 * (q3 - q1) },
39
+ iqr_max: (v) => { const q1 = sortedQuantile(v, 0.25); const q3 = sortedQuantile(v, 0.75); return q3 + 1.5 * (q3 - q1) }
40
+ })
41
+ .rollup()
42
+ .select()
43
+ }
44
+
45
+ /**
46
+ * Aggregates data by one or more grouping fields, reducing the value field
47
+ * using the given stat. Accepts a built-in name or a custom function.
48
+ *
49
+ * @param {Object[]} data
50
+ * @param {{ by: string[], value: string, stat: string|Function }} opts
51
+ * @returns {Object[]}
52
+ */
53
+ export function applyAggregate(data, { by, value, stat }) {
54
+ if (stat === 'identity' || by.length === 0 || value === null || value === undefined) return data
55
+ const fn = typeof stat === 'function' ? stat : STAT_FNS[stat]
56
+ if (fn === null || fn === undefined) return data
57
+ return dataset(data)
58
+ .groupBy(...by)
59
+ .summarize((row) => row[value], { [value]: fn })
60
+ .rollup()
61
+ .select()
62
+ }
@@ -0,0 +1,10 @@
1
+ export const SYMBOL_ORDER = ['circle', 'square', 'triangle', 'diamond', 'plus', 'cross', 'star']
2
+
3
+ /**
4
+ * Assigns shapes from SYMBOL_ORDER to an array of distinct values.
5
+ * @param {unknown[]} values
6
+ * @returns {Map<unknown, string>}
7
+ */
8
+ export function assignSymbols(values) {
9
+ return new Map(values.map((v, i) => [v, SYMBOL_ORDER[i % SYMBOL_ORDER.length]]))
10
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Shared JSDoc type definitions for chart component props.
3
+ * Import these in chart components with:
4
+ * @type {import('./lib/plot/chartProps.js').ChartProps}
5
+ */
6
+
7
+ /**
8
+ * @typedef {Object} ChartProps
9
+ * Common props shared by all high-level chart wrappers.
10
+ *
11
+ * @property {Object[]} [data=[]] - Data array to visualize
12
+ * @property {string} [x] - Field name for the X axis
13
+ * @property {string} [y] - Field name for the Y axis
14
+ * @property {string} [fill] - Field name for fill color grouping (alias for color)
15
+ * @property {string} [color] - Field name for color grouping
16
+ * @property {string} [pattern] - Field name for pattern fill grouping
17
+ * @property {string} [stat='identity']- Aggregation stat: 'identity' | 'sum' | 'mean' | 'count' | 'min' | 'max' | 'median' | 'boxplot'
18
+ * @property {number} [width=600] - SVG width in pixels
19
+ * @property {number} [height=400] - SVG height in pixels
20
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
21
+ * @property {boolean} [grid=true] - Whether to show grid lines
22
+ * @property {boolean} [legend=false] - Whether to show the legend
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} BarChartProps
27
+ * @extends ChartProps
28
+ * @property {boolean} [stack=false] - Stack bars (true) or group them (false)
29
+ */
30
+
31
+ /**
32
+ * @typedef {Object} PieChartProps
33
+ * @property {Object[]} [data=[]] - Data array to visualize
34
+ * @property {string} [label] - Field name for slice labels (drives color key)
35
+ * @property {string} [y] - Field name for slice values (theta)
36
+ * @property {string} [fill] - Alternative to label for color grouping
37
+ * @property {number} [innerRadius=0] - Inner radius as fraction of outer (0=pie, 0.5=donut)
38
+ * @property {string} [stat='sum'] - Aggregation stat (default sum for pie charts)
39
+ * @property {number} [width=400] - SVG width in pixels
40
+ * @property {number} [height=400] - SVG height in pixels
41
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
42
+ * @property {boolean} [legend=false] - Whether to show the legend
43
+ */
44
+
45
+ /**
46
+ * @typedef {Object} BoxViolinChartProps
47
+ * @property {Object[]} [data=[]] - Data array to visualize
48
+ * @property {string} [x] - Field name for the category axis (groups)
49
+ * @property {string} [y] - Field name for the value axis (raw observations)
50
+ * @property {string} [fill] - Field name for fill color; when different from x, sub-groups within each x-band (like grouped bars); lighter shade used for body, darker shade for whiskers/outline
51
+ * @property {number} [width=600] - SVG width in pixels
52
+ * @property {number} [height=400] - SVG height in pixels
53
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
54
+ * @property {boolean} [grid=true] - Whether to show grid lines
55
+ * @property {boolean} [legend=false] - Whether to show the legend
56
+ */
57
+
58
+ /**
59
+ * @typedef {Object} ScatterBubbleChartProps
60
+ * @property {Object[]} [data=[]] - Data array to visualize
61
+ * @property {string} [x] - Field name for X position
62
+ * @property {string} [y] - Field name for Y position
63
+ * @property {string} [color] - Field name for color grouping
64
+ * @property {string} [size] - Field name for point radius (BubbleChart: required)
65
+ * @property {number} [width=600] - SVG width in pixels
66
+ * @property {number} [height=400] - SVG height in pixels
67
+ * @property {string} [mode='light'] - Color mode: 'light' | 'dark'
68
+ * @property {boolean} [grid=true] - Whether to show grid lines
69
+ * @property {boolean} [legend=false] - Whether to show the legend
70
+ */
71
+
72
+ /**
73
+ * @typedef {Object} LineAreaChartProps
74
+ * @extends ChartProps
75
+ * @property {string} [curve] - Line interpolation: 'linear' | 'smooth' | 'step'
76
+ */
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Applies dimming state to data rows using crossfilter instance.
3
+ * Maps each row to { data: row, dimmed: boolean }.
4
+ *
5
+ * @param {Object[]} data - raw data array
6
+ * @param {Object} cf - crossfilter instance (from createCrossFilter)
7
+ * @param {Object} channels - { x, y, color, ... } field name mapping
8
+ * @returns {{ data: Object, dimmed: boolean }[]}
9
+ */
10
+ export function applyDimming(data, cf, channels) {
11
+ const fields = Object.values(channels).filter(Boolean)
12
+ return data.map((row) => {
13
+ const dimmed = fields.some((field) => cf.isDimmed(field, row[field]))
14
+ return { data: row, dimmed }
15
+ })
16
+ }
@@ -0,0 +1,58 @@
1
+ import { extent } from 'd3-array'
2
+
3
+ /**
4
+ * Splits data into a Map of panels keyed by facet field value.
5
+ * Preserves insertion order of first occurrence.
6
+ *
7
+ * @param {Object[]} data
8
+ * @param {string} field
9
+ * @returns {Map<unknown, Object[]>}
10
+ */
11
+ export function splitByField(data, field) {
12
+ const map = new Map()
13
+ for (const row of data) {
14
+ const key = row[field]
15
+ if (!map.has(key)) map.set(key, [])
16
+ map.get(key).push(row)
17
+ }
18
+ return map
19
+ }
20
+
21
+ /**
22
+ * Computes x/y domains for each panel.
23
+ *
24
+ * @param {Map<unknown, Object[]>} panels
25
+ * @param {{ x: string, y: string }} channels
26
+ * @param {'fixed'|'free'|'free_x'|'free_y'} scalesMode
27
+ * @returns {Map<unknown, { xDomain: unknown[], yDomain: [number, number] }>}
28
+ */
29
+ export function getFacetDomains(panels, channels, scalesMode = 'fixed') {
30
+ const { x: xf, y: yf } = channels
31
+ const allData = [...panels.values()].flat()
32
+
33
+ // Determine if x is categorical (string) or numeric
34
+ const sampleXVal = allData.find(d => d[xf] !== null && d[xf] !== undefined)?.[xf]
35
+ const xIsCategorical = typeof sampleXVal === 'string'
36
+
37
+ // Global domains (for fixed mode)
38
+ const globalXDomain = xIsCategorical
39
+ ? [...new Set(allData.map((d) => d[xf]))]
40
+ : extent(allData, (d) => Number(d[xf]))
41
+ const globalYDomain = extent(allData, (d) => Number(d[yf]))
42
+
43
+ const result = new Map()
44
+ for (const [key, rows] of panels.entries()) {
45
+ const freeX = scalesMode === 'free' || scalesMode === 'free_x'
46
+ const freeY = scalesMode === 'free' || scalesMode === 'free_y'
47
+
48
+ const xDomain = freeX
49
+ ? (xIsCategorical ? [...new Set(rows.map((d) => d[xf]))] : extent(rows, (d) => Number(d[xf])))
50
+ : globalXDomain
51
+ const yDomain = freeY
52
+ ? extent(rows, (d) => Number(d[yf]))
53
+ : globalYDomain
54
+
55
+ result.set(key, { xDomain, yDomain })
56
+ }
57
+ return result
58
+ }