@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,103 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { svg } from "@inglorious/web"
5
+ import { render } from "@inglorious/web/test"
6
+ import { beforeEach, describe, expect, it, vi } from "vitest"
7
+
8
+ import { renderLegend } from "./legend.js"
9
+
10
+ describe("renderLegend", () => {
11
+ let entity
12
+ let props
13
+ let api
14
+
15
+ beforeEach(() => {
16
+ entity = {
17
+ id: "test-chart",
18
+ type: "line",
19
+ data: [],
20
+ width: 800,
21
+ height: 400,
22
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
23
+ }
24
+
25
+ props = {
26
+ series: [
27
+ { name: "Product A", color: "#3b82f6" },
28
+ { name: "Product B", color: "#ef4444" },
29
+ { name: "Product C", color: "#10b981" },
30
+ ],
31
+ colors: ["#3b82f6", "#ef4444", "#10b981"],
32
+ width: 800,
33
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
34
+ }
35
+
36
+ api = {
37
+ notify: vi.fn(),
38
+ }
39
+ })
40
+
41
+ describe("basic rendering", () => {
42
+ it("should render legend with series", () => {
43
+ const result = renderLegend(entity, props, api)
44
+ const container = document.createElement("div")
45
+ // Wrap in SVG since renderLegend returns SVG fragment
46
+ render(svg`<svg>${result}</svg>`, container)
47
+
48
+ const legend = container.querySelector("g.iw-chart-legend-wrapper")
49
+ expect(legend).toBeTruthy()
50
+
51
+ const items = container.querySelectorAll("g.iw-chart-legend-item")
52
+ expect(items.length).toBe(3)
53
+ })
54
+
55
+ it("should render legend items with colors and labels", () => {
56
+ const result = renderLegend(entity, props, api)
57
+ const container = document.createElement("div")
58
+ render(svg`<svg>${result}</svg>`, container)
59
+
60
+ const items = container.querySelectorAll("g.iw-chart-legend-item")
61
+ items.forEach((item, index) => {
62
+ const rect = item.querySelector("rect")
63
+ const text = item.querySelector("text")
64
+
65
+ expect(rect).toBeTruthy()
66
+ expect(text).toBeTruthy()
67
+ expect(rect.getAttribute("fill")).toBe(props.series[index].color)
68
+ expect(text.textContent).toContain(props.series[index].name)
69
+ })
70
+ })
71
+ })
72
+
73
+ describe("with empty series", () => {
74
+ it("should return empty template for empty series", () => {
75
+ props.series = []
76
+
77
+ const result = renderLegend(entity, props, api)
78
+ const container = document.createElement("div")
79
+ render(svg`<svg>${result}</svg>`, container)
80
+
81
+ // Should return empty template
82
+ const legend = container.querySelector("g.iw-chart-legend-wrapper")
83
+ expect(legend).toBeFalsy()
84
+ })
85
+ })
86
+
87
+ describe("with default colors", () => {
88
+ it("should use default colors when series color is not provided", () => {
89
+ props.series = [{ name: "Series A" }, { name: "Series B" }]
90
+ props.colors = ["#3b82f6", "#ef4444"]
91
+
92
+ const result = renderLegend(entity, props, api)
93
+ const container = document.createElement("div")
94
+ render(svg`<svg>${result}</svg>`, container)
95
+
96
+ const items = container.querySelectorAll("g.iw-chart-legend-item")
97
+ expect(items.length).toBe(2)
98
+
99
+ const firstRect = items[0].querySelector("rect")
100
+ expect(firstRect.getAttribute("fill")).toBe("#3b82f6")
101
+ })
102
+ })
103
+ })
@@ -0,0 +1,65 @@
1
+ import { html } from "@inglorious/web"
2
+
3
+ import { formatNumber } from "../utils/data-utils.js"
4
+
5
+ /**
6
+ * Renders the chart tooltip overlay.
7
+ * Reused by cartesian charts (line, area, bar).
8
+ *
9
+ * @param {import('../types/charts').ChartEntity} entity
10
+ * @param {Object} props
11
+ * @param {any} api
12
+ * @returns {import('lit-html').TemplateResult}
13
+ */
14
+ // eslint-disable-next-line no-unused-vars
15
+ export function renderTooltip(entity, props, api) {
16
+ if (!entity?.tooltip) {
17
+ return html``
18
+ }
19
+
20
+ return html`
21
+ <div
22
+ class="iw-chart-modal"
23
+ style="left:${entity.tooltipX}px; top:${entity.tooltipY}px"
24
+ >
25
+ <div class="iw-chart-modal-header">
26
+ <span
27
+ class="iw-chart-modal-color"
28
+ style="background-color: ${entity.tooltip.color};"
29
+ ></span>
30
+ <span class="iw-chart-modal-label">${entity.tooltip.label}</span>
31
+ </div>
32
+ <div class="iw-chart-modal-body">
33
+ <div class="iw-chart-modal-value">
34
+ ${formatNumber(entity.tooltip.value)}
35
+ </div>
36
+ </div>
37
+ </div>
38
+ `
39
+ }
40
+
41
+ /**
42
+ * Creates a tooltip component for composition mode (Recharts-style)
43
+ * This factory function returns a component function that can be used in chart composition
44
+ *
45
+ * @returns {Function} Component function that accepts (entity, props, api) and returns a composition function
46
+ *
47
+ * @example
48
+ * // In line.js, area.js, bar.js:
49
+ * import { createTooltipComponent } from "../component/tooltip.js"
50
+ *
51
+ * export const line = {
52
+ * renderTooltip: createTooltipComponent(),
53
+ * }
54
+ */
55
+ export function createTooltipComponent() {
56
+ return (entity, props, api) => {
57
+ const tooltipFn = (ctx) => {
58
+ const entityFromContext = ctx.entity || entity
59
+ return renderTooltip(entityFromContext, {}, api)
60
+ }
61
+ // Mark as tooltip component for stable identification during processing
62
+ tooltipFn.isTooltip = true
63
+ return tooltipFn
64
+ }
65
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { render } from "@inglorious/web/test"
5
+ import { describe, expect, it } from "vitest"
6
+
7
+ import { renderTooltip } from "./tooltip.js"
8
+
9
+ describe("renderTooltip", () => {
10
+ const mockApi = {}
11
+
12
+ it("should render tooltip when entity.tooltip is provided", () => {
13
+ const entity = {
14
+ id: "test",
15
+ data: [{ name: "Jan", value: 100 }],
16
+ tooltip: {
17
+ label: "Jan",
18
+ value: 100,
19
+ color: "#3b82f6",
20
+ },
21
+ tooltipX: 100,
22
+ tooltipY: 200,
23
+ }
24
+
25
+ const template = renderTooltip(entity, {}, mockApi)
26
+ const container = document.createElement("div")
27
+ render(template, container)
28
+
29
+ const tooltip = container.querySelector(".iw-chart-modal")
30
+ expect(tooltip).toBeTruthy()
31
+ expect(tooltip.textContent).toContain("Jan")
32
+ expect(tooltip.textContent).toContain("100")
33
+ })
34
+
35
+ it("should not render tooltip when entity.tooltip is null", () => {
36
+ const entity = {
37
+ id: "test",
38
+ data: [{ name: "Jan", value: 100 }],
39
+ tooltip: null,
40
+ tooltipX: 100,
41
+ tooltipY: 200,
42
+ }
43
+
44
+ const template = renderTooltip(entity, {}, mockApi)
45
+ const container = document.createElement("div")
46
+ render(template, container)
47
+
48
+ const tooltip = container.querySelector(".iw-chart-modal")
49
+ expect(tooltip).toBeNull()
50
+ })
51
+
52
+ it("should position tooltip at entity.tooltipX and entity.tooltipY", () => {
53
+ const entity = {
54
+ id: "test",
55
+ data: [{ name: "Jan", value: 100 }],
56
+ tooltip: {
57
+ label: "Jan",
58
+ value: 100,
59
+ },
60
+ tooltipX: 150,
61
+ tooltipY: 250,
62
+ }
63
+
64
+ const template = renderTooltip(entity, {}, mockApi)
65
+ const container = document.createElement("div")
66
+ render(template, container)
67
+
68
+ const tooltip = container.querySelector(".iw-chart-modal")
69
+ expect(tooltip).toBeTruthy()
70
+ expect(tooltip.style.left).toBe("150px")
71
+ expect(tooltip.style.top).toBe("250px")
72
+ })
73
+
74
+ it("should render tooltip with formatted number", () => {
75
+ const entity = {
76
+ id: "test",
77
+ data: [{ name: "Jan", value: 100 }],
78
+ tooltip: {
79
+ label: "Jan",
80
+ value: 1234.56,
81
+ color: "#3b82f6",
82
+ },
83
+ tooltipX: 100,
84
+ tooltipY: 200,
85
+ }
86
+
87
+ const template = renderTooltip(entity, {}, mockApi)
88
+ const container = document.createElement("div")
89
+ render(template, container)
90
+
91
+ const tooltip = container.querySelector(".iw-chart-modal")
92
+ expect(tooltip).toBeTruthy()
93
+ // formatNumber uses ",.2f" by default, so 1234.56 becomes "1,234.56"
94
+ expect(tooltip.textContent).toContain("1,234.56")
95
+ })
96
+ })
@@ -0,0 +1,212 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ import { repeat, svg } from "@inglorious/web"
4
+
5
+ import {
6
+ ensureValidNumber,
7
+ formatDate,
8
+ formatNumber,
9
+ isValidNumber,
10
+ } from "../utils/data-utils.js"
11
+ import { calculateXTicks } from "../utils/scales.js"
12
+
13
+ /**
14
+ * @param {any} entity
15
+ * @param {Object} props
16
+ * @param {import('d3-scale').ScaleBand|import('d3-scale').ScaleLinear|import('d3-scale').ScaleTime} props.xScale
17
+ * @param {import('d3-scale').ScaleLinear} props.yScale
18
+ * @param {number} props.width
19
+ * @param {number} props.height
20
+ * @param {Object} props.padding
21
+ * @param {any} api
22
+ * @returns {import('lit-html').TemplateResult}
23
+ */
24
+
25
+ // eslint-disable-next-line no-unused-vars
26
+ export function renderXAxis(entity, props, api) {
27
+ const { xScale, yScale, width, height, padding } = props
28
+
29
+ if (xScale.bandwidth) {
30
+ // Following Recharts logic: for scaleBand, use the domain directly
31
+ // and calculate the center as scale(category) + bandwidth() / 2
32
+ const allCategories = xScale.domain()
33
+ if (allCategories.length === 0) {
34
+ return svg`<g class="iw-chart-xAxis"></g>`
35
+ }
36
+
37
+ // Limit number of ticks to avoid overlapping labels
38
+ // Similar to Recharts behavior: show fewer ticks when there are many categories
39
+ let categories = allCategories
40
+ if (allCategories.length > 20) {
41
+ // Calculate optimal number of ticks based on available width
42
+ // Estimate ~60px per label to avoid overlap
43
+ const availableWidth =
44
+ (width || 800) - (padding?.left || 0) - (padding?.right || 0)
45
+ const maxTicks = Math.max(5, Math.floor(availableWidth / 60))
46
+ const step = Math.ceil(allCategories.length / maxTicks)
47
+ categories = allCategories.filter((_, i) => i % step === 0)
48
+ }
49
+
50
+ let xAxisY = height - padding.bottom
51
+ if (yScale) {
52
+ const domain = yScale.domain()
53
+ if (domain[0] < 0) {
54
+ const zeroY = yScale(0)
55
+ if (isValidNumber(zeroY)) {
56
+ xAxisY = zeroY
57
+ }
58
+ }
59
+ }
60
+ // Ensure xAxisY is a valid number
61
+ if (!isValidNumber(xAxisY)) {
62
+ const fallbackY = height - (padding?.bottom || 0)
63
+ xAxisY = ensureValidNumber(fallbackY, height || 0)
64
+ }
65
+
66
+ // Offset for scaleBand: bandwidth() / 2 (as in Recharts)
67
+ // bandwidth() returns the available band width (without internal padding)
68
+ // Following Recharts logic exactly: offsetForBand = bandwidth() / 2
69
+ const bandwidth = xScale.bandwidth()
70
+ if (!isValidNumber(bandwidth) || bandwidth <= 0) {
71
+ // If bandwidth is not valid, don't render the axis
72
+ return svg`<g class="iw-chart-xAxis"></g>`
73
+ }
74
+ const offsetForBand = bandwidth / 2
75
+
76
+ return svg`
77
+ <g class="iw-chart-xAxis">
78
+ <!-- X Axis Line -->
79
+ <line
80
+ x1=${padding?.left || 0}
81
+ y1=${xAxisY}
82
+ x2=${(width || 0) - (padding?.right || 0)}
83
+ y2=${xAxisY}
84
+ stroke="#ddd"
85
+ stroke-width="0.0625em"
86
+ class="iw-chart-xAxis-line"
87
+ />
88
+ ${repeat(
89
+ categories,
90
+ (cat) => cat,
91
+ (cat) => {
92
+ // Following Recharts: coordinate = scale(category) + offset
93
+ // where offset = bandwidth() / 2 for scaleBand
94
+ // xScale(cat) returns the initial position of the band (including external padding)
95
+ // We add bandwidth() / 2 to get the center of the band
96
+ const scaled = xScale(cat)
97
+ if (scaled == null || !isValidNumber(scaled)) {
98
+ return svg``
99
+ }
100
+ const coordinate = scaled + offsetForBand
101
+ // Ensure coordinate is a valid number
102
+ if (!isValidNumber(coordinate)) {
103
+ return svg``
104
+ }
105
+ return svg`
106
+ <g class="iw-chart-xAxis-tick">
107
+ <!-- Tick line (vertical line) - uses the same coordinate -->
108
+ <line
109
+ x1=${coordinate}
110
+ y1=${xAxisY}
111
+ x2=${coordinate}
112
+ y2=${xAxisY + 5}
113
+ stroke="#ccc"
114
+ stroke-width="0.0625em"
115
+ />
116
+ <!-- Label - usa a mesma coordinate -->
117
+ <text
118
+ x=${coordinate}
119
+ y=${xAxisY + 20}
120
+ text-anchor="middle"
121
+ font-size="0.6875em"
122
+ fill="#777"
123
+ class="iw-chart-xAxis-tick-label"
124
+ >
125
+ ${cat}
126
+ </text>
127
+ </g>
128
+ `
129
+ },
130
+ )}
131
+ </g>
132
+ `
133
+ }
134
+
135
+ // Calculate ticks using helper function
136
+ const ticks = xScale.bandwidth
137
+ ? xScale.domain()
138
+ : calculateXTicks(entity.data, xScale)
139
+
140
+ let xAxisY = height - padding.bottom
141
+ if (yScale) {
142
+ const yDomain = yScale.domain()
143
+ if (yDomain[0] < 0) {
144
+ const zeroY = yScale(0)
145
+ if (isValidNumber(zeroY)) {
146
+ xAxisY = zeroY
147
+ }
148
+ }
149
+ }
150
+ // Ensure xAxisY is a valid number
151
+ if (!isValidNumber(xAxisY)) {
152
+ const fallbackY = height - (padding?.bottom || 0)
153
+ xAxisY = ensureValidNumber(fallbackY, height || 0)
154
+ }
155
+
156
+ // If entity has xLabels, use them for display (for categorical data)
157
+ const useLabels = entity.xLabels && Array.isArray(entity.xLabels)
158
+
159
+ return svg`
160
+ <g class="iw-chart-xAxis">
161
+ <!-- X Axis Line -->
162
+ <line
163
+ x1=${padding?.left || 0}
164
+ y1=${xAxisY}
165
+ x2=${(width || 0) - (padding?.right || 0)}
166
+ y2=${xAxisY}
167
+ stroke="#ddd"
168
+ stroke-width="0.0625em"
169
+ class="iw-chart-xAxis-line"
170
+ />
171
+ ${repeat(
172
+ ticks,
173
+ (t) => t,
174
+ (t, i) => {
175
+ const x = xScale(t)
176
+ // Ensure x is a valid number
177
+ if (!isValidNumber(x)) {
178
+ return svg``
179
+ }
180
+ // Use custom labels if available, otherwise format the tick value
181
+ const label =
182
+ useLabels && entity.xLabels[i] !== undefined
183
+ ? entity.xLabels[i]
184
+ : entity.xAxisType === "time"
185
+ ? formatDate(t)
186
+ : formatNumber(t)
187
+
188
+ return svg`
189
+ <g class="iw-chart-xAxis-tick">
190
+ <line
191
+ x1=${x}
192
+ y1=${xAxisY}
193
+ x2=${x}
194
+ y2=${xAxisY + 5}
195
+ stroke="#ccc"
196
+ stroke-width="0.0625em"
197
+ />
198
+ <text
199
+ x=${x}
200
+ y=${xAxisY + 20}
201
+ text-anchor="middle"
202
+ font-size="0.6875em"
203
+ fill="#777"
204
+ class="iw-chart-xAxis-tick-label"
205
+ >${label}</text>
206
+ </g>
207
+ `
208
+ },
209
+ )}
210
+ </g>
211
+ `
212
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { render } from "@inglorious/web/test"
5
+ import { beforeEach, describe, expect, it, vi } from "vitest"
6
+
7
+ import { renderXAxis } from "./x-axis.js"
8
+
9
+ describe("renderXAxis", () => {
10
+ let entity
11
+ let props
12
+ let api
13
+
14
+ beforeEach(() => {
15
+ entity = {
16
+ id: "test-chart",
17
+ type: "line",
18
+ data: [
19
+ { x: 0, y: 50 },
20
+ { x: 1, y: 150 },
21
+ { x: 2, y: 120 },
22
+ ],
23
+ width: 800,
24
+ height: 400,
25
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
26
+ }
27
+
28
+ props = {
29
+ xScale: vi.fn((x) => 50 + x * 200),
30
+ yScale: vi.fn((y) => 370 - (y / 200) * 350),
31
+ width: 800,
32
+ height: 400,
33
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
34
+ }
35
+
36
+ props.xScale.domain = vi.fn(() => [0, 2])
37
+ props.xScale.range = vi.fn(() => [50, 650])
38
+ props.xScale.bandwidth = vi.fn(() => 200)
39
+
40
+ props.yScale.domain = vi.fn(() => [0, 200])
41
+ props.yScale.range = vi.fn(() => [370, 20])
42
+
43
+ api = {
44
+ notify: vi.fn(),
45
+ }
46
+ })
47
+
48
+ describe("with scaleBand (categorical)", () => {
49
+ it("should render X axis with band scale", () => {
50
+ entity.data = [
51
+ { label: "Jan", value: 100 },
52
+ { label: "Feb", value: 150 },
53
+ { label: "Mar", value: 120 },
54
+ ]
55
+
56
+ const mockBandScale = vi.fn((cat) => {
57
+ const map = { Jan: 100, Feb: 350, Mar: 600 }
58
+ return map[cat] || 0
59
+ })
60
+ mockBandScale.domain = vi.fn(() => ["Jan", "Feb", "Mar"])
61
+ mockBandScale.range = vi.fn(() => [50, 750])
62
+ mockBandScale.bandwidth = vi.fn(() => 200)
63
+
64
+ props.xScale = mockBandScale
65
+
66
+ const result = renderXAxis(entity, props, api)
67
+ const container = document.createElement("div")
68
+ render(result, container)
69
+
70
+ const axisLine = container.querySelector("line")
71
+ expect(axisLine).toBeTruthy()
72
+
73
+ const ticks = container.querySelectorAll("g.iw-chart-xAxis-tick")
74
+ expect(ticks.length).toBe(3)
75
+ })
76
+ })
77
+
78
+ describe("with scaleLinear (numeric)", () => {
79
+ it("should render X axis with linear scale", () => {
80
+ const result = renderXAxis(entity, props, api)
81
+ const container = document.createElement("div")
82
+ render(result, container)
83
+
84
+ const axisLine = container.querySelector("line")
85
+ expect(axisLine).toBeTruthy()
86
+
87
+ const ticks = container.querySelectorAll("g.iw-chart-xAxis-tick")
88
+ expect(ticks.length).toBeGreaterThan(0)
89
+ })
90
+
91
+ it("should format integer ticks without decimals", () => {
92
+ const result = renderXAxis(entity, props, api)
93
+ const container = document.createElement("div")
94
+ render(result, container)
95
+
96
+ const labels = container.querySelectorAll(".iw-chart-xAxis-tick-label")
97
+ labels.forEach((label) => {
98
+ const text = label.textContent.trim()
99
+ // Should not contain ".00" for integers
100
+ if (text.match(/^\d+$/)) {
101
+ expect(text).not.toContain(".00")
102
+ }
103
+ })
104
+ })
105
+ })
106
+
107
+ describe("with xLabels (custom labels)", () => {
108
+ it("should use custom xLabels when provided", () => {
109
+ entity.xLabels = ["Q1", "Q2", "Q3"]
110
+ entity.data = [
111
+ { x: 0, y: 50 },
112
+ { x: 1, y: 150 },
113
+ { x: 2, y: 120 },
114
+ ]
115
+
116
+ // Mock calculateXTicks to return [0, 1, 2]
117
+ const result = renderXAxis(entity, props, api)
118
+ const container = document.createElement("div")
119
+ render(result, container)
120
+
121
+ const labels = container.querySelectorAll(".iw-chart-xAxis-tick-label")
122
+ // Should use xLabels when available
123
+ expect(labels.length).toBeGreaterThan(0)
124
+ })
125
+ })
126
+
127
+ describe("with time axis", () => {
128
+ it("should format dates when xAxisType is time", () => {
129
+ entity.xAxisType = "time"
130
+ entity.data = [
131
+ { date: "2024-01-01", value: 100 },
132
+ { date: "2024-01-02", value: 150 },
133
+ ]
134
+
135
+ props.xScale.domain = vi.fn(() => [
136
+ new Date("2024-01-01"),
137
+ new Date("2024-01-02"),
138
+ ])
139
+
140
+ const result = renderXAxis(entity, props, api)
141
+ const container = document.createElement("div")
142
+ render(result, container)
143
+
144
+ const axisLine = container.querySelector("line")
145
+ expect(axisLine).toBeTruthy()
146
+ })
147
+ })
148
+ })
@@ -0,0 +1,77 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ import { repeat, svg } from "@inglorious/web"
4
+
5
+ import { formatNumber, isValidNumber } from "../utils/data-utils.js"
6
+
7
+ /**
8
+ * @param {any} entity
9
+ * @param {Object} props
10
+ * @param {import('d3-scale').ScaleLinear} props.yScale
11
+ * @param {number} props.height
12
+ * @param {Object} props.padding
13
+ * @param {Array} props.customTicks
14
+ * @param {any} api
15
+ * @returns {import('lit-html').TemplateResult}
16
+ */
17
+ // eslint-disable-next-line no-unused-vars
18
+ export function renderYAxis(entity, props, api) {
19
+ const { yScale, height, padding, customTicks } = props
20
+ // Use custom ticks if provided, otherwise use scale ticks
21
+ const ticks =
22
+ customTicks && Array.isArray(customTicks) ? customTicks : yScale.ticks(5)
23
+
24
+ // Ensure height and padding are valid numbers
25
+ const axisLineY2 = height - padding.bottom
26
+ if (!isValidNumber(axisLineY2)) {
27
+ return svg``
28
+ }
29
+
30
+ return svg`
31
+ <g class="iw-chart-yAxis">
32
+ <!-- Y Axis Line -->
33
+ <line
34
+ x1=${padding.left}
35
+ y1=${padding.top}
36
+ x2=${padding.left}
37
+ y2=${axisLineY2}
38
+ stroke="#ddd"
39
+ stroke-width="0.0625em"
40
+ class="iw-chart-yAxis-line"
41
+ />
42
+ ${repeat(
43
+ ticks,
44
+ (t) => t,
45
+ (t) => {
46
+ const y = yScale(t)
47
+ // Ensure y is a valid number
48
+ if (!isValidNumber(y)) {
49
+ return svg``
50
+ }
51
+ return svg`
52
+ <g class="iw-chart-yAxis-tick">
53
+ <line
54
+ x1=${padding.left}
55
+ y1=${y}
56
+ x2=${padding.left - 5}
57
+ y2=${y}
58
+ stroke="#ccc"
59
+ stroke-width="0.0625em"
60
+ />
61
+ <text
62
+ x=${padding.left - 10}
63
+ y=${y + 4}
64
+ text-anchor="end"
65
+ font-size="0.6875em"
66
+ fill="#777"
67
+ class="iw-chart-yAxis-tick-label"
68
+ >
69
+ ${formatNumber(t)}
70
+ </text>
71
+ </g>
72
+ `
73
+ },
74
+ )}
75
+ </g>
76
+ `
77
+ }