@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,164 @@
1
+ /**
2
+ * Cartesian layout utilities
3
+ * Common layout logic for cartesian charts (area, line, bar)
4
+ */
5
+
6
+ import { html, svg } from "@inglorious/web"
7
+
8
+ import { renderBrush } from "../component/brush.js"
9
+ import { renderEmptyState } from "../component/empty-state.js"
10
+ import { renderGrid } from "../component/grid.js"
11
+ import { renderLegend } from "../component/legend.js"
12
+ import { renderTooltip } from "../component/tooltip.js"
13
+ import { renderXAxis } from "../component/x-axis.js"
14
+ import { renderYAxis } from "../component/y-axis.js"
15
+ import { isMultiSeries } from "./data-utils.js"
16
+ import { createCartesianContext } from "./scales.js"
17
+ import { createTooltipMoveHandler } from "./tooltip-handlers.js"
18
+
19
+ /**
20
+ * Renders the common cartesian chart layout
21
+ * @param {any} entity - Chart entity
22
+ * @param {Object} props
23
+ * @param {string} props.chartType - Chart type ("area", "line", "bar")
24
+ * @param {import('lit-html').TemplateResult} props.chartContent - Chart-specific content (areas, lines, bars)
25
+ * @param {boolean} [props.showLegend] - Whether to show legend (defaults to entity.showLegend for multi-series)
26
+ * @param {import('@inglorious/web').Api} api - Web API instance
27
+ * @returns {import('lit-html').TemplateResult} Complete chart HTML
28
+ */
29
+ export function renderCartesianLayout(entity, props, api) {
30
+ const { chartType, chartContent, showLegend = undefined } = props || {}
31
+ // Check for empty state
32
+ // eslint-disable-next-line no-magic-numbers
33
+ if (!entity.data || entity.data.length === 0) {
34
+ return html`
35
+ <div class="iw-chart">
36
+ ${renderEmptyState(
37
+ entity,
38
+ {
39
+ width: entity.width,
40
+ height: entity.height,
41
+ },
42
+ api,
43
+ )}
44
+ </div>
45
+ `
46
+ }
47
+
48
+ // Check if brush is enabled and adjust height accordingly
49
+ // eslint-disable-next-line no-magic-numbers
50
+ const brushHeight = entity.brush?.enabled ? 60 : 0
51
+ // eslint-disable-next-line no-magic-numbers
52
+ const chartHeight = entity.height || 400
53
+ const totalHeight = chartHeight + brushHeight
54
+
55
+ // Create context with scales and dimensions
56
+ const context = createCartesianContext(entity, chartType)
57
+ let { xScale, yScale, dimensions } = context
58
+ const { width, height, padding } = dimensions
59
+
60
+ // Apply zoom if brush is enabled (similar to composition mode)
61
+ if (entity.brush?.enabled && entity.brush.startIndex !== undefined) {
62
+ const { startIndex, endIndex } = entity.brush
63
+ xScale = xScale.copy().domain([startIndex, endIndex])
64
+ }
65
+
66
+ // Independent components - declarative composition
67
+ const grid = entity.showGrid
68
+ ? renderGrid(
69
+ entity,
70
+ {
71
+ xScale,
72
+ yScale,
73
+ width,
74
+ height,
75
+ padding,
76
+ },
77
+ api,
78
+ )
79
+ : svg``
80
+
81
+ const xAxis = renderXAxis(
82
+ entity,
83
+ {
84
+ xScale,
85
+ yScale,
86
+ width,
87
+ height,
88
+ padding,
89
+ },
90
+ api,
91
+ )
92
+
93
+ const yAxis = renderYAxis(
94
+ entity,
95
+ {
96
+ yScale,
97
+ height,
98
+ padding,
99
+ },
100
+ api,
101
+ )
102
+
103
+ // Legend - only for multiple series
104
+ const shouldShowLegend =
105
+ showLegend !== undefined
106
+ ? showLegend
107
+ : isMultiSeries(entity.data) && entity.showLegend
108
+ const legend = shouldShowLegend
109
+ ? renderLegend(
110
+ entity,
111
+ {
112
+ series: entity.data,
113
+ colors: entity.colors,
114
+ width,
115
+ padding,
116
+ },
117
+ api,
118
+ )
119
+ : svg``
120
+
121
+ // Brush - render if enabled in config mode
122
+ const brush = entity.brush?.enabled
123
+ ? renderBrush(
124
+ entity,
125
+ {
126
+ xScale,
127
+ width,
128
+ height,
129
+ padding,
130
+ // eslint-disable-next-line no-magic-numbers
131
+ brushHeight: entity.brush.height || 30,
132
+ dataKey: entity.dataKey || "name",
133
+ },
134
+ api,
135
+ )
136
+ : svg``
137
+
138
+ // SVG container
139
+ const svgContent = svg`
140
+ <svg
141
+ width=${width}
142
+ height=${totalHeight}
143
+ viewBox="0 0 ${width} ${totalHeight}"
144
+ class="iw-chart-svg"
145
+ @mousemove=${createTooltipMoveHandler({ entity, api })}
146
+ >
147
+ ${grid}
148
+ ${xAxis}
149
+ ${yAxis}
150
+ ${chartContent}
151
+ ${legend}
152
+ ${brush}
153
+ </svg>
154
+ `
155
+
156
+ return html`
157
+ <div
158
+ class="iw-chart"
159
+ style="display: block; margin: 0; padding: 0; position: relative; width: 100%; box-sizing: border-box;"
160
+ >
161
+ ${svgContent} ${renderTooltip(entity, {}, api)}
162
+ </div>
163
+ `
164
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Chart utilities
3
+ * Functions for chart creation and configuration
4
+ */
5
+
6
+ import { area } from "../cartesian/area.js"
7
+ import { bar } from "../cartesian/bar.js"
8
+ import { line } from "../cartesian/line.js"
9
+ import * as handlers from "../handlers.js"
10
+ import { donut } from "../polar/donut.js"
11
+ import { pie } from "../polar/pie.js"
12
+ import { render } from "../template.js"
13
+
14
+ export const areaChart = combineRenderer(area)
15
+ export const barChart = combineRenderer(bar)
16
+ export const lineChart = combineRenderer(line)
17
+ export const pieChart = combineRenderer(pie)
18
+ export const donutChart = combineRenderer(donut)
19
+
20
+ // Export chart object for declarative helpers (composition style)
21
+ export { chart } from "../index.js"
22
+
23
+ /**
24
+ * Combines handlers, template, and chart-specific renderer into a complete chart type.
25
+ * @param {Object} chartRenderer - Chart-specific renderer (e.g., { renderChart })
26
+ * @returns {Object} Combined chart object with handlers, template, and renderer methods
27
+ */
28
+ function combineRenderer(chartRenderer) {
29
+ return { ...handlers, render, ...chartRenderer }
30
+ }
@@ -0,0 +1,77 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ const DEFAULT_COLORS = ["#3b82f6", "#ef4444", "#10b981", "#f59e0b", "#8b5cf6"]
4
+
5
+ const EXTENDED_COLORS = [
6
+ "#3b82f6",
7
+ "#ef4444",
8
+ "#10b981",
9
+ "#f59e0b",
10
+ "#8b5cf6",
11
+ "#ec4899",
12
+ "#06b6d4",
13
+ "#84cc16",
14
+ "#f97316",
15
+ "#6366f1",
16
+ "#14b8a6",
17
+ "#a855f7",
18
+ "#eab308",
19
+ "#22c55e",
20
+ "#3b82f6",
21
+ "#64748b",
22
+ "#f43f5e",
23
+ "#8b5cf6",
24
+ "#0ea5e9",
25
+ "#06b6d4",
26
+ "#10b981",
27
+ "#f59e0b",
28
+ "#ef4444",
29
+ "#6366f1",
30
+ "#ec4899",
31
+ "#14b8a6",
32
+ ]
33
+
34
+ /**
35
+ * @param {number} count
36
+ * @param {string[]} [customColors]
37
+ * @returns {string[]}
38
+ */
39
+ export function generateColors(count, customColors = null) {
40
+ const baseColors = customColors || EXTENDED_COLORS
41
+
42
+ if (count <= baseColors.length) {
43
+ return baseColors.slice(0, count)
44
+ }
45
+
46
+ const colors = [...baseColors]
47
+ const needed = count - baseColors.length
48
+
49
+ for (let i = 0; i < needed; i++) {
50
+ const baseIndex = i % baseColors.length
51
+ const baseColor = baseColors[baseIndex]
52
+ const variation = Math.floor(i / baseColors.length) + 1
53
+
54
+ const hex = baseColor.replace("#", "")
55
+ const r = parseInt(hex.substr(0, 2), 16)
56
+ const g = parseInt(hex.substr(2, 2), 16)
57
+ const b = parseInt(hex.substr(4, 2), 16)
58
+
59
+ const factor = 0.7 + variation * 0.1
60
+ const newR = Math.min(255, Math.floor(r * factor))
61
+ const newG = Math.min(255, Math.floor(g * factor))
62
+ const newB = Math.min(255, Math.floor(b * factor))
63
+
64
+ colors.push(
65
+ `#${newR.toString(16).padStart(2, "0")}${newG.toString(16).padStart(2, "0")}${newB.toString(16).padStart(2, "0")}`,
66
+ )
67
+ }
68
+
69
+ return colors
70
+ }
71
+
72
+ /**
73
+ * @returns {string[]}
74
+ */
75
+ export function getDefaultColors() {
76
+ return [...DEFAULT_COLORS]
77
+ }
@@ -0,0 +1,155 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ /**
4
+ * Data utilities
5
+ * Functions for data manipulation and formatting
6
+ */
7
+
8
+ import { format } from "d3-format"
9
+ import { timeFormat } from "d3-time-format"
10
+
11
+ /**
12
+ * Format a number with the specified format
13
+ * @param {number} value - Number to format
14
+ * @param {string} [fmt] - Format string (d3-format syntax). If not provided, uses smart formatting (integers without decimals)
15
+ * @returns {string} Formatted number string
16
+ */
17
+ export function formatNumber(value, fmt) {
18
+ if (fmt) {
19
+ return format(fmt)(value)
20
+ }
21
+ // Smart formatting: if integer, show without decimals; otherwise show 2 decimals
22
+ if (Number.isInteger(value)) {
23
+ return format(",")(value) // No decimals for integers
24
+ }
25
+ return format(",.2f")(value) // 2 decimals for non-integers
26
+ }
27
+
28
+ /**
29
+ * Format a date with the specified format
30
+ * @param {Date|string|number} date - Date to format
31
+ * @param {string} [fmt="%Y-%m-%d"] - Format string (d3-time-format syntax)
32
+ * @returns {string} Formatted date string
33
+ */
34
+ export function formatDate(date, fmt = "%Y-%m-%d") {
35
+ return timeFormat(fmt)(new Date(date))
36
+ }
37
+
38
+ /**
39
+ * Gets values from a series object
40
+ * @param {any} series - Series object that may have a values array or be a single value
41
+ * @returns {any[]} Array of values
42
+ */
43
+ export function getSeriesValues(series) {
44
+ return Array.isArray(series.values) ? series.values : [series]
45
+ }
46
+
47
+ /**
48
+ * Checks if the data represents multiple series
49
+ * @param {any[]} data - Chart data array
50
+ * @returns {boolean} True if data represents multiple series (has values array)
51
+ */
52
+ export function isMultiSeries(data) {
53
+ return (
54
+ Array.isArray(data) && data.length > 0 && Array.isArray(data[0]?.values)
55
+ )
56
+ }
57
+
58
+ /**
59
+ * Gets the X coordinate value from a data point
60
+ * @param {any} d - Data point object
61
+ * @param {any} [fallback=0] - Fallback value if x/date is not found
62
+ * @returns {any} X coordinate value (x, date, or fallback)
63
+ */
64
+ export function getDataPointX(d, fallback = 0) {
65
+ return d?.x ?? d?.date ?? fallback
66
+ }
67
+
68
+ /**
69
+ * Gets the Y coordinate value from a data point
70
+ * @param {any} d - Data point object
71
+ * @param {any} [fallback=0] - Fallback value if y/value is not found
72
+ * @returns {any} Y coordinate value (y, value, or fallback)
73
+ */
74
+ export function getDataPointY(d, fallback = 0) {
75
+ return d?.y ?? d?.value ?? fallback
76
+ }
77
+
78
+ /**
79
+ * Gets the label from a data point
80
+ * @param {any} d - Data point object
81
+ * @param {string} [fallback="Value"] - Fallback label if x/date is not found
82
+ * @returns {string} Label value (x, date, or fallback)
83
+ */
84
+ export function getDataPointLabel(d, fallback = "Value") {
85
+ return d?.x ?? d?.date ?? fallback
86
+ }
87
+
88
+ /**
89
+ * Checks if a value is a valid number (not NaN and finite)
90
+ * @param {any} value - Value to check
91
+ * @returns {boolean} True if value is a valid number
92
+ */
93
+ export function isValidNumber(value) {
94
+ return typeof value === "number" && !isNaN(value) && isFinite(value)
95
+ }
96
+
97
+ /**
98
+ * Ensures a value is a valid number, returning a fallback if not
99
+ * @param {any} value - Value to validate
100
+ * @param {number} [fallback=0] - Fallback value if validation fails
101
+ * @returns {number} Valid number value
102
+ */
103
+ export function ensureValidNumber(value, fallback = 0) {
104
+ return isValidNumber(value) ? value : fallback
105
+ }
106
+
107
+ /**
108
+ * Transforms entity data to standardized format with x, y, and name properties.
109
+ * Used for rendering lines and areas in composition mode.
110
+ *
111
+ * @param {any} entity - Chart entity with data
112
+ * @param {string} dataKey - Data key to extract values from
113
+ * @returns {Array|null} Transformed data array or null if entity/data is invalid
114
+ */
115
+ export function getTransformedData(entity, dataKey) {
116
+ if (!entity || !entity.data) return null
117
+
118
+ // Transform data to use indices for x (like Recharts does with categorical data)
119
+ // Ensure y is always a valid number (handles negatives, NaN, etc.)
120
+ return entity.data.map((d, i) => {
121
+ const yValue =
122
+ d[dataKey] !== undefined
123
+ ? d[dataKey]
124
+ : d.y !== undefined
125
+ ? d.y
126
+ : d.value !== undefined
127
+ ? d.value
128
+ : 0
129
+ const y = typeof yValue === "number" && !isNaN(yValue) ? yValue : 0
130
+
131
+ return {
132
+ x: i, // Use index for positioning
133
+ y,
134
+ name: d[dataKey] || d.name || d.x || d.date || i, // Keep name for labels
135
+ }
136
+ })
137
+ }
138
+
139
+ /**
140
+ * Parses a dimension value (width/height) from config or entity
141
+ * Returns numeric value if possible, undefined otherwise
142
+ * @param {number|string|undefined} value - Dimension value to parse
143
+ * @returns {number|undefined} Parsed numeric value or undefined
144
+ */
145
+ export function parseDimension(value) {
146
+ if (typeof value === "number") return value
147
+ if (typeof value === "string") {
148
+ // Try to parse as number (e.g., "800" -> 800)
149
+ const num = parseFloat(value)
150
+ if (!isNaN(num) && !value.includes("%") && !value.includes("px")) {
151
+ return num
152
+ }
153
+ }
154
+ return undefined
155
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { describe, expect, it } from "vitest"
5
+
6
+ import {
7
+ ensureValidNumber,
8
+ formatDate,
9
+ formatNumber,
10
+ getDataPointLabel,
11
+ getDataPointX,
12
+ getDataPointY,
13
+ getSeriesValues,
14
+ getTransformedData,
15
+ isMultiSeries,
16
+ isValidNumber,
17
+ parseDimension,
18
+ } from "./data-utils.js"
19
+
20
+ describe("data-utils", () => {
21
+ describe("formatNumber", () => {
22
+ it("should format number with default format", () => {
23
+ expect(formatNumber(1234.56)).toBe("1,234.56")
24
+ })
25
+
26
+ it("should format number with custom format", () => {
27
+ expect(formatNumber(1234.56, ".0f")).toBe("1235")
28
+ })
29
+ })
30
+
31
+ describe("formatDate", () => {
32
+ it("should format date with default format", () => {
33
+ const date = new Date("2024-01-15")
34
+ const result = formatDate(date)
35
+ expect(result).toContain("2024")
36
+ })
37
+
38
+ it("should format date with custom format", () => {
39
+ const date = new Date("2024-01-15")
40
+ const result = formatDate(date, "%Y-%m-%d")
41
+ expect(result).toBe("2024-01-15")
42
+ })
43
+ })
44
+
45
+ describe("getSeriesValues", () => {
46
+ it("should return values array if present", () => {
47
+ const series = { name: "Series A", values: [{ x: 0, y: 10 }] }
48
+ expect(getSeriesValues(series)).toEqual([{ x: 0, y: 10 }])
49
+ })
50
+
51
+ it("should wrap single value in array", () => {
52
+ const series = { x: 0, y: 10 }
53
+ expect(getSeriesValues(series)).toEqual([{ x: 0, y: 10 }])
54
+ })
55
+ })
56
+
57
+ describe("isMultiSeries", () => {
58
+ it("should return true for multi-series data", () => {
59
+ const data = [
60
+ { name: "Series A", values: [{ x: 0, y: 10 }] },
61
+ { name: "Series B", values: [{ x: 0, y: 20 }] },
62
+ ]
63
+ expect(isMultiSeries(data)).toBe(true)
64
+ })
65
+
66
+ it("should return false for single series data", () => {
67
+ const data = [
68
+ { x: 0, y: 10 },
69
+ { x: 1, y: 20 },
70
+ ]
71
+ expect(isMultiSeries(data)).toBe(false)
72
+ })
73
+
74
+ it("should return false for empty array", () => {
75
+ expect(isMultiSeries([])).toBe(false)
76
+ })
77
+ })
78
+
79
+ describe("getDataPointX", () => {
80
+ it("should return x value if present", () => {
81
+ expect(getDataPointX({ x: 5 })).toBe(5)
82
+ })
83
+
84
+ it("should return date value if x is not present", () => {
85
+ expect(getDataPointX({ date: "2024-01-01" })).toBe("2024-01-01")
86
+ })
87
+
88
+ it("should return fallback if neither x nor date present", () => {
89
+ expect(getDataPointX({ value: 10 }, 0)).toBe(0)
90
+ })
91
+ })
92
+
93
+ describe("getDataPointY", () => {
94
+ it("should return y value if present", () => {
95
+ expect(getDataPointY({ y: 100 })).toBe(100)
96
+ })
97
+
98
+ it("should return value if y is not present", () => {
99
+ expect(getDataPointY({ value: 200 })).toBe(200)
100
+ })
101
+
102
+ it("should return fallback if neither y nor value present", () => {
103
+ expect(getDataPointY({ name: "Jan" }, 0)).toBe(0)
104
+ })
105
+ })
106
+
107
+ describe("getDataPointLabel", () => {
108
+ it("should return x value if present", () => {
109
+ expect(getDataPointLabel({ x: "Jan" })).toBe("Jan")
110
+ })
111
+
112
+ it("should return date value if x is not present", () => {
113
+ expect(getDataPointLabel({ date: "2024-01-01" })).toBe("2024-01-01")
114
+ })
115
+
116
+ it("should return fallback if neither x nor date present", () => {
117
+ expect(getDataPointLabel({ value: 10 }, "Value")).toBe("Value")
118
+ })
119
+ })
120
+
121
+ describe("isValidNumber", () => {
122
+ it("should return true for valid numbers", () => {
123
+ expect(isValidNumber(100)).toBe(true)
124
+ expect(isValidNumber(0)).toBe(true)
125
+ expect(isValidNumber(-50)).toBe(true)
126
+ expect(isValidNumber(3.14)).toBe(true)
127
+ })
128
+
129
+ it("should return false for invalid numbers", () => {
130
+ expect(isValidNumber(NaN)).toBe(false)
131
+ expect(isValidNumber(Infinity)).toBe(false)
132
+ expect(isValidNumber("100")).toBe(false)
133
+ expect(isValidNumber(null)).toBe(false)
134
+ expect(isValidNumber(undefined)).toBe(false)
135
+ })
136
+ })
137
+
138
+ describe("ensureValidNumber", () => {
139
+ it("should return value if valid", () => {
140
+ expect(ensureValidNumber(100)).toBe(100)
141
+ })
142
+
143
+ it("should return fallback if invalid", () => {
144
+ expect(ensureValidNumber(NaN, 0)).toBe(0)
145
+ expect(ensureValidNumber("100", 0)).toBe(0)
146
+ })
147
+ })
148
+
149
+ describe("parseDimension", () => {
150
+ it("should return number if already a number", () => {
151
+ expect(parseDimension(800)).toBe(800)
152
+ })
153
+
154
+ it("should parse numeric string", () => {
155
+ expect(parseDimension("800")).toBe(800)
156
+ })
157
+
158
+ it("should return undefined for percentage strings", () => {
159
+ expect(parseDimension("100%")).toBeUndefined()
160
+ })
161
+
162
+ it("should return undefined for pixel strings", () => {
163
+ expect(parseDimension("800px")).toBeUndefined()
164
+ })
165
+
166
+ it("should return undefined for non-numeric strings", () => {
167
+ expect(parseDimension("auto")).toBeUndefined()
168
+ })
169
+ })
170
+
171
+ describe("getTransformedData", () => {
172
+ it("should transform data with dataKey", () => {
173
+ const entity = {
174
+ id: "test",
175
+ data: [
176
+ { name: "Jan", value: 100 },
177
+ { name: "Feb", value: 200 },
178
+ ],
179
+ }
180
+
181
+ const result = getTransformedData(entity, "value")
182
+
183
+ // When dataKey exists, name fallback uses d[dataKey] first, then d.name
184
+ // So name will be the value (100, 200) if dataKey exists
185
+ expect(result).toEqual([
186
+ { x: 0, y: 100, name: 100 },
187
+ { x: 1, y: 200, name: 200 },
188
+ ])
189
+ })
190
+
191
+ it("should fallback to y or value if dataKey not found", () => {
192
+ const entity = {
193
+ id: "test",
194
+ data: [{ name: "Jan", y: 100 }],
195
+ }
196
+
197
+ const result = getTransformedData(entity, "missingKey")
198
+
199
+ expect(result).toEqual([{ x: 0, y: 100, name: "Jan" }])
200
+ })
201
+
202
+ it("should return null if entity is missing", () => {
203
+ expect(getTransformedData(null, "value")).toBeNull()
204
+ })
205
+
206
+ it("should return null if entity.data is missing", () => {
207
+ expect(getTransformedData({ id: "test" }, "value")).toBeNull()
208
+ })
209
+ })
210
+ })
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Extracts dataKeys from chart children components automatically.
3
+ * This allows the chart to determine which data fields to use for Y-axis scaling
4
+ * without requiring explicit dataKeys configuration.
5
+ *
6
+ * @param {Array|Function} children - Array of chart child components (Line, Area, Bar, etc.)
7
+ * @returns {Array<string>} Array of unique dataKeys found in the children
8
+ */
9
+ export function extractDataKeysFromChildren(children) {
10
+ const dataKeys = new Set()
11
+ const childrenArray = Array.isArray(children) ? children : [children]
12
+
13
+ for (const child of childrenArray) {
14
+ if (typeof child === "function") {
15
+ if (child.dataKey) {
16
+ dataKeys.add(child.dataKey)
17
+ }
18
+ }
19
+ }
20
+
21
+ return Array.from(dataKeys)
22
+ }
@@ -0,0 +1,16 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ /**
4
+ * Calculate padding based on chart dimensions
5
+ * @param {number} [width=800] - Chart width
6
+ * @param {number} [height=400] - Chart height
7
+ * @returns {Object} Padding object with top, right, bottom, left
8
+ */
9
+ export function calculatePadding(width = 800, height = 400) {
10
+ return {
11
+ top: Math.max(20, height * 0.05),
12
+ right: Math.max(20, width * 0.05),
13
+ bottom: Math.max(40, height * 0.1),
14
+ left: Math.max(50, width * 0.1),
15
+ }
16
+ }