@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,392 @@
1
+ /* eslint-disable no-magic-numbers */
2
+ import { html, repeat, svg } from "@inglorious/web"
3
+
4
+ import { createBrushComponent } from "../component/brush.js"
5
+ import { renderGrid } from "../component/grid.js"
6
+ import { createTooltipComponent, renderTooltip } from "../component/tooltip.js"
7
+ import { renderXAxis } from "../component/x-axis.js"
8
+ import { renderYAxis } from "../component/y-axis.js"
9
+ import { chart } from "../index.js"
10
+ import { renderCurve } from "../shape/curve.js"
11
+ import { renderDot } from "../shape/dot.js"
12
+ import { getTransformedData, isMultiSeries } from "../utils/data-utils.js"
13
+ import {
14
+ generateAreaPath,
15
+ generateLinePath,
16
+ generateStackedAreaPath,
17
+ } from "../utils/paths.js"
18
+ import { processDeclarativeChild } from "../utils/process-declarative-child.js"
19
+ import { createSharedContext } from "../utils/shared-context.js"
20
+ import { createTooltipHandlers } from "../utils/tooltip-handlers.js"
21
+
22
+ export const area = {
23
+ /**
24
+ * Config-based rendering entry point.
25
+ * Builds default composition children from entity options and delegates to
26
+ * `renderAreaChart`.
27
+ * @param {import('../types/charts').ChartEntity} entity
28
+ * @param {import('@inglorious/web').Api} api
29
+ * @returns {import('lit-html').TemplateResult}
30
+ */
31
+ render(entity, api) {
32
+ const type = api.getType(entity.type)
33
+ const children = buildChildrenFromConfig(entity)
34
+
35
+ return type.renderAreaChart(
36
+ entity,
37
+ {
38
+ children,
39
+ config: {
40
+ width: entity.width,
41
+ height: entity.height,
42
+ stacked: entity.stacked === true,
43
+ },
44
+ },
45
+ api,
46
+ )
47
+ },
48
+
49
+ /**
50
+ * Composition rendering entry point for area charts.
51
+ * @param {import('../types/charts').ChartEntity} entity
52
+ * @param {{ children: any[]|any, config?: Record<string, any> }} params
53
+ * @param {import('@inglorious/web').Api} api
54
+ * @returns {import('lit-html').TemplateResult}
55
+ */
56
+ renderAreaChart(entity, { children, config = {} }, api) {
57
+ if (!entity) return svg`<text>Entity not found</text>`
58
+
59
+ const entityWithData = config.data
60
+ ? { ...entity, data: config.data }
61
+ : entity
62
+ const context = createSharedContext(
63
+ entityWithData,
64
+ {
65
+ width: config.width,
66
+ height: config.height,
67
+ padding: config.padding,
68
+ chartType: "area",
69
+ stacked: config.stacked === true,
70
+ },
71
+ api,
72
+ )
73
+ context.api = api
74
+
75
+ if (config.stacked === true) {
76
+ context.stack = {
77
+ sumsByStackId: new Map(),
78
+ computedByKey: new Map(),
79
+ }
80
+ }
81
+
82
+ const childrenArray = Array.isArray(children) ? children : [children]
83
+
84
+ const processedChildrenArray = childrenArray
85
+ .map((child) =>
86
+ processDeclarativeChild(child, entityWithData, "area", api),
87
+ )
88
+ .filter(Boolean)
89
+
90
+ const grid = [],
91
+ axes = [],
92
+ areas = [],
93
+ dots = [],
94
+ tooltip = [],
95
+ others = []
96
+
97
+ for (const child of processedChildrenArray) {
98
+ if (typeof child === "function") {
99
+ if (child.isGrid) grid.push(child)
100
+ else if (child.isAxis) axes.push(child)
101
+ else if (child.isArea) areas.push(child)
102
+ else if (child.isDots) dots.push(child)
103
+ else if (child.isTooltip) tooltip.push(child)
104
+ else others.push(child)
105
+ } else {
106
+ others.push(child)
107
+ }
108
+ }
109
+
110
+ const isStacked = config.stacked === true
111
+ const sortedChildren = [
112
+ ...grid,
113
+ ...(isStacked ? areas : [...areas].reverse()),
114
+ ...axes,
115
+ ...dots,
116
+ ...tooltip,
117
+ ...others,
118
+ ]
119
+
120
+ const finalRendered = sortedChildren.map((child) => {
121
+ if (typeof child !== "function") return child
122
+ const result = child(context)
123
+ return typeof result === "function" ? result(context) : result
124
+ })
125
+
126
+ return html`
127
+ <div
128
+ class="iw-chart"
129
+ style="display: block; position: relative; width: 100%;"
130
+ >
131
+ <svg
132
+ width=${context.dimensions.width}
133
+ height=${context.dimensions.height}
134
+ viewBox="0 0 ${context.dimensions.width} ${context.dimensions.height}"
135
+ >
136
+ ${finalRendered}
137
+ </svg>
138
+ ${renderTooltip(entityWithData, {}, api)}
139
+ </div>
140
+ `
141
+ },
142
+
143
+ /**
144
+ * Composition sub-render for cartesian grid.
145
+ * @param {import('../types/charts').ChartEntity} entity
146
+ * @param {{ config?: Record<string, any> }} params
147
+ * @param {import('@inglorious/web').Api} api
148
+ * @returns {(ctx: Record<string, any>) => import('lit-html').TemplateResult}
149
+ */
150
+ renderCartesianGrid(entity, { config = {} }, api) {
151
+ const gridFn = (ctx) => {
152
+ const { xScale, yScale, dimensions } = ctx
153
+ return renderGrid(
154
+ ctx.entity || entity,
155
+ { xScale, yScale, ...dimensions, ...config },
156
+ api,
157
+ )
158
+ }
159
+ gridFn.isGrid = true
160
+ return gridFn
161
+ },
162
+
163
+ /**
164
+ * Composition sub-render for X axis.
165
+ * @param {import('../types/charts').ChartEntity} entity
166
+ * @param {{ config?: Record<string, any> }} params
167
+ * @param {import('@inglorious/web').Api} api
168
+ * @returns {(ctx: Record<string, any>) => import('lit-html').TemplateResult}
169
+ */
170
+ renderXAxis(entity, { config = {} }, api) {
171
+ const axisFn = (ctx) => {
172
+ const { xScale, yScale, dimensions } = ctx
173
+ const ent = ctx.entity || entity
174
+ const labels = ent.data.map(
175
+ (d, i) => d[config.dataKey] || d.name || String(i),
176
+ )
177
+ return renderXAxis(
178
+ { ...ent, xLabels: labels },
179
+ { xScale, yScale, ...dimensions },
180
+ api,
181
+ )
182
+ }
183
+ axisFn.isAxis = true
184
+ return axisFn
185
+ },
186
+
187
+ /**
188
+ * Composition sub-render for Y axis.
189
+ * @param {import('../types/charts').ChartEntity} entity
190
+ * @param {{ config?: Record<string, any> }} params
191
+ * @param {import('@inglorious/web').Api} api
192
+ * @returns {(ctx: Record<string, any>) => import('lit-html').TemplateResult}
193
+ */
194
+ renderYAxis(entity, props, api) {
195
+ const axisFn = (ctx) => {
196
+ const { yScale, dimensions } = ctx
197
+ return renderYAxis(ctx.entity || entity, { yScale, ...dimensions }, api)
198
+ }
199
+ axisFn.isAxis = true
200
+ return axisFn
201
+ },
202
+
203
+ /**
204
+ * Composition sub-render for area paths.
205
+ * @param {import('../types/charts').ChartEntity} entity
206
+ * @param {{ config?: Record<string, any> }} params
207
+ * @param {import('@inglorious/web').Api} api
208
+ * @returns {(ctx: Record<string, any>) => import('lit-html').TemplateResult}
209
+ */
210
+ // eslint-disable-next-line no-unused-vars
211
+ renderArea(entity, { config = {} }, api) {
212
+ const areaFn = (ctx) => {
213
+ const { xScale, yScale } = ctx
214
+ const {
215
+ dataKey,
216
+ fill = "#8884d8",
217
+ fillOpacity = "0.6",
218
+ stroke,
219
+ stackId,
220
+ } = config
221
+ const data = getTransformedData(ctx.entity || entity, dataKey)
222
+ if (!data) return svg``
223
+
224
+ const isStacked = Boolean(stackId) && Boolean(ctx.stack)
225
+ let areaPath, linePath
226
+
227
+ if (isStacked) {
228
+ const stackKey = String(stackId)
229
+ const sums =
230
+ ctx.stack.sumsByStackId.get(stackKey) || Array(data.length).fill(0)
231
+ const seriesStack = data.map((d, i) => [sums[i], sums[i] + (d.y || 0)])
232
+ ctx.stack.sumsByStackId.set(
233
+ stackKey,
234
+ seriesStack.map((p) => p[1]),
235
+ )
236
+ ctx.stack.computedByKey.set(`${stackKey}:${dataKey}`, seriesStack)
237
+ areaPath = generateStackedAreaPath(data, xScale, yScale, seriesStack)
238
+ linePath = generateLinePath(
239
+ data.map((d, i) => ({ ...d, y: seriesStack[i][1] })),
240
+ xScale,
241
+ yScale,
242
+ )
243
+ } else {
244
+ areaPath = generateAreaPath(data, xScale, yScale, 0)
245
+ linePath = generateLinePath(data, xScale, yScale)
246
+ }
247
+
248
+ return svg`
249
+ <g class="iw-chart-area">
250
+ ${renderCurve({ d: areaPath, fill, fillOpacity })}
251
+ ${linePath ? renderCurve({ d: linePath, stroke: stroke || fill }) : ""}
252
+ </g>`
253
+ }
254
+ areaFn.isArea = true
255
+ return areaFn
256
+ },
257
+
258
+ /**
259
+ * Composition sub-render for area dots.
260
+ * @param {import('../types/charts').ChartEntity} entity
261
+ * @param {{ config?: Record<string, any> }} params
262
+ * @param {import('@inglorious/web').Api} api
263
+ * @returns {(ctx: Record<string, any>) => import('lit-html').TemplateResult}
264
+ */
265
+ renderDots(entity, { config = {} }, api) {
266
+ const dotsFn = (ctx) => {
267
+ const { xScale, yScale } = ctx
268
+ const entityFromContext = ctx.entity || entity
269
+ const { dataKey, fill = "#8884d8" } = config
270
+ const data = getTransformedData(entityFromContext, dataKey)
271
+ if (!data || data.length === 0) return svg``
272
+
273
+ const seriesStack = ctx.stack?.computedByKey.get(
274
+ `${config.stackId}:${dataKey}`,
275
+ )
276
+
277
+ return svg`
278
+ <g class="iw-chart-dots">
279
+ ${repeat(
280
+ data,
281
+ (d, i) => `${dataKey}-${i}`,
282
+ (d, i) => {
283
+ const x = xScale(d.x)
284
+ const y = yScale(seriesStack ? seriesStack[i]?.[1] : d.y)
285
+ // Get label from original data point (like line chart does)
286
+ const originalDataPoint = entityFromContext.data[i]
287
+ const label =
288
+ originalDataPoint?.name ||
289
+ originalDataPoint?.label ||
290
+ String(d.x)
291
+ const value = seriesStack ? seriesStack[i]?.[1] : d.y
292
+
293
+ const { onMouseEnter, onMouseLeave } = createTooltipHandlers({
294
+ entity: entityFromContext,
295
+ api: ctx.api || api,
296
+ tooltipData: { label, value, color: fill },
297
+ })
298
+ return renderDot({
299
+ cx: x,
300
+ cy: y,
301
+ fill,
302
+ onMouseEnter,
303
+ onMouseLeave,
304
+ })
305
+ },
306
+ )}
307
+ </g>`
308
+ }
309
+ dotsFn.isDots = true
310
+ return dotsFn
311
+ },
312
+
313
+ /**
314
+ * Composition sub-render for tooltip overlay.
315
+ * @type {(entity: import('../types/charts').ChartEntity, params: { config?: Record<string, any> }, api: import('@inglorious/web').Api) => (ctx: Record<string, any>) => import('lit-html').TemplateResult}
316
+ */
317
+ renderTooltip: createTooltipComponent(),
318
+
319
+ /**
320
+ * Composition sub-render for brush control.
321
+ * @type {(entity: import('../types/charts').ChartEntity, params: { config?: Record<string, any> }, api: import('@inglorious/web').Api) => (ctx: Record<string, any>) => import('lit-html').TemplateResult}
322
+ */
323
+ renderBrush: createBrushComponent(),
324
+ }
325
+
326
+ /**
327
+ * Builds declarative children from entity config for render (config style)
328
+ * Converts entity configuration into children objects that renderAreaChart can process
329
+ */
330
+ function buildChildrenFromConfig(entity) {
331
+ const children = []
332
+
333
+ if (entity.showGrid !== false) {
334
+ children.push(
335
+ chart.CartesianGrid({ stroke: "#eee", strokeDasharray: "5 5" }),
336
+ )
337
+ }
338
+
339
+ let xAxisDataKey = entity.dataKey
340
+ if (!xAxisDataKey && entity.data && entity.data.length > 0) {
341
+ const firstItem = entity.data[0]
342
+ xAxisDataKey = firstItem.name || firstItem.x || firstItem.date || "name"
343
+ }
344
+ children.push(chart.XAxis({ dataKey: xAxisDataKey || "name" }))
345
+ children.push(chart.YAxis({ width: "auto" }))
346
+
347
+ let dataKeys = []
348
+ if (isMultiSeries(entity.data)) {
349
+ dataKeys = entity.data.map(
350
+ (series, idx) => series.dataKey || series.name || `series${idx}`,
351
+ )
352
+ } else {
353
+ dataKeys = ["y", "value"].filter(
354
+ (key) =>
355
+ entity.data &&
356
+ entity.data.length > 0 &&
357
+ entity.data[0][key] !== undefined,
358
+ )
359
+ if (dataKeys.length === 0) dataKeys = ["value"]
360
+ }
361
+
362
+ const colors = entity.colors || ["#8884d8", "#82ca9d", "#ffc658", "#ff8042"]
363
+ const isStacked = entity.stacked === true
364
+
365
+ dataKeys.forEach((dataKey, index) => {
366
+ children.push(
367
+ chart.Area({
368
+ dataKey,
369
+ fill: colors[index % colors.length],
370
+ fillOpacity: "0.6",
371
+ stroke: colors[index % colors.length],
372
+ stackId: isStacked ? "1" : undefined,
373
+ }),
374
+ )
375
+ })
376
+
377
+ if (entity.showPoints !== false) {
378
+ dataKeys.forEach((dataKey, index) => {
379
+ children.push(
380
+ chart.Dots({
381
+ dataKey,
382
+ fill: colors[index % colors.length],
383
+ stackId: isStacked ? "1" : undefined,
384
+ }),
385
+ )
386
+ })
387
+ }
388
+
389
+ if (entity.showTooltip !== false) children.push(chart.Tooltip({}))
390
+
391
+ return children
392
+ }