@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,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
|
+
}
|