@rokkit/chart 1.0.0-next.14 → 1.0.0-next.140
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.
- package/LICENSE +1 -1
- package/README.md +150 -46
- package/dist/Plot/index.d.ts +5 -0
- package/dist/elements/index.d.ts +6 -0
- package/dist/index.d.ts +14 -0
- package/dist/lib/brewing/axes.svelte.d.ts +66 -0
- package/dist/lib/brewing/bars.svelte.d.ts +56 -0
- package/dist/lib/brewing/dimensions.svelte.d.ts +35 -0
- package/dist/lib/brewing/index.svelte.d.ts +118 -0
- package/dist/lib/brewing/legends.svelte.d.ts +48 -0
- package/dist/lib/brewing/scales.svelte.d.ts +24 -0
- package/dist/lib/brewing/types.d.ts +162 -0
- package/dist/lib/context.d.ts +13 -0
- package/dist/lib/scales.svelte.d.ts +35 -0
- package/dist/lib/utils.d.ts +60 -0
- package/dist/old_lib/brewer.d.ts +9 -0
- package/dist/old_lib/chart.d.ts +40 -0
- package/dist/old_lib/grid.d.ts +72 -0
- package/dist/old_lib/index.d.ts +4 -0
- package/dist/old_lib/plots.d.ts +3 -0
- package/dist/old_lib/swatch.d.ts +285 -0
- package/dist/old_lib/ticks.d.ts +36 -0
- package/dist/old_lib/utils.d.ts +1 -0
- package/dist/patterns/index.d.ts +9 -0
- package/dist/patterns/paths/constants.d.ts +1 -0
- package/dist/symbols/constants/index.d.ts +1 -0
- package/dist/symbols/index.d.ts +5 -0
- package/dist/template/constants.d.ts +43 -0
- package/dist/template/shapes/index.d.ts +4 -0
- package/package.json +34 -44
- package/src/Plot/Axis.svelte +95 -0
- package/src/Plot/Bar.svelte +96 -0
- package/src/Plot/Grid.svelte +68 -0
- package/src/Plot/Legend.svelte +127 -0
- package/src/Plot/Root.svelte +107 -0
- package/src/Plot/index.js +5 -0
- package/src/Symbol.svelte +21 -0
- package/src/Texture.svelte +18 -0
- package/src/elements/Bar.svelte +22 -24
- package/src/elements/ColorRamp.svelte +20 -22
- package/src/elements/ContinuousLegend.svelte +20 -17
- package/src/elements/DefinePatterns.svelte +24 -0
- package/src/elements/DiscreteLegend.svelte +15 -15
- package/src/elements/Label.svelte +4 -8
- package/src/elements/SymbolGrid.svelte +23 -0
- package/src/elements/index.js +6 -0
- package/src/examples/BarChartExample.svelte +81 -0
- package/src/index.js +18 -16
- package/src/lib/brewing/axes.svelte.js +265 -0
- package/src/lib/brewing/bars.svelte.js +177 -0
- package/src/lib/brewing/dimensions.svelte.js +56 -0
- package/src/lib/brewing/index.svelte.js +205 -0
- package/src/lib/brewing/legends.svelte.js +137 -0
- package/src/lib/brewing/scales.svelte.js +106 -0
- package/src/lib/brewing/types.js +73 -0
- package/src/lib/context.js +133 -0
- package/src/lib/scales.svelte.js +172 -0
- package/src/lib/utils.js +107 -120
- package/src/old_lib/brewer.js +25 -0
- package/src/old_lib/chart.js +213 -0
- package/src/old_lib/grid.js +85 -0
- package/src/old_lib/index.js +4 -0
- package/src/old_lib/plots.js +27 -0
- package/src/old_lib/swatch.js +16 -0
- package/src/old_lib/ticks.js +46 -0
- package/src/old_lib/utils.js +8 -0
- package/src/patterns/Brick.svelte +15 -0
- package/src/patterns/Circles.svelte +18 -0
- package/src/patterns/CrossHatch.svelte +12 -0
- package/src/patterns/CurvedWave.svelte +7 -0
- package/src/patterns/Dots.svelte +20 -0
- package/src/patterns/OutlineCircles.svelte +13 -0
- package/src/patterns/README.md +3 -0
- package/src/patterns/Tile.svelte +16 -0
- package/src/patterns/Triangles.svelte +13 -0
- package/src/patterns/Waves.svelte +9 -0
- package/src/patterns/index.js +14 -0
- package/src/patterns/paths/NamedPattern.svelte +9 -0
- package/src/patterns/paths/constants.js +4 -0
- package/src/symbols/RoundedSquare.svelte +33 -0
- package/src/symbols/Shape.svelte +37 -0
- package/src/symbols/constants/index.js +4 -0
- package/src/symbols/index.js +9 -0
- package/src/symbols/outline.svelte +60 -0
- package/src/symbols/solid.svelte +60 -0
- package/src/template/Texture.svelte +13 -0
- package/src/template/constants.js +43 -0
- package/src/template/shapes/Circles.svelte +15 -0
- package/src/template/shapes/Lines.svelte +16 -0
- package/src/template/shapes/Path.svelte +9 -0
- package/src/template/shapes/Polygons.svelte +15 -0
- package/src/template/shapes/index.js +4 -0
- package/src/chart/FacetGrid.svelte +0 -51
- package/src/chart/Grid.svelte +0 -34
- package/src/chart/Legend.svelte +0 -16
- package/src/chart/PatternDefs.svelte +0 -13
- package/src/chart/Swatch.svelte +0 -93
- package/src/chart/SwatchButton.svelte +0 -29
- package/src/chart/SwatchGrid.svelte +0 -55
- package/src/chart/Symbol.svelte +0 -37
- package/src/chart/Texture.svelte +0 -16
- package/src/chart/TexturedShape.svelte +0 -27
- package/src/chart/TimelapseChart.svelte +0 -97
- package/src/chart/Timer.svelte +0 -27
- package/src/chart.js +0 -9
- package/src/components/charts/Axis.svelte +0 -66
- package/src/components/charts/Chart.svelte +0 -35
- package/src/components/index.js +0 -23
- package/src/components/lib/axis.js +0 -0
- package/src/components/lib/chart.js +0 -187
- package/src/components/lib/color.js +0 -327
- package/src/components/lib/funnel.js +0 -204
- package/src/components/lib/index.js +0 -19
- package/src/components/lib/pattern.js +0 -190
- package/src/components/lib/rollup.js +0 -55
- package/src/components/lib/shape.js +0 -199
- package/src/components/lib/summary.js +0 -145
- package/src/components/lib/theme.js +0 -23
- package/src/components/lib/timer.js +0 -41
- package/src/components/lib/utils.js +0 -165
- package/src/components/plots/BarPlot.svelte +0 -36
- package/src/components/plots/BoxPlot.svelte +0 -54
- package/src/components/plots/ScatterPlot.svelte +0 -30
- package/src/components/store.js +0 -70
- package/src/constants.js +0 -66
- package/src/elements/PatternDefs.svelte +0 -13
- package/src/elements/PatternMask.svelte +0 -20
- package/src/elements/Symbol.svelte +0 -38
- package/src/elements/Tooltip.svelte +0 -23
- package/src/funnel.svelte +0 -35
- package/src/geom.js +0 -105
- package/src/lib/axis.js +0 -75
- package/src/lib/colors.js +0 -32
- package/src/lib/geom.js +0 -4
- package/src/lib/shapes.js +0 -144
- package/src/lib/timer.js +0 -44
- package/src/lookup.js +0 -29
- package/src/plots/BarPlot.svelte +0 -55
- package/src/plots/BoxPlot.svelte +0 -0
- package/src/plots/FunnelPlot.svelte +0 -33
- package/src/plots/HeatMap.svelte +0 -5
- package/src/plots/HeatMapCalendar.svelte +0 -129
- package/src/plots/LinePlot.svelte +0 -55
- package/src/plots/Plot.svelte +0 -25
- package/src/plots/RankBarPlot.svelte +0 -38
- package/src/plots/ScatterPlot.svelte +0 -20
- package/src/plots/ViolinPlot.svelte +0 -11
- package/src/plots/heatmap.js +0 -70
- package/src/plots/index.js +0 -10
- package/src/swatch.js +0 -11
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import {} from './types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('./types').LegendItem} LegendItem
|
|
6
|
+
* @typedef {import('./types').LegendData} LegendData
|
|
7
|
+
* @typedef {import('./types').ScaleFields} ScaleFields
|
|
8
|
+
* @typedef {import('./types').ChartScales} ChartScales
|
|
9
|
+
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const LEGEND_DEFAULTS = { title: '', align: 'right', shape: 'rect', markerSize: 10 }
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compute the x-position for legend alignment
|
|
16
|
+
* @param {string} align
|
|
17
|
+
* @param {number} innerWidth
|
|
18
|
+
* @param {number} approximateWidth
|
|
19
|
+
* @returns {number}
|
|
20
|
+
*/
|
|
21
|
+
function legendX(align, innerWidth, approximateWidth) {
|
|
22
|
+
if (align === 'right') return innerWidth - approximateWidth
|
|
23
|
+
if (align === 'center') return (innerWidth - approximateWidth) / 2
|
|
24
|
+
return 0
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Array} colorValues
|
|
29
|
+
* @param {Function} colorScale
|
|
30
|
+
* @param {{ shape: string, markerSize: number, titleOffset: number }} style
|
|
31
|
+
* @returns {LegendItem[]}
|
|
32
|
+
*/
|
|
33
|
+
function buildLegendItems(colorValues, colorScale, style) {
|
|
34
|
+
const { shape, markerSize, titleOffset } = style
|
|
35
|
+
return colorValues.map((value, index) => ({
|
|
36
|
+
value,
|
|
37
|
+
color: colorScale(value),
|
|
38
|
+
y: index * (markerSize + 5) + titleOffset,
|
|
39
|
+
shape,
|
|
40
|
+
markerSize
|
|
41
|
+
}))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @param {Array} colorValues
|
|
46
|
+
* @param {number} markerSize
|
|
47
|
+
* @returns {number}
|
|
48
|
+
*/
|
|
49
|
+
function approximateLegendWidth(colorValues, markerSize) {
|
|
50
|
+
return Math.max(...colorValues.map((v) => v.toString().length)) * 8 + markerSize + 10
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @returns {{ dimensions: Object, title: string, align: string, shape: string, markerSize: number }}
|
|
56
|
+
*/
|
|
57
|
+
function parseLegendOptions(options) {
|
|
58
|
+
const merged = Object.assign({}, LEGEND_DEFAULTS, options || {})
|
|
59
|
+
return {
|
|
60
|
+
dimensions: merged.dimensions,
|
|
61
|
+
title: merged.title,
|
|
62
|
+
align: merged.align,
|
|
63
|
+
shape: merged.shape,
|
|
64
|
+
markerSize: merged.markerSize
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {Object|undefined} dimensions
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
function innerWidth(dimensions) {
|
|
73
|
+
return dimensions ? dimensions.innerWidth : 0
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Creates legend data for rendering
|
|
78
|
+
*
|
|
79
|
+
* @param {Array} data - Chart data
|
|
80
|
+
* @param {Object} fields - Field mappings
|
|
81
|
+
* @param {string} fields.color - Color field
|
|
82
|
+
* @param {Object} scales - Chart scales
|
|
83
|
+
* @param {Function} scales.color - Color scale
|
|
84
|
+
* @param {Object} options - Legend options including dimensions
|
|
85
|
+
* @param {Object} options.dimensions - Chart dimensions
|
|
86
|
+
* @param {string} [options.title=''] - Legend title
|
|
87
|
+
* @param {string} [options.align='right'] - Legend alignment ('left', 'center', or 'right')
|
|
88
|
+
* @param {string} [options.shape='rect'] - Legend marker shape ('rect' or 'circle')
|
|
89
|
+
* @param {number} [options.markerSize=10] - Size of legend markers
|
|
90
|
+
* @returns {LegendData} Legend rendering data
|
|
91
|
+
*/
|
|
92
|
+
export function createLegend(data, fields, scales, options) {
|
|
93
|
+
if (!data || !fields.color || !scales.color) {
|
|
94
|
+
return { items: [], title: '', transform: 'translate(0, 0)' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const { dimensions, title, align, shape, markerSize } = parseLegendOptions(options)
|
|
98
|
+
const colorValues = [...new SvelteSet(data.map((d) => d[fields.color]))]
|
|
99
|
+
const titleOffset = title ? 15 : 0
|
|
100
|
+
const style = { shape, markerSize, titleOffset }
|
|
101
|
+
const items = buildLegendItems(colorValues, scales.color, style)
|
|
102
|
+
const approxWidth = approximateLegendWidth(colorValues, markerSize)
|
|
103
|
+
const x = legendX(align, innerWidth(dimensions), approxWidth)
|
|
104
|
+
|
|
105
|
+
return { items, title, transform: `translate(${x}, 0)` }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Filter data based on legend selection
|
|
110
|
+
*
|
|
111
|
+
* @param {Array} data - Chart data
|
|
112
|
+
* @param {string} colorField - Field used for color mapping
|
|
113
|
+
* @param {Array} selectedValues - Selected legend values
|
|
114
|
+
* @returns {Array} Filtered data
|
|
115
|
+
*/
|
|
116
|
+
export function filterByLegend(data, colorField, selectedValues) {
|
|
117
|
+
if (!selectedValues || selectedValues.length === 0) {
|
|
118
|
+
return data
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return data.filter((d) => selectedValues.includes(d[colorField]))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Create attributes for legend items
|
|
126
|
+
*
|
|
127
|
+
* @param {LegendItem} item - Legend item
|
|
128
|
+
* @returns {Object} Attributes for the legend item
|
|
129
|
+
*/
|
|
130
|
+
export function createLegendItemAttributes(item) {
|
|
131
|
+
return {
|
|
132
|
+
'data-plot-legend-item': '',
|
|
133
|
+
transform: `translate(0, ${item.y})`,
|
|
134
|
+
role: 'img',
|
|
135
|
+
'aria-label': `Legend item for ${item.value}`
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import { min, max } from 'd3-array'
|
|
3
|
+
import { scaleBand, scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale'
|
|
4
|
+
import { schemeCategory10 } from 'd3-scale-chromatic'
|
|
5
|
+
import {} from './types.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./types').ChartScales} ChartScales
|
|
9
|
+
* @typedef {import('./types').ScaleFields} ScaleFields
|
|
10
|
+
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
11
|
+
*/
|
|
12
|
+
|
|
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
|
+
/**
|
|
39
|
+
* @param {Array} data
|
|
40
|
+
* @param {string} colorField
|
|
41
|
+
* @returns {import('d3-scale').ScaleOrdinal}
|
|
42
|
+
*/
|
|
43
|
+
function buildColorScale(data, colorField) {
|
|
44
|
+
const colorValues = [...new SvelteSet(data.map((d) => d[colorField]))]
|
|
45
|
+
return scaleOrdinal().domain(colorValues).range(schemeCategory10)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {Array} data
|
|
50
|
+
* @param {string} yField
|
|
51
|
+
* @param {Object} dimensions
|
|
52
|
+
* @returns {import('d3-scale').ScaleContinuousNumeric}
|
|
53
|
+
*/
|
|
54
|
+
function buildYScale(data, yField, dimensions) {
|
|
55
|
+
const yValues = data.map((d) => d[yField])
|
|
56
|
+
return scaleLinear()
|
|
57
|
+
.domain([0, max(yValues) * 1.1])
|
|
58
|
+
.nice()
|
|
59
|
+
.range([dimensions.innerHeight, 0])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {Array} data
|
|
64
|
+
* @param {ScaleFields} fields
|
|
65
|
+
* @returns {boolean}
|
|
66
|
+
*/
|
|
67
|
+
function hasRequiredFields(data, fields) {
|
|
68
|
+
return Boolean(data && data.length && fields.x && fields.y)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates scales based on data, fields, and dimensions
|
|
73
|
+
*
|
|
74
|
+
* @param {Array} data - Chart data
|
|
75
|
+
* @param {ScaleFields} fields - Field mappings
|
|
76
|
+
* @param {Object} dimensions - Chart dimensions
|
|
77
|
+
* @param {Object} options - Scale options
|
|
78
|
+
* @param {number} [options.padding=0.2] - Padding for band scales
|
|
79
|
+
* @returns {ChartScales} Chart scales
|
|
80
|
+
*/
|
|
81
|
+
export function createScales(data, fields, dimensions, options = {}) {
|
|
82
|
+
if (!hasRequiredFields(data, fields)) return { x: null, y: null, color: null }
|
|
83
|
+
|
|
84
|
+
const padding = options.padding ?? 0.2
|
|
85
|
+
const xValues = data.map((d) => d[fields.x])
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
x: buildXScale(xValues, dimensions, padding),
|
|
89
|
+
y: buildYScale(data, fields.y, dimensions),
|
|
90
|
+
color: fields.color ? buildColorScale(data, fields.color) : null
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gets the origin coordinates for the axes
|
|
96
|
+
*
|
|
97
|
+
* @param {ChartScales} scales - Chart scales
|
|
98
|
+
* @param {Object} dimensions - Chart dimensions
|
|
99
|
+
* @returns {Object} Origin coordinates
|
|
100
|
+
*/
|
|
101
|
+
export function getOrigin(scales, dimensions) {
|
|
102
|
+
return {
|
|
103
|
+
x: scales.y ? scales.y(0) : dimensions.innerHeight,
|
|
104
|
+
y: scales.x ? (scales.x.ticks ? scales.x(Math.max(0, min(scales.x.domain()))) : 0) : 0
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -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 {}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { getContext, setContext } from 'svelte'
|
|
2
|
+
import { writable, derived } from 'svelte/store'
|
|
3
|
+
import * as d3 from 'd3'
|
|
4
|
+
|
|
5
|
+
const CHART_CONTEXT = 'chart-context'
|
|
6
|
+
|
|
7
|
+
const DEFAULT_OPTIONS = {
|
|
8
|
+
width: 600,
|
|
9
|
+
height: 400,
|
|
10
|
+
margin: { top: 20, right: 30, bottom: 40, left: 50 },
|
|
11
|
+
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
12
|
+
responsive: true,
|
|
13
|
+
animationDuration: 300,
|
|
14
|
+
data: []
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {Object} config
|
|
19
|
+
* @returns {import('svelte/store').Writable}
|
|
20
|
+
*/
|
|
21
|
+
function createDimensionsStore(config) {
|
|
22
|
+
return writable({
|
|
23
|
+
width: config.width,
|
|
24
|
+
height: config.height,
|
|
25
|
+
margin: { ...config.margin },
|
|
26
|
+
padding: { ...config.padding }
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {import('svelte/store').Writable} dimensions
|
|
32
|
+
* @returns {import('svelte/store').Readable}
|
|
33
|
+
*/
|
|
34
|
+
function createInnerDimensions(dimensions) {
|
|
35
|
+
return derived(dimensions, ($d) => ({
|
|
36
|
+
width:
|
|
37
|
+
$d.width - $d.margin.left - $d.margin.right - $d.padding.left - $d.padding.right,
|
|
38
|
+
height:
|
|
39
|
+
$d.height - $d.margin.top - $d.margin.bottom - $d.padding.top - $d.padding.bottom
|
|
40
|
+
}))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {import('svelte/store').Writable} plots
|
|
45
|
+
* @returns {(plot: unknown) => () => void}
|
|
46
|
+
*/
|
|
47
|
+
function makeAddPlot(plots) {
|
|
48
|
+
return function addPlot(plot) {
|
|
49
|
+
plots.update((currentPlots) => [...currentPlots, plot])
|
|
50
|
+
return () => {
|
|
51
|
+
plots.update((currentPlots) => currentPlots.filter((p) => p !== plot))
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {import('svelte/store').Writable} data
|
|
58
|
+
* @param {import('svelte/store').Readable} innerDimensions
|
|
59
|
+
* @returns {(xKey: string, yKey: string, colorKey?: string|null) => import('svelte/store').Readable}
|
|
60
|
+
*/
|
|
61
|
+
function makeUpdateScales(data, innerDimensions) {
|
|
62
|
+
return function updateScales(xKey, yKey, colorKey = null) {
|
|
63
|
+
return derived([data, innerDimensions], ([$data, $innerDimensions]) => {
|
|
64
|
+
if (!$data || $data.length === 0) return null
|
|
65
|
+
|
|
66
|
+
const xScale = d3
|
|
67
|
+
.scaleBand()
|
|
68
|
+
.domain($data.map((d) => d[xKey]))
|
|
69
|
+
.range([0, $innerDimensions.width])
|
|
70
|
+
.padding(0.2)
|
|
71
|
+
|
|
72
|
+
const yScale = d3
|
|
73
|
+
.scaleLinear()
|
|
74
|
+
.domain([0, d3.max($data, (d) => d[yKey])])
|
|
75
|
+
.nice()
|
|
76
|
+
.range([$innerDimensions.height, 0])
|
|
77
|
+
|
|
78
|
+
let colorScale = null
|
|
79
|
+
if (colorKey) {
|
|
80
|
+
const uniqueCategories = [...new Set($data.map((d) => d[colorKey]))]
|
|
81
|
+
colorScale = d3.scaleOrdinal().domain(uniqueCategories).range(d3.schemeCategory10)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return { xScale, yScale, colorScale }
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Creates chart context and provides it to child components
|
|
91
|
+
*
|
|
92
|
+
* @param {Object} options Initial chart options
|
|
93
|
+
* @returns {Object} Chart context with all stores and methods
|
|
94
|
+
*/
|
|
95
|
+
export function createChartContext(options = {}) {
|
|
96
|
+
const config = { ...DEFAULT_OPTIONS, ...options }
|
|
97
|
+
|
|
98
|
+
const dimensions = createDimensionsStore(config)
|
|
99
|
+
const data = writable(config.data)
|
|
100
|
+
const scales = writable({})
|
|
101
|
+
const innerDimensions = createInnerDimensions(dimensions)
|
|
102
|
+
const plots = writable([])
|
|
103
|
+
const axes = writable({ x: null, y: null })
|
|
104
|
+
const legend = writable({ enabled: false, items: [] })
|
|
105
|
+
|
|
106
|
+
const addPlot = makeAddPlot(plots)
|
|
107
|
+
const updateScales = makeUpdateScales(data, innerDimensions)
|
|
108
|
+
|
|
109
|
+
const chartContext = {
|
|
110
|
+
dimensions,
|
|
111
|
+
innerDimensions,
|
|
112
|
+
data,
|
|
113
|
+
scales,
|
|
114
|
+
plots,
|
|
115
|
+
axes,
|
|
116
|
+
legend,
|
|
117
|
+
addPlot,
|
|
118
|
+
updateScales
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
setContext(CHART_CONTEXT, chartContext)
|
|
122
|
+
|
|
123
|
+
return chartContext
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Gets chart context provided by parent component
|
|
128
|
+
*
|
|
129
|
+
* @returns {Object} Chart context
|
|
130
|
+
*/
|
|
131
|
+
export function getChartContext() {
|
|
132
|
+
return getContext(CHART_CONTEXT)
|
|
133
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
2
|
+
import { scaleBand, scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale'
|
|
3
|
+
import { schemeCategory10 } from 'd3-scale-chromatic'
|
|
4
|
+
import { min, max } from 'd3-array'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {Array} xValues
|
|
8
|
+
* @param {Object} dimensions
|
|
9
|
+
* @param {number} padding
|
|
10
|
+
* @returns {Object}
|
|
11
|
+
*/
|
|
12
|
+
function buildXScale(xValues, dimensions, padding) {
|
|
13
|
+
const xIsDate = xValues.some((v) => v instanceof Date)
|
|
14
|
+
const xIsNumeric = !xIsDate && xValues.every((v) => !isNaN(parseFloat(v)))
|
|
15
|
+
|
|
16
|
+
if (xIsDate) {
|
|
17
|
+
return scaleTime()
|
|
18
|
+
.domain([min(xValues), max(xValues)])
|
|
19
|
+
.range([0, dimensions.innerWidth])
|
|
20
|
+
.nice()
|
|
21
|
+
}
|
|
22
|
+
if (xIsNumeric) {
|
|
23
|
+
return scaleLinear()
|
|
24
|
+
.domain([min([0, ...xValues]), max(xValues)])
|
|
25
|
+
.range([0, dimensions.innerWidth])
|
|
26
|
+
.nice()
|
|
27
|
+
}
|
|
28
|
+
return scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates appropriate scales based on data and dimensions
|
|
33
|
+
*
|
|
34
|
+
* @param {Array} data The dataset
|
|
35
|
+
* @param {Object} dimensions Chart dimensions
|
|
36
|
+
* @param {Object} options Additional options
|
|
37
|
+
* @param {string} options.xKey Field to use for x-axis
|
|
38
|
+
* @param {string} options.yKey Field to use for y-axis
|
|
39
|
+
* @param {string} [options.colorKey] Field to use for color mapping
|
|
40
|
+
* @param {number} [options.padding=0.2] Padding for band scales
|
|
41
|
+
* @returns {Object} Object containing xScale, yScale, and colorScale
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* @param {Array} data
|
|
45
|
+
* @param {string} colorKey
|
|
46
|
+
* @returns {Object}
|
|
47
|
+
*/
|
|
48
|
+
function buildColorScale(data, colorKey) {
|
|
49
|
+
const uniqueCategories = [...new SvelteSet(data.map((d) => d[colorKey]))]
|
|
50
|
+
return scaleOrdinal().domain(uniqueCategories).range(schemeCategory10)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @param {Object} options
|
|
55
|
+
* @returns {{ xKey: string, yKey: string, colorKey: string|undefined, padding: number }}
|
|
56
|
+
*/
|
|
57
|
+
function parseScaleOptions(options) {
|
|
58
|
+
const opts = options || {}
|
|
59
|
+
return {
|
|
60
|
+
xKey: opts.xKey,
|
|
61
|
+
yKey: opts.yKey,
|
|
62
|
+
colorKey: opts.colorKey,
|
|
63
|
+
padding: opts.padding !== undefined ? opts.padding : 0.2
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createScales(data, dimensions, options) {
|
|
68
|
+
if (!data || !data.length) return {}
|
|
69
|
+
|
|
70
|
+
const { xKey, yKey, colorKey, padding } = parseScaleOptions(options)
|
|
71
|
+
const xValues = data.map((d) => d[xKey])
|
|
72
|
+
const yValues = data.map((d) => d[yKey])
|
|
73
|
+
const colorScale = colorKey ? buildColorScale(data, colorKey) : null
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
xScale: buildXScale(xValues, dimensions, padding),
|
|
77
|
+
yScale: scaleLinear().domain([0, max(yValues) * 1.1]).nice().range([dimensions.innerHeight, 0]),
|
|
78
|
+
colorScale
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Calculates the actual chart dimensions after applying margins
|
|
84
|
+
*
|
|
85
|
+
* @param {number} width
|
|
86
|
+
* @param {number} height
|
|
87
|
+
* @param {Object} margin
|
|
88
|
+
* @returns {Object} Dimensions with calculated inner width and height
|
|
89
|
+
*/
|
|
90
|
+
export function calculateChartDimensions(width, height, margin) {
|
|
91
|
+
return {
|
|
92
|
+
width,
|
|
93
|
+
height,
|
|
94
|
+
margin,
|
|
95
|
+
innerWidth: width - margin.left - margin.right,
|
|
96
|
+
innerHeight: height - margin.top - margin.bottom
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Gets the axis origin value
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} scale D3 scale
|
|
104
|
+
* @returns {number} Origin value
|
|
105
|
+
*/
|
|
106
|
+
export function getOriginValue(scale) {
|
|
107
|
+
return scale.ticks ? scale(Math.max(0, min(scale.domain()))) : scale.range()[0]
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Resolve tick values for a band scale, applying downsampling when needed
|
|
112
|
+
* @param {Object} scale
|
|
113
|
+
* @param {number} count
|
|
114
|
+
* @returns {{ ticks: Array, offset: number }}
|
|
115
|
+
*/
|
|
116
|
+
function bandTicks(scale, count) {
|
|
117
|
+
const offset = scale.bandwidth() / 2
|
|
118
|
+
const domain = scale.domain()
|
|
119
|
+
const cappedCount = Math.min(Math.round(count), domain.length)
|
|
120
|
+
const step = Math.ceil(domain.length / cappedCount)
|
|
121
|
+
const ticks = cappedCount < domain.length ? domain.filter((_, i) => i % step === 0) : domain
|
|
122
|
+
return { ticks, offset }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {number} rangeSize
|
|
127
|
+
* @param {string} axis
|
|
128
|
+
* @param {number} fontSize
|
|
129
|
+
* @returns {number}
|
|
130
|
+
*/
|
|
131
|
+
function defaultTickCount(rangeSize, axis, fontSize) {
|
|
132
|
+
const divisor = fontSize * (axis === 'y' ? 3 : 6)
|
|
133
|
+
return Math.abs(rangeSize / divisor)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @param {Array} ticks
|
|
138
|
+
* @param {Object} scale
|
|
139
|
+
* @param {number} offset
|
|
140
|
+
* @param {boolean} isXAxis
|
|
141
|
+
* @returns {Array}
|
|
142
|
+
*/
|
|
143
|
+
function formatTicks(ticks, scale, offset, isXAxis) {
|
|
144
|
+
const pos = isXAxis ? offset : 0
|
|
145
|
+
return ticks.map((t) => ({ value: t, position: scale(t) + pos }))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Creates axis ticks
|
|
150
|
+
*
|
|
151
|
+
* @param {Object} scale D3 scale
|
|
152
|
+
* @param {string} axis Axis type ('x' or 'y')
|
|
153
|
+
* @param {number} count Number of ticks
|
|
154
|
+
* @param {number} fontSize Font size for determining tick density
|
|
155
|
+
* @returns {Array} Array of tick objects
|
|
156
|
+
*/
|
|
157
|
+
export function createTicks(scale, axis, count = null, fontSize = 12) {
|
|
158
|
+
const [minRange, maxRange] = scale.range()
|
|
159
|
+
const tickCount = count ?? defaultTickCount(maxRange - minRange, axis, fontSize)
|
|
160
|
+
|
|
161
|
+
let ticks, offset
|
|
162
|
+
if (scale.ticks) {
|
|
163
|
+
ticks = scale.ticks(Math.round(tickCount))
|
|
164
|
+
offset = 0
|
|
165
|
+
} else {
|
|
166
|
+
const band = bandTicks(scale, tickCount)
|
|
167
|
+
ticks = band.ticks
|
|
168
|
+
offset = band.offset
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return formatTicks(ticks, scale, offset, axis === 'x')
|
|
172
|
+
}
|