@internetstiftelsen/charts 0.3.3 → 0.4.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/bar.d.ts +5 -2
- package/bar.js +43 -7
- package/base-chart.d.ts +7 -2
- package/base-chart.js +117 -9
- package/chart-interface.d.ts +4 -2
- package/donut-center-content.d.ts +9 -3
- package/donut-center-content.js +25 -0
- package/donut-chart.d.ts +2 -0
- package/donut-chart.js +33 -2
- package/grid.d.ts +5 -2
- package/grid.js +21 -0
- package/legend.d.ts +5 -3
- package/legend.js +25 -30
- package/line.d.ts +5 -2
- package/line.js +23 -1
- package/package.json +1 -1
- package/title.d.ts +5 -2
- package/title.js +26 -0
- package/tooltip.d.ts +5 -2
- package/tooltip.js +22 -1
- package/types.d.ts +47 -8
- package/utils.d.ts +2 -0
- package/utils.js +60 -0
- package/x-axis.d.ts +5 -2
- package/x-axis.js +29 -5
- package/xy-chart.d.ts +2 -0
- package/xy-chart.js +34 -0
- package/y-axis.d.ts +5 -2
- package/y-axis.js +23 -1
package/bar.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Selection } from 'd3';
|
|
2
|
-
import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType } from './types.js';
|
|
2
|
+
import type { BarConfig, BarStackingContext, BarValueLabelConfig, ChartTheme, D3Scale, DataItem, Orientation, ScaleType, ExportHooks, BarConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
-
export declare class Bar implements ChartComponent {
|
|
4
|
+
export declare class Bar implements ChartComponent<BarConfigBase> {
|
|
5
5
|
readonly type: "bar";
|
|
6
6
|
readonly dataKey: string;
|
|
7
7
|
readonly fill: string;
|
|
@@ -9,7 +9,10 @@ export declare class Bar implements ChartComponent {
|
|
|
9
9
|
readonly orientation: Orientation;
|
|
10
10
|
readonly maxBarSize?: number;
|
|
11
11
|
readonly valueLabel?: BarValueLabelConfig;
|
|
12
|
+
readonly exportHooks?: ExportHooks<BarConfigBase>;
|
|
12
13
|
constructor(config: BarConfig);
|
|
14
|
+
getExportConfig(): BarConfigBase;
|
|
15
|
+
createExportComponent(override?: Partial<BarConfigBase>): ChartComponent;
|
|
13
16
|
private getScaledPosition;
|
|
14
17
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType?: ScaleType, theme?: ChartTheme, stackingContext?: BarStackingContext): void;
|
|
15
18
|
private renderVertical;
|
package/bar.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { sanitizeForCSS } from './utils.js';
|
|
1
|
+
import { getContrastTextColor, sanitizeForCSS, mergeDeep } from './utils.js';
|
|
2
2
|
export class Bar {
|
|
3
3
|
constructor(config) {
|
|
4
4
|
Object.defineProperty(this, "type", {
|
|
@@ -43,12 +43,36 @@ export class Bar {
|
|
|
43
43
|
writable: true,
|
|
44
44
|
value: void 0
|
|
45
45
|
});
|
|
46
|
+
Object.defineProperty(this, "exportHooks", {
|
|
47
|
+
enumerable: true,
|
|
48
|
+
configurable: true,
|
|
49
|
+
writable: true,
|
|
50
|
+
value: void 0
|
|
51
|
+
});
|
|
46
52
|
this.dataKey = config.dataKey;
|
|
47
53
|
this.fill = config.fill || '#8884d8';
|
|
48
54
|
this.colorAdapter = config.colorAdapter;
|
|
49
55
|
this.orientation = config.orientation || 'vertical';
|
|
50
56
|
this.maxBarSize = config.maxBarSize;
|
|
51
57
|
this.valueLabel = config.valueLabel;
|
|
58
|
+
this.exportHooks = config.exportHooks;
|
|
59
|
+
}
|
|
60
|
+
getExportConfig() {
|
|
61
|
+
return {
|
|
62
|
+
dataKey: this.dataKey,
|
|
63
|
+
fill: this.fill,
|
|
64
|
+
colorAdapter: this.colorAdapter,
|
|
65
|
+
orientation: this.orientation,
|
|
66
|
+
maxBarSize: this.maxBarSize,
|
|
67
|
+
valueLabel: this.valueLabel,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
createExportComponent(override) {
|
|
71
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
72
|
+
return new Bar({
|
|
73
|
+
...merged,
|
|
74
|
+
exportHooks: this.exportHooks,
|
|
75
|
+
});
|
|
52
76
|
}
|
|
53
77
|
getScaledPosition(data, key, scale, scaleType) {
|
|
54
78
|
const value = data[key];
|
|
@@ -338,7 +362,7 @@ export class Bar {
|
|
|
338
362
|
const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
|
|
339
363
|
const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
|
|
340
364
|
const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
|
|
341
|
-
const
|
|
365
|
+
const defaultLabelColor = config.color ?? theme.valueLabel.color;
|
|
342
366
|
const background = config.background ?? theme.valueLabel.background;
|
|
343
367
|
const border = config.border ?? theme.valueLabel.border;
|
|
344
368
|
const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
|
|
@@ -346,11 +370,14 @@ export class Bar {
|
|
|
346
370
|
const labelGroup = plotGroup
|
|
347
371
|
.append('g')
|
|
348
372
|
.attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
349
|
-
data.forEach((d) => {
|
|
373
|
+
data.forEach((d, i) => {
|
|
350
374
|
const categoryKey = String(d[xKey]);
|
|
351
375
|
const value = parseValue(d[this.dataKey]);
|
|
352
376
|
const valueText = String(value);
|
|
353
377
|
const xPos = this.getScaledPosition(d, xKey, x, xScaleType);
|
|
378
|
+
const barColor = this.colorAdapter
|
|
379
|
+
? this.colorAdapter(d, i)
|
|
380
|
+
: this.fill;
|
|
354
381
|
// Calculate bar position based on stacking mode
|
|
355
382
|
let barTop;
|
|
356
383
|
let barBottom;
|
|
@@ -473,6 +500,9 @@ export class Bar {
|
|
|
473
500
|
}
|
|
474
501
|
tempText.remove();
|
|
475
502
|
if (shouldRender) {
|
|
503
|
+
const labelColor = position === 'inside' && config.color === undefined
|
|
504
|
+
? getContrastTextColor(barColor)
|
|
505
|
+
: defaultLabelColor;
|
|
476
506
|
const group = labelGroup.append('g');
|
|
477
507
|
if (position === 'outside') {
|
|
478
508
|
// Draw rounded rectangle background
|
|
@@ -498,7 +528,7 @@ export class Bar {
|
|
|
498
528
|
.style('font-size', `${fontSize}px`)
|
|
499
529
|
.style('font-family', fontFamily)
|
|
500
530
|
.style('font-weight', fontWeight)
|
|
501
|
-
.style('fill',
|
|
531
|
+
.style('fill', labelColor)
|
|
502
532
|
.style('pointer-events', 'none')
|
|
503
533
|
.text(valueText);
|
|
504
534
|
}
|
|
@@ -550,7 +580,7 @@ export class Bar {
|
|
|
550
580
|
const fontSize = config.fontSize ?? theme.valueLabel.fontSize;
|
|
551
581
|
const fontFamily = config.fontFamily ?? theme.valueLabel.fontFamily;
|
|
552
582
|
const fontWeight = config.fontWeight ?? theme.valueLabel.fontWeight;
|
|
553
|
-
const
|
|
583
|
+
const defaultLabelColor = config.color ?? theme.valueLabel.color;
|
|
554
584
|
const background = config.background ?? theme.valueLabel.background;
|
|
555
585
|
const border = config.border ?? theme.valueLabel.border;
|
|
556
586
|
const borderRadius = config.borderRadius ?? theme.valueLabel.borderRadius;
|
|
@@ -558,11 +588,14 @@ export class Bar {
|
|
|
558
588
|
const labelGroup = plotGroup
|
|
559
589
|
.append('g')
|
|
560
590
|
.attr('class', `bar-value-labels-${sanitizeForCSS(this.dataKey)}`);
|
|
561
|
-
data.forEach((d) => {
|
|
591
|
+
data.forEach((d, i) => {
|
|
562
592
|
const categoryKey = String(d[xKey]);
|
|
563
593
|
const value = parseValue(d[this.dataKey]);
|
|
564
594
|
const valueText = String(value);
|
|
565
595
|
const yPos = this.getScaledPosition(d, xKey, y, yScaleType);
|
|
596
|
+
const barColor = this.colorAdapter
|
|
597
|
+
? this.colorAdapter(d, i)
|
|
598
|
+
: this.fill;
|
|
566
599
|
// Calculate bar position based on stacking mode
|
|
567
600
|
let barLeft;
|
|
568
601
|
let barRight;
|
|
@@ -687,6 +720,9 @@ export class Bar {
|
|
|
687
720
|
}
|
|
688
721
|
tempText.remove();
|
|
689
722
|
if (shouldRender) {
|
|
723
|
+
const labelColor = position === 'inside' && config.color === undefined
|
|
724
|
+
? getContrastTextColor(barColor)
|
|
725
|
+
: defaultLabelColor;
|
|
690
726
|
const group = labelGroup.append('g');
|
|
691
727
|
if (position === 'outside') {
|
|
692
728
|
// Draw rounded rectangle background
|
|
@@ -712,7 +748,7 @@ export class Bar {
|
|
|
712
748
|
.style('font-size', `${fontSize}px`)
|
|
713
749
|
.style('font-family', fontFamily)
|
|
714
750
|
.style('font-weight', fontWeight)
|
|
715
|
-
.style('fill',
|
|
751
|
+
.style('fill', labelColor)
|
|
716
752
|
.style('pointer-events', 'none')
|
|
717
753
|
.text(valueText);
|
|
718
754
|
}
|
package/base-chart.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale } from './types.js';
|
|
2
|
+
import type { DataItem, ChartTheme, AxisScaleConfig, ExportFormat, ExportOptions, D3Scale, ExportHookContext, ExportRenderContext } from './types.js';
|
|
3
3
|
import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
|
|
4
4
|
import type { XAxis } from './x-axis.js';
|
|
5
5
|
import type { YAxis } from './y-axis.js';
|
|
@@ -54,6 +54,10 @@ export declare abstract class BaseChart {
|
|
|
54
54
|
* Override in subclasses to provide chart-specific components
|
|
55
55
|
*/
|
|
56
56
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
57
|
+
protected getExportComponents(): ChartComponent[];
|
|
58
|
+
protected collectExportOverrides(context: ExportRenderContext): Map<ChartComponent, Record<string, unknown>>;
|
|
59
|
+
protected runExportHooks(context: ExportHookContext): void;
|
|
60
|
+
private renderExportChart;
|
|
57
61
|
protected prepareLayout(): void;
|
|
58
62
|
/**
|
|
59
63
|
* Setup ResizeObserver for automatic resize handling
|
|
@@ -63,6 +67,7 @@ export declare abstract class BaseChart {
|
|
|
63
67
|
* Subclasses must implement this method to define their rendering logic
|
|
64
68
|
*/
|
|
65
69
|
protected abstract renderChart(): void;
|
|
70
|
+
protected abstract createExportChart(): BaseChart;
|
|
66
71
|
/**
|
|
67
72
|
* Updates the chart with new data
|
|
68
73
|
*/
|
|
@@ -83,6 +88,6 @@ export declare abstract class BaseChart {
|
|
|
83
88
|
* Downloads the exported content as a file
|
|
84
89
|
*/
|
|
85
90
|
private downloadContent;
|
|
86
|
-
protected exportSVG(): string;
|
|
91
|
+
protected exportSVG(options?: ExportOptions): string;
|
|
87
92
|
protected exportJSON(): string;
|
|
88
93
|
}
|
package/base-chart.js
CHANGED
|
@@ -185,16 +185,87 @@ export class BaseChart {
|
|
|
185
185
|
*/
|
|
186
186
|
getLayoutComponents() {
|
|
187
187
|
const components = [];
|
|
188
|
-
if (this.title)
|
|
188
|
+
if (this.title) {
|
|
189
189
|
components.push(this.title);
|
|
190
|
-
|
|
190
|
+
}
|
|
191
|
+
if (this.xAxis) {
|
|
191
192
|
components.push(this.xAxis);
|
|
192
|
-
|
|
193
|
+
}
|
|
194
|
+
if (this.yAxis) {
|
|
193
195
|
components.push(this.yAxis);
|
|
194
|
-
|
|
196
|
+
}
|
|
197
|
+
if (this.legend) {
|
|
195
198
|
components.push(this.legend);
|
|
199
|
+
}
|
|
196
200
|
return components;
|
|
197
201
|
}
|
|
202
|
+
getExportComponents() {
|
|
203
|
+
const components = [];
|
|
204
|
+
if (this.title) {
|
|
205
|
+
components.push(this.title);
|
|
206
|
+
}
|
|
207
|
+
if (this.grid) {
|
|
208
|
+
components.push(this.grid);
|
|
209
|
+
}
|
|
210
|
+
if (this.xAxis) {
|
|
211
|
+
components.push(this.xAxis);
|
|
212
|
+
}
|
|
213
|
+
if (this.yAxis) {
|
|
214
|
+
components.push(this.yAxis);
|
|
215
|
+
}
|
|
216
|
+
if (this.tooltip) {
|
|
217
|
+
components.push(this.tooltip);
|
|
218
|
+
}
|
|
219
|
+
if (this.legend) {
|
|
220
|
+
components.push(this.legend);
|
|
221
|
+
}
|
|
222
|
+
return components;
|
|
223
|
+
}
|
|
224
|
+
collectExportOverrides(context) {
|
|
225
|
+
const overrides = new Map();
|
|
226
|
+
const components = this.getExportComponents();
|
|
227
|
+
components.forEach((component) => {
|
|
228
|
+
const exportable = component;
|
|
229
|
+
const currentConfig = exportable.getExportConfig?.() ?? {};
|
|
230
|
+
const result = component.exportHooks?.beforeRender?.call(component, context, currentConfig);
|
|
231
|
+
if (result &&
|
|
232
|
+
typeof result === 'object' &&
|
|
233
|
+
exportable.createExportComponent) {
|
|
234
|
+
overrides.set(component, result);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return overrides;
|
|
238
|
+
}
|
|
239
|
+
runExportHooks(context) {
|
|
240
|
+
const components = this.getExportComponents();
|
|
241
|
+
components.forEach((component) => {
|
|
242
|
+
component.exportHooks?.before?.call(component, context);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
renderExportChart(chart, width, height) {
|
|
246
|
+
const container = document.createElement('div');
|
|
247
|
+
const containerId = `chart-export-${Math.random()
|
|
248
|
+
.toString(36)
|
|
249
|
+
.slice(2)}`;
|
|
250
|
+
container.id = containerId;
|
|
251
|
+
container.style.position = 'absolute';
|
|
252
|
+
container.style.left = '-99999px';
|
|
253
|
+
container.style.top = '0';
|
|
254
|
+
container.style.width = `${width}px`;
|
|
255
|
+
container.style.height = `${height}px`;
|
|
256
|
+
container.style.visibility = 'hidden';
|
|
257
|
+
document.body.appendChild(container);
|
|
258
|
+
chart.render(`#${containerId}`);
|
|
259
|
+
const svg = chart.svg?.node();
|
|
260
|
+
if (!svg) {
|
|
261
|
+
chart.destroy();
|
|
262
|
+
document.body.removeChild(container);
|
|
263
|
+
throw new Error('Failed to render export SVG');
|
|
264
|
+
}
|
|
265
|
+
chart.destroy();
|
|
266
|
+
document.body.removeChild(container);
|
|
267
|
+
return svg;
|
|
268
|
+
}
|
|
198
269
|
// Hook for subclasses to update component layout estimates before layout calc
|
|
199
270
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
200
271
|
prepareLayout() { }
|
|
@@ -255,7 +326,7 @@ export class BaseChart {
|
|
|
255
326
|
* @returns The exported content as a string if download is false/undefined, void if download is true
|
|
256
327
|
*/
|
|
257
328
|
export(format, options) {
|
|
258
|
-
const content = format === 'svg' ? this.exportSVG() : this.exportJSON();
|
|
329
|
+
const content = format === 'svg' ? this.exportSVG(options) : this.exportJSON();
|
|
259
330
|
if (options?.download) {
|
|
260
331
|
this.downloadContent(content, format, options);
|
|
261
332
|
return;
|
|
@@ -277,15 +348,52 @@ export class BaseChart {
|
|
|
277
348
|
document.body.removeChild(link);
|
|
278
349
|
URL.revokeObjectURL(url);
|
|
279
350
|
}
|
|
280
|
-
exportSVG() {
|
|
351
|
+
exportSVG(options) {
|
|
281
352
|
if (!this.svg) {
|
|
282
353
|
throw new Error('Chart must be rendered before export');
|
|
283
354
|
}
|
|
355
|
+
const exportWidth = options?.width ?? this.width;
|
|
356
|
+
const exportHeight = options?.height ?? this.height;
|
|
357
|
+
const requiresExportRender = exportWidth !== this.width || exportHeight !== this.height;
|
|
284
358
|
const clone = this.svg.node().cloneNode(true);
|
|
285
359
|
clone.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
286
|
-
clone.setAttribute('width', String(
|
|
287
|
-
clone.setAttribute('height', String(
|
|
288
|
-
|
|
360
|
+
clone.setAttribute('width', String(exportWidth));
|
|
361
|
+
clone.setAttribute('height', String(exportHeight));
|
|
362
|
+
const baseContext = {
|
|
363
|
+
format: 'svg',
|
|
364
|
+
options,
|
|
365
|
+
width: exportWidth,
|
|
366
|
+
height: exportHeight,
|
|
367
|
+
};
|
|
368
|
+
const overrides = this.collectExportOverrides(baseContext);
|
|
369
|
+
if (overrides.size === 0 && !requiresExportRender) {
|
|
370
|
+
this.runExportHooks({
|
|
371
|
+
...baseContext,
|
|
372
|
+
svg: clone,
|
|
373
|
+
});
|
|
374
|
+
return clone.outerHTML;
|
|
375
|
+
}
|
|
376
|
+
const exportChart = this.createExportChart();
|
|
377
|
+
const components = this.getExportComponents();
|
|
378
|
+
components.forEach((component) => {
|
|
379
|
+
const exportable = component;
|
|
380
|
+
const override = overrides.get(component);
|
|
381
|
+
if (exportable.createExportComponent) {
|
|
382
|
+
exportChart.addChild(exportable.createExportComponent(override));
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
exportChart.addChild(component);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
const exportSvg = this.renderExportChart(exportChart, exportWidth, exportHeight);
|
|
389
|
+
exportSvg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
|
|
390
|
+
exportSvg.setAttribute('width', String(exportWidth));
|
|
391
|
+
exportSvg.setAttribute('height', String(exportHeight));
|
|
392
|
+
this.runExportHooks({
|
|
393
|
+
...baseContext,
|
|
394
|
+
svg: exportSvg,
|
|
395
|
+
});
|
|
396
|
+
return exportSvg.outerHTML;
|
|
289
397
|
}
|
|
290
398
|
exportJSON() {
|
|
291
399
|
return JSON.stringify({
|
package/chart-interface.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import type { ExportHooks } from './types.js';
|
|
2
|
+
export interface ChartComponent<TConfig = any> {
|
|
2
3
|
type: 'line' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
4
|
+
exportHooks?: ExportHooks<TConfig>;
|
|
3
5
|
}
|
|
4
6
|
export type ComponentSpace = {
|
|
5
7
|
width: number;
|
|
6
8
|
height: number;
|
|
7
9
|
position: 'top' | 'bottom' | 'left' | 'right';
|
|
8
10
|
};
|
|
9
|
-
export interface LayoutAwareComponent extends ChartComponent {
|
|
11
|
+
export interface LayoutAwareComponent<TConfig = any> extends ChartComponent<TConfig> {
|
|
10
12
|
getRequiredSpace(): ComponentSpace;
|
|
11
13
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { ChartTheme } from './types.js';
|
|
2
|
+
import type { ChartTheme, ExportHooks } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
type TextStyle = {
|
|
5
5
|
fontSize?: number;
|
|
@@ -7,7 +7,7 @@ type TextStyle = {
|
|
|
7
7
|
fontFamily?: string;
|
|
8
8
|
color?: string;
|
|
9
9
|
};
|
|
10
|
-
export type
|
|
10
|
+
export type DonutCenterContentConfigBase = {
|
|
11
11
|
mainValue?: string;
|
|
12
12
|
title?: string;
|
|
13
13
|
subtitle?: string;
|
|
@@ -15,13 +15,19 @@ export type DonutCenterContentConfig = {
|
|
|
15
15
|
titleStyle?: TextStyle;
|
|
16
16
|
subtitleStyle?: TextStyle;
|
|
17
17
|
};
|
|
18
|
-
export
|
|
18
|
+
export type DonutCenterContentConfig = DonutCenterContentConfigBase & {
|
|
19
|
+
exportHooks?: ExportHooks<DonutCenterContentConfigBase>;
|
|
20
|
+
};
|
|
21
|
+
export declare class DonutCenterContent implements ChartComponent<DonutCenterContentConfigBase> {
|
|
19
22
|
readonly type: "donutCenterContent";
|
|
20
23
|
readonly mainValue?: string;
|
|
21
24
|
readonly title?: string;
|
|
22
25
|
readonly subtitle?: string;
|
|
26
|
+
readonly exportHooks?: ExportHooks<DonutCenterContentConfigBase>;
|
|
23
27
|
private readonly config;
|
|
24
28
|
constructor(config?: DonutCenterContentConfig);
|
|
29
|
+
getExportConfig(): DonutCenterContentConfigBase;
|
|
30
|
+
createExportComponent(override?: Partial<DonutCenterContentConfigBase>): ChartComponent;
|
|
25
31
|
render(svg: Selection<SVGSVGElement, undefined, null, undefined>, cx: number, cy: number, theme: ChartTheme): void;
|
|
26
32
|
}
|
|
27
33
|
export {};
|
package/donut-center-content.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeDeep } from './utils.js';
|
|
1
2
|
export class DonutCenterContent {
|
|
2
3
|
constructor(config = {}) {
|
|
3
4
|
Object.defineProperty(this, "type", {
|
|
@@ -24,6 +25,12 @@ export class DonutCenterContent {
|
|
|
24
25
|
writable: true,
|
|
25
26
|
value: void 0
|
|
26
27
|
});
|
|
28
|
+
Object.defineProperty(this, "exportHooks", {
|
|
29
|
+
enumerable: true,
|
|
30
|
+
configurable: true,
|
|
31
|
+
writable: true,
|
|
32
|
+
value: void 0
|
|
33
|
+
});
|
|
27
34
|
Object.defineProperty(this, "config", {
|
|
28
35
|
enumerable: true,
|
|
29
36
|
configurable: true,
|
|
@@ -33,8 +40,26 @@ export class DonutCenterContent {
|
|
|
33
40
|
this.mainValue = config.mainValue;
|
|
34
41
|
this.title = config.title;
|
|
35
42
|
this.subtitle = config.subtitle;
|
|
43
|
+
this.exportHooks = config.exportHooks;
|
|
36
44
|
this.config = config;
|
|
37
45
|
}
|
|
46
|
+
getExportConfig() {
|
|
47
|
+
return {
|
|
48
|
+
mainValue: this.mainValue,
|
|
49
|
+
title: this.title,
|
|
50
|
+
subtitle: this.subtitle,
|
|
51
|
+
mainValueStyle: this.config.mainValueStyle,
|
|
52
|
+
titleStyle: this.config.titleStyle,
|
|
53
|
+
subtitleStyle: this.config.subtitleStyle,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
createExportComponent(override) {
|
|
57
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
58
|
+
return new DonutCenterContent({
|
|
59
|
+
...merged,
|
|
60
|
+
exportHooks: this.exportHooks,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
38
63
|
render(svg, cx, cy, theme) {
|
|
39
64
|
const defaults = theme.donut.centerContent;
|
|
40
65
|
const elements = [];
|
package/donut-chart.d.ts
CHANGED
|
@@ -23,8 +23,10 @@ export declare class DonutChart extends BaseChart {
|
|
|
23
23
|
private validateDonutData;
|
|
24
24
|
private prepareSegments;
|
|
25
25
|
addChild(component: ChartComponent): this;
|
|
26
|
+
protected getExportComponents(): ChartComponent[];
|
|
26
27
|
update(data: DataItem[]): void;
|
|
27
28
|
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
29
|
+
protected createExportChart(): BaseChart;
|
|
28
30
|
protected renderChart(): void;
|
|
29
31
|
private positionTooltip;
|
|
30
32
|
private buildTooltipContent;
|
package/donut-chart.js
CHANGED
|
@@ -97,6 +97,22 @@ export class DonutChart extends BaseChart {
|
|
|
97
97
|
}
|
|
98
98
|
return this;
|
|
99
99
|
}
|
|
100
|
+
getExportComponents() {
|
|
101
|
+
const components = [];
|
|
102
|
+
if (this.title) {
|
|
103
|
+
components.push(this.title);
|
|
104
|
+
}
|
|
105
|
+
if (this.centerContent) {
|
|
106
|
+
components.push(this.centerContent);
|
|
107
|
+
}
|
|
108
|
+
if (this.tooltip) {
|
|
109
|
+
components.push(this.tooltip);
|
|
110
|
+
}
|
|
111
|
+
if (this.legend) {
|
|
112
|
+
components.push(this.legend);
|
|
113
|
+
}
|
|
114
|
+
return components;
|
|
115
|
+
}
|
|
100
116
|
update(data) {
|
|
101
117
|
this.data = data;
|
|
102
118
|
this.validateDonutData();
|
|
@@ -105,12 +121,27 @@ export class DonutChart extends BaseChart {
|
|
|
105
121
|
}
|
|
106
122
|
getLayoutComponents() {
|
|
107
123
|
const components = [];
|
|
108
|
-
if (this.title)
|
|
124
|
+
if (this.title) {
|
|
109
125
|
components.push(this.title);
|
|
110
|
-
|
|
126
|
+
}
|
|
127
|
+
if (this.legend) {
|
|
111
128
|
components.push(this.legend);
|
|
129
|
+
}
|
|
112
130
|
return components;
|
|
113
131
|
}
|
|
132
|
+
createExportChart() {
|
|
133
|
+
return new DonutChart({
|
|
134
|
+
data: this.data,
|
|
135
|
+
theme: this.theme,
|
|
136
|
+
donut: {
|
|
137
|
+
innerRadius: this.innerRadiusRatio,
|
|
138
|
+
padAngle: this.padAngle,
|
|
139
|
+
cornerRadius: this.cornerRadius,
|
|
140
|
+
},
|
|
141
|
+
valueKey: this.valueKey,
|
|
142
|
+
labelKey: this.labelKey,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
114
145
|
renderChart() {
|
|
115
146
|
if (!this.plotArea || !this.svg || !this.plotGroup) {
|
|
116
147
|
throw new Error('Plot area not calculated');
|
package/grid.d.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { GridConfig, ChartTheme, D3Scale } from './types.js';
|
|
2
|
+
import type { GridConfig, ChartTheme, D3Scale, ExportHooks, GridConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
-
export declare class Grid implements ChartComponent {
|
|
4
|
+
export declare class Grid implements ChartComponent<GridConfigBase> {
|
|
5
5
|
readonly type: "grid";
|
|
6
6
|
readonly horizontal: boolean;
|
|
7
7
|
readonly vertical: boolean;
|
|
8
|
+
readonly exportHooks?: ExportHooks<GridConfigBase>;
|
|
8
9
|
constructor(config?: GridConfig);
|
|
10
|
+
getExportConfig(): GridConfigBase;
|
|
11
|
+
createExportComponent(override?: Partial<GridConfigBase>): ChartComponent;
|
|
9
12
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, x: D3Scale, y: D3Scale, theme: ChartTheme): void;
|
|
10
13
|
}
|
package/grid.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { axisBottom, axisLeft } from 'd3';
|
|
2
|
+
import { mergeDeep } from './utils.js';
|
|
2
3
|
export class Grid {
|
|
3
4
|
constructor(config) {
|
|
4
5
|
Object.defineProperty(this, "type", {
|
|
@@ -19,8 +20,28 @@ export class Grid {
|
|
|
19
20
|
writable: true,
|
|
20
21
|
value: void 0
|
|
21
22
|
});
|
|
23
|
+
Object.defineProperty(this, "exportHooks", {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
configurable: true,
|
|
26
|
+
writable: true,
|
|
27
|
+
value: void 0
|
|
28
|
+
});
|
|
22
29
|
this.horizontal = config?.horizontal ?? true;
|
|
23
30
|
this.vertical = config?.vertical ?? true;
|
|
31
|
+
this.exportHooks = config?.exportHooks;
|
|
32
|
+
}
|
|
33
|
+
getExportConfig() {
|
|
34
|
+
return {
|
|
35
|
+
horizontal: this.horizontal,
|
|
36
|
+
vertical: this.vertical,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
createExportComponent(override) {
|
|
40
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
41
|
+
return new Grid({
|
|
42
|
+
...merged,
|
|
43
|
+
exportHooks: this.exportHooks,
|
|
44
|
+
});
|
|
24
45
|
}
|
|
25
46
|
render(plotGroup, x, y, theme) {
|
|
26
47
|
// Get plot area dimensions from the scale ranges
|
package/legend.d.ts
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { LegendConfig, ChartTheme, LegendSeries } from './types.js';
|
|
2
|
+
import type { LegendConfig, ChartTheme, LegendSeries, ExportHooks, LegendConfigBase } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
|
-
export declare class Legend implements LayoutAwareComponent {
|
|
4
|
+
export declare class Legend implements LayoutAwareComponent<LegendConfigBase> {
|
|
5
5
|
readonly type: "legend";
|
|
6
6
|
readonly position: LegendConfig['position'];
|
|
7
|
+
readonly exportHooks?: ExportHooks<LegendConfigBase>;
|
|
7
8
|
private readonly marginTop;
|
|
8
9
|
private readonly marginBottom;
|
|
9
10
|
private readonly itemHeight;
|
|
10
11
|
private visibilityState;
|
|
11
12
|
private onToggleCallback?;
|
|
12
13
|
constructor(config?: LegendConfig);
|
|
14
|
+
getExportConfig(): LegendConfigBase;
|
|
15
|
+
createExportComponent(override?: Partial<LegendConfigBase>): LayoutAwareComponent;
|
|
13
16
|
setToggleCallback(callback: () => void): void;
|
|
14
17
|
isSeriesVisible(dataKey: string): boolean;
|
|
15
18
|
private getCheckmarkPath;
|
|
16
|
-
private parseColor;
|
|
17
19
|
/**
|
|
18
20
|
* Returns the space required by the legend
|
|
19
21
|
*/
|
package/legend.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getSeriesColor } from './types.js';
|
|
2
|
+
import { getContrastTextColor, mergeDeep } from './utils.js';
|
|
2
3
|
export class Legend {
|
|
3
4
|
constructor(config) {
|
|
4
5
|
Object.defineProperty(this, "type", {
|
|
@@ -13,6 +14,12 @@ export class Legend {
|
|
|
13
14
|
writable: true,
|
|
14
15
|
value: void 0
|
|
15
16
|
});
|
|
17
|
+
Object.defineProperty(this, "exportHooks", {
|
|
18
|
+
enumerable: true,
|
|
19
|
+
configurable: true,
|
|
20
|
+
writable: true,
|
|
21
|
+
value: void 0
|
|
22
|
+
});
|
|
16
23
|
Object.defineProperty(this, "marginTop", {
|
|
17
24
|
enumerable: true,
|
|
18
25
|
configurable: true,
|
|
@@ -46,6 +53,23 @@ export class Legend {
|
|
|
46
53
|
this.position = config?.position || 'bottom';
|
|
47
54
|
this.marginTop = config?.marginTop ?? 20;
|
|
48
55
|
this.marginBottom = config?.marginBottom ?? 10;
|
|
56
|
+
this.exportHooks = config?.exportHooks;
|
|
57
|
+
}
|
|
58
|
+
getExportConfig() {
|
|
59
|
+
return {
|
|
60
|
+
position: this.position,
|
|
61
|
+
marginTop: this.marginTop,
|
|
62
|
+
marginBottom: this.marginBottom,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
createExportComponent(override) {
|
|
66
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
67
|
+
const legend = new Legend({
|
|
68
|
+
...merged,
|
|
69
|
+
exportHooks: this.exportHooks,
|
|
70
|
+
});
|
|
71
|
+
legend.visibilityState = new Map(this.visibilityState);
|
|
72
|
+
return legend;
|
|
49
73
|
}
|
|
50
74
|
setToggleCallback(callback) {
|
|
51
75
|
this.onToggleCallback = callback;
|
|
@@ -59,29 +83,6 @@ export class Legend {
|
|
|
59
83
|
const offsetY = size * 0.15;
|
|
60
84
|
return `M ${4 * scale + offsetX} ${12 * scale + offsetY} L ${9 * scale + offsetX} ${17 * scale + offsetY} L ${20 * scale + offsetX} ${6 * scale + offsetY}`;
|
|
61
85
|
}
|
|
62
|
-
parseColor(color) {
|
|
63
|
-
// Handle hex colors
|
|
64
|
-
if (color.startsWith('#')) {
|
|
65
|
-
const hex = color.slice(1);
|
|
66
|
-
const r = parseInt(hex.slice(0, 2), 16);
|
|
67
|
-
const g = parseInt(hex.slice(2, 4), 16);
|
|
68
|
-
const b = parseInt(hex.slice(4, 6), 16);
|
|
69
|
-
return { r, g, b };
|
|
70
|
-
}
|
|
71
|
-
// Handle rgb/rgba colors
|
|
72
|
-
if (color.startsWith('rgb')) {
|
|
73
|
-
const match = color.match(/\d+/g);
|
|
74
|
-
if (match) {
|
|
75
|
-
return {
|
|
76
|
-
r: parseInt(match[0]),
|
|
77
|
-
g: parseInt(match[1]),
|
|
78
|
-
b: parseInt(match[2]),
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
// Default to black if we can't parse
|
|
83
|
-
return { r: 0, g: 0, b: 0 };
|
|
84
|
-
}
|
|
85
86
|
/**
|
|
86
87
|
* Returns the space required by the legend
|
|
87
88
|
*/
|
|
@@ -161,13 +162,7 @@ export class Legend {
|
|
|
161
162
|
.append('path')
|
|
162
163
|
.attr('d', this.getCheckmarkPath(boxSize))
|
|
163
164
|
.attr('fill', 'none')
|
|
164
|
-
.attr('stroke', (d) =>
|
|
165
|
-
// Calculate luminance to determine if we need black or white checkmark
|
|
166
|
-
const color = d.color;
|
|
167
|
-
const rgb = this.parseColor(color);
|
|
168
|
-
const luminance = (0.299 * rgb.r + 0.587 * rgb.g + 0.114 * rgb.b) / 255;
|
|
169
|
-
return luminance > 0.5 ? '#000' : '#fff';
|
|
170
|
-
})
|
|
165
|
+
.attr('stroke', (d) => getContrastTextColor(d.color))
|
|
171
166
|
.attr('stroke-width', 2)
|
|
172
167
|
.attr('stroke-linecap', 'round')
|
|
173
168
|
.attr('stroke-linejoin', 'round')
|
package/line.d.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig } from './types.js';
|
|
2
|
+
import type { LineConfig, DataItem, D3Scale, ScaleType, ChartTheme, LineValueLabelConfig, ExportHooks, LineConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
-
export declare class Line implements ChartComponent {
|
|
4
|
+
export declare class Line implements ChartComponent<LineConfigBase> {
|
|
5
5
|
readonly type: "line";
|
|
6
6
|
readonly dataKey: string;
|
|
7
7
|
readonly stroke: string;
|
|
8
8
|
readonly strokeWidth?: number;
|
|
9
9
|
readonly valueLabel?: LineValueLabelConfig;
|
|
10
|
+
readonly exportHooks?: ExportHooks<LineConfigBase>;
|
|
10
11
|
constructor(config: LineConfig);
|
|
12
|
+
getExportConfig(): LineConfigBase;
|
|
13
|
+
createExportComponent(override?: Partial<LineConfigBase>): ChartComponent;
|
|
11
14
|
render(plotGroup: Selection<SVGGElement, undefined, null, undefined>, data: DataItem[], xKey: string, x: D3Scale, y: D3Scale, parseValue: (value: unknown) => number, xScaleType: ScaleType | undefined, theme: ChartTheme): void;
|
|
12
15
|
private renderValueLabels;
|
|
13
16
|
}
|
package/line.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { line } from 'd3';
|
|
2
|
-
import { sanitizeForCSS } from './utils.js';
|
|
2
|
+
import { sanitizeForCSS, mergeDeep } from './utils.js';
|
|
3
3
|
export class Line {
|
|
4
4
|
constructor(config) {
|
|
5
5
|
Object.defineProperty(this, "type", {
|
|
@@ -32,10 +32,32 @@ export class Line {
|
|
|
32
32
|
writable: true,
|
|
33
33
|
value: void 0
|
|
34
34
|
});
|
|
35
|
+
Object.defineProperty(this, "exportHooks", {
|
|
36
|
+
enumerable: true,
|
|
37
|
+
configurable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
value: void 0
|
|
40
|
+
});
|
|
35
41
|
this.dataKey = config.dataKey;
|
|
36
42
|
this.stroke = config.stroke || '#8884d8';
|
|
37
43
|
this.strokeWidth = config.strokeWidth;
|
|
38
44
|
this.valueLabel = config.valueLabel;
|
|
45
|
+
this.exportHooks = config.exportHooks;
|
|
46
|
+
}
|
|
47
|
+
getExportConfig() {
|
|
48
|
+
return {
|
|
49
|
+
dataKey: this.dataKey,
|
|
50
|
+
stroke: this.stroke,
|
|
51
|
+
strokeWidth: this.strokeWidth,
|
|
52
|
+
valueLabel: this.valueLabel,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
createExportComponent(override) {
|
|
56
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
57
|
+
return new Line({
|
|
58
|
+
...merged,
|
|
59
|
+
exportHooks: this.exportHooks,
|
|
60
|
+
});
|
|
39
61
|
}
|
|
40
62
|
render(plotGroup, data, xKey, x, y, parseValue, xScaleType = 'band', theme) {
|
|
41
63
|
const getXPosition = (d) => {
|
package/package.json
CHANGED
package/title.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { TitleConfig, ChartTheme } from './types.js';
|
|
2
|
+
import type { TitleConfig, ChartTheme, ExportHooks, TitleConfigBase } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
|
-
export declare class Title implements LayoutAwareComponent {
|
|
4
|
+
export declare class Title implements LayoutAwareComponent<TitleConfigBase> {
|
|
5
5
|
readonly type: "title";
|
|
6
6
|
readonly text: string;
|
|
7
|
+
readonly exportHooks?: ExportHooks<TitleConfigBase>;
|
|
7
8
|
private readonly fontSize;
|
|
8
9
|
private readonly fontWeight;
|
|
9
10
|
private readonly fontFamily?;
|
|
@@ -11,6 +12,8 @@ export declare class Title implements LayoutAwareComponent {
|
|
|
11
12
|
private readonly marginTop;
|
|
12
13
|
private readonly marginBottom;
|
|
13
14
|
constructor(config: TitleConfig);
|
|
15
|
+
getExportConfig(): TitleConfigBase;
|
|
16
|
+
createExportComponent(override?: Partial<TitleConfigBase>): LayoutAwareComponent;
|
|
14
17
|
/**
|
|
15
18
|
* Returns the space required by the title
|
|
16
19
|
*/
|
package/title.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { mergeDeep } from './utils.js';
|
|
1
2
|
export class Title {
|
|
2
3
|
constructor(config) {
|
|
3
4
|
Object.defineProperty(this, "type", {
|
|
@@ -12,6 +13,12 @@ export class Title {
|
|
|
12
13
|
writable: true,
|
|
13
14
|
value: void 0
|
|
14
15
|
});
|
|
16
|
+
Object.defineProperty(this, "exportHooks", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
15
22
|
Object.defineProperty(this, "fontSize", {
|
|
16
23
|
enumerable: true,
|
|
17
24
|
configurable: true,
|
|
@@ -55,6 +62,25 @@ export class Title {
|
|
|
55
62
|
this.align = config.align ?? 'center';
|
|
56
63
|
this.marginTop = config.marginTop ?? 10;
|
|
57
64
|
this.marginBottom = config.marginBottom ?? 15;
|
|
65
|
+
this.exportHooks = config.exportHooks;
|
|
66
|
+
}
|
|
67
|
+
getExportConfig() {
|
|
68
|
+
return {
|
|
69
|
+
text: this.text,
|
|
70
|
+
fontSize: this.fontSize,
|
|
71
|
+
fontWeight: this.fontWeight,
|
|
72
|
+
fontFamily: this.fontFamily,
|
|
73
|
+
align: this.align,
|
|
74
|
+
marginTop: this.marginTop,
|
|
75
|
+
marginBottom: this.marginBottom,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
createExportComponent(override) {
|
|
79
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
80
|
+
return new Title({
|
|
81
|
+
...merged,
|
|
82
|
+
exportHooks: this.exportHooks,
|
|
83
|
+
});
|
|
58
84
|
}
|
|
59
85
|
/**
|
|
60
86
|
* Returns the space required by the title
|
package/tooltip.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme } from './types.js';
|
|
2
|
+
import type { TooltipConfig, DataItem, DataValue, D3Scale, ChartTheme, ExportHooks, TooltipConfigBase } from './types.js';
|
|
3
3
|
import type { ChartComponent } from './chart-interface.js';
|
|
4
4
|
import type { Line } from './line.js';
|
|
5
5
|
import type { Bar } from './bar.js';
|
|
6
6
|
import type { PlotAreaBounds } from './layout-manager.js';
|
|
7
|
-
export declare class Tooltip implements ChartComponent {
|
|
7
|
+
export declare class Tooltip implements ChartComponent<TooltipConfigBase> {
|
|
8
8
|
readonly id = "iisChartTooltip";
|
|
9
9
|
readonly type: "tooltip";
|
|
10
10
|
readonly formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
|
@@ -14,8 +14,11 @@ export declare class Tooltip implements ChartComponent {
|
|
|
14
14
|
stroke?: string;
|
|
15
15
|
fill?: string;
|
|
16
16
|
}[]) => string;
|
|
17
|
+
readonly exportHooks?: ExportHooks<TooltipConfigBase>;
|
|
17
18
|
private tooltipDiv;
|
|
18
19
|
constructor(config?: TooltipConfig);
|
|
20
|
+
getExportConfig(): TooltipConfigBase;
|
|
21
|
+
createExportComponent(override?: Partial<TooltipConfigBase>): ChartComponent;
|
|
19
22
|
initialize(theme: ChartTheme): void;
|
|
20
23
|
attachToArea(svg: Selection<SVGSVGElement, undefined, null, undefined>, data: DataItem[], series: (Line | Bar)[], xKey: string, x: D3Scale, y: D3Scale, theme: ChartTheme, plotArea: PlotAreaBounds, parseValue: (value: unknown) => number, isHorizontal?: boolean): void;
|
|
21
24
|
cleanup(): void;
|
package/tooltip.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { pointer, select } from 'd3';
|
|
2
2
|
import { getSeriesColor } from './types.js';
|
|
3
|
-
import { sanitizeForCSS } from './utils.js';
|
|
3
|
+
import { sanitizeForCSS, mergeDeep } from './utils.js';
|
|
4
4
|
export class Tooltip {
|
|
5
5
|
constructor(config) {
|
|
6
6
|
Object.defineProperty(this, "id", {
|
|
@@ -33,6 +33,12 @@ export class Tooltip {
|
|
|
33
33
|
writable: true,
|
|
34
34
|
value: void 0
|
|
35
35
|
});
|
|
36
|
+
Object.defineProperty(this, "exportHooks", {
|
|
37
|
+
enumerable: true,
|
|
38
|
+
configurable: true,
|
|
39
|
+
writable: true,
|
|
40
|
+
value: void 0
|
|
41
|
+
});
|
|
36
42
|
Object.defineProperty(this, "tooltipDiv", {
|
|
37
43
|
enumerable: true,
|
|
38
44
|
configurable: true,
|
|
@@ -42,6 +48,21 @@ export class Tooltip {
|
|
|
42
48
|
this.formatter = config?.formatter;
|
|
43
49
|
this.labelFormatter = config?.labelFormatter;
|
|
44
50
|
this.customFormatter = config?.customFormatter;
|
|
51
|
+
this.exportHooks = config?.exportHooks;
|
|
52
|
+
}
|
|
53
|
+
getExportConfig() {
|
|
54
|
+
return {
|
|
55
|
+
formatter: this.formatter,
|
|
56
|
+
labelFormatter: this.labelFormatter,
|
|
57
|
+
customFormatter: this.customFormatter,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
createExportComponent(override) {
|
|
61
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
62
|
+
return new Tooltip({
|
|
63
|
+
...merged,
|
|
64
|
+
exportHooks: this.exportHooks,
|
|
65
|
+
});
|
|
45
66
|
}
|
|
46
67
|
initialize(theme) {
|
|
47
68
|
this.cleanup();
|
package/types.d.ts
CHANGED
|
@@ -4,6 +4,21 @@ export type ExportFormat = 'svg' | 'json';
|
|
|
4
4
|
export type ExportOptions = {
|
|
5
5
|
download?: boolean;
|
|
6
6
|
filename?: string;
|
|
7
|
+
width?: number;
|
|
8
|
+
height?: number;
|
|
9
|
+
};
|
|
10
|
+
export type ExportRenderContext = {
|
|
11
|
+
format: ExportFormat;
|
|
12
|
+
options?: ExportOptions;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
};
|
|
16
|
+
export type ExportHookContext = ExportRenderContext & {
|
|
17
|
+
svg: SVGSVGElement;
|
|
18
|
+
};
|
|
19
|
+
export type ExportHooks<TConfig = Record<string, unknown>> = {
|
|
20
|
+
beforeRender?: (context: ExportRenderContext, currentConfig: TConfig) => void | Partial<TConfig>;
|
|
21
|
+
before?: (context: ExportHookContext) => void;
|
|
7
22
|
};
|
|
8
23
|
export type ColorPalette = string[];
|
|
9
24
|
export type ChartTheme = {
|
|
@@ -94,13 +109,16 @@ export type BarValueLabelConfig = ValueLabelConfig & {
|
|
|
94
109
|
position?: 'inside' | 'outside';
|
|
95
110
|
insidePosition?: 'top' | 'middle' | 'bottom';
|
|
96
111
|
};
|
|
97
|
-
export type
|
|
112
|
+
export type LineConfigBase = {
|
|
98
113
|
dataKey: string;
|
|
99
114
|
stroke?: string;
|
|
100
115
|
strokeWidth?: number;
|
|
101
116
|
valueLabel?: LineValueLabelConfig;
|
|
102
117
|
};
|
|
103
|
-
export type
|
|
118
|
+
export type LineConfig = LineConfigBase & {
|
|
119
|
+
exportHooks?: ExportHooks<LineConfigBase>;
|
|
120
|
+
};
|
|
121
|
+
export type BarConfigBase = {
|
|
104
122
|
dataKey: string;
|
|
105
123
|
fill?: string;
|
|
106
124
|
colorAdapter?: (data: DataItem, index: number) => string;
|
|
@@ -108,6 +126,9 @@ export type BarConfig = {
|
|
|
108
126
|
maxBarSize?: number;
|
|
109
127
|
valueLabel?: BarValueLabelConfig;
|
|
110
128
|
};
|
|
129
|
+
export type BarConfig = BarConfigBase & {
|
|
130
|
+
exportHooks?: ExportHooks<BarConfigBase>;
|
|
131
|
+
};
|
|
111
132
|
export type BarStackConfig = {
|
|
112
133
|
mode?: BarStackMode;
|
|
113
134
|
gap?: number;
|
|
@@ -117,7 +138,7 @@ export declare function getSeriesColor(series: {
|
|
|
117
138
|
fill?: string;
|
|
118
139
|
}): string;
|
|
119
140
|
export type LabelOversizedBehavior = 'truncate' | 'wrap' | 'hide';
|
|
120
|
-
export type
|
|
141
|
+
export type XAxisConfigBase = {
|
|
121
142
|
dataKey?: string;
|
|
122
143
|
rotatedLabels?: boolean;
|
|
123
144
|
maxLabelWidth?: number;
|
|
@@ -127,17 +148,26 @@ export type XAxisConfig = {
|
|
|
127
148
|
minLabelGap?: number;
|
|
128
149
|
preserveEndLabels?: boolean;
|
|
129
150
|
};
|
|
130
|
-
export type
|
|
151
|
+
export type XAxisConfig = XAxisConfigBase & {
|
|
152
|
+
exportHooks?: ExportHooks<XAxisConfigBase>;
|
|
153
|
+
};
|
|
154
|
+
export type YAxisConfigBase = {
|
|
131
155
|
tickFormat?: string | ((value: number) => string) | null;
|
|
132
156
|
rotatedLabels?: boolean;
|
|
133
157
|
maxLabelWidth?: number;
|
|
134
158
|
oversizedBehavior?: LabelOversizedBehavior;
|
|
135
159
|
};
|
|
136
|
-
export type
|
|
160
|
+
export type YAxisConfig = YAxisConfigBase & {
|
|
161
|
+
exportHooks?: ExportHooks<YAxisConfigBase>;
|
|
162
|
+
};
|
|
163
|
+
export type GridConfigBase = {
|
|
137
164
|
horizontal?: boolean;
|
|
138
165
|
vertical?: boolean;
|
|
139
166
|
};
|
|
140
|
-
export type
|
|
167
|
+
export type GridConfig = GridConfigBase & {
|
|
168
|
+
exportHooks?: ExportHooks<GridConfigBase>;
|
|
169
|
+
};
|
|
170
|
+
export type TooltipConfigBase = {
|
|
141
171
|
formatter?: (dataKey: string, value: DataValue, data: DataItem) => string;
|
|
142
172
|
labelFormatter?: (label: string, data: DataItem) => string;
|
|
143
173
|
customFormatter?: (data: DataItem, series: {
|
|
@@ -146,17 +176,23 @@ export type TooltipConfig = {
|
|
|
146
176
|
fill?: string;
|
|
147
177
|
}[]) => string;
|
|
148
178
|
};
|
|
149
|
-
export type
|
|
179
|
+
export type TooltipConfig = TooltipConfigBase & {
|
|
180
|
+
exportHooks?: ExportHooks<TooltipConfigBase>;
|
|
181
|
+
};
|
|
182
|
+
export type LegendConfigBase = {
|
|
150
183
|
position?: 'bottom';
|
|
151
184
|
marginTop?: number;
|
|
152
185
|
marginBottom?: number;
|
|
153
186
|
};
|
|
187
|
+
export type LegendConfig = LegendConfigBase & {
|
|
188
|
+
exportHooks?: ExportHooks<LegendConfigBase>;
|
|
189
|
+
};
|
|
154
190
|
export type LegendSeries = {
|
|
155
191
|
dataKey: string;
|
|
156
192
|
stroke?: string;
|
|
157
193
|
fill?: string;
|
|
158
194
|
};
|
|
159
|
-
export type
|
|
195
|
+
export type TitleConfigBase = {
|
|
160
196
|
text: string;
|
|
161
197
|
fontSize?: number;
|
|
162
198
|
fontWeight?: string;
|
|
@@ -165,6 +201,9 @@ export type TitleConfig = {
|
|
|
165
201
|
marginTop?: number;
|
|
166
202
|
marginBottom?: number;
|
|
167
203
|
};
|
|
204
|
+
export type TitleConfig = TitleConfigBase & {
|
|
205
|
+
exportHooks?: ExportHooks<TitleConfigBase>;
|
|
206
|
+
};
|
|
168
207
|
export type ScaleType = 'band' | 'linear' | 'time' | 'log';
|
|
169
208
|
export type D3Scale = any;
|
|
170
209
|
export type ScaleDomainValue = string | number | Date;
|
package/utils.d.ts
CHANGED
|
@@ -60,3 +60,5 @@ export declare function breakWord(word: string, maxWidth: number, fontSize: stri
|
|
|
60
60
|
* @returns Array of lines
|
|
61
61
|
*/
|
|
62
62
|
export declare function wrapText(text: string, maxWidth: number, fontSize: string | number, fontFamily: string, fontWeight: string, svg: SVGSVGElement): string[];
|
|
63
|
+
export declare function getContrastTextColor(backgroundColor: string, lightTextColor?: string, darkTextColor?: string): string;
|
|
64
|
+
export declare function mergeDeep<T extends Record<string, unknown>>(base: T, override?: Partial<T>): T;
|
package/utils.js
CHANGED
|
@@ -168,3 +168,63 @@ export function wrapText(text, maxWidth, fontSize, fontFamily, fontWeight, svg)
|
|
|
168
168
|
}
|
|
169
169
|
return lines.length > 0 ? lines : [text];
|
|
170
170
|
}
|
|
171
|
+
function parseColorToRGB(color) {
|
|
172
|
+
const normalizedColor = color.trim();
|
|
173
|
+
if (normalizedColor.startsWith('#')) {
|
|
174
|
+
const hex = normalizedColor.slice(1);
|
|
175
|
+
if (hex.length === 3) {
|
|
176
|
+
const r = parseInt(hex[0] + hex[0], 16);
|
|
177
|
+
const g = parseInt(hex[1] + hex[1], 16);
|
|
178
|
+
const b = parseInt(hex[2] + hex[2], 16);
|
|
179
|
+
return { r, g, b };
|
|
180
|
+
}
|
|
181
|
+
if (hex.length === 6) {
|
|
182
|
+
const r = parseInt(hex.slice(0, 2), 16);
|
|
183
|
+
const g = parseInt(hex.slice(2, 4), 16);
|
|
184
|
+
const b = parseInt(hex.slice(4, 6), 16);
|
|
185
|
+
return { r, g, b };
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (normalizedColor.startsWith('rgb')) {
|
|
189
|
+
const matches = normalizedColor.match(/\d+/g);
|
|
190
|
+
if (matches && matches.length >= 3) {
|
|
191
|
+
return {
|
|
192
|
+
r: parseInt(matches[0], 10),
|
|
193
|
+
g: parseInt(matches[1], 10),
|
|
194
|
+
b: parseInt(matches[2], 10),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// Fallback matches legend behavior: unknown colors are treated as dark.
|
|
199
|
+
return { r: 0, g: 0, b: 0 };
|
|
200
|
+
}
|
|
201
|
+
export function getContrastTextColor(backgroundColor, lightTextColor = '#fff', darkTextColor = '#000') {
|
|
202
|
+
const { r, g, b } = parseColorToRGB(backgroundColor);
|
|
203
|
+
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
204
|
+
if (luminance > 0.5) {
|
|
205
|
+
return darkTextColor;
|
|
206
|
+
}
|
|
207
|
+
return lightTextColor;
|
|
208
|
+
}
|
|
209
|
+
export function mergeDeep(base, override) {
|
|
210
|
+
if (!override) {
|
|
211
|
+
return { ...base };
|
|
212
|
+
}
|
|
213
|
+
const result = { ...base };
|
|
214
|
+
Object.keys(override).forEach((key) => {
|
|
215
|
+
const overrideValue = override[key];
|
|
216
|
+
const baseValue = base[key];
|
|
217
|
+
if (overrideValue &&
|
|
218
|
+
typeof overrideValue === 'object' &&
|
|
219
|
+
!Array.isArray(overrideValue) &&
|
|
220
|
+
baseValue &&
|
|
221
|
+
typeof baseValue === 'object' &&
|
|
222
|
+
!Array.isArray(baseValue)) {
|
|
223
|
+
result[key] = mergeDeep(baseValue, overrideValue);
|
|
224
|
+
}
|
|
225
|
+
else if (overrideValue !== undefined) {
|
|
226
|
+
result[key] = overrideValue;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
return result;
|
|
230
|
+
}
|
package/x-axis.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { XAxisConfig, ChartTheme, D3Scale } from './types.js';
|
|
2
|
+
import type { XAxisConfig, ChartTheme, D3Scale, ExportHooks, XAxisConfigBase } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
|
-
export declare class XAxis implements LayoutAwareComponent {
|
|
4
|
+
export declare class XAxis implements LayoutAwareComponent<XAxisConfigBase> {
|
|
5
5
|
readonly type: "xAxis";
|
|
6
6
|
readonly dataKey?: string;
|
|
7
7
|
private readonly rotatedLabels;
|
|
@@ -15,7 +15,10 @@ export declare class XAxis implements LayoutAwareComponent {
|
|
|
15
15
|
private readonly autoHideOverlapping;
|
|
16
16
|
private readonly minLabelGap;
|
|
17
17
|
private readonly preserveEndLabels;
|
|
18
|
+
readonly exportHooks?: ExportHooks<XAxisConfigBase>;
|
|
18
19
|
constructor(config?: XAxisConfig);
|
|
20
|
+
getExportConfig(): XAxisConfigBase;
|
|
21
|
+
createExportComponent(override?: Partial<XAxisConfigBase>): LayoutAwareComponent;
|
|
19
22
|
/**
|
|
20
23
|
* Returns the space required by the x-axis
|
|
21
24
|
*/
|
package/x-axis.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { axisBottom } from 'd3';
|
|
2
|
-
import { measureTextWidth, truncateText, wrapText } from './utils.js';
|
|
2
|
+
import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
|
|
3
3
|
export class XAxis {
|
|
4
4
|
constructor(config) {
|
|
5
5
|
Object.defineProperty(this, "type", {
|
|
@@ -81,6 +81,12 @@ export class XAxis {
|
|
|
81
81
|
writable: true,
|
|
82
82
|
value: void 0
|
|
83
83
|
});
|
|
84
|
+
Object.defineProperty(this, "exportHooks", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: void 0
|
|
89
|
+
});
|
|
84
90
|
this.dataKey = config?.dataKey;
|
|
85
91
|
this.rotatedLabels = config?.rotatedLabels ?? false;
|
|
86
92
|
this.maxLabelWidth = config?.maxLabelWidth;
|
|
@@ -89,6 +95,26 @@ export class XAxis {
|
|
|
89
95
|
this.autoHideOverlapping = config?.autoHideOverlapping ?? false;
|
|
90
96
|
this.minLabelGap = config?.minLabelGap ?? 8;
|
|
91
97
|
this.preserveEndLabels = config?.preserveEndLabels ?? true;
|
|
98
|
+
this.exportHooks = config?.exportHooks;
|
|
99
|
+
}
|
|
100
|
+
getExportConfig() {
|
|
101
|
+
return {
|
|
102
|
+
dataKey: this.dataKey,
|
|
103
|
+
rotatedLabels: this.rotatedLabels,
|
|
104
|
+
maxLabelWidth: this.maxLabelWidth,
|
|
105
|
+
oversizedBehavior: this.oversizedBehavior,
|
|
106
|
+
tickFormat: this.tickFormat,
|
|
107
|
+
autoHideOverlapping: this.autoHideOverlapping,
|
|
108
|
+
minLabelGap: this.minLabelGap,
|
|
109
|
+
preserveEndLabels: this.preserveEndLabels,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
createExportComponent(override) {
|
|
113
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
114
|
+
return new XAxis({
|
|
115
|
+
...merged,
|
|
116
|
+
exportHooks: this.exportHooks,
|
|
117
|
+
});
|
|
92
118
|
}
|
|
93
119
|
/**
|
|
94
120
|
* Returns the space required by the x-axis
|
|
@@ -153,10 +179,8 @@ export class XAxis {
|
|
|
153
179
|
const textHeight = lineHeight * maxLines;
|
|
154
180
|
if (this.rotatedLabels) {
|
|
155
181
|
const radians = Math.PI / 4;
|
|
156
|
-
const verticalFootprint = Math.sin(radians) * maxWidth +
|
|
157
|
-
|
|
158
|
-
this.estimatedHeight =
|
|
159
|
-
this.tickPadding + verticalFootprint + 5;
|
|
182
|
+
const verticalFootprint = Math.sin(radians) * maxWidth + Math.cos(radians) * textHeight;
|
|
183
|
+
this.estimatedHeight = this.tickPadding + verticalFootprint + 5;
|
|
160
184
|
}
|
|
161
185
|
else {
|
|
162
186
|
this.estimatedHeight = this.tickPadding + textHeight + 5;
|
package/xy-chart.d.ts
CHANGED
|
@@ -10,6 +10,8 @@ export declare class XYChart extends BaseChart {
|
|
|
10
10
|
private barStackGap;
|
|
11
11
|
constructor(config: XYChartConfig);
|
|
12
12
|
addChild(component: ChartComponent): this;
|
|
13
|
+
protected getExportComponents(): ChartComponent[];
|
|
14
|
+
protected createExportChart(): BaseChart;
|
|
13
15
|
private rerender;
|
|
14
16
|
protected prepareLayout(): void;
|
|
15
17
|
protected renderChart(): void;
|
package/xy-chart.js
CHANGED
|
@@ -68,6 +68,40 @@ export class XYChart extends BaseChart {
|
|
|
68
68
|
}
|
|
69
69
|
return this;
|
|
70
70
|
}
|
|
71
|
+
getExportComponents() {
|
|
72
|
+
const components = [];
|
|
73
|
+
if (this.title) {
|
|
74
|
+
components.push(this.title);
|
|
75
|
+
}
|
|
76
|
+
if (this.grid) {
|
|
77
|
+
components.push(this.grid);
|
|
78
|
+
}
|
|
79
|
+
components.push(...this.series);
|
|
80
|
+
if (this.xAxis) {
|
|
81
|
+
components.push(this.xAxis);
|
|
82
|
+
}
|
|
83
|
+
if (this.yAxis) {
|
|
84
|
+
components.push(this.yAxis);
|
|
85
|
+
}
|
|
86
|
+
if (this.tooltip) {
|
|
87
|
+
components.push(this.tooltip);
|
|
88
|
+
}
|
|
89
|
+
if (this.legend) {
|
|
90
|
+
components.push(this.legend);
|
|
91
|
+
}
|
|
92
|
+
return components;
|
|
93
|
+
}
|
|
94
|
+
createExportChart() {
|
|
95
|
+
return new XYChart({
|
|
96
|
+
data: this.data,
|
|
97
|
+
theme: this.theme,
|
|
98
|
+
scales: this.scaleConfig,
|
|
99
|
+
barStack: {
|
|
100
|
+
mode: this.barStackMode,
|
|
101
|
+
gap: this.barStackGap,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
}
|
|
71
105
|
rerender() {
|
|
72
106
|
this.update(this.data);
|
|
73
107
|
}
|
package/y-axis.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type Selection } from 'd3';
|
|
2
|
-
import type { ChartTheme, YAxisConfig, D3Scale } from './types.js';
|
|
2
|
+
import type { ChartTheme, YAxisConfig, D3Scale, ExportHooks, YAxisConfigBase } from './types.js';
|
|
3
3
|
import type { LayoutAwareComponent, ComponentSpace } from './chart-interface.js';
|
|
4
|
-
export declare class YAxis implements LayoutAwareComponent {
|
|
4
|
+
export declare class YAxis implements LayoutAwareComponent<YAxisConfigBase> {
|
|
5
5
|
readonly type: "yAxis";
|
|
6
6
|
private readonly tickPadding;
|
|
7
7
|
private readonly fontSize;
|
|
@@ -9,7 +9,10 @@ export declare class YAxis implements LayoutAwareComponent {
|
|
|
9
9
|
private readonly tickFormat;
|
|
10
10
|
private readonly rotatedLabels;
|
|
11
11
|
private readonly oversizedBehavior;
|
|
12
|
+
readonly exportHooks?: ExportHooks<YAxisConfigBase>;
|
|
12
13
|
constructor(config?: YAxisConfig);
|
|
14
|
+
getExportConfig(): YAxisConfigBase;
|
|
15
|
+
createExportComponent(override?: Partial<YAxisConfigBase>): LayoutAwareComponent;
|
|
13
16
|
/**
|
|
14
17
|
* Returns the space required by the y-axis
|
|
15
18
|
*/
|
package/y-axis.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { axisLeft } from 'd3';
|
|
2
|
-
import { measureTextWidth, truncateText, wrapText } from './utils.js';
|
|
2
|
+
import { measureTextWidth, truncateText, wrapText, mergeDeep } from './utils.js';
|
|
3
3
|
export class YAxis {
|
|
4
4
|
constructor(config) {
|
|
5
5
|
Object.defineProperty(this, "type", {
|
|
@@ -45,10 +45,32 @@ export class YAxis {
|
|
|
45
45
|
writable: true,
|
|
46
46
|
value: void 0
|
|
47
47
|
});
|
|
48
|
+
Object.defineProperty(this, "exportHooks", {
|
|
49
|
+
enumerable: true,
|
|
50
|
+
configurable: true,
|
|
51
|
+
writable: true,
|
|
52
|
+
value: void 0
|
|
53
|
+
});
|
|
48
54
|
this.tickFormat = config?.tickFormat ?? null;
|
|
49
55
|
this.rotatedLabels = config?.rotatedLabels ?? false;
|
|
50
56
|
this.maxLabelWidth = config?.maxLabelWidth ?? 40; // Default 40 for backward compatibility
|
|
51
57
|
this.oversizedBehavior = config?.oversizedBehavior ?? 'truncate';
|
|
58
|
+
this.exportHooks = config?.exportHooks;
|
|
59
|
+
}
|
|
60
|
+
getExportConfig() {
|
|
61
|
+
return {
|
|
62
|
+
tickFormat: this.tickFormat,
|
|
63
|
+
rotatedLabels: this.rotatedLabels,
|
|
64
|
+
maxLabelWidth: this.maxLabelWidth,
|
|
65
|
+
oversizedBehavior: this.oversizedBehavior,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
createExportComponent(override) {
|
|
69
|
+
const merged = mergeDeep(this.getExportConfig(), override);
|
|
70
|
+
return new YAxis({
|
|
71
|
+
...merged,
|
|
72
|
+
exportHooks: this.exportHooks,
|
|
73
|
+
});
|
|
52
74
|
}
|
|
53
75
|
/**
|
|
54
76
|
* Returns the space required by the y-axis
|