@internetstiftelsen/charts 0.8.0 → 0.9.0
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/README.md +53 -8
- package/area.d.ts +0 -1
- package/area.js +2 -19
- package/bar.d.ts +0 -1
- package/bar.js +5 -22
- package/base-chart.d.ts +54 -5
- package/base-chart.js +260 -73
- package/donut-chart.d.ts +7 -9
- package/donut-chart.js +51 -131
- package/gauge-chart.d.ts +18 -7
- package/gauge-chart.js +315 -106
- package/line.js +3 -25
- package/package.json +3 -1
- package/pie-chart.d.ts +7 -11
- package/pie-chart.js +30 -153
- package/radial-chart-base.d.ts +25 -0
- package/radial-chart-base.js +77 -0
- package/scale-utils.d.ts +3 -0
- package/scale-utils.js +14 -0
- package/utils.d.ts +7 -0
- package/utils.js +24 -0
- package/word-cloud-chart.d.ts +32 -0
- package/word-cloud-chart.js +199 -0
- package/xy-chart.d.ts +6 -4
- package/xy-chart.js +77 -126
package/pie-chart.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { DataItem, LegendSeries } from './types.js';
|
|
2
|
-
import {
|
|
3
|
-
import type { ChartComponent
|
|
2
|
+
import { type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
3
|
+
import type { ChartComponent } from './chart-interface.js';
|
|
4
|
+
import { RadialChartBase } from './radial-chart-base.js';
|
|
4
5
|
export type PieSort = 'none' | 'ascending' | 'descending' | ((a: PieSegmentData, b: PieSegmentData) => number);
|
|
5
6
|
export type PieConfig = {
|
|
6
7
|
innerRadius?: number;
|
|
@@ -40,7 +41,7 @@ type PieSegmentData = {
|
|
|
40
41
|
color: string;
|
|
41
42
|
source: DataItem;
|
|
42
43
|
};
|
|
43
|
-
export declare class PieChart extends
|
|
44
|
+
export declare class PieChart extends RadialChartBase {
|
|
44
45
|
private readonly innerRadiusRatio;
|
|
45
46
|
private readonly startAngle;
|
|
46
47
|
private readonly endAngle;
|
|
@@ -55,22 +56,17 @@ export declare class PieChart extends BaseChart {
|
|
|
55
56
|
private validatePieData;
|
|
56
57
|
private prepareSegments;
|
|
57
58
|
private warnOnTinySlices;
|
|
58
|
-
addChild(component: ChartComponent): this;
|
|
59
59
|
protected getExportComponents(): ChartComponent[];
|
|
60
60
|
update(data: DataItem[]): void;
|
|
61
|
-
protected
|
|
62
|
-
protected
|
|
63
|
-
protected
|
|
64
|
-
protected renderChart(): void;
|
|
65
|
-
private resolveFontScale;
|
|
61
|
+
protected createExportChart(): RadialChartBase;
|
|
62
|
+
protected syncDerivedState(): void;
|
|
63
|
+
protected renderChart({ svg, plotGroup, plotArea, }: BaseRenderContext): void;
|
|
66
64
|
protected getLegendSeries(): LegendSeries[];
|
|
67
65
|
private resolveSortComparator;
|
|
68
66
|
private renderSegments;
|
|
69
67
|
private handleSegmentKeyNavigation;
|
|
70
68
|
private getRenderedSegments;
|
|
71
69
|
private buildAriaLabel;
|
|
72
|
-
private positionTooltip;
|
|
73
|
-
private positionTooltipAtElement;
|
|
74
70
|
private buildTooltipContent;
|
|
75
71
|
private getArcPoint;
|
|
76
72
|
private renderLabels;
|
package/pie-chart.js
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
import { arc, pie, select } from 'd3';
|
|
2
|
-
import { BaseChart } from './base-chart.js';
|
|
3
2
|
import { ChartValidator } from './validation.js';
|
|
4
3
|
import { getContrastTextColor, measureTextWidth, sanitizeForCSS, } from './utils.js';
|
|
4
|
+
import { RadialChartBase } from './radial-chart-base.js';
|
|
5
5
|
const HOVER_EXPAND_PX = 8;
|
|
6
6
|
const ANIMATION_DURATION_MS = 150;
|
|
7
|
-
const TOOLTIP_OFFSET_PX = 12;
|
|
8
|
-
const EDGE_MARGIN_PX = 10;
|
|
9
7
|
const FULL_CIRCLE_RADIANS = Math.PI * 2;
|
|
10
8
|
const OUTSIDE_LABEL_TEXT_OFFSET_PX = 10;
|
|
11
9
|
const OUTSIDE_LABEL_LINE_INSET_PX = 4;
|
|
12
10
|
const TINY_SLICE_THRESHOLD_RATIO = 0.03;
|
|
13
|
-
export class PieChart extends
|
|
11
|
+
export class PieChart extends RadialChartBase {
|
|
14
12
|
constructor(config) {
|
|
15
13
|
super(config);
|
|
16
14
|
Object.defineProperty(this, "innerRadiusRatio", {
|
|
@@ -99,8 +97,7 @@ export class PieChart extends BaseChart {
|
|
|
99
97
|
legacyLabels?.minVerticalSpacing ??
|
|
100
98
|
14,
|
|
101
99
|
};
|
|
102
|
-
this.
|
|
103
|
-
this.prepareSegments();
|
|
100
|
+
this.initializeDataState();
|
|
104
101
|
}
|
|
105
102
|
validatePieData() {
|
|
106
103
|
ChartValidator.validateDataKey(this.data, this.labelKey, 'PieChart');
|
|
@@ -176,60 +173,16 @@ export class PieChart extends BaseChart {
|
|
|
176
173
|
ChartValidator.warn(`PieChart: ${tinySliceCount} slices are below ${(TINY_SLICE_THRESHOLD_RATIO * 100).toFixed(0)}% and label readability may degrade`);
|
|
177
174
|
}
|
|
178
175
|
}
|
|
179
|
-
addChild(component) {
|
|
180
|
-
const type = component.type;
|
|
181
|
-
if (type === 'tooltip') {
|
|
182
|
-
this.tooltip = component;
|
|
183
|
-
}
|
|
184
|
-
else if (type === 'legend') {
|
|
185
|
-
this.legend = component;
|
|
186
|
-
this.legend.setToggleCallback(() => {
|
|
187
|
-
if (!this.container) {
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
this.update(this.data);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
else if (type === 'title') {
|
|
194
|
-
this.title = component;
|
|
195
|
-
}
|
|
196
|
-
return this;
|
|
197
|
-
}
|
|
198
176
|
getExportComponents() {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
components.push(this.tooltip);
|
|
205
|
-
}
|
|
206
|
-
if (this.legend?.isInlineMode()) {
|
|
207
|
-
components.push(this.legend);
|
|
208
|
-
}
|
|
209
|
-
return components;
|
|
177
|
+
return this.getBaseExportComponents({
|
|
178
|
+
title: true,
|
|
179
|
+
tooltip: true,
|
|
180
|
+
legend: this.legend?.isInlineMode(),
|
|
181
|
+
});
|
|
210
182
|
}
|
|
211
183
|
update(data) {
|
|
212
|
-
this.data = data;
|
|
213
|
-
this.validatePieData();
|
|
214
|
-
this.prepareSegments();
|
|
215
184
|
super.update(data);
|
|
216
185
|
}
|
|
217
|
-
getLayoutComponents() {
|
|
218
|
-
const components = [];
|
|
219
|
-
if (this.title) {
|
|
220
|
-
components.push(this.title);
|
|
221
|
-
}
|
|
222
|
-
if (this.legend) {
|
|
223
|
-
components.push(this.legend);
|
|
224
|
-
}
|
|
225
|
-
return components;
|
|
226
|
-
}
|
|
227
|
-
prepareLayout() {
|
|
228
|
-
const svgNode = this.svg?.node();
|
|
229
|
-
if (svgNode && this.legend?.isInlineMode()) {
|
|
230
|
-
this.legend.estimateLayoutSpace(this.getLegendSeries(), this.theme, this.width, svgNode);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
186
|
createExportChart() {
|
|
234
187
|
return new PieChart({
|
|
235
188
|
data: this.data,
|
|
@@ -248,53 +201,27 @@ export class PieChart extends BaseChart {
|
|
|
248
201
|
labelKey: this.labelKey,
|
|
249
202
|
});
|
|
250
203
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
const visibleSegments = this.legend
|
|
260
|
-
? this.segments.filter((seg) => this.legend.isSeriesVisible(seg.label))
|
|
261
|
-
: this.segments;
|
|
262
|
-
if (this.tooltip) {
|
|
263
|
-
this.tooltip.initialize(this.theme);
|
|
264
|
-
}
|
|
204
|
+
syncDerivedState() {
|
|
205
|
+
this.validatePieData();
|
|
206
|
+
this.prepareSegments();
|
|
207
|
+
}
|
|
208
|
+
renderChart({ svg, plotGroup, plotArea, }) {
|
|
209
|
+
this.renderTitle(svg);
|
|
210
|
+
const visibleSegments = this.getVisibleRadialItems(this.segments);
|
|
211
|
+
this.initializeTooltip();
|
|
265
212
|
if (visibleSegments.length > 0) {
|
|
266
|
-
const cx = this.plotArea
|
|
267
|
-
const
|
|
268
|
-
const outerRadius = Math.min(this.plotArea.width, this.plotArea.height) / 2;
|
|
269
|
-
const innerRadius = outerRadius * this.innerRadiusRatio;
|
|
270
|
-
const fontScale = this.resolveFontScale(outerRadius);
|
|
271
|
-
const { segmentGroup, pieData } = this.renderSegments(visibleSegments, cx, cy, innerRadius, outerRadius);
|
|
213
|
+
const { cx, cy, outerRadius, innerRadius, fontScale } = this.getRadialLayout(plotArea, this.innerRadiusRatio);
|
|
214
|
+
const { segmentGroup, pieData } = this.renderSegments(plotGroup, visibleSegments, cx, cy, innerRadius, outerRadius);
|
|
272
215
|
if (this.valueLabel.show) {
|
|
273
216
|
this.renderLabels(segmentGroup, pieData, innerRadius, outerRadius, visibleSegments.reduce((sum, segment) => {
|
|
274
217
|
return sum + segment.value;
|
|
275
218
|
}, 0), fontScale);
|
|
276
219
|
}
|
|
277
220
|
}
|
|
278
|
-
|
|
279
|
-
const pos = this.layoutManager.getComponentPosition(this.legend);
|
|
280
|
-
this.legend.render(this.svg, this.getLegendSeries(), this.theme, this.width, pos.x, pos.y);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
resolveFontScale(outerRadius) {
|
|
284
|
-
const plotHeight = Math.max(1, this.theme.height -
|
|
285
|
-
this.theme.margins.top -
|
|
286
|
-
this.theme.margins.bottom);
|
|
287
|
-
const referenceRadius = Math.max(1, plotHeight / 2);
|
|
288
|
-
const rawScale = outerRadius / referenceRadius;
|
|
289
|
-
return Math.max(0.5, Math.min(1, rawScale));
|
|
221
|
+
this.renderInlineLegend(svg);
|
|
290
222
|
}
|
|
291
223
|
getLegendSeries() {
|
|
292
|
-
return this.segments
|
|
293
|
-
return {
|
|
294
|
-
dataKey: segment.label,
|
|
295
|
-
fill: segment.color,
|
|
296
|
-
};
|
|
297
|
-
});
|
|
224
|
+
return this.getRadialLegendSeries(this.segments);
|
|
298
225
|
}
|
|
299
226
|
resolveSortComparator() {
|
|
300
227
|
if (typeof this.sort === 'function') {
|
|
@@ -308,10 +235,7 @@ export class PieChart extends BaseChart {
|
|
|
308
235
|
}
|
|
309
236
|
return null;
|
|
310
237
|
}
|
|
311
|
-
renderSegments(segments, cx, cy, innerRadius, outerRadius) {
|
|
312
|
-
if (!this.plotGroup || !this.svg) {
|
|
313
|
-
throw new Error('Plot group not initialized');
|
|
314
|
-
}
|
|
238
|
+
renderSegments(plotGroup, segments, cx, cy, innerRadius, outerRadius) {
|
|
315
239
|
const pieGenerator = pie()
|
|
316
240
|
.value((d) => d.value)
|
|
317
241
|
.startAngle(this.startAngle)
|
|
@@ -333,7 +257,7 @@ export class PieChart extends BaseChart {
|
|
|
333
257
|
.outerRadius(outerRadius + HOVER_EXPAND_PX)
|
|
334
258
|
.cornerRadius(this.cornerRadius);
|
|
335
259
|
const pieData = pieGenerator(segments);
|
|
336
|
-
const segmentGroup =
|
|
260
|
+
const segmentGroup = plotGroup
|
|
337
261
|
.append('g')
|
|
338
262
|
.attr('class', 'pie-segments')
|
|
339
263
|
.attr('transform', `translate(${cx}, ${cy})`);
|
|
@@ -367,13 +291,13 @@ export class PieChart extends BaseChart {
|
|
|
367
291
|
tooltipDiv
|
|
368
292
|
.style('visibility', 'visible')
|
|
369
293
|
.html(this.buildTooltipContent(d, segments));
|
|
370
|
-
this.
|
|
294
|
+
this.positionTooltipFromPointer(event, tooltipDiv);
|
|
371
295
|
}
|
|
372
296
|
})
|
|
373
297
|
.on('mousemove', (event) => {
|
|
374
298
|
const tooltipDiv = resolveTooltipDiv();
|
|
375
299
|
if (tooltipDiv && !tooltipDiv.empty()) {
|
|
376
|
-
this.
|
|
300
|
+
this.positionTooltipFromPointer(event, tooltipDiv);
|
|
377
301
|
}
|
|
378
302
|
})
|
|
379
303
|
.on('mouseleave', (event, d) => {
|
|
@@ -458,53 +382,6 @@ export class PieChart extends BaseChart {
|
|
|
458
382
|
const percentage = total > 0 ? ((d.data.value / total) * 100).toFixed(1) : '0.0';
|
|
459
383
|
return `${d.data.label}: ${d.data.value} (${percentage}%)`;
|
|
460
384
|
}
|
|
461
|
-
positionTooltip(event, tooltipDiv) {
|
|
462
|
-
const node = tooltipDiv.node();
|
|
463
|
-
if (!node) {
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
const rect = node.getBoundingClientRect();
|
|
467
|
-
let x = event.pageX + TOOLTIP_OFFSET_PX;
|
|
468
|
-
let y = event.pageY - rect.height / 2;
|
|
469
|
-
if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
470
|
-
x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
|
|
471
|
-
}
|
|
472
|
-
x = Math.max(EDGE_MARGIN_PX, x);
|
|
473
|
-
y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
|
|
474
|
-
window.scrollY -
|
|
475
|
-
rect.height -
|
|
476
|
-
EDGE_MARGIN_PX));
|
|
477
|
-
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
478
|
-
}
|
|
479
|
-
positionTooltipAtElement(target, tooltipDiv) {
|
|
480
|
-
const node = tooltipDiv.node();
|
|
481
|
-
if (!node) {
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
const targetRect = target.getBoundingClientRect();
|
|
485
|
-
const tooltipRect = node.getBoundingClientRect();
|
|
486
|
-
let x = targetRect.left +
|
|
487
|
-
window.scrollX +
|
|
488
|
-
targetRect.width / 2 +
|
|
489
|
-
TOOLTIP_OFFSET_PX;
|
|
490
|
-
let y = targetRect.top +
|
|
491
|
-
window.scrollY +
|
|
492
|
-
targetRect.height / 2 -
|
|
493
|
-
tooltipRect.height / 2;
|
|
494
|
-
if (x + tooltipRect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
495
|
-
x =
|
|
496
|
-
targetRect.left +
|
|
497
|
-
window.scrollX -
|
|
498
|
-
tooltipRect.width -
|
|
499
|
-
TOOLTIP_OFFSET_PX;
|
|
500
|
-
}
|
|
501
|
-
x = Math.max(EDGE_MARGIN_PX, x);
|
|
502
|
-
y = Math.max(EDGE_MARGIN_PX, Math.min(y, window.innerHeight +
|
|
503
|
-
window.scrollY -
|
|
504
|
-
tooltipRect.height -
|
|
505
|
-
EDGE_MARGIN_PX));
|
|
506
|
-
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
507
|
-
}
|
|
508
385
|
buildTooltipContent(d, segments) {
|
|
509
386
|
const total = segments.reduce((sum, segment) => sum + segment.value, 0);
|
|
510
387
|
const percentage = total > 0 ? ((d.data.value / total) * 100).toFixed(1) : '0.0';
|
|
@@ -534,8 +411,8 @@ export class PieChart extends BaseChart {
|
|
|
534
411
|
const insideArc = arc()
|
|
535
412
|
.innerRadius(insideLabelRadius)
|
|
536
413
|
.outerRadius(insideLabelRadius);
|
|
537
|
-
const fontSize = this.
|
|
538
|
-
const fontWeight = this.
|
|
414
|
+
const fontSize = this.renderTheme.legend.fontSize * fontScale;
|
|
415
|
+
const fontWeight = this.renderTheme.axis.fontWeight ?? 'normal';
|
|
539
416
|
const outsideLabels = [];
|
|
540
417
|
pieData.forEach((d) => {
|
|
541
418
|
const percentage = total > 0 ? (d.data.value / total) * 100 : 0;
|
|
@@ -549,7 +426,7 @@ export class PieChart extends BaseChart {
|
|
|
549
426
|
.attr('y', y)
|
|
550
427
|
.attr('text-anchor', 'middle')
|
|
551
428
|
.attr('dominant-baseline', 'middle')
|
|
552
|
-
.attr('font-family', this.
|
|
429
|
+
.attr('font-family', this.renderTheme.axis.fontFamily)
|
|
553
430
|
.attr('font-size', `${fontSize}px`)
|
|
554
431
|
.attr('font-weight', fontWeight)
|
|
555
432
|
.attr('fill', getContrastTextColor(d.data.color))
|
|
@@ -594,10 +471,10 @@ export class PieChart extends BaseChart {
|
|
|
594
471
|
.attr('y', outsideLabel.y)
|
|
595
472
|
.attr('text-anchor', outsideLabel.textAnchor)
|
|
596
473
|
.attr('dominant-baseline', 'middle')
|
|
597
|
-
.attr('font-family', this.
|
|
474
|
+
.attr('font-family', this.renderTheme.axis.fontFamily)
|
|
598
475
|
.attr('font-size', `${fontSize}px`)
|
|
599
476
|
.attr('font-weight', fontWeight)
|
|
600
|
-
.attr('fill', this.
|
|
477
|
+
.attr('fill', this.renderTheme.valueLabel.color)
|
|
601
478
|
.text(outsideLabel.datum.data.label);
|
|
602
479
|
});
|
|
603
480
|
}
|
|
@@ -625,7 +502,7 @@ export class PieChart extends BaseChart {
|
|
|
625
502
|
if (!svgNode) {
|
|
626
503
|
return false;
|
|
627
504
|
}
|
|
628
|
-
const textWidth = measureTextWidth(datum.data.label, fontSize, this.
|
|
505
|
+
const textWidth = measureTextWidth(datum.data.label, fontSize, this.renderTheme.axis.fontFamily, fontWeight, svgNode);
|
|
629
506
|
const angle = Math.max(0, datum.endAngle - datum.startAngle);
|
|
630
507
|
const availableArcLength = angle * insideLabelRadius - this.valueLabel.insideMargin * 2;
|
|
631
508
|
const availableRadialThickness = outerRadius - innerRadius - this.valueLabel.insideMargin * 2;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Selection } from 'd3';
|
|
2
|
+
import { BaseChart } from './base-chart.js';
|
|
3
|
+
import type { PlotAreaBounds } from './layout-manager.js';
|
|
4
|
+
import type { LegendSeries } from './types.js';
|
|
5
|
+
type RadialLegendItem = {
|
|
6
|
+
label: string;
|
|
7
|
+
color: string;
|
|
8
|
+
};
|
|
9
|
+
export declare abstract class RadialChartBase extends BaseChart {
|
|
10
|
+
protected initializeTooltip(): void;
|
|
11
|
+
protected getVisibleRadialItems<T extends RadialLegendItem>(items: T[]): T[];
|
|
12
|
+
protected getRadialLayout(plotArea: PlotAreaBounds, innerRadiusRatio: number): {
|
|
13
|
+
cx: number;
|
|
14
|
+
cy: number;
|
|
15
|
+
outerRadius: number;
|
|
16
|
+
innerRadius: number;
|
|
17
|
+
fontScale: number;
|
|
18
|
+
};
|
|
19
|
+
protected getRadialLegendSeries<T extends RadialLegendItem>(items: T[]): LegendSeries[];
|
|
20
|
+
protected positionTooltipFromPointer(event: MouseEvent, tooltipDiv: Selection<HTMLDivElement, unknown, HTMLElement, undefined>): void;
|
|
21
|
+
protected positionTooltipAtElement(target: Element, tooltipDiv: Selection<HTMLDivElement, unknown, HTMLElement, undefined>): void;
|
|
22
|
+
private applyTooltipPosition;
|
|
23
|
+
private resolveRadialFontScale;
|
|
24
|
+
}
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { BaseChart } from './base-chart.js';
|
|
2
|
+
const TOOLTIP_OFFSET_PX = 12;
|
|
3
|
+
const EDGE_MARGIN_PX = 10;
|
|
4
|
+
export class RadialChartBase extends BaseChart {
|
|
5
|
+
initializeTooltip() {
|
|
6
|
+
this.tooltip?.initialize(this.renderTheme);
|
|
7
|
+
}
|
|
8
|
+
getVisibleRadialItems(items) {
|
|
9
|
+
return this.filterVisibleItems(items, (item) => item.label);
|
|
10
|
+
}
|
|
11
|
+
getRadialLayout(plotArea, innerRadiusRatio) {
|
|
12
|
+
const cx = plotArea.left + plotArea.width / 2;
|
|
13
|
+
const cy = plotArea.top + plotArea.height / 2;
|
|
14
|
+
const outerRadius = Math.min(plotArea.width, plotArea.height) / 2;
|
|
15
|
+
return {
|
|
16
|
+
cx,
|
|
17
|
+
cy,
|
|
18
|
+
outerRadius,
|
|
19
|
+
innerRadius: outerRadius * innerRadiusRatio,
|
|
20
|
+
fontScale: this.resolveRadialFontScale(outerRadius, this.renderTheme),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
getRadialLegendSeries(items) {
|
|
24
|
+
return items.map((item) => ({
|
|
25
|
+
dataKey: item.label,
|
|
26
|
+
fill: item.color,
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
positionTooltipFromPointer(event, tooltipDiv) {
|
|
30
|
+
const node = tooltipDiv.node();
|
|
31
|
+
if (!node) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const rect = node.getBoundingClientRect();
|
|
35
|
+
let x = event.pageX + TOOLTIP_OFFSET_PX;
|
|
36
|
+
const y = event.pageY - rect.height / 2;
|
|
37
|
+
if (x + rect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
38
|
+
x = event.pageX - rect.width - TOOLTIP_OFFSET_PX;
|
|
39
|
+
}
|
|
40
|
+
this.applyTooltipPosition(tooltipDiv, x, y, rect.height, rect.width);
|
|
41
|
+
}
|
|
42
|
+
positionTooltipAtElement(target, tooltipDiv) {
|
|
43
|
+
const node = tooltipDiv.node();
|
|
44
|
+
if (!node) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const targetRect = target.getBoundingClientRect();
|
|
48
|
+
const tooltipRect = node.getBoundingClientRect();
|
|
49
|
+
let x = targetRect.left +
|
|
50
|
+
window.scrollX +
|
|
51
|
+
targetRect.width / 2 +
|
|
52
|
+
TOOLTIP_OFFSET_PX;
|
|
53
|
+
const y = targetRect.top +
|
|
54
|
+
window.scrollY +
|
|
55
|
+
targetRect.height / 2 -
|
|
56
|
+
tooltipRect.height / 2;
|
|
57
|
+
if (x + tooltipRect.width > window.innerWidth - EDGE_MARGIN_PX) {
|
|
58
|
+
x =
|
|
59
|
+
targetRect.left +
|
|
60
|
+
window.scrollX -
|
|
61
|
+
tooltipRect.width -
|
|
62
|
+
TOOLTIP_OFFSET_PX;
|
|
63
|
+
}
|
|
64
|
+
this.applyTooltipPosition(tooltipDiv, x, y, tooltipRect.height, tooltipRect.width);
|
|
65
|
+
}
|
|
66
|
+
applyTooltipPosition(tooltipDiv, rawX, rawY, height, width) {
|
|
67
|
+
const x = Math.max(EDGE_MARGIN_PX, Math.min(rawX, window.innerWidth + window.scrollX - width - EDGE_MARGIN_PX));
|
|
68
|
+
const y = Math.max(EDGE_MARGIN_PX, Math.min(rawY, window.innerHeight + window.scrollY - height - EDGE_MARGIN_PX));
|
|
69
|
+
tooltipDiv.style('left', `${x}px`).style('top', `${y}px`);
|
|
70
|
+
}
|
|
71
|
+
resolveRadialFontScale(outerRadius, theme) {
|
|
72
|
+
const plotHeight = Math.max(1, theme.height - theme.margins.top - theme.margins.bottom);
|
|
73
|
+
const referenceRadius = Math.max(1, plotHeight / 2);
|
|
74
|
+
const rawScale = outerRadius / referenceRadius;
|
|
75
|
+
return Math.max(0.5, Math.min(1, rawScale));
|
|
76
|
+
}
|
|
77
|
+
}
|
package/scale-utils.d.ts
ADDED
package/scale-utils.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function resolveScaleValue(value, scaleType) {
|
|
2
|
+
switch (scaleType) {
|
|
3
|
+
case 'band':
|
|
4
|
+
return String(value);
|
|
5
|
+
case 'time':
|
|
6
|
+
return value instanceof Date ? value : new Date(String(value));
|
|
7
|
+
case 'linear':
|
|
8
|
+
case 'log':
|
|
9
|
+
return typeof value === 'number' ? value : Number(value);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function getScalePosition(scale, value, scaleType) {
|
|
13
|
+
return scale(resolveScaleValue(value, scaleType)) || 0;
|
|
14
|
+
}
|
package/utils.d.ts
CHANGED
|
@@ -11,6 +11,13 @@ export declare function cn(...inputs: ClassValue[]): string;
|
|
|
11
11
|
* @returns A valid CSS class/ID string
|
|
12
12
|
*/
|
|
13
13
|
export declare function sanitizeForCSS(str: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Escapes a string for safe insertion into HTML content.
|
|
16
|
+
*
|
|
17
|
+
* @param str - The string to escape
|
|
18
|
+
* @returns The escaped HTML string
|
|
19
|
+
*/
|
|
20
|
+
export declare function escapeHtml(str: string): string;
|
|
14
21
|
/**
|
|
15
22
|
* Measures the width of text in pixels using an SVG text element.
|
|
16
23
|
*
|
package/utils.js
CHANGED
|
@@ -20,6 +20,30 @@ export function sanitizeForCSS(str) {
|
|
|
20
20
|
.replace(/^-+|-+$/g, '') // Remove leading/trailing hyphens
|
|
21
21
|
.toLowerCase(); // Convert to lowercase for consistency
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Escapes a string for safe insertion into HTML content.
|
|
25
|
+
*
|
|
26
|
+
* @param str - The string to escape
|
|
27
|
+
* @returns The escaped HTML string
|
|
28
|
+
*/
|
|
29
|
+
export function escapeHtml(str) {
|
|
30
|
+
return str.replace(/[&<>"']/g, (character) => {
|
|
31
|
+
switch (character) {
|
|
32
|
+
case '&':
|
|
33
|
+
return '&';
|
|
34
|
+
case '<':
|
|
35
|
+
return '<';
|
|
36
|
+
case '>':
|
|
37
|
+
return '>';
|
|
38
|
+
case '"':
|
|
39
|
+
return '"';
|
|
40
|
+
case "'":
|
|
41
|
+
return ''';
|
|
42
|
+
default:
|
|
43
|
+
return character;
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
}
|
|
23
47
|
/**
|
|
24
48
|
* Measures the width of text in pixels using an SVG text element.
|
|
25
49
|
*
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { BaseChart, type BaseChartConfig, type BaseRenderContext } from './base-chart.js';
|
|
2
|
+
import type { ChartData } from './types.js';
|
|
3
|
+
export type WordCloudRotationMode = 'none' | 'right-angle';
|
|
4
|
+
export type WordCloudSpiral = 'archimedean' | 'rectangular';
|
|
5
|
+
export type WordCloudConfig = {
|
|
6
|
+
maxWords?: number;
|
|
7
|
+
minWordLength?: number;
|
|
8
|
+
minValue?: number;
|
|
9
|
+
minFontSize?: number;
|
|
10
|
+
maxFontSize?: number;
|
|
11
|
+
padding?: number;
|
|
12
|
+
rotation?: WordCloudRotationMode;
|
|
13
|
+
spiral?: WordCloudSpiral;
|
|
14
|
+
};
|
|
15
|
+
export type WordCloudChartConfig = Pick<BaseChartConfig, 'data' | 'theme' | 'responsive'> & {
|
|
16
|
+
wordCloud?: WordCloudConfig;
|
|
17
|
+
};
|
|
18
|
+
export declare class WordCloudChart extends BaseChart {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private layout;
|
|
21
|
+
private layoutRunId;
|
|
22
|
+
private resolvePendingReady;
|
|
23
|
+
constructor(config: WordCloudChartConfig);
|
|
24
|
+
destroy(): void;
|
|
25
|
+
protected validateSourceData(data: ChartData): void;
|
|
26
|
+
protected renderChart({ svg, plotArea }: BaseRenderContext): void;
|
|
27
|
+
protected createExportChart(): BaseChart;
|
|
28
|
+
private startLayout;
|
|
29
|
+
private renderWords;
|
|
30
|
+
private stopLayout;
|
|
31
|
+
private finishReady;
|
|
32
|
+
}
|