@rokkit/chart 1.0.0-next.99 → 1.0.2

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 (137) hide show
  1. package/README.md +150 -46
  2. package/package.json +40 -51
  3. package/src/AnimatedPlot.svelte +383 -0
  4. package/src/Chart.svelte +87 -59
  5. package/src/ChartExporter.svelte +125 -0
  6. package/src/ChartLegend.svelte +148 -0
  7. package/src/ChartProvider.svelte +10 -0
  8. package/src/FacetPlot/Panel.svelte +37 -0
  9. package/src/FacetPlot.svelte +114 -0
  10. package/src/Plot/Arc.svelte +29 -0
  11. package/src/Plot/Area.svelte +32 -0
  12. package/src/Plot/Axis.svelte +164 -0
  13. package/src/Plot/Bar.svelte +54 -0
  14. package/src/Plot/Grid.svelte +34 -0
  15. package/src/Plot/Legend.svelte +268 -0
  16. package/src/Plot/Line.svelte +37 -0
  17. package/src/Plot/Point.svelte +40 -0
  18. package/src/Plot/Root.svelte +62 -0
  19. package/src/Plot/Timeline.svelte +95 -0
  20. package/src/Plot/Tooltip.svelte +87 -0
  21. package/src/Plot/index.js +9 -0
  22. package/src/Plot.svelte +313 -0
  23. package/src/PlotState.svelte.js +375 -0
  24. package/src/Sparkline.svelte +108 -0
  25. package/src/Symbol.svelte +15 -11
  26. package/src/Texture.svelte +9 -7
  27. package/src/charts/AreaChart.svelte +30 -0
  28. package/src/charts/BarChart.svelte +31 -0
  29. package/src/charts/BoxPlot.svelte +24 -0
  30. package/src/charts/BubbleChart.svelte +28 -0
  31. package/src/charts/LineChart.svelte +31 -0
  32. package/src/charts/PieChart.svelte +35 -0
  33. package/src/charts/ScatterPlot.svelte +31 -0
  34. package/src/charts/ViolinPlot.svelte +24 -0
  35. package/src/crossfilter/CrossFilter.svelte +42 -0
  36. package/src/crossfilter/FilterBar.svelte +24 -0
  37. package/src/crossfilter/FilterHistogram.svelte +290 -0
  38. package/src/crossfilter/FilterSlider.svelte +83 -0
  39. package/src/crossfilter/createCrossFilter.svelte.js +124 -0
  40. package/src/elements/Bar.svelte +20 -16
  41. package/src/elements/ColorRamp.svelte +19 -19
  42. package/src/elements/ContinuousLegend.svelte +41 -19
  43. package/src/elements/DefinePatterns.svelte +9 -7
  44. package/src/elements/DiscreteLegend.svelte +13 -11
  45. package/src/elements/Label.svelte +4 -9
  46. package/src/elements/SymbolGrid.svelte +8 -9
  47. package/src/examples/BarChartExample.svelte +81 -0
  48. package/src/geoms/Arc.svelte +126 -0
  49. package/src/geoms/Area.svelte +78 -0
  50. package/src/geoms/Bar.svelte +200 -0
  51. package/src/geoms/Box.svelte +113 -0
  52. package/src/geoms/Candlestick.svelte +110 -0
  53. package/src/geoms/Heatmap.svelte +92 -0
  54. package/src/geoms/Hexbin.svelte +114 -0
  55. package/src/geoms/LabelPill.svelte +17 -0
  56. package/src/geoms/Line.svelte +123 -0
  57. package/src/geoms/Point.svelte +145 -0
  58. package/src/geoms/Ribbon.svelte +161 -0
  59. package/src/geoms/Violin.svelte +56 -0
  60. package/src/geoms/Waterfall.svelte +114 -0
  61. package/src/geoms/lib/areas.js +154 -0
  62. package/src/geoms/lib/bars.js +223 -0
  63. package/src/index.js +81 -2
  64. package/src/lib/brewing/BoxBrewer.svelte.js +14 -0
  65. package/src/lib/brewing/CartesianBrewer.svelte.js +21 -0
  66. package/src/lib/brewing/PieBrewer.svelte.js +14 -0
  67. package/src/lib/brewing/QuartileBrewer.svelte.js +51 -0
  68. package/src/lib/brewing/ViolinBrewer.svelte.js +14 -0
  69. package/src/lib/brewing/axes.svelte.js +270 -0
  70. package/src/lib/brewing/bars.svelte.js +201 -0
  71. package/src/lib/brewing/brewer.svelte.js +277 -0
  72. package/src/lib/brewing/colors.js +157 -0
  73. package/src/lib/brewing/dimensions.svelte.js +56 -0
  74. package/src/lib/brewing/index.svelte.js +205 -0
  75. package/src/lib/brewing/legends.svelte.js +137 -0
  76. package/src/lib/brewing/marks/arcs.js +43 -0
  77. package/src/lib/brewing/marks/areas.js +72 -0
  78. package/src/lib/brewing/marks/bars.js +49 -0
  79. package/src/lib/brewing/marks/boxes.js +75 -0
  80. package/src/lib/brewing/marks/lines.js +55 -0
  81. package/src/lib/brewing/marks/points.js +105 -0
  82. package/src/lib/brewing/marks/violins.js +90 -0
  83. package/src/lib/brewing/patterns.js +45 -0
  84. package/src/lib/brewing/scales.js +51 -0
  85. package/src/lib/brewing/scales.svelte.js +82 -0
  86. package/src/lib/brewing/stats.js +74 -0
  87. package/src/lib/brewing/symbols.js +10 -0
  88. package/src/lib/brewing/types.js +73 -0
  89. package/src/lib/chart.js +16 -8
  90. package/src/lib/context.js +131 -0
  91. package/src/lib/keyboard-nav.js +37 -0
  92. package/src/lib/plot/chartProps.js +76 -0
  93. package/src/lib/plot/crossfilter.js +16 -0
  94. package/src/lib/plot/facet.js +58 -0
  95. package/src/lib/plot/frames.js +81 -0
  96. package/src/lib/plot/helpers.js +14 -0
  97. package/src/lib/plot/preset.js +67 -0
  98. package/src/lib/plot/scales.js +81 -0
  99. package/src/lib/plot/stat.js +92 -0
  100. package/src/lib/plot/types.js +65 -0
  101. package/src/lib/preset.js +41 -0
  102. package/src/lib/scales.svelte.js +151 -0
  103. package/src/lib/swatch.js +4 -7
  104. package/src/lib/utils.js +142 -0
  105. package/src/lib/xscale.js +31 -0
  106. package/src/patterns/DefinePatterns.svelte +32 -0
  107. package/src/patterns/PatternDef.svelte +27 -0
  108. package/src/patterns/index.js +4 -14
  109. package/src/patterns/patterns.js +360 -0
  110. package/src/patterns/scale.js +116 -0
  111. package/src/spec/chart-spec.js +72 -0
  112. package/src/symbols/RoundedSquare.svelte +18 -12
  113. package/src/symbols/Shape.svelte +22 -16
  114. package/src/symbols/constants/index.js +1 -1
  115. package/LICENSE +0 -21
  116. package/src/lib/index.js +0 -4
  117. package/src/lib/plots.js +0 -27
  118. package/src/patterns/Brick.svelte +0 -17
  119. package/src/patterns/Circles.svelte +0 -18
  120. package/src/patterns/CrossHatch.svelte +0 -14
  121. package/src/patterns/CurvedWave.svelte +0 -9
  122. package/src/patterns/Dots.svelte +0 -19
  123. package/src/patterns/OutlineCircles.svelte +0 -15
  124. package/src/patterns/Tile.svelte +0 -17
  125. package/src/patterns/Triangles.svelte +0 -15
  126. package/src/patterns/Waves.svelte +0 -13
  127. package/src/patterns/paths/NamedPattern.svelte +0 -12
  128. package/src/patterns/paths/constants.js +0 -4
  129. package/src/plots/Plot.svelte +0 -40
  130. package/src/plots/index.js +0 -1
  131. package/src/template/Texture.svelte +0 -16
  132. package/src/template/constants.js +0 -43
  133. package/src/template/shapes/Circles.svelte +0 -16
  134. package/src/template/shapes/Lines.svelte +0 -17
  135. package/src/template/shapes/Path.svelte +0 -12
  136. package/src/template/shapes/Polygons.svelte +0 -18
  137. package/src/template/shapes/index.js +0 -4
@@ -0,0 +1,105 @@
1
+ import {
2
+ symbol,
3
+ symbolCircle,
4
+ symbolSquare,
5
+ symbolTriangle,
6
+ symbolDiamond,
7
+ symbolCross,
8
+ symbolStar
9
+ } from 'd3-shape'
10
+ import { defaultPreset } from '../../preset.js'
11
+
12
+ const SYMBOL_TYPES = [
13
+ symbolCircle,
14
+ symbolSquare,
15
+ symbolTriangle,
16
+ symbolDiamond,
17
+ symbolCross,
18
+ symbolStar
19
+ ]
20
+ const SYMBOL_NAMES = ['circle', 'square', 'triangle', 'diamond', 'cross', 'star']
21
+
22
+ /**
23
+ * Returns a Map assigning shape names to distinct values, cycling through available shapes.
24
+ * @param {unknown[]} values
25
+ * @param {typeof defaultPreset} preset
26
+ * @returns {Map<unknown, string>}
27
+ */
28
+ export function assignSymbols(values, preset = defaultPreset) {
29
+ const names = preset.symbols
30
+ return new Map(values.map((v, i) => [v, names[i % names.length]]))
31
+ }
32
+
33
+ /**
34
+ * Builds an SVG path string for a given shape name and radius.
35
+ * @param {string} shapeName
36
+ * @param {number} r
37
+ * @returns {string}
38
+ */
39
+ export function buildSymbolPath(shapeName, r) {
40
+ const idx = SYMBOL_NAMES.indexOf(shapeName)
41
+ const type = idx >= 0 ? SYMBOL_TYPES[idx] : symbolCircle
42
+ return (
43
+ symbol()
44
+ .type(type)
45
+ .size(Math.PI * r * r)() ?? ''
46
+ )
47
+ }
48
+
49
+ /**
50
+ * Returns a stable pseudo-random offset for a given index.
51
+ * Uses a linear congruential generator seeded by index — no external dependency,
52
+ * stable across re-renders.
53
+ * @param {number} i - row index (seed)
54
+ * @param {number} range - total spread (jitter is ±range/2)
55
+ * @returns {number}
56
+ */
57
+ export function jitterOffset(i, range) {
58
+ const r = ((i * 1664525 + 1013904223) >>> 0) / 0xffffffff
59
+ return (r - 0.5) * range
60
+ }
61
+
62
+ /**
63
+ * Builds point geometry for scatter/bubble charts.
64
+ * @param {Object[]} data
65
+ * @param {{ x: string, y: string, color?: string, size?: string, symbol?: string }} channels
66
+ * @param {Function} xScale
67
+ * @param {Function} yScale
68
+ * @param {Map} colors
69
+ * @param {Function|null} sizeScale
70
+ * @param {Map<unknown, string>|null} symbolMap — maps symbol field value → shape name
71
+ * @param {number} defaultRadius
72
+ * @param {{ width?: number, height?: number }|null} jitter
73
+ */
74
+ export function buildPoints(
75
+ data,
76
+ channels,
77
+ xScale,
78
+ yScale,
79
+ colors,
80
+ sizeScale,
81
+ symbolMap,
82
+ defaultRadius = 5,
83
+ jitter = null
84
+ ) {
85
+ const { x: xf, y: yf, color: cf, size: sf, symbol: symf } = channels
86
+ return data.map((d, i) => {
87
+ const colorKey = cf ? d[cf] : null
88
+ const colorEntry = colors?.get(colorKey) ?? { fill: '#888', stroke: '#444' }
89
+ const r = sf && sizeScale ? sizeScale(d[sf]) : defaultRadius
90
+ const shapeName = symf && symbolMap ? (symbolMap.get(d[symf]) ?? 'circle') : null
91
+ const symbolPath = shapeName ? buildSymbolPath(shapeName, r) : null
92
+ const jx = jitter?.width ? jitterOffset(i, jitter.width) : 0
93
+ const jy = jitter?.height ? jitterOffset(i + 100000, jitter.height) : 0
94
+ return {
95
+ data: d,
96
+ cx: xScale(d[xf]) + jx,
97
+ cy: yScale(d[yf]) + jy,
98
+ r,
99
+ fill: colorEntry.fill,
100
+ stroke: colorEntry.stroke,
101
+ symbolPath,
102
+ key: colorKey
103
+ }
104
+ })
105
+ }
@@ -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,45 @@
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)
9
+ .replace(/\s+/g, '-')
10
+ .replace(/[^\w-]/g, '_')}`
11
+ }
12
+
13
+ // Keys must match the keys in packages/chart/src/patterns/patterns.js
14
+ export const PATTERN_ORDER = [
15
+ 'diagonal',
16
+ 'dots',
17
+ 'triangles',
18
+ 'hatch',
19
+ 'lattice',
20
+ 'swell',
21
+ 'checkerboard',
22
+ 'waves',
23
+ 'petals',
24
+ 'brick',
25
+ 'diamonds',
26
+ 'tile',
27
+ 'scales',
28
+ 'circles',
29
+ 'pip',
30
+ 'rings',
31
+ 'chevrons',
32
+ 'shards',
33
+ 'wedge',
34
+ 'argyle',
35
+ 'shell'
36
+ ]
37
+
38
+ /**
39
+ * Assigns patterns from PATTERN_ORDER to an array of distinct values.
40
+ * @param {unknown[]} values
41
+ * @returns {Map<unknown, string>}
42
+ */
43
+ export function assignPatterns(values) {
44
+ return new Map(values.map((v, i) => [v, PATTERN_ORDER[i % PATTERN_ORDER.length]]))
45
+ }
@@ -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
+ function maxFromLayer(layer) {
25
+ if (layer.data && layer.y) return max(layer.data, (d) => Number(d[layer.y])) ?? 0
26
+ return 0
27
+ }
28
+
29
+ /**
30
+ * Builds a y linear scale from 0 to max, extended by any layer overrides.
31
+ * @param {Object[]} data
32
+ * @param {string} field
33
+ * @param {number} height - inner height (pixels)
34
+ * @param {Array<{data?: Object[], y?: string}>} layers
35
+ */
36
+ export function buildYScale(data, field, height, layers = []) {
37
+ const dataMax = max(data, (d) => Number(d[field])) ?? 0
38
+ const maxVal = layers.reduce((m, layer) => Math.max(m, maxFromLayer(layer)), dataMax)
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
+ }
@@ -0,0 +1,82 @@
1
+ import { SvelteSet } from 'svelte/reactivity'
2
+ import { min, max } from 'd3-array'
3
+ import { scaleLinear, scaleOrdinal } from 'd3-scale'
4
+ import { schemeCategory10 } from 'd3-scale-chromatic'
5
+ import {} from './types.js'
6
+ import { buildXScale } from '../xscale.js'
7
+
8
+ /**
9
+ * @typedef {import('./types').ChartScales} ChartScales
10
+ * @typedef {import('./types').ScaleFields} ScaleFields
11
+ * @typedef {import('./types').ChartDimensions} ChartDimensions
12
+ */
13
+
14
+ /**
15
+ * @param {Array} data
16
+ * @param {string} colorField
17
+ * @returns {import('d3-scale').ScaleOrdinal}
18
+ */
19
+ function buildColorScale(data, colorField) {
20
+ const colorValues = [...new SvelteSet(data.map((d) => d[colorField]))]
21
+ return scaleOrdinal().domain(colorValues).range(schemeCategory10)
22
+ }
23
+
24
+ /**
25
+ * @param {Array} data
26
+ * @param {string} yField
27
+ * @param {Object} dimensions
28
+ * @returns {import('d3-scale').ScaleContinuousNumeric}
29
+ */
30
+ function buildYScale(data, yField, dimensions) {
31
+ const yValues = data.map((d) => d[yField])
32
+ return scaleLinear()
33
+ .domain([0, max(yValues) * 1.1])
34
+ .nice()
35
+ .range([dimensions.innerHeight, 0])
36
+ }
37
+
38
+ /**
39
+ * @param {Array} data
40
+ * @param {ScaleFields} fields
41
+ * @returns {boolean}
42
+ */
43
+ function hasRequiredFields(data, fields) {
44
+ return Boolean(data && data.length && fields.x && fields.y)
45
+ }
46
+
47
+ /**
48
+ * Creates scales based on data, fields, and dimensions
49
+ *
50
+ * @param {Array} data - Chart data
51
+ * @param {ScaleFields} fields - Field mappings
52
+ * @param {Object} dimensions - Chart dimensions
53
+ * @param {Object} options - Scale options
54
+ * @param {number} [options.padding=0.2] - Padding for band scales
55
+ * @returns {ChartScales} Chart scales
56
+ */
57
+ export function createScales(data, fields, dimensions, options = {}) {
58
+ if (!hasRequiredFields(data, fields)) return { x: null, y: null, color: null }
59
+
60
+ const padding = options.padding ?? 0.2
61
+ const xValues = data.map((d) => d[fields.x])
62
+
63
+ return {
64
+ x: buildXScale(xValues, dimensions, padding),
65
+ y: buildYScale(data, fields.y, dimensions),
66
+ color: fields.color ? buildColorScale(data, fields.color) : null
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Gets the origin coordinates for the axes
72
+ *
73
+ * @param {ChartScales} scales - Chart scales
74
+ * @param {Object} dimensions - Chart dimensions
75
+ * @returns {Object} Origin coordinates
76
+ */
77
+ export function getOrigin(scales, dimensions) {
78
+ return {
79
+ x: scales.y ? scales.y(0) : dimensions.innerHeight,
80
+ y: scales.x ? (scales.x.ticks ? scales.x(Math.max(0, min(scales.x.domain()))) : 0) : 0
81
+ }
82
+ }
@@ -0,0 +1,74 @@
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) => {
39
+ const q1 = sortedQuantile(v, 0.25)
40
+ const q3 = sortedQuantile(v, 0.75)
41
+ return q1 - 1.5 * (q3 - q1)
42
+ },
43
+ iqr_max: (v) => {
44
+ const q1 = sortedQuantile(v, 0.25)
45
+ const q3 = sortedQuantile(v, 0.75)
46
+ return q3 + 1.5 * (q3 - q1)
47
+ }
48
+ })
49
+ .rollup()
50
+ .select()
51
+ }
52
+
53
+ /**
54
+ * Aggregates data by one or more grouping fields, reducing the value field
55
+ * using the given stat. Accepts a built-in name or a custom function.
56
+ *
57
+ * @param {Object[]} data
58
+ * @param {{ by: string[], value: string, stat: string|Function }} opts
59
+ * @returns {Object[]}
60
+ */
61
+ function isIdentityOrEmpty(stat, by, value) {
62
+ return stat === 'identity' || by.length === 0 || value === null || value === undefined
63
+ }
64
+
65
+ export function applyAggregate(data, { by, value, stat }) {
66
+ if (isIdentityOrEmpty(stat, by, value)) return data
67
+ const fn = typeof stat === 'function' ? stat : STAT_FNS[stat]
68
+ if (fn === null || fn === undefined) return data
69
+ return dataset(data)
70
+ .groupBy(...by)
71
+ .summarize((row) => row[value], { [value]: fn })
72
+ .rollup()
73
+ .select()
74
+ }
@@ -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,73 @@
1
+ /**
2
+ * @typedef {Object} ChartMargin
3
+ * @property {number} top - Top margin
4
+ * @property {number} right - Right margin
5
+ * @property {number} bottom - Bottom margin
6
+ * @property {number} left - Left margin
7
+ */
8
+
9
+ /**
10
+ * @typedef {Object} ChartDimensions
11
+ * @property {number} width - Total chart width
12
+ * @property {number} height - Total chart height
13
+ * @property {ChartMargin} margin - Chart margins
14
+ * @property {number} innerWidth - Chart width without margins
15
+ * @property {number} innerHeight - Chart height without margins
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} ChartScales
20
+ * @property {Function} x - X-axis scale function
21
+ * @property {Function} y - Y-axis scale function
22
+ * @property {Function} color - Color scale function
23
+ */
24
+
25
+ /**
26
+ * @typedef {Object} ScaleFields
27
+ * @property {string} x - X-axis field
28
+ * @property {string} y - Y-axis field
29
+ * @property {string} color - Color field
30
+ */
31
+
32
+ /**
33
+ * @typedef {Object} TickData
34
+ * @property {*} value - Tick value
35
+ * @property {number} position - Tick position in pixels
36
+ * @property {string} formattedValue - Formatted tick label
37
+ */
38
+
39
+ /**
40
+ * @typedef {Object} AxisData
41
+ * @property {TickData[]} ticks - Tick data
42
+ * @property {string} label - Axis label
43
+ * @property {string} transform - SVG transform attribute value
44
+ * @property {string} labelTransform - SVG transform for the label
45
+ */
46
+
47
+ /**
48
+ * @typedef {Object} BarData
49
+ * @property {Object} data - Original data point
50
+ * @property {number} x - X position
51
+ * @property {number} y - Y position
52
+ * @property {number} width - Width of the bar
53
+ * @property {number} height - Height of the bar
54
+ * @property {string} color - Color of the bar
55
+ */
56
+
57
+ /**
58
+ * @typedef {Object} LegendItem
59
+ * @property {*} value - Legend item value
60
+ * @property {string} color - Item color
61
+ * @property {number} y - Y position
62
+ * @property {string} shape - Shape type ('rect' or 'circle')
63
+ * @property {number} markerSize - Size of the marker
64
+ */
65
+
66
+ /**
67
+ * @typedef {Object} LegendData
68
+ * @property {LegendItem[]} items - Legend items
69
+ * @property {string} title - Legend title
70
+ * @property {string} transform - SVG transform attribute value
71
+ */
72
+
73
+ export {}
package/src/lib/chart.js CHANGED
@@ -1,6 +1,10 @@
1
1
  import { min, max } from 'd3-array'
2
2
  import { scaleBand, scaleLinear, scaleTime } from 'd3-scale'
3
3
 
4
+ function getOriginValue(scale) {
5
+ return scale.ticks ? scale(Math.max(0, Math.min(...scale.domain()))) : scale.range()[0]
6
+ }
7
+
4
8
  function getScale(domain, range, padding = 0) {
5
9
  if (domain.some(isNaN)) {
6
10
  return scaleBand().domain(domain).range(range).padding(padding)
@@ -42,7 +46,7 @@ class Chart {
42
46
  // padding
43
47
  // flipCoords = false
44
48
 
45
- constructor(data, opts) {
49
+ #initFields(opts) {
46
50
  this.width = Number(opts.width) || 2048
47
51
  this.height = Number(opts.height) || 2048
48
52
  this.flipCoords = opts.flipCoords || false
@@ -54,9 +58,7 @@ class Chart {
54
58
  this.fill = opts.fill || opts.x
55
59
  this.color = opts.color || opts.fill
56
60
  this.shape = opts.shape || opts.fill
57
-
58
61
  this.padding = opts.padding !== undefined ? Number(opts.padding) : 32
59
-
60
62
  this.spacing =
61
63
  Number(opts.spacing) >= 0 && Number(opts.spacing) <= 0.5 ? Number(opts.spacing) : 0
62
64
  this.margin = {
@@ -65,6 +67,10 @@ class Chart {
65
67
  right: Number(opts.margin?.right) || 0,
66
68
  bottom: Number(opts.margin?.bottom) || 0
67
69
  }
70
+ this.stat = opts.stat || 'identity'
71
+ }
72
+
73
+ #initDomain(data) {
68
74
  this.domain = {
69
75
  x: [...new Set(data.map((d) => d[this.x]))],
70
76
  y: [...new Set(data.map((d) => d[this.y]))]
@@ -72,8 +78,9 @@ class Chart {
72
78
  if (this.flipCoords) {
73
79
  this.domain = { y: this.domain.x, x: this.domain.y }
74
80
  }
75
- this.stat = opts.stat || 'identity'
81
+ }
76
82
 
83
+ #initData(data) {
77
84
  this.data = data.map((d) => ({
78
85
  x: this.flipCoords ? d[this.y] : d[this.x],
79
86
  y: this.flipCoords ? d[this.x] : d[this.y],
@@ -81,7 +88,12 @@ class Chart {
81
88
  color: d[this.color]
82
89
  // shape: d[this.shape]
83
90
  }))
91
+ }
84
92
 
93
+ constructor(data, opts) {
94
+ this.#initFields(opts)
95
+ this.#initDomain(data)
96
+ this.#initData(data)
85
97
  this.refresh()
86
98
  }
87
99
 
@@ -204,10 +216,6 @@ class Chart {
204
216
  }
205
217
  }
206
218
 
207
- function getOriginValue(scale) {
208
- return scale.ticks ? scale(Math.max(0, Math.min(...scale.domain()))) : scale.range()[0]
209
- }
210
-
211
219
  export function chart(data, aes) {
212
220
  return new Chart(data, aes)
213
221
  }