@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,177 @@
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 { line } from "./line.js"
8
+
9
+ describe("line", () => {
10
+ let entity
11
+ let api
12
+
13
+ beforeEach(() => {
14
+ entity = {
15
+ id: "test-line",
16
+ type: "line",
17
+ data: [
18
+ { name: "Jan", value: 100 },
19
+ { name: "Feb", value: 200 },
20
+ { name: "Mar", value: 150 },
21
+ ],
22
+ width: 800,
23
+ height: 400,
24
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
25
+ colors: ["#3b82f6", "#ef4444", "#10b981"],
26
+ }
27
+
28
+ api = {
29
+ getEntity: vi.fn((id) => (id === "test-line" ? entity : null)),
30
+ getType: vi.fn((type) => {
31
+ if (type === "line") return line
32
+ return null
33
+ }),
34
+ notify: vi.fn(),
35
+ }
36
+ })
37
+
38
+ describe("render", () => {
39
+ it("should render line chart with data", () => {
40
+ const result = line.render(entity, api)
41
+ const container = document.createElement("div")
42
+ render(result, container)
43
+
44
+ const svg = container.querySelector("svg")
45
+ expect(svg).toBeTruthy()
46
+ })
47
+
48
+ it("should handle empty data gracefully", () => {
49
+ entity.data = []
50
+
51
+ const result = line.render(entity, api)
52
+ expect(result).toBeDefined()
53
+ // render may return empty template or handle empty state internally
54
+ })
55
+ })
56
+
57
+ describe("renderLineChart (composition mode)", () => {
58
+ it("should render line chart with children", () => {
59
+ const children = [() => null, () => null, () => null, () => null]
60
+
61
+ const result = line.renderLineChart(entity, { children, config: {} }, api)
62
+ const container = document.createElement("div")
63
+ render(result, container)
64
+
65
+ const chart = container.querySelector(".iw-chart")
66
+ expect(chart).toBeTruthy()
67
+ })
68
+
69
+ it("should use config.data to override entity.data", () => {
70
+ const configData = [
71
+ { name: "Apr", value: 300 },
72
+ { name: "May", value: 400 },
73
+ ]
74
+
75
+ const children = []
76
+ const result = line.renderLineChart(
77
+ entity,
78
+ { children, config: { data: configData } },
79
+ api,
80
+ )
81
+
82
+ // The context should use configData, not entity.data
83
+ expect(result).toBeDefined()
84
+ })
85
+
86
+ it("should use config dimensions if provided", () => {
87
+ const children = []
88
+ const result = line.renderLineChart(
89
+ entity,
90
+ {
91
+ children,
92
+ config: {
93
+ width: 1000,
94
+ height: 500,
95
+ },
96
+ },
97
+ api,
98
+ )
99
+
100
+ expect(result).toBeDefined()
101
+ })
102
+ })
103
+
104
+ describe("renderLine", () => {
105
+ it("should render line with dataKey when context is provided", () => {
106
+ const context = {
107
+ xScale: (x) => x * 100,
108
+ yScale: (y) => 400 - y * 2,
109
+ dimensions: {
110
+ width: 800,
111
+ height: 400,
112
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
113
+ },
114
+ entity,
115
+ api,
116
+ }
117
+
118
+ const lineFn = line.renderLine(
119
+ entity,
120
+ { config: { dataKey: "value" } },
121
+ api,
122
+ )
123
+ const result = lineFn(context)
124
+ const container = document.createElement("div")
125
+ render(result, container)
126
+
127
+ const path = container.querySelector("path")
128
+ expect(path).toBeTruthy()
129
+ })
130
+
131
+ it("should render dots when showDots is true", () => {
132
+ const context = {
133
+ xScale: (x) => x * 100,
134
+ yScale: (y) => 400 - y * 2,
135
+ dimensions: {
136
+ width: 800,
137
+ height: 400,
138
+ padding: { top: 20, right: 50, bottom: 30, left: 50 },
139
+ },
140
+ entity,
141
+ api,
142
+ }
143
+
144
+ const lineFn = line.renderLine(
145
+ entity,
146
+ { config: { dataKey: "value", showDots: true } },
147
+ api,
148
+ )
149
+ const result = lineFn(context)
150
+ const container = document.createElement("div")
151
+ render(result, container)
152
+
153
+ const dots = container.querySelectorAll("circle")
154
+ expect(dots.length).toBeGreaterThan(0)
155
+ })
156
+ })
157
+
158
+ describe("renderXAxis", () => {
159
+ it("should return template result", () => {
160
+ const result = line.renderXAxis(
161
+ entity,
162
+ { config: { dataKey: "value" } },
163
+ api,
164
+ )
165
+ expect(result).toBeDefined()
166
+ // renderXAxis returns svg template, which may be empty if context is missing
167
+ })
168
+ })
169
+
170
+ describe("renderYAxis", () => {
171
+ it("should return template result", () => {
172
+ const result = line.renderYAxis(entity, { config: {} }, api)
173
+ expect(result).toBeDefined()
174
+ // renderYAxis returns svg template, which may be empty if context is missing
175
+ })
176
+ })
177
+ })
@@ -0,0 +1,444 @@
1
+ /**
2
+ * @vitest-environment jsdom
3
+ */
4
+ import { describe, expect, it, vi } from "vitest"
5
+
6
+ import { chart } from "./index.js"
7
+ import {
8
+ areaChart,
9
+ barChart,
10
+ donutChart,
11
+ lineChart,
12
+ pieChart,
13
+ } from "./utils/chart-utils.js"
14
+
15
+ const sampleBarData = [
16
+ { label: "Jan", value: 100 },
17
+ { label: "Feb", value: 150 },
18
+ { label: "Mar", value: 120 },
19
+ ]
20
+
21
+ const sampleLineData = [
22
+ { x: 0, y: 50 },
23
+ { x: 1, y: 150 },
24
+ { x: 2, y: 120 },
25
+ ]
26
+
27
+ const samplePieData = [
28
+ { label: "Category A", value: 20 },
29
+ { label: "Category B", value: 35 },
30
+ { label: "Category C", value: 15 },
31
+ ]
32
+
33
+ describe("chart", () => {
34
+ describe("logic", () => {
35
+ describe("create()", () => {
36
+ it("should initialize with default state", () => {
37
+ const entity = {
38
+ id: "test-chart",
39
+ type: "bar",
40
+ }
41
+
42
+ chart.create(entity)
43
+
44
+ expect(entity.width).toBe(800)
45
+ expect(entity.height).toBe(400)
46
+ expect(entity.padding).toBeDefined()
47
+ expect(entity.data).toEqual([])
48
+ expect(entity.colors).toEqual([
49
+ "#3b82f6",
50
+ "#ef4444",
51
+ "#10b981",
52
+ "#f59e0b",
53
+ "#8b5cf6",
54
+ ])
55
+ expect(entity.showLegend).toBe(true)
56
+ expect(entity.showGrid).toBe(true)
57
+ expect(entity.showTooltip).toBe(true)
58
+ expect(entity.tooltip).toBe(null)
59
+ expect(entity.tooltipX).toBe(0)
60
+ expect(entity.tooltipY).toBe(0)
61
+ expect(entity.labelPosition).toBe("outside")
62
+ })
63
+
64
+ it("should preserve existing values", () => {
65
+ const entity = {
66
+ id: "test-chart",
67
+ type: "bar",
68
+ width: 1000,
69
+ height: 500,
70
+ data: sampleBarData,
71
+ }
72
+
73
+ chart.create(entity)
74
+
75
+ expect(entity.width).toBe(1000)
76
+ expect(entity.height).toBe(500)
77
+ expect(entity.data).toEqual(sampleBarData)
78
+ })
79
+
80
+ it("should detect time axis type from dates", () => {
81
+ const entity = {
82
+ id: "test-chart",
83
+ type: "line",
84
+ data: [{ date: "2024-01-01", value: 100 }],
85
+ }
86
+
87
+ chart.create(entity)
88
+
89
+ expect(entity.xAxisType).toBe("time")
90
+ })
91
+
92
+ it("should default to linear axis type without dates", () => {
93
+ const entity = {
94
+ id: "test-chart",
95
+ type: "line",
96
+ data: sampleLineData,
97
+ }
98
+
99
+ chart.create(entity)
100
+
101
+ expect(entity.xAxisType).toBe("linear")
102
+ })
103
+ })
104
+
105
+ describe("dataUpdate()", () => {
106
+ it("should update chart data", () => {
107
+ const entity = {
108
+ id: "test-chart",
109
+ type: "bar",
110
+ data: sampleBarData,
111
+ }
112
+
113
+ chart.create(entity)
114
+ const newData = [{ label: "Apr", value: 200 }]
115
+ chart.dataUpdate(entity, newData)
116
+
117
+ expect(entity.data).toEqual(newData)
118
+ })
119
+ })
120
+
121
+ describe("sizeUpdate()", () => {
122
+ it("should update width and height", () => {
123
+ const entity = {
124
+ id: "test-chart",
125
+ type: "bar",
126
+ }
127
+
128
+ chart.create(entity)
129
+ chart.sizeUpdate(entity, 1200, 600)
130
+
131
+ expect(entity.width).toBe(1200)
132
+ expect(entity.height).toBe(600)
133
+ expect(entity.padding).toBeDefined()
134
+ })
135
+ })
136
+
137
+ describe("tooltipShow()", () => {
138
+ it("should set tooltip data and position", () => {
139
+ const entity = {
140
+ id: "test-chart",
141
+ type: "bar",
142
+ }
143
+
144
+ chart.create(entity)
145
+ chart.tooltipShow(entity, {
146
+ label: "Jan",
147
+ value: 100,
148
+ color: "#3b82f6",
149
+ x: 100,
150
+ y: 200,
151
+ })
152
+
153
+ expect(entity.tooltip).toEqual({
154
+ label: "Jan",
155
+ value: 100,
156
+ color: "#3b82f6",
157
+ })
158
+ expect(entity.tooltipX).toBe(100)
159
+ expect(entity.tooltipY).toBe(200)
160
+ })
161
+
162
+ it("should set tooltip with percentage", () => {
163
+ const entity = {
164
+ id: "test-chart",
165
+ type: "pie",
166
+ }
167
+
168
+ chart.create(entity)
169
+ chart.tooltipShow(entity, {
170
+ label: "Category A",
171
+ value: 20,
172
+ percentage: 25,
173
+ color: "#3b82f6",
174
+ x: 150,
175
+ y: 250,
176
+ })
177
+
178
+ expect(entity.tooltip).toEqual({
179
+ label: "Category A",
180
+ value: 20,
181
+ percentage: 25,
182
+ color: "#3b82f6",
183
+ })
184
+ })
185
+ })
186
+
187
+ describe("tooltipHide()", () => {
188
+ it("should clear tooltip", () => {
189
+ const entity = {
190
+ id: "test-chart",
191
+ type: "bar",
192
+ }
193
+
194
+ chart.create(entity)
195
+ chart.tooltipShow(entity, {
196
+ label: "Jan",
197
+ value: 100,
198
+ color: "#3b82f6",
199
+ x: 100,
200
+ y: 200,
201
+ })
202
+ chart.tooltipHide(entity)
203
+
204
+ expect(entity.tooltip).toBe(null)
205
+ })
206
+ })
207
+
208
+ describe("tooltipMove()", () => {
209
+ it("should update tooltip position", () => {
210
+ const entity = {
211
+ id: "test-chart",
212
+ type: "bar",
213
+ }
214
+
215
+ chart.create(entity)
216
+ chart.tooltipShow(entity, {
217
+ label: "Jan",
218
+ value: 100,
219
+ color: "#3b82f6",
220
+ x: 100,
221
+ y: 200,
222
+ })
223
+ chart.tooltipMove(entity, { x: 150, y: 250 })
224
+
225
+ expect(entity.tooltipX).toBe(150)
226
+ expect(entity.tooltipY).toBe(250)
227
+ })
228
+
229
+ it("should not update position if tooltip is not shown", () => {
230
+ const entity = {
231
+ id: "test-chart",
232
+ type: "bar",
233
+ }
234
+
235
+ chart.create(entity)
236
+ chart.tooltipMove(entity, { x: 150, y: 250 })
237
+
238
+ expect(entity.tooltipX).toBe(0)
239
+ expect(entity.tooltipY).toBe(0)
240
+ })
241
+ })
242
+ })
243
+
244
+ describe("rendering", () => {
245
+ describe("render()", () => {
246
+ it("should render chart using api.getType", () => {
247
+ const entity = {
248
+ id: "test-chart",
249
+ type: "bar",
250
+ data: sampleBarData,
251
+ width: 800,
252
+ height: 400,
253
+ }
254
+
255
+ chart.create(entity)
256
+
257
+ const mockApi = {
258
+ getType: vi.fn(() => barChart),
259
+ }
260
+
261
+ const result = chart.render(entity, mockApi)
262
+
263
+ expect(mockApi.getType).toHaveBeenCalledWith("bar")
264
+ expect(result).toBeDefined()
265
+ })
266
+
267
+ it("should return error message for unknown chart type", () => {
268
+ const entity = {
269
+ id: "test-chart",
270
+ type: "unknown",
271
+ }
272
+
273
+ chart.create(entity)
274
+
275
+ const mockApi = {
276
+ getType: vi.fn(() => null),
277
+ }
278
+
279
+ const result = chart.render(entity, mockApi)
280
+
281
+ expect(mockApi.getType).toHaveBeenCalledWith("unknown")
282
+ expect(result).toBeDefined()
283
+ })
284
+ })
285
+ })
286
+
287
+ describe("chart types", () => {
288
+ describe("barChart", () => {
289
+ it("should have create, render, and render methods", () => {
290
+ expect(barChart.create).toBeDefined()
291
+ expect(barChart.render).toBeDefined()
292
+ expect(barChart.render).toBeDefined()
293
+ })
294
+
295
+ it("should render chart with data", () => {
296
+ const entity = {
297
+ id: "test-bar",
298
+ type: "bar",
299
+ data: sampleBarData,
300
+ width: 800,
301
+ height: 400,
302
+ }
303
+
304
+ barChart.create(entity)
305
+ const mockApi = {
306
+ getType: vi.fn((type) => {
307
+ if (type === "bar") return barChart
308
+ return null
309
+ }),
310
+ notify: vi.fn(),
311
+ }
312
+ const result = barChart.render(entity, mockApi)
313
+
314
+ expect(result).toBeDefined()
315
+ })
316
+
317
+ it("should render empty state when no data", () => {
318
+ const entity = {
319
+ id: "test-bar",
320
+ type: "bar",
321
+ data: [],
322
+ width: 800,
323
+ height: 400,
324
+ }
325
+
326
+ barChart.create(entity)
327
+ const mockApi = {
328
+ getType: vi.fn((type) => {
329
+ if (type === "bar") return barChart
330
+ return null
331
+ }),
332
+ notify: vi.fn(),
333
+ }
334
+ const result = barChart.render(entity, mockApi)
335
+
336
+ expect(result).toBeDefined()
337
+ })
338
+ })
339
+
340
+ describe("lineChart", () => {
341
+ it("should have create, render, and render methods", () => {
342
+ expect(lineChart.create).toBeDefined()
343
+ expect(lineChart.render).toBeDefined()
344
+ expect(lineChart.render).toBeDefined()
345
+ })
346
+
347
+ it("should render chart with data", () => {
348
+ const entity = {
349
+ id: "test-line",
350
+ type: "line",
351
+ data: sampleLineData,
352
+ width: 800,
353
+ height: 400,
354
+ }
355
+
356
+ lineChart.create(entity)
357
+ const mockApi = {
358
+ getType: vi.fn((type) => {
359
+ if (type === "line") return lineChart
360
+ return null
361
+ }),
362
+ notify: vi.fn(),
363
+ }
364
+ const result = lineChart.render(entity, mockApi)
365
+
366
+ expect(result).toBeDefined()
367
+ })
368
+ })
369
+
370
+ describe("areaChart", () => {
371
+ it("should have render method", () => {
372
+ expect(areaChart.render).toBeDefined()
373
+ })
374
+
375
+ it("should render chart with data", () => {
376
+ const entity = {
377
+ id: "test-area",
378
+ type: "area",
379
+ data: sampleLineData,
380
+ width: 800,
381
+ height: 400,
382
+ }
383
+
384
+ chart.create(entity)
385
+ const mockApi = {
386
+ getType: vi.fn((type) => {
387
+ if (type === "area") return areaChart
388
+ return null
389
+ }),
390
+ notify: vi.fn(),
391
+ }
392
+ const result = areaChart.render(entity, mockApi)
393
+
394
+ expect(result).toBeDefined()
395
+ })
396
+ })
397
+
398
+ describe("pieChart", () => {
399
+ it("should have create, render, and render methods", () => {
400
+ expect(pieChart.create).toBeDefined()
401
+ expect(pieChart.render).toBeDefined()
402
+ expect(pieChart.render).toBeDefined()
403
+ })
404
+
405
+ it("should render chart with data", () => {
406
+ const entity = {
407
+ id: "test-pie",
408
+ type: "pie",
409
+ data: samplePieData,
410
+ width: 800,
411
+ height: 400,
412
+ }
413
+
414
+ pieChart.create(entity)
415
+ const result = pieChart.render(entity, {})
416
+
417
+ expect(result).toBeDefined()
418
+ })
419
+ })
420
+
421
+ describe("donutChart", () => {
422
+ it("should have create, render, and render methods", () => {
423
+ expect(donutChart.create).toBeDefined()
424
+ expect(donutChart.render).toBeDefined()
425
+ expect(donutChart.render).toBeDefined()
426
+ })
427
+
428
+ it("should render chart with data", () => {
429
+ const entity = {
430
+ id: "test-donut",
431
+ type: "donut",
432
+ data: samplePieData,
433
+ width: 400,
434
+ height: 400,
435
+ }
436
+
437
+ donutChart.create(entity)
438
+ const result = donutChart.render(entity, {})
439
+
440
+ expect(result).toBeDefined()
441
+ })
442
+ })
443
+ })
444
+ })