@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.
@@ -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} dimensions - Chart dimensions
23
- * @param {string} defaultColor - Default color if no color scale is provided
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, dimensions, defaultColor = '#4682b4') {
27
- if (!data || data.length === 0 || !scales.x || !scales.y) return []
28
-
29
- const { x: xField, y: yField, color: colorField } = fields
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} dimensions - Chart dimensions
71
- * @param {Object} options - 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, dimensions, options = {}) {
75
- if (!data || data.length === 0 || !fields.group) return { groups: [], bars: [] }
152
+ export function createGroupedBars(data, fields, scales, options) {
153
+ if (!data || !data.length || !fields.group) return { groups: [], bars: [] }
76
154
 
77
- const { x: xField, y: yField, group: groupField, color: colorField = groupField } = fields
78
- const { padding = 0.1 } = options
155
+ const { xField, yField, groupField, colorField, dimensions, padding } =
156
+ parseGroupedBarsConfig(fields, options)
79
157
 
80
- // Get unique groups and x values
81
- const groups = [...new Set(data.map((d) => d[groupField]))]
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 item = groupItems.find((d) => d[groupField] === group)
98
- if (!item) return
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, this.#dimensions, options)
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} dimensions - Chart dimensions
20
- * @param {Object} options - Legend 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, dimensions, options = {}) {
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 = '', align = 'right', shape = 'rect', markerSize = 10 } = options
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
- // Get unique color values
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
- * Creates scales based on data, fields, and dimensions
14
- *
15
- * @param {Array} data - Chart data
16
- * @param {ScaleFields} fields - Field mappings
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
- export function createScales(data, fields, dimensions, options = {}) {
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
- scales.x = scaleTime()
24
+ return scaleTime()
46
25
  .domain([min(xValues), max(xValues)])
47
26
  .range([0, dimensions.innerWidth])
48
27
  .nice()
49
- } else if (xIsNumeric) {
50
- scales.x = scaleLinear()
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
- // Create y scale
59
- scales.y = scaleLinear()
60
- .domain([0, max(yValues) * 1.1]) // Add 10% padding on top
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
- // Create color scale if color field is set
65
- if (fields.color) {
66
- const colorValues = [...new Set(data.map((d) => d[fields.color]))]
67
- scales.color = scaleOrdinal().domain(colorValues).range(schemeCategory10)
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 scales
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
  /**
@@ -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
- * Creates chart context and provides it to child components
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
- export function createChartContext(options = {}) {
14
- // Default config values
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
- const data = writable(config.data)
37
- const scales = writable({})
38
-
39
- // Compute inner dimensions (subtracting margins)
40
- const innerDimensions = derived(dimensions, ($dimensions) => {
41
- return {
42
- width:
43
- $dimensions.width -
44
- $dimensions.margin.left -
45
- $dimensions.margin.right -
46
- $dimensions.padding.left -
47
- $dimensions.padding.right,
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
- // Helper to add a new plot
72
- function addPlot(plot) {
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
- // Helper to update scales based on data and dimensions
80
- function updateScales(xKey, yKey, colorKey = null) {
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,