@inglorious/charts 1.0.1

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 (49) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +554 -0
  3. package/package.json +64 -0
  4. package/src/base.css +86 -0
  5. package/src/cartesian/area.js +392 -0
  6. package/src/cartesian/area.test.js +366 -0
  7. package/src/cartesian/bar.js +445 -0
  8. package/src/cartesian/bar.test.js +346 -0
  9. package/src/cartesian/line.js +823 -0
  10. package/src/cartesian/line.test.js +177 -0
  11. package/src/chart.test.js +444 -0
  12. package/src/component/brush.js +264 -0
  13. package/src/component/empty-state.js +33 -0
  14. package/src/component/empty-state.test.js +81 -0
  15. package/src/component/grid.js +123 -0
  16. package/src/component/grid.test.js +123 -0
  17. package/src/component/legend.js +76 -0
  18. package/src/component/legend.test.js +103 -0
  19. package/src/component/tooltip.js +65 -0
  20. package/src/component/tooltip.test.js +96 -0
  21. package/src/component/x-axis.js +212 -0
  22. package/src/component/x-axis.test.js +148 -0
  23. package/src/component/y-axis.js +77 -0
  24. package/src/component/y-axis.test.js +107 -0
  25. package/src/handlers.js +150 -0
  26. package/src/index.js +264 -0
  27. package/src/polar/donut.js +181 -0
  28. package/src/polar/donut.test.js +152 -0
  29. package/src/polar/pie.js +758 -0
  30. package/src/polar/pie.test.js +268 -0
  31. package/src/shape/curve.js +55 -0
  32. package/src/shape/dot.js +104 -0
  33. package/src/shape/rectangle.js +46 -0
  34. package/src/shape/sector.js +58 -0
  35. package/src/template.js +25 -0
  36. package/src/theme.css +90 -0
  37. package/src/utils/cartesian-layout.js +164 -0
  38. package/src/utils/chart-utils.js +30 -0
  39. package/src/utils/colors.js +77 -0
  40. package/src/utils/data-utils.js +155 -0
  41. package/src/utils/data-utils.test.js +210 -0
  42. package/src/utils/extract-data-keys.js +22 -0
  43. package/src/utils/padding.js +16 -0
  44. package/src/utils/paths.js +279 -0
  45. package/src/utils/process-declarative-child.js +46 -0
  46. package/src/utils/scales.js +250 -0
  47. package/src/utils/shared-context.js +166 -0
  48. package/src/utils/shared-context.test.js +237 -0
  49. package/src/utils/tooltip-handlers.js +129 -0
@@ -0,0 +1,279 @@
1
+ import {
2
+ arc,
3
+ area,
4
+ curveLinear,
5
+ curveMonotoneX,
6
+ line,
7
+ pie,
8
+ stack,
9
+ } from "d3-shape"
10
+
11
+ import { getDataPointX, getDataPointY } from "./data-utils.js"
12
+
13
+ // Fallback Y value when scale returns NaN
14
+ const FALLBACK_Y_VALUE = 0
15
+
16
+ export function generateLinePath(data, xScale, yScale, curveType = "linear") {
17
+ const curve = curveType === "monotone" ? curveMonotoneX : curveLinear
18
+ const lineGenerator = line()
19
+ .x((d, i) => {
20
+ // Always pass index as fallback to ensure we have a valid x value
21
+ const xVal = getDataPointX(d, i)
22
+ const scaled = xScale(xVal)
23
+ if (isNaN(scaled)) {
24
+ console.warn("[generateLinePath] NaN x value:", {
25
+ d,
26
+ i,
27
+ xVal,
28
+ scaled,
29
+ xScaleDomain: xScale.domain(),
30
+ xScaleRange: xScale.range(),
31
+ })
32
+ // Fallback to index if scaled is NaN
33
+ return xScale(i)
34
+ }
35
+ return scaled
36
+ })
37
+ .y((d) => {
38
+ const yVal = getDataPointY(d)
39
+ const scaled = yScale(yVal)
40
+ if (isNaN(scaled)) {
41
+ console.warn("[generateLinePath] NaN y value:", {
42
+ d,
43
+ yVal,
44
+ scaled,
45
+ yScaleDomain: yScale.domain(),
46
+ yScaleRange: yScale.range(),
47
+ })
48
+ // Fallback to 0 if scaled is NaN
49
+ return yScale(FALLBACK_Y_VALUE)
50
+ }
51
+ return scaled
52
+ })
53
+ .curve(curve)
54
+ return lineGenerator(data)
55
+ }
56
+
57
+ /* eslint-disable no-magic-numbers */
58
+ export function generateAreaPath(
59
+ data,
60
+ xScale,
61
+ yScale,
62
+ baseValue = 0,
63
+ curveType = "linear",
64
+ ) {
65
+ const curve = curveType === "monotone" ? curveMonotoneX : curveLinear
66
+ const areaGenerator = area()
67
+ .x((d, i) => {
68
+ // Always pass index as fallback to ensure we have a valid x value
69
+ const xVal = getDataPointX(d, i)
70
+ const scaled = xScale(xVal)
71
+ if (isNaN(scaled)) {
72
+ // Fallback to index if scaled is NaN
73
+ return xScale(i)
74
+ }
75
+ return scaled
76
+ })
77
+ .y0(() => yScale(baseValue))
78
+ .y1((d) => {
79
+ const yVal = getDataPointY(d)
80
+ const scaled = yScale(yVal)
81
+ if (isNaN(scaled)) {
82
+ // Fallback to 0 if scaled is NaN
83
+ return yScale(0)
84
+ }
85
+ return scaled
86
+ })
87
+ .curve(curve)
88
+ return areaGenerator(data)
89
+ }
90
+
91
+ /**
92
+ * Generate stacked area path with dynamic y0 and y1 values
93
+ * @param {any[]} data - Data points
94
+ * @param {import('d3-scale').ScaleLinear|import('d3-scale').ScaleTime} xScale - X scale
95
+ * @param {import('d3-scale').ScaleLinear} yScale - Y scale
96
+ * @param {Array<[number, number]>} stackedData - Array of [y0, y1] tuples for each data point
97
+ * @param {string} curveType - Curve type ("linear" or "monotone")
98
+ * @returns {string} SVG path string
99
+ */
100
+ export function generateStackedAreaPath(
101
+ data,
102
+ xScale,
103
+ yScale,
104
+ stackedData,
105
+ curveType = "linear",
106
+ ) {
107
+ const curve = curveType === "monotone" ? curveMonotoneX : curveLinear
108
+ const areaGenerator = area()
109
+ .x((d) => xScale(getDataPointX(d, null)))
110
+ .y0((d, i) => yScale(stackedData[i]?.[0] ?? 0))
111
+ .y1((d, i) => yScale(stackedData[i]?.[1] ?? 0))
112
+ .curve(curve)
113
+ return areaGenerator(data)
114
+ }
115
+
116
+ /**
117
+ * Calculate stacked data for multiple series
118
+ * Returns array where each element is an array of [y0, y1] tuples for each data point
119
+ * @param {any[]} seriesData - Array of series objects with values arrays
120
+ * @param {Function} valueAccessor - Function to extract value from data point
121
+ * @returns {Array<Array<[number, number]>>} Stacked data for each series
122
+ */
123
+ export function calculateStackedData(
124
+ seriesData,
125
+ valueAccessor = (d) => getDataPointY(d),
126
+ ) {
127
+ if (!seriesData || seriesData.length === 0) {
128
+ return []
129
+ }
130
+
131
+ // Get all series values
132
+ const allSeriesValues = seriesData.map((series) =>
133
+ Array.isArray(series.values) ? series.values : [series],
134
+ )
135
+
136
+ // Find maximum length to handle different series lengths
137
+ const maxLength = Math.max(...allSeriesValues.map((s) => s.length))
138
+
139
+ // Create data structure for d3-shape stack
140
+ // Each entry represents one x position with values from all series
141
+ const data = []
142
+ for (let i = 0; i < maxLength; i++) {
143
+ const entry = {}
144
+ allSeriesValues.forEach((values, seriesIndex) => {
145
+ const point = values[i]
146
+ entry[seriesIndex] = point ? valueAccessor(point) : 0
147
+ })
148
+ data.push(entry)
149
+ }
150
+
151
+ // Create stack generator
152
+ const keys = seriesData.map((_, i) => i)
153
+ const stackGenerator = stack()
154
+ .keys(keys)
155
+ .value((d, key) => d[key] ?? 0)
156
+
157
+ // Generate stacked data
158
+ const stacked = stackGenerator(data)
159
+
160
+ // Transform to array of [y0, y1] tuples for each series
161
+ return stacked.map((series) => series.map((point) => [point[0], point[1]]))
162
+ }
163
+
164
+ /**
165
+ * Calculates pie data with support for startAngle, endAngle, paddingAngle, and minAngle
166
+ * @param {any[]} data - The data array
167
+ * @param {Function} valueAccessor - Function to extract value from data (default: d => d.value)
168
+ * @param {number} startAngle - Start angle in degrees (default: 0)
169
+ * @param {number} endAngle - End angle in degrees (default: 360)
170
+ * @param {number} paddingAngle - Padding angle between sectors in degrees (default: 0)
171
+ * @param {number} minAngle - Minimum angle for each sector in degrees (default: 0)
172
+ * @returns {any[]} Pie data with calculated angles
173
+ */
174
+ export function calculatePieData(
175
+ data,
176
+ valueAccessor = (d) => d.value,
177
+ startAngle = 0,
178
+ endAngle = 360,
179
+ paddingAngle = 0,
180
+ minAngle = 0,
181
+ ) {
182
+ // Calculate total angle range
183
+ const deltaAngle = endAngle - startAngle
184
+ const absDeltaAngle = Math.abs(deltaAngle)
185
+ const sign = Math.sign(deltaAngle) || 1
186
+
187
+ // Count non-zero items
188
+ const notZeroItemCount = data.filter(
189
+ (entry) => valueAccessor(entry) !== 0,
190
+ ).length
191
+
192
+ // Calculate total padding angle
193
+ const totalPaddingAngle =
194
+ absDeltaAngle >= 360
195
+ ? notZeroItemCount * paddingAngle
196
+ : (notZeroItemCount - 1) * paddingAngle
197
+
198
+ // Calculate real total angle (subtract minAngle for each non-zero item and padding)
199
+ const minAngleRad = Math.abs(minAngle) * (Math.PI / 180)
200
+ const realTotalAngle =
201
+ absDeltaAngle * (Math.PI / 180) -
202
+ notZeroItemCount * minAngleRad -
203
+ totalPaddingAngle * (Math.PI / 180)
204
+
205
+ // Calculate sum of values
206
+ const sum = data.reduce((result, entry) => {
207
+ const val = valueAccessor(entry)
208
+ return result + (typeof val === "number" ? val : 0)
209
+ }, 0)
210
+
211
+ // Build pie data manually to apply minAngle correctly
212
+ if (sum > 0) {
213
+ const startAngleRad = (startAngle - 90) * (Math.PI / 180) // Offset by -90° (SVG starts at top)
214
+ const paddingAngleRad = paddingAngle * (Math.PI / 180)
215
+
216
+ let currentAngle = startAngleRad
217
+
218
+ return data.map((entry, i) => {
219
+ const val = valueAccessor(entry)
220
+ const isZero = val === 0 || typeof val !== "number"
221
+
222
+ const percent = isZero ? 0 : val / sum
223
+ const sliceAngle = isZero ? 0 : minAngleRad + percent * realTotalAngle
224
+
225
+ const sliceStartAngle = currentAngle
226
+ const sliceEndAngle = currentAngle + sign * sliceAngle
227
+
228
+ // Move to next slice (add padding if not last)
229
+ if (!isZero && i < data.length - 1) {
230
+ currentAngle = sliceEndAngle + sign * paddingAngleRad
231
+ } else {
232
+ currentAngle = sliceEndAngle
233
+ }
234
+
235
+ return {
236
+ data: entry,
237
+ value: isZero ? 0 : val,
238
+ index: i,
239
+ startAngle: sliceStartAngle,
240
+ endAngle: sliceEndAngle,
241
+ padAngle: isZero ? 0 : paddingAngleRad,
242
+ }
243
+ })
244
+ }
245
+
246
+ // Fallback to d3-shape if no data or sum is 0
247
+ const pieGenerator = pie()
248
+ .value(valueAccessor)
249
+ .sort(null)
250
+ .startAngle((startAngle - 90) * (Math.PI / 180))
251
+ .endAngle((endAngle - 90) * (Math.PI / 180))
252
+ .padAngle(paddingAngle * (Math.PI / 180))
253
+
254
+ return pieGenerator(data)
255
+ }
256
+
257
+ /**
258
+ * Generates SVG arc path with support for cornerRadius
259
+ * @param {number} innerRadius - Inner radius
260
+ * @param {number} outerRadius - Outer radius
261
+ * @param {number} startAngle - Start angle in radians
262
+ * @param {number} endAngle - End angle in radians
263
+ * @param {number} cornerRadius - Corner radius for rounded edges (default: 0)
264
+ * @returns {string} SVG path string
265
+ */
266
+ export function generateArcPath(
267
+ innerRadius,
268
+ outerRadius,
269
+ startAngle,
270
+ endAngle,
271
+ cornerRadius = 0,
272
+ ) {
273
+ return arc()
274
+ .innerRadius(innerRadius)
275
+ .outerRadius(outerRadius)
276
+ .startAngle(startAngle)
277
+ .endAngle(endAngle)
278
+ .cornerRadius(cornerRadius)()
279
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Processes declarative child objects (intention objects) into rendered functions
3
+ * Converts { type: 'XAxis', config } into a rendered function
4
+ *
5
+ * @param {Object} child - Child object with type and config properties
6
+ * @param {Object} entity - Chart entity
7
+ * @param {string} chartTypeName - Chart type name
8
+ * @param {Object} api - Web API instance
9
+ * @returns {Function|null}
10
+ */
11
+ export function processDeclarativeChild(child, entity, chartTypeName, api) {
12
+ if (
13
+ child &&
14
+ typeof child === "object" &&
15
+ child.type &&
16
+ child.config !== undefined
17
+ ) {
18
+ const chartType = api.getType(chartTypeName)
19
+ const methodName = `render${child.type}`
20
+
21
+ if (chartType?.[methodName]) {
22
+ // Execute the render method to get the actual component function (e.g., brushFn)
23
+ const rendered = chartType[methodName](
24
+ entity,
25
+ { config: child.config },
26
+ api,
27
+ )
28
+
29
+ // Inject flag based on the declarative object TYPE
30
+ // This ensures the returned function has the correct flag for identification
31
+ if (typeof rendered === "function") {
32
+ if (child.type === "Brush") rendered.isBrush = true
33
+ if (child.type === "XAxis" || child.type === "YAxis")
34
+ rendered.isAxis = true
35
+ if (child.type === "CartesianGrid") rendered.isGrid = true
36
+ if (child.type === "Line") rendered.isLine = true
37
+ if (child.type === "Dots") rendered.isDots = true
38
+ if (child.type === "Tooltip") rendered.isTooltip = true
39
+ if (child.type === "Legend") rendered.isLegend = true
40
+ }
41
+
42
+ return rendered
43
+ }
44
+ }
45
+ return child
46
+ }
@@ -0,0 +1,250 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ import { extent } from "d3-array"
4
+ import { scaleBand, scaleLinear, scaleTime } from "d3-scale"
5
+
6
+ import { getDataPointX, getDataPointY, isMultiSeries } from "./data-utils.js"
7
+
8
+ export function createYScale(data, height, padding, isStacked = false) {
9
+ let values
10
+ if (isMultiSeries(data)) {
11
+ if (isStacked) {
12
+ // For stacked areas, calculate the sum of all series at each point
13
+ const allSeriesValues = data.map((series) =>
14
+ Array.isArray(series.values) ? series.values : [series],
15
+ )
16
+ const maxLength = Math.max(...allSeriesValues.map((s) => s.length))
17
+
18
+ // Calculate stacked sums for each x position
19
+ values = []
20
+ for (let i = 0; i < maxLength; i++) {
21
+ let sum = 0
22
+ allSeriesValues.forEach((seriesValues) => {
23
+ const point = seriesValues[i]
24
+ if (point) {
25
+ sum += point.y ?? point.value ?? 0
26
+ }
27
+ })
28
+ values.push(sum)
29
+ }
30
+ } else {
31
+ // For non-stacked (line charts), use all individual values
32
+ values = data.flatMap((series) =>
33
+ Array.isArray(series.values)
34
+ ? series.values.map((v) => v.y ?? v.value ?? 0)
35
+ : [series.y ?? series.value ?? 0],
36
+ )
37
+ }
38
+ } else {
39
+ // Single series: extract directly
40
+ values = data.map((d) => getDataPointY(d))
41
+ }
42
+
43
+ const [minVal, maxVal] = extent(values)
44
+ // Always start at 0 to avoid visual distortions
45
+ const min = Math.min(0, minVal ?? 0)
46
+ const max = maxVal ?? 100
47
+ return scaleLinear()
48
+ .domain([min, max])
49
+ .nice() // Make domain "nice" (round to nice numbers like 1000 instead of 980)
50
+ .range([height - padding.bottom, padding.top])
51
+ }
52
+
53
+ export function createXScale(data, width, padding) {
54
+ // Handle multi-series data (array of series with values)
55
+ let values
56
+ if (isMultiSeries(data)) {
57
+ // Extract all x values from all series
58
+ values = data.flatMap((series) =>
59
+ series.values ? series.values.map((v) => v.x ?? v.date) : [],
60
+ )
61
+ } else {
62
+ // Single series: extract directly
63
+ // getDataPointX returns d.x ?? d.date ?? fallback (index)
64
+ // So if data has no x/date, it will use index as fallback
65
+ values = data.map((d, i) => {
66
+ const xVal = getDataPointX(d, i)
67
+ // If getDataPointX returns the fallback (index), ensure it's a valid number
68
+ return xVal != null && !isNaN(xVal) ? xVal : i
69
+ })
70
+ }
71
+
72
+ // Filter out null/undefined values and ensure we have valid numbers
73
+ const validValues = values.filter((v) => v != null && !isNaN(v))
74
+
75
+ if (validValues.length === 0) {
76
+ // If no valid values found, use index-based domain
77
+ // This happens when data has no x/date fields and getDataPointX fails
78
+ return scaleLinear()
79
+ .domain([0, Math.max(0, data.length - 1)])
80
+ .range([padding.left, width - padding.right])
81
+ }
82
+
83
+ const [minVal, maxVal] = extent(validValues)
84
+ if (minVal == null || maxVal == null || isNaN(minVal) || isNaN(maxVal)) {
85
+ console.warn(
86
+ "[createXScale] Invalid extent, using default domain [0, data.length-1]",
87
+ { minVal, maxVal, values, validValues },
88
+ )
89
+ return scaleLinear()
90
+ .domain([0, data.length - 1])
91
+ .range([padding.left, width - padding.right])
92
+ }
93
+
94
+ return scaleLinear()
95
+ .domain([minVal, maxVal])
96
+ .range([padding.left, width - padding.right])
97
+ }
98
+
99
+ export function createTimeScale(data, width, padding) {
100
+ const dates = data.map((d) => new Date(d.date ?? d.x))
101
+ const [minDate, maxDate] = extent(dates)
102
+ return scaleTime()
103
+ .domain([minDate, maxDate])
104
+ .range([padding.left, width - padding.right])
105
+ }
106
+
107
+ export function createOrdinalScale(categories, width, padding) {
108
+ return scaleBand()
109
+ .domain(categories)
110
+ .range([padding.left, width - padding.right])
111
+ .padding(0.1)
112
+ }
113
+
114
+ /**
115
+ * Get filtered data based on brush state
116
+ * @param {any} entity - Chart entity
117
+ * @returns {any[]} Filtered data array
118
+ */
119
+ export function getFilteredData(entity) {
120
+ if (!entity.brush?.enabled || !entity.data) {
121
+ return entity.data
122
+ }
123
+
124
+ const startIndex = entity.brush.startIndex ?? 0
125
+ const endIndex = entity.brush.endIndex ?? entity.data.length - 1
126
+
127
+ return entity.data.slice(startIndex, endIndex + 1)
128
+ }
129
+
130
+ /**
131
+ * Helper to create scales based on chart type
132
+ * Returns { xScale, yScale }
133
+ */
134
+ export function createScales(entity, chartType) {
135
+ // Use filtered data if brush is enabled
136
+ const isZoomable = chartType === "line" || chartType === "area"
137
+ const dataForScales = isZoomable ? entity.data : getFilteredData(entity)
138
+
139
+ // Area charts use stacked scale only if entity.stacked is true
140
+ // Default to false (non-stacked) for area charts
141
+ const isStacked = chartType === "area" && entity.stacked === true
142
+ const yScale = createYScale(
143
+ dataForScales,
144
+ entity.height,
145
+ entity.padding,
146
+ isStacked,
147
+ )
148
+
149
+ let xScale
150
+ if (chartType === "bar") {
151
+ const categories = dataForScales.map((d) => d.label || d.name || d.category)
152
+ xScale = createOrdinalScale(categories, entity.width, entity.padding)
153
+ } else {
154
+ // Line chart or others that use linear/time scale
155
+ xScale =
156
+ entity.xAxisType === "time"
157
+ ? createTimeScale(dataForScales, entity.width, entity.padding)
158
+ : createXScale(dataForScales, entity.width, entity.padding)
159
+ }
160
+
161
+ return { xScale, yScale }
162
+ }
163
+
164
+ /**
165
+ * @typedef {Object} CartesianContext
166
+ * @property {import('d3-scale').ScaleBand|import('d3-scale').ScaleLinear|import('d3-scale').ScaleTime} xScale
167
+ * @property {import('d3-scale').ScaleLinear} yScale
168
+ * @property {Object} dimensions
169
+ * @property {number} dimensions.width
170
+ * @property {number} dimensions.height
171
+ * @property {Object} dimensions.padding
172
+ * @property {any} entity
173
+ */
174
+
175
+ /**
176
+ * Create cartesian context with scales, dimensions, and entity
177
+ * @param {any} entity
178
+ * @param {string} chartType
179
+ * @returns {CartesianContext}
180
+ */
181
+ export function createCartesianContext(entity, chartType) {
182
+ const { xScale, yScale } = createScales(entity, chartType)
183
+
184
+ return {
185
+ xScale,
186
+ yScale,
187
+ dimensions: {
188
+ width: entity.width,
189
+ height: entity.height,
190
+ padding: entity.padding,
191
+ },
192
+ entity,
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Calculate X-axis ticks based on data
198
+ * For few data points (≤15), shows all actual x values
199
+ * For many data points, uses scale ticks
200
+ *
201
+ * @param {any[]} data - Chart data
202
+ * @param {import('d3-scale').ScaleLinear|import('d3-scale').ScaleTime} xScale - X scale
203
+ * @returns {number[]|Date[]} Array of tick values
204
+ */
205
+ export function calculateXTicks(data, xScale) {
206
+ if (!data || data.length === 0) {
207
+ return xScale.ticks ? xScale.ticks(5) : xScale.domain()
208
+ }
209
+
210
+ // Extract all unique x values from data using Set for O(n) performance
211
+ // This avoids O(n²) complexity from includes() in a loop
212
+ const uniqueXValues = new Set()
213
+ if (isMultiSeries(data)) {
214
+ // Multi-series: extract from values arrays
215
+ data.forEach((series) => {
216
+ if (series.values) {
217
+ series.values.forEach((v) => {
218
+ const xVal = v.x ?? v.date
219
+ if (xVal != null) {
220
+ uniqueXValues.add(xVal)
221
+ }
222
+ })
223
+ }
224
+ })
225
+ } else {
226
+ // Single series: extract directly
227
+ // Use index as fallback when x/date is not present (for categorical data)
228
+ data.forEach((d, i) => {
229
+ const xVal = getDataPointX(d, i)
230
+ if (xVal != null) {
231
+ uniqueXValues.add(xVal)
232
+ }
233
+ })
234
+ }
235
+
236
+ // Convert Set to sorted array
237
+ const allXValues = Array.from(uniqueXValues).sort((a, b) => a - b)
238
+
239
+ // If we have few data points (≤15), show all actual x values
240
+ // Otherwise use D3's automatic tick calculation (like Recharts)
241
+ if (xScale.ticks && allXValues.length <= 15) {
242
+ return allXValues
243
+ }
244
+ if (xScale.ticks) {
245
+ // Let D3 automatically choose optimal ticks (default behavior, like Recharts)
246
+ // D3 will choose nice intervals automatically based on the scale
247
+ return xScale.ticks()
248
+ }
249
+ return xScale.domain()
250
+ }