@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,64 +1,90 @@
1
+ import { SvelteSet } from 'svelte/reactivity'
1
2
  import { scaleBand, scaleLinear, scaleTime, scaleOrdinal } from 'd3-scale'
2
3
  import { schemeCategory10 } from 'd3-scale-chromatic'
3
4
  import { min, max } from 'd3-array'
4
5
 
5
6
  /**
6
- * Creates appropriate scales based on data and dimensions
7
- *
8
- * @param {Array} data The dataset
9
- * @param {string} xKey Field to use for x-axis
10
- * @param {string} yKey Field to use for y-axis
11
- * @param {Object} dimensions Chart dimensions
12
- * @param {Object} options Additional options
13
- * @returns {Object} Object containing xScale, yScale, and colorScale
7
+ * @param {Array} xValues
8
+ * @param {Object} dimensions
9
+ * @param {number} padding
10
+ * @returns {Object}
14
11
  */
15
- export function createScales(data, xKey, yKey, dimensions, options = {}) {
16
- if (!data || data.length === 0) return {}
17
-
18
- const { colorKey = null, padding = 0.2 } = options
19
-
20
- // Determine if x values are numeric, dates, or categorical
21
- const xValues = data.map((d) => d[xKey])
12
+ function buildXScale(xValues, dimensions, padding) {
22
13
  const xIsDate = xValues.some((v) => v instanceof Date)
23
14
  const xIsNumeric = !xIsDate && xValues.every((v) => !isNaN(parseFloat(v)))
24
15
 
25
- // Create x-scale based on data type
26
- let xScale
27
16
  if (xIsDate) {
28
- xScale = scaleTime()
17
+ return scaleTime()
29
18
  .domain([min(xValues), max(xValues)])
30
19
  .range([0, dimensions.innerWidth])
31
20
  .nice()
32
- } else if (xIsNumeric) {
33
- xScale = scaleLinear()
21
+ }
22
+ if (xIsNumeric) {
23
+ return scaleLinear()
34
24
  .domain([min([0, ...xValues]), max(xValues)])
35
25
  .range([0, dimensions.innerWidth])
36
26
  .nice()
37
- } else {
38
- xScale = scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
39
27
  }
28
+ return scaleBand().domain(xValues).range([0, dimensions.innerWidth]).padding(padding)
29
+ }
40
30
 
41
- // Create y-scale
42
- const yValues = data.map((d) => d[yKey])
43
- const yScale = scaleLinear()
44
- .domain([0, max(yValues) * 1.1]) // Add 10% padding on top
45
- .nice()
46
- .range([dimensions.innerHeight, 0])
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
+ }
47
52
 
48
- // Create color scale if colorKey is provided
49
- let colorScale = null
50
- if (colorKey) {
51
- const uniqueCategories = [...new Set(data.map((d) => d[colorKey]))]
52
- colorScale = scaleOrdinal().domain(uniqueCategories).range(schemeCategory10)
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
53
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
54
74
 
55
- return { xScale, yScale, colorScale }
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
+ }
56
80
  }
57
81
 
58
82
  /**
59
83
  * Calculates the actual chart dimensions after applying margins
60
84
  *
61
- * @param {Object} dimensions Original dimensions
85
+ * @param {number} width
86
+ * @param {number} height
87
+ * @param {Object} margin
62
88
  * @returns {Object} Dimensions with calculated inner width and height
63
89
  */
64
90
  export function calculateChartDimensions(width, height, margin) {
@@ -81,6 +107,44 @@ export function getOriginValue(scale) {
81
107
  return scale.ticks ? scale(Math.max(0, min(scale.domain()))) : scale.range()[0]
82
108
  }
83
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
+
84
148
  /**
85
149
  * Creates axis ticks
86
150
  *
@@ -92,31 +156,17 @@ export function getOriginValue(scale) {
92
156
  */
93
157
  export function createTicks(scale, axis, count = null, fontSize = 12) {
94
158
  const [minRange, maxRange] = scale.range()
95
- let ticks = []
96
- let offset = 0
159
+ const tickCount = count ?? defaultTickCount(maxRange - minRange, axis, fontSize)
97
160
 
98
- // Calculate default count based on available space
99
- if (!count) {
100
- count = Math.abs((maxRange - minRange) / (fontSize * (axis === 'y' ? 3 : 6)))
101
- }
102
-
103
- // Get ticks based on scale type
161
+ let ticks, offset
104
162
  if (scale.ticks) {
105
- ticks = scale.ticks(Math.round(count))
163
+ ticks = scale.ticks(Math.round(tickCount))
164
+ offset = 0
106
165
  } else {
107
- offset = scale.bandwidth() / 2
108
- count = Math.min(Math.round(count), scale.domain().length)
109
-
110
- ticks = scale.domain()
111
- if (count < scale.domain().length) {
112
- const step = Math.ceil(scale.domain().length / count)
113
- ticks = ticks.filter((_, i) => i % step === 0)
114
- }
166
+ const band = bandTicks(scale, tickCount)
167
+ ticks = band.ticks
168
+ offset = band.offset
115
169
  }
116
170
 
117
- // Format ticks with positions
118
- return ticks.map((t) => ({
119
- value: t,
120
- position: scale(t) + (axis === 'x' ? offset : 0)
121
- }))
171
+ return formatTicks(ticks, scale, offset, axis === 'x')
122
172
  }
package/src/lib/utils.js CHANGED
@@ -6,16 +6,18 @@ import { max } from 'd3-array'
6
6
  * Creates appropriate scales based on data and dimensions
7
7
  *
8
8
  * @param {Array} data The dataset
9
- * @param {string} xKey Field to use for x-axis
10
- * @param {string} yKey Field to use for y-axis
11
9
  * @param {Object} dimensions Chart dimensions
12
10
  * @param {Object} [options] Additional options
11
+ * @param {string} options.xKey Field to use for x-axis
12
+ * @param {string} options.yKey Field to use for y-axis
13
13
  * @param {string} [options.colorKey] Field to use for color mapping
14
14
  * @returns {Object} Object containing xScale, yScale, and colorScale
15
15
  */
16
- export function createScales(data, xKey, yKey, dimensions, options = {}) {
16
+ export function createScales(data, dimensions, options = {}) {
17
17
  if (!data || data.length === 0) return {}
18
18
 
19
+ const { xKey, yKey, colorKey } = options
20
+
19
21
  const xScale = scaleBand()
20
22
  .domain(data.map((d) => d[xKey]))
21
23
  .range([0, dimensions.width])
@@ -28,8 +30,8 @@ export function createScales(data, xKey, yKey, dimensions, options = {}) {
28
30
 
29
31
  let colorScale = null
30
32
 
31
- if (options.colorKey) {
32
- const uniqueCategories = [...new Set(data.map((d) => d[options.colorKey]))]
33
+ if (colorKey) {
34
+ const uniqueCategories = [...new Set(data.map((d) => d[colorKey]))]
33
35
  colorScale = scaleOrdinal().domain(uniqueCategories).range(schemeCategory10)
34
36
  }
35
37
 
@@ -84,6 +86,18 @@ export function uniqueId(prefix = 'chart') {
84
86
  return `${prefix}-${Math.random().toString(36).substring(2, 10)}`
85
87
  }
86
88
 
89
+ /**
90
+ * Format a single key-value pair for tooltip
91
+ * @param {string} key
92
+ * @param {unknown} value
93
+ * @param {Function|undefined} formatter
94
+ * @returns {string}
95
+ */
96
+ function formatField(key, value, formatter) {
97
+ const formatted = formatter ? formatter(value) : value
98
+ return `${key}: ${formatted}`
99
+ }
100
+
87
101
  /**
88
102
  * Formats tooltip content for a data point
89
103
  *
@@ -97,13 +111,10 @@ export function formatTooltipContent(d, options = {}) {
97
111
  const { xKey, yKey, xFormat, yFormat } = options
98
112
 
99
113
  if (xKey && yKey) {
100
- const xValue = d[xKey]
101
- const yValue = d[yKey]
102
-
103
- const xFormatted = xFormat ? xFormat(xValue) : xValue
104
- const yFormatted = yFormat ? yFormat(yValue) : yValue
105
-
106
- return `${xKey}: ${xFormatted}<br>${yKey}: ${yFormatted}`
114
+ return [
115
+ formatField(xKey, d[xKey], xFormat),
116
+ formatField(yKey, d[yKey], yFormat)
117
+ ].join('<br>')
107
118
  }
108
119
 
109
120
  return Object.entries(d)