@rokkit/chart 1.0.0-next.136 → 1.0.0-next.138
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/dist/lib/brewing/axes.svelte.d.ts +3 -9
- package/dist/lib/brewing/bars.svelte.d.ts +14 -12
- package/dist/lib/brewing/legends.svelte.d.ts +4 -10
- package/dist/lib/brewing/scales.svelte.d.ts +0 -5
- package/dist/lib/scales.svelte.d.ts +13 -13
- package/dist/lib/utils.d.ts +5 -3
- package/package.json +1 -1
- package/src/lib/brewing/axes.svelte.js +174 -86
- package/src/lib/brewing/bars.svelte.js +109 -46
- package/src/lib/brewing/index.svelte.js +5 -2
- package/src/lib/brewing/legends.svelte.js +76 -33
- package/src/lib/brewing/scales.svelte.js +62 -41
- package/src/lib/context.js +62 -61
- package/src/lib/scales.svelte.js +107 -57
- package/src/lib/utils.js +23 -12
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
1
2
|
import {} from './types.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -7,6 +8,45 @@ import {} from './types.js'
|
|
|
7
8
|
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
8
9
|
*/
|
|
9
10
|
|
|
11
|
+
const DEFAULT_COLOR = '#4682b4'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {Object} d - data item
|
|
15
|
+
* @param {{ fields: Object, scales: Object, dimensions: Object, defaultColor: string }} ctx
|
|
16
|
+
* @returns {BarData}
|
|
17
|
+
*/
|
|
18
|
+
function buildBar(d, ctx) {
|
|
19
|
+
const { fields, scales, dimensions, defaultColor } = ctx
|
|
20
|
+
const { x: xField, y: yField, color: colorField } = fields
|
|
21
|
+
const barWidth = scales.x.bandwidth ? scales.x.bandwidth() : 10
|
|
22
|
+
const barX = scales.x.bandwidth ? scales.x(d[xField]) : scales.x(d[xField]) - barWidth / 2
|
|
23
|
+
return {
|
|
24
|
+
data: d,
|
|
25
|
+
x: barX,
|
|
26
|
+
y: scales.y(d[yField]),
|
|
27
|
+
width: barWidth,
|
|
28
|
+
height: dimensions.innerHeight - scales.y(d[yField]),
|
|
29
|
+
color: colorField && scales.color ? scales.color(d[colorField]) : defaultColor
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {Object} options
|
|
35
|
+
* @param {Object} fields
|
|
36
|
+
* @param {Object} scales
|
|
37
|
+
* @returns {{ ctx: Object }|null} null if input is invalid
|
|
38
|
+
*/
|
|
39
|
+
function parseBarsInput(options, fields, scales) {
|
|
40
|
+
if (!scales.x || !scales.y) return null
|
|
41
|
+
const opts = options || {}
|
|
42
|
+
return {
|
|
43
|
+
fields,
|
|
44
|
+
scales,
|
|
45
|
+
dimensions: opts.dimensions,
|
|
46
|
+
defaultColor: opts.defaultColor !== undefined ? opts.defaultColor : DEFAULT_COLOR
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
10
50
|
/**
|
|
11
51
|
* Creates bar data for rendering
|
|
12
52
|
*
|
|
@@ -19,28 +59,16 @@ import {} from './types.js'
|
|
|
19
59
|
* @param {Function} scales.x - X-axis scale
|
|
20
60
|
* @param {Function} scales.y - Y-axis scale
|
|
21
61
|
* @param {Function} scales.color - Color scale
|
|
22
|
-
* @param {Object}
|
|
23
|
-
* @param {
|
|
62
|
+
* @param {Object} options - Options including dimensions and defaultColor
|
|
63
|
+
* @param {Object} options.dimensions - Chart dimensions
|
|
64
|
+
* @param {string} [options.defaultColor='#4682b4'] - Default color if no color scale
|
|
24
65
|
* @returns {BarData[]} Bar data for rendering
|
|
25
66
|
*/
|
|
26
|
-
export function createBars(data, fields, scales,
|
|
27
|
-
if (!data || data.length
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return data.map((d) => {
|
|
32
|
-
const barWidth = scales.x.bandwidth ? scales.x.bandwidth() : 10
|
|
33
|
-
const barX = scales.x.bandwidth ? scales.x(d[xField]) : scales.x(d[xField]) - barWidth / 2
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
data: d,
|
|
37
|
-
x: barX,
|
|
38
|
-
y: scales.y(d[yField]),
|
|
39
|
-
width: barWidth,
|
|
40
|
-
height: dimensions.innerHeight - scales.y(d[yField]),
|
|
41
|
-
color: colorField && scales.color ? scales.color(d[colorField]) : defaultColor
|
|
42
|
-
}
|
|
43
|
-
})
|
|
67
|
+
export function createBars(data, fields, scales, options) {
|
|
68
|
+
if (!data || !data.length) return []
|
|
69
|
+
const ctx = parseBarsInput(options, fields, scales)
|
|
70
|
+
if (!ctx) return []
|
|
71
|
+
return data.map((d) => buildBar(d, ctx))
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
/**
|
|
@@ -61,52 +89,87 @@ export function filterBars(bars, selection) {
|
|
|
61
89
|
})
|
|
62
90
|
}
|
|
63
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @param {Object} item - data item
|
|
94
|
+
* @param {Object} scales - chart scales
|
|
95
|
+
* @param {{ yField: string, colorField: string, group: unknown, barX: number, barWidth: number, dimensions: Object }} ctx
|
|
96
|
+
* @returns {BarData}
|
|
97
|
+
*/
|
|
98
|
+
function buildGroupBar(item, scales, ctx) {
|
|
99
|
+
const { yField, colorField, group, barX, barWidth, dimensions } = ctx
|
|
100
|
+
return {
|
|
101
|
+
data: item,
|
|
102
|
+
group,
|
|
103
|
+
x: barX,
|
|
104
|
+
y: scales.y(item[yField]),
|
|
105
|
+
width: barWidth,
|
|
106
|
+
height: dimensions.innerHeight - scales.y(item[yField]),
|
|
107
|
+
color: scales.color ? scales.color(item[colorField]) : DEFAULT_COLOR
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {Object} scales
|
|
113
|
+
* @param {{ groupItems: Array, groupField: string, group: unknown, i: number, yField: string, colorField: string, barWidth: number, padding: number, dimensions: Object, xPos: number }} ctx
|
|
114
|
+
* @returns {BarData|null}
|
|
115
|
+
*/
|
|
116
|
+
function buildOneGroupBar(scales, ctx) {
|
|
117
|
+
const { groupItems, groupField, group, i, yField, colorField, barWidth, padding, dimensions, xPos } = ctx
|
|
118
|
+
const item = groupItems.find((d) => d[groupField] === group)
|
|
119
|
+
if (!item) return null
|
|
120
|
+
const barX = xPos + i * (barWidth + padding)
|
|
121
|
+
return buildGroupBar(item, scales, { yField, colorField, group, barX, barWidth, dimensions })
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* @param {Object} fields
|
|
126
|
+
* @param {Object} options
|
|
127
|
+
* @returns {{ xField: string, yField: string, groupField: string, colorField: string, dimensions: Object, padding: number }}
|
|
128
|
+
*/
|
|
129
|
+
function parseGroupedBarsConfig(fields, options) {
|
|
130
|
+
const opts = options || {}
|
|
131
|
+
return {
|
|
132
|
+
xField: fields.x,
|
|
133
|
+
yField: fields.y,
|
|
134
|
+
groupField: fields.group,
|
|
135
|
+
colorField: fields.color !== undefined ? fields.color : fields.group,
|
|
136
|
+
dimensions: opts.dimensions,
|
|
137
|
+
padding: opts.padding !== undefined ? opts.padding : 0.1
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
64
141
|
/**
|
|
65
142
|
* Creates a grouped bars layout
|
|
66
143
|
*
|
|
67
144
|
* @param {Array} data - Chart data
|
|
68
145
|
* @param {Object} fields - Field mappings
|
|
69
146
|
* @param {Object} scales - Chart scales
|
|
70
|
-
* @param {Object}
|
|
71
|
-
* @param {Object} options -
|
|
147
|
+
* @param {Object} options - Options including dimensions and padding
|
|
148
|
+
* @param {Object} options.dimensions - Chart dimensions
|
|
149
|
+
* @param {number} [options.padding=0.1] - Padding between bars in a group
|
|
72
150
|
* @returns {Object} Grouped bar data
|
|
73
151
|
*/
|
|
74
|
-
export function createGroupedBars(data, fields, scales,
|
|
75
|
-
if (!data || data.length
|
|
152
|
+
export function createGroupedBars(data, fields, scales, options) {
|
|
153
|
+
if (!data || !data.length || !fields.group) return { groups: [], bars: [] }
|
|
76
154
|
|
|
77
|
-
const {
|
|
78
|
-
|
|
155
|
+
const { xField, yField, groupField, colorField, dimensions, padding } =
|
|
156
|
+
parseGroupedBarsConfig(fields, options)
|
|
79
157
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const xValues = [...new Set(data.map((d) => d[xField]))]
|
|
158
|
+
const groups = [...new SvelteSet(data.map((d) => d[groupField]))]
|
|
159
|
+
const xValues = [...new SvelteSet(data.map((d) => d[xField]))]
|
|
83
160
|
|
|
84
|
-
// Calculate group width and individual bar width
|
|
85
161
|
const xScale = scales.x
|
|
86
162
|
const groupWidth = xScale.bandwidth ? xScale.bandwidth() : 20
|
|
87
163
|
const barWidth = (groupWidth - padding * (groups.length - 1)) / groups.length
|
|
88
164
|
|
|
89
|
-
// Create bars for each group
|
|
90
165
|
const bars = []
|
|
91
|
-
|
|
92
166
|
xValues.forEach((xValue) => {
|
|
93
167
|
const groupItems = data.filter((d) => d[xField] === xValue)
|
|
94
168
|
const xPos = xScale(xValue)
|
|
95
|
-
|
|
96
169
|
groups.forEach((group, i) => {
|
|
97
|
-
const
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
const barX = xPos + i * (barWidth + padding)
|
|
101
|
-
bars.push({
|
|
102
|
-
data: item,
|
|
103
|
-
group,
|
|
104
|
-
x: barX,
|
|
105
|
-
y: scales.y(item[yField]),
|
|
106
|
-
width: barWidth,
|
|
107
|
-
height: dimensions.innerHeight - scales.y(item[yField]),
|
|
108
|
-
color: scales.color ? scales.color(item[colorField]) : '#4682b4'
|
|
109
|
-
})
|
|
170
|
+
const ctx = { groupItems, groupField, group, i, yField, colorField, barWidth, padding, dimensions, xPos }
|
|
171
|
+
const bar = buildOneGroupBar(scales, ctx)
|
|
172
|
+
if (bar) bars.push(bar)
|
|
110
173
|
})
|
|
111
174
|
})
|
|
112
175
|
|
|
@@ -112,7 +112,7 @@ export class ChartBrewer {
|
|
|
112
112
|
* @returns {Array} Data for rendering bars
|
|
113
113
|
*/
|
|
114
114
|
createBars() {
|
|
115
|
-
return createBars(this.#data, this.#fields, this.#scales, this.#dimensions)
|
|
115
|
+
return createBars(this.#data, this.#fields, this.#scales, { dimensions: this.#dimensions })
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
/**
|
|
@@ -152,7 +152,10 @@ export class ChartBrewer {
|
|
|
152
152
|
* @returns {Object} Legend rendering data
|
|
153
153
|
*/
|
|
154
154
|
createLegend(options = {}) {
|
|
155
|
-
return createLegend(this.#data, this.#fields, this.#scales,
|
|
155
|
+
return createLegend(this.#data, this.#fields, this.#scales, {
|
|
156
|
+
...options,
|
|
157
|
+
dimensions: this.#dimensions
|
|
158
|
+
})
|
|
156
159
|
}
|
|
157
160
|
|
|
158
161
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
1
2
|
import {} from './types.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -8,6 +9,70 @@ import {} from './types.js'
|
|
|
8
9
|
* @typedef {import('./types').ChartDimensions} ChartDimensions
|
|
9
10
|
*/
|
|
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
|
+
|
|
11
76
|
/**
|
|
12
77
|
* Creates legend data for rendering
|
|
13
78
|
*
|
|
@@ -16,50 +81,28 @@ import {} from './types.js'
|
|
|
16
81
|
* @param {string} fields.color - Color field
|
|
17
82
|
* @param {Object} scales - Chart scales
|
|
18
83
|
* @param {Function} scales.color - Color scale
|
|
19
|
-
* @param {Object}
|
|
20
|
-
* @param {Object} options -
|
|
84
|
+
* @param {Object} options - Legend options including dimensions
|
|
85
|
+
* @param {Object} options.dimensions - Chart dimensions
|
|
21
86
|
* @param {string} [options.title=''] - Legend title
|
|
22
87
|
* @param {string} [options.align='right'] - Legend alignment ('left', 'center', or 'right')
|
|
23
88
|
* @param {string} [options.shape='rect'] - Legend marker shape ('rect' or 'circle')
|
|
24
89
|
* @param {number} [options.markerSize=10] - Size of legend markers
|
|
25
90
|
* @returns {LegendData} Legend rendering data
|
|
26
91
|
*/
|
|
27
|
-
export function createLegend(data, fields, scales,
|
|
92
|
+
export function createLegend(data, fields, scales, options) {
|
|
28
93
|
if (!data || !fields.color || !scales.color) {
|
|
29
94
|
return { items: [], title: '', transform: 'translate(0, 0)' }
|
|
30
95
|
}
|
|
31
96
|
|
|
32
|
-
const { title
|
|
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)
|
|
33
104
|
|
|
34
|
-
|
|
35
|
-
const colorValues = [...new Set(data.map((d) => d[fields.color]))]
|
|
36
|
-
|
|
37
|
-
// Create legend items
|
|
38
|
-
const items = colorValues.map((value, index) => ({
|
|
39
|
-
value,
|
|
40
|
-
color: scales.color(value),
|
|
41
|
-
y: index * (markerSize + 5) + (title ? 15 : 0),
|
|
42
|
-
shape,
|
|
43
|
-
markerSize
|
|
44
|
-
}))
|
|
45
|
-
|
|
46
|
-
// Calculate approximate width for alignment
|
|
47
|
-
const approximateWidth =
|
|
48
|
-
Math.max(...colorValues.map((v) => v.toString().length)) * 8 + markerSize + 10
|
|
49
|
-
|
|
50
|
-
// Calculate position based on alignment
|
|
51
|
-
let x = 0
|
|
52
|
-
if (align === 'right') {
|
|
53
|
-
x = dimensions.innerWidth - approximateWidth
|
|
54
|
-
} else if (align === 'center') {
|
|
55
|
-
x = (dimensions.innerWidth - approximateWidth) / 2
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
items,
|
|
60
|
-
title,
|
|
61
|
-
transform: `translate(${x}, 0)`
|
|
62
|
-
}
|
|
105
|
+
return { items, title, transform: `translate(${x}, 0)` }
|
|
63
106
|
}
|
|
64
107
|
|
|
65
108
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SvelteSet } from 'svelte/reactivity'
|
|
1
2
|
import { min, max } from 'd3-array'
|
|
2
3
|
import { scaleBand, scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale'
|
|
3
4
|
import { schemeCategory10 } from 'd3-scale-chromatic'
|
|
@@ -10,64 +11,84 @@ import {} from './types.js'
|
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param {
|
|
16
|
-
* @
|
|
17
|
-
* @param {Object} dimensions - Chart dimensions
|
|
18
|
-
* @param {Object} options - Scale options
|
|
19
|
-
* @param {number} [options.padding=0.2] - Padding for band scales
|
|
20
|
-
* @returns {ChartScales} Chart scales
|
|
14
|
+
* @param {Array} xValues
|
|
15
|
+
* @param {Object} dimensions
|
|
16
|
+
* @param {number} padding
|
|
17
|
+
* @returns {import('d3-scale').ScaleContinuousNumeric|import('d3-scale').ScaleBand}
|
|
21
18
|
*/
|
|
22
|
-
|
|
23
|
-
const scales = {
|
|
24
|
-
x: null,
|
|
25
|
-
y: null,
|
|
26
|
-
color: null
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!data || data.length === 0 || !fields.x || !fields.y) {
|
|
30
|
-
return scales
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const padding = options.padding !== undefined ? options.padding : 0.2
|
|
34
|
-
|
|
35
|
-
// Extract values
|
|
36
|
-
const xValues = data.map((d) => d[fields.x])
|
|
37
|
-
const yValues = data.map((d) => d[fields.y])
|
|
38
|
-
|
|
39
|
-
// Determine x scale type
|
|
19
|
+
function buildXScale(xValues, dimensions, padding) {
|
|
40
20
|
const xIsDate = xValues.some((v) => v instanceof Date)
|
|
41
21
|
const xIsNumeric = !xIsDate && xValues.every((v) => !isNaN(parseFloat(v)))
|
|
42
22
|
|
|
43
|
-
// Create x scale based on data type
|
|
44
23
|
if (xIsDate) {
|
|
45
|
-
|
|
24
|
+
return scaleTime()
|
|
46
25
|
.domain([min(xValues), max(xValues)])
|
|
47
26
|
.range([0, dimensions.innerWidth])
|
|
48
27
|
.nice()
|
|
49
|
-
}
|
|
50
|
-
|
|
28
|
+
}
|
|
29
|
+
if (xIsNumeric) {
|
|
30
|
+
return scaleLinear()
|
|
51
31
|
.domain([min([0, ...xValues]), max(xValues)])
|
|
52
32
|
.range([0, dimensions.innerWidth])
|
|
53
33
|
.nice()
|
|
54
|
-
} else {
|
|
55
|
-
scales.x = scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
|
|
56
34
|
}
|
|
35
|
+
return scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
|
|
36
|
+
}
|
|
57
37
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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])
|
|
61
58
|
.nice()
|
|
62
59
|
.range([dimensions.innerHeight, 0])
|
|
60
|
+
}
|
|
63
61
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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])
|
|
69
86
|
|
|
70
|
-
return
|
|
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
|
+
}
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
/**
|
package/src/lib/context.js
CHANGED
|
@@ -4,80 +4,62 @@ import * as d3 from 'd3'
|
|
|
4
4
|
|
|
5
5
|
const CHART_CONTEXT = 'chart-context'
|
|
6
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
|
+
|
|
7
17
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} options Initial chart options
|
|
11
|
-
* @returns {Object} Chart context with all stores and methods
|
|
18
|
+
* @param {Object} config
|
|
19
|
+
* @returns {import('svelte/store').Writable}
|
|
12
20
|
*/
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const defaultOptions = {
|
|
16
|
-
width: 600,
|
|
17
|
-
height: 400,
|
|
18
|
-
margin: { top: 20, right: 30, bottom: 40, left: 50 },
|
|
19
|
-
padding: { top: 0, right: 0, bottom: 0, left: 0 },
|
|
20
|
-
responsive: true,
|
|
21
|
-
animationDuration: 300,
|
|
22
|
-
data: []
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Merge options with defaults
|
|
26
|
-
const config = { ...defaultOptions, ...options }
|
|
27
|
-
|
|
28
|
-
// Create stores for reactive properties
|
|
29
|
-
const dimensions = writable({
|
|
21
|
+
function createDimensionsStore(config) {
|
|
22
|
+
return writable({
|
|
30
23
|
width: config.width,
|
|
31
24
|
height: config.height,
|
|
32
25
|
margin: { ...config.margin },
|
|
33
26
|
padding: { ...config.padding }
|
|
34
27
|
})
|
|
28
|
+
}
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
height:
|
|
49
|
-
$dimensions.height -
|
|
50
|
-
$dimensions.margin.top -
|
|
51
|
-
$dimensions.margin.bottom -
|
|
52
|
-
$dimensions.padding.top -
|
|
53
|
-
$dimensions.padding.bottom
|
|
54
|
-
}
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
// Store for plot elements (bars, lines, etc.)
|
|
58
|
-
const plots = writable([])
|
|
59
|
-
|
|
60
|
-
// Store for axes
|
|
61
|
-
const axes = writable({
|
|
62
|
-
x: null,
|
|
63
|
-
y: null
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
const legend = writable({
|
|
67
|
-
enabled: false,
|
|
68
|
-
items: []
|
|
69
|
-
})
|
|
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
|
+
}
|
|
70
42
|
|
|
71
|
-
|
|
72
|
-
|
|
43
|
+
/**
|
|
44
|
+
* @param {import('svelte/store').Writable} plots
|
|
45
|
+
* @returns {(plot: unknown) => () => void}
|
|
46
|
+
*/
|
|
47
|
+
function makeAddPlot(plots) {
|
|
48
|
+
return function addPlot(plot) {
|
|
73
49
|
plots.update((currentPlots) => [...currentPlots, plot])
|
|
74
50
|
return () => {
|
|
75
51
|
plots.update((currentPlots) => currentPlots.filter((p) => p !== plot))
|
|
76
52
|
}
|
|
77
53
|
}
|
|
54
|
+
}
|
|
78
55
|
|
|
79
|
-
|
|
80
|
-
|
|
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) {
|
|
81
63
|
return derived([data, innerDimensions], ([$data, $innerDimensions]) => {
|
|
82
64
|
if (!$data || $data.length === 0) return null
|
|
83
65
|
|
|
@@ -94,7 +76,6 @@ export function createChartContext(options = {}) {
|
|
|
94
76
|
.range([$innerDimensions.height, 0])
|
|
95
77
|
|
|
96
78
|
let colorScale = null
|
|
97
|
-
|
|
98
79
|
if (colorKey) {
|
|
99
80
|
const uniqueCategories = [...new Set($data.map((d) => d[colorKey]))]
|
|
100
81
|
colorScale = d3.scaleOrdinal().domain(uniqueCategories).range(d3.schemeCategory10)
|
|
@@ -103,8 +84,28 @@ export function createChartContext(options = {}) {
|
|
|
103
84
|
return { xScale, yScale, colorScale }
|
|
104
85
|
})
|
|
105
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)
|
|
106
108
|
|
|
107
|
-
// Create and set context
|
|
108
109
|
const chartContext = {
|
|
109
110
|
dimensions,
|
|
110
111
|
innerDimensions,
|