@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.
- package/LICENSE +9 -0
- package/README.md +554 -0
- package/package.json +64 -0
- package/src/base.css +86 -0
- package/src/cartesian/area.js +392 -0
- package/src/cartesian/area.test.js +366 -0
- package/src/cartesian/bar.js +445 -0
- package/src/cartesian/bar.test.js +346 -0
- package/src/cartesian/line.js +823 -0
- package/src/cartesian/line.test.js +177 -0
- package/src/chart.test.js +444 -0
- package/src/component/brush.js +264 -0
- package/src/component/empty-state.js +33 -0
- package/src/component/empty-state.test.js +81 -0
- package/src/component/grid.js +123 -0
- package/src/component/grid.test.js +123 -0
- package/src/component/legend.js +76 -0
- package/src/component/legend.test.js +103 -0
- package/src/component/tooltip.js +65 -0
- package/src/component/tooltip.test.js +96 -0
- package/src/component/x-axis.js +212 -0
- package/src/component/x-axis.test.js +148 -0
- package/src/component/y-axis.js +77 -0
- package/src/component/y-axis.test.js +107 -0
- package/src/handlers.js +150 -0
- package/src/index.js +264 -0
- package/src/polar/donut.js +181 -0
- package/src/polar/donut.test.js +152 -0
- package/src/polar/pie.js +758 -0
- package/src/polar/pie.test.js +268 -0
- package/src/shape/curve.js +55 -0
- package/src/shape/dot.js +104 -0
- package/src/shape/rectangle.js +46 -0
- package/src/shape/sector.js +58 -0
- package/src/template.js +25 -0
- package/src/theme.css +90 -0
- package/src/utils/cartesian-layout.js +164 -0
- package/src/utils/chart-utils.js +30 -0
- package/src/utils/colors.js +77 -0
- package/src/utils/data-utils.js +155 -0
- package/src/utils/data-utils.test.js +210 -0
- package/src/utils/extract-data-keys.js +22 -0
- package/src/utils/padding.js +16 -0
- package/src/utils/paths.js +279 -0
- package/src/utils/process-declarative-child.js +46 -0
- package/src/utils/scales.js +250 -0
- package/src/utils/shared-context.js +166 -0
- package/src/utils/shared-context.test.js +237 -0
- package/src/utils/tooltip-handlers.js +129 -0
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @vitest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
import { html, svg } from "@inglorious/web"
|
|
5
|
+
import { render } from "@inglorious/web/test"
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest"
|
|
7
|
+
|
|
8
|
+
import { area } from "./area.js"
|
|
9
|
+
|
|
10
|
+
describe("area", () => {
|
|
11
|
+
let entity
|
|
12
|
+
let api
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
entity = {
|
|
16
|
+
id: "test-area",
|
|
17
|
+
type: "area",
|
|
18
|
+
data: [
|
|
19
|
+
{ x: 0, y: 50 },
|
|
20
|
+
{ x: 1, y: 150 },
|
|
21
|
+
{ x: 2, y: 120 },
|
|
22
|
+
{ x: 3, y: 180 },
|
|
23
|
+
],
|
|
24
|
+
width: 800,
|
|
25
|
+
height: 400,
|
|
26
|
+
padding: { top: 20, right: 50, bottom: 30, left: 50 },
|
|
27
|
+
colors: ["#3b82f6", "#ef4444", "#10b981"],
|
|
28
|
+
showGrid: true,
|
|
29
|
+
showTooltip: true,
|
|
30
|
+
showPoints: true,
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
api = {
|
|
34
|
+
getEntity: vi.fn((id) => (id === "test-area" ? entity : null)),
|
|
35
|
+
notify: vi.fn(),
|
|
36
|
+
getType: vi.fn((type) => (type === "area" ? area : null)),
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
describe("render()", () => {
|
|
41
|
+
it("should render area chart with data", () => {
|
|
42
|
+
const result = area.render(entity, api)
|
|
43
|
+
const container = document.createElement("div")
|
|
44
|
+
render(result, container)
|
|
45
|
+
|
|
46
|
+
const svg = container.querySelector("svg")
|
|
47
|
+
expect(svg).toBeTruthy()
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it("should handle empty data gracefully", () => {
|
|
51
|
+
entity.data = []
|
|
52
|
+
|
|
53
|
+
const result = area.render(entity, api)
|
|
54
|
+
const container = document.createElement("div")
|
|
55
|
+
render(result, container)
|
|
56
|
+
|
|
57
|
+
expect(result).toBeDefined()
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe("renderAreaChart()", () => {
|
|
62
|
+
it("should render area chart with children", () => {
|
|
63
|
+
const children = [
|
|
64
|
+
area.renderCartesianGrid(entity, {}, api),
|
|
65
|
+
area.renderXAxis(entity, {}, api),
|
|
66
|
+
area.renderYAxis(entity, {}, api),
|
|
67
|
+
area.renderArea(entity, { config: { dataKey: "value" } }, api),
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
const result = area.renderAreaChart(
|
|
71
|
+
entity,
|
|
72
|
+
{ children, config: { width: 800, height: 400 } },
|
|
73
|
+
api,
|
|
74
|
+
)
|
|
75
|
+
const container = document.createElement("div")
|
|
76
|
+
render(result, container)
|
|
77
|
+
|
|
78
|
+
const svg = container.querySelector("svg")
|
|
79
|
+
expect(svg).toBeTruthy()
|
|
80
|
+
expect(svg.getAttribute("width")).toBe("800")
|
|
81
|
+
expect(svg.getAttribute("height")).toBe("400")
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it("should return error message if entity is missing", () => {
|
|
85
|
+
const result = area.renderAreaChart(null, { children: [] }, api)
|
|
86
|
+
const container = document.createElement("div")
|
|
87
|
+
render(result, container)
|
|
88
|
+
|
|
89
|
+
expect(container.textContent).toContain("Entity not found")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("should handle stacked areas when stackId is provided", () => {
|
|
93
|
+
const children = [
|
|
94
|
+
area.renderArea(
|
|
95
|
+
entity,
|
|
96
|
+
{
|
|
97
|
+
config: { dataKey: "value", stackId: "stack1" },
|
|
98
|
+
},
|
|
99
|
+
api,
|
|
100
|
+
),
|
|
101
|
+
area.renderArea(
|
|
102
|
+
entity,
|
|
103
|
+
{
|
|
104
|
+
config: { dataKey: "value2", stackId: "stack1" },
|
|
105
|
+
},
|
|
106
|
+
api,
|
|
107
|
+
),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
entity.data = [
|
|
111
|
+
{ name: "0", value: 50, value2: 30 },
|
|
112
|
+
{ name: "1", value: 150, value2: 80 },
|
|
113
|
+
]
|
|
114
|
+
|
|
115
|
+
const result = area.renderAreaChart(
|
|
116
|
+
entity,
|
|
117
|
+
{
|
|
118
|
+
children,
|
|
119
|
+
config: {
|
|
120
|
+
width: 800,
|
|
121
|
+
height: 400,
|
|
122
|
+
dataKeys: ["value", "value2"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
api,
|
|
126
|
+
)
|
|
127
|
+
const container = document.createElement("div")
|
|
128
|
+
render(result, container)
|
|
129
|
+
|
|
130
|
+
const svg = container.querySelector("svg")
|
|
131
|
+
expect(svg).toBeTruthy()
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should fallback to entity dimensions when config width/height are percentages ("100%")', () => {
|
|
135
|
+
entity.data = [
|
|
136
|
+
{ name: "Jan", value: 50 },
|
|
137
|
+
{ name: "Feb", value: 150 },
|
|
138
|
+
]
|
|
139
|
+
|
|
140
|
+
const children = [
|
|
141
|
+
area.renderArea(entity, { config: { dataKey: "value" } }, api),
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
const result = area.renderAreaChart(
|
|
145
|
+
entity,
|
|
146
|
+
{ children, config: { width: "100%", height: "100%" } },
|
|
147
|
+
api,
|
|
148
|
+
)
|
|
149
|
+
const container = document.createElement("div")
|
|
150
|
+
render(result, container)
|
|
151
|
+
|
|
152
|
+
const svgEl = container.querySelector("svg")
|
|
153
|
+
expect(svgEl).toBeTruthy()
|
|
154
|
+
expect(svgEl.getAttribute("width")).toBe(String(entity.width))
|
|
155
|
+
expect(svgEl.getAttribute("height")).toBe(String(entity.height))
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
describe("renderArea()", () => {
|
|
160
|
+
it("should return a function marked as isArea", () => {
|
|
161
|
+
const result = area.renderArea(
|
|
162
|
+
entity,
|
|
163
|
+
{
|
|
164
|
+
config: { dataKey: "value" },
|
|
165
|
+
},
|
|
166
|
+
api,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
expect(typeof result).toBe("function")
|
|
170
|
+
expect(result.isArea).toBe(true)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it("should render area when called with context", () => {
|
|
174
|
+
const areaFn = area.renderArea(
|
|
175
|
+
entity,
|
|
176
|
+
{
|
|
177
|
+
config: { dataKey: "value" },
|
|
178
|
+
},
|
|
179
|
+
api,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
const context = {
|
|
183
|
+
entity,
|
|
184
|
+
xScale: vi.fn((x) => 50 + x * 200),
|
|
185
|
+
yScale: vi.fn((y) => 370 - (y / 200) * 350),
|
|
186
|
+
dimensions: {
|
|
187
|
+
width: 800,
|
|
188
|
+
height: 400,
|
|
189
|
+
padding: { top: 20, right: 50, bottom: 30, left: 50 },
|
|
190
|
+
},
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
context.xScale.domain = vi.fn(() => [0, 3])
|
|
194
|
+
context.xScale.range = vi.fn(() => [50, 750])
|
|
195
|
+
|
|
196
|
+
context.yScale.domain = vi.fn(() => [0, 200])
|
|
197
|
+
context.yScale.range = vi.fn(() => [370, 20])
|
|
198
|
+
|
|
199
|
+
entity.data = [
|
|
200
|
+
{ name: "0", value: 50 },
|
|
201
|
+
{ name: "1", value: 150 },
|
|
202
|
+
{ name: "2", value: 120 },
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
const result = areaFn(context)
|
|
206
|
+
const container = document.createElement("div")
|
|
207
|
+
render(result, container)
|
|
208
|
+
|
|
209
|
+
// Should render path for area
|
|
210
|
+
const path = container.querySelector("path")
|
|
211
|
+
expect(path).toBeTruthy()
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
it("should support fill and stroke colors", () => {
|
|
215
|
+
const areaFn = area.renderArea(
|
|
216
|
+
entity,
|
|
217
|
+
{
|
|
218
|
+
config: { dataKey: "value", fill: "#ff0000", stroke: "#00ff00" },
|
|
219
|
+
},
|
|
220
|
+
api,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
expect(typeof areaFn).toBe("function")
|
|
224
|
+
expect(areaFn.isArea).toBe(true)
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
describe("renderDots()", () => {
|
|
229
|
+
it("should return a function marked as isDots", () => {
|
|
230
|
+
const result = area.renderDots(
|
|
231
|
+
entity,
|
|
232
|
+
{ config: { dataKey: "value" } },
|
|
233
|
+
api,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
expect(typeof result).toBe("function")
|
|
237
|
+
expect(result.isDots).toBe(true)
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
it("should render dots when called with context", () => {
|
|
241
|
+
const dotsFn = area.renderDots(
|
|
242
|
+
entity,
|
|
243
|
+
{
|
|
244
|
+
config: { dataKey: "value" },
|
|
245
|
+
},
|
|
246
|
+
api,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
const context = {
|
|
250
|
+
entity,
|
|
251
|
+
xScale: vi.fn((x) => 50 + x * 200),
|
|
252
|
+
yScale: vi.fn((y) => 370 - (y / 200) * 350),
|
|
253
|
+
dimensions: {
|
|
254
|
+
width: 800,
|
|
255
|
+
height: 400,
|
|
256
|
+
padding: { top: 20, right: 50, bottom: 30, left: 50 },
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
context.xScale.domain = vi.fn(() => [0, 3])
|
|
261
|
+
context.xScale.range = vi.fn(() => [50, 750])
|
|
262
|
+
|
|
263
|
+
context.yScale.domain = vi.fn(() => [0, 200])
|
|
264
|
+
context.yScale.range = vi.fn(() => [370, 20])
|
|
265
|
+
|
|
266
|
+
entity.data = [
|
|
267
|
+
{ name: "0", value: 50 },
|
|
268
|
+
{ name: "1", value: 150 },
|
|
269
|
+
]
|
|
270
|
+
|
|
271
|
+
const result = dotsFn(context)
|
|
272
|
+
const container = document.createElement("div")
|
|
273
|
+
render(result, container)
|
|
274
|
+
|
|
275
|
+
// Should render circles for dots
|
|
276
|
+
const circles = container.querySelectorAll("circle")
|
|
277
|
+
expect(circles.length).toBeGreaterThan(0)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
it("should notify api on dot hover (showTooltip/hideTooltip)", () => {
|
|
281
|
+
entity.data = [
|
|
282
|
+
{ name: "Jan", value: 50 },
|
|
283
|
+
{ name: "Feb", value: 150 },
|
|
284
|
+
]
|
|
285
|
+
|
|
286
|
+
const dotsFn = area.renderDots(
|
|
287
|
+
entity,
|
|
288
|
+
{ config: { dataKey: "value", fill: "#ff0000" } },
|
|
289
|
+
api,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
const context = {
|
|
293
|
+
entity,
|
|
294
|
+
api,
|
|
295
|
+
xScale: (x) => 50 + x * 200,
|
|
296
|
+
yScale: (y) => 370 - (y / 200) * 350,
|
|
297
|
+
dimensions: {
|
|
298
|
+
width: 800,
|
|
299
|
+
height: 400,
|
|
300
|
+
padding: { top: 20, right: 50, bottom: 30, left: 50 },
|
|
301
|
+
},
|
|
302
|
+
}
|
|
303
|
+
context.xScale.domain = vi.fn(() => [0, 1])
|
|
304
|
+
context.xScale.range = vi.fn(() => [50, 750])
|
|
305
|
+
context.yScale.domain = vi.fn(() => [0, 200])
|
|
306
|
+
context.yScale.range = vi.fn(() => [370, 20])
|
|
307
|
+
|
|
308
|
+
const dots = dotsFn(context)
|
|
309
|
+
const container = document.createElement("div")
|
|
310
|
+
render(
|
|
311
|
+
html`<div class="iw-chart">
|
|
312
|
+
${svg`<svg width="800" height="400">${dots}</svg>`}
|
|
313
|
+
</div>`,
|
|
314
|
+
container,
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
const circle = container.querySelector("circle")
|
|
318
|
+
expect(circle).toBeTruthy()
|
|
319
|
+
|
|
320
|
+
circle.dispatchEvent(
|
|
321
|
+
new MouseEvent("mouseenter", {
|
|
322
|
+
bubbles: true,
|
|
323
|
+
clientX: 100,
|
|
324
|
+
clientY: 50,
|
|
325
|
+
}),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
expect(api.notify).toHaveBeenCalledWith(
|
|
329
|
+
`#${entity.id}:tooltipShow`,
|
|
330
|
+
expect.objectContaining({
|
|
331
|
+
label: "Jan",
|
|
332
|
+
value: 50,
|
|
333
|
+
color: "#ff0000",
|
|
334
|
+
}),
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
circle.dispatchEvent(new MouseEvent("mouseleave", { bubbles: true }))
|
|
338
|
+
expect(api.notify).toHaveBeenCalledWith(`#${entity.id}:tooltipHide`)
|
|
339
|
+
})
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
describe("renderXAxis()", () => {
|
|
343
|
+
it("should return a function", () => {
|
|
344
|
+
const result = area.renderXAxis(entity, {}, api)
|
|
345
|
+
|
|
346
|
+
expect(typeof result).toBe("function")
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
describe("renderYAxis()", () => {
|
|
351
|
+
it("should return a function", () => {
|
|
352
|
+
const result = area.renderYAxis(entity, {}, api)
|
|
353
|
+
|
|
354
|
+
expect(typeof result).toBe("function")
|
|
355
|
+
})
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
describe("renderCartesianGrid()", () => {
|
|
359
|
+
it("should return a function marked as isGrid", () => {
|
|
360
|
+
const result = area.renderCartesianGrid(entity, {}, api)
|
|
361
|
+
|
|
362
|
+
expect(typeof result).toBe("function")
|
|
363
|
+
expect(result.isGrid).toBe(true)
|
|
364
|
+
})
|
|
365
|
+
})
|
|
366
|
+
})
|