@internetstiftelsen/charts 0.11.0 → 0.13.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 +23 -1
- package/dist/base-chart.d.ts +50 -3
- package/dist/base-chart.js +188 -40
- package/dist/chart-group.d.ts +15 -2
- package/dist/chart-group.js +181 -45
- package/dist/chart-interface.d.ts +3 -3
- package/dist/grouped-data.d.ts +1 -0
- package/dist/grouped-data.js +80 -40
- package/dist/grouped-tabular.js +12 -3
- package/dist/layout-manager.js +4 -4
- package/dist/text.d.ts +40 -0
- package/dist/text.js +217 -0
- package/dist/theme.js +59 -3
- package/dist/title.d.ts +6 -12
- package/dist/title.js +29 -82
- package/dist/tooltip.d.ts +4 -2
- package/dist/tooltip.js +101 -77
- package/dist/types.d.ts +34 -1
- package/dist/xy-chart.js +1 -1
- package/docs/chart-group.md +24 -5
- package/docs/components.md +99 -15
- package/docs/donut-chart.md +2 -1
- package/docs/gauge-chart.md +2 -1
- package/docs/getting-started.md +25 -1
- package/docs/pie-chart.md +2 -1
- package/docs/theming.md +35 -0
- package/docs/word-cloud-chart.md +1 -0
- package/package.json +1 -1
package/dist/chart-group.js
CHANGED
|
@@ -174,11 +174,11 @@ export class ChartGroup {
|
|
|
174
174
|
writable: true,
|
|
175
175
|
value: null
|
|
176
176
|
});
|
|
177
|
-
Object.defineProperty(this, "
|
|
177
|
+
Object.defineProperty(this, "textComponents", {
|
|
178
178
|
enumerable: true,
|
|
179
179
|
configurable: true,
|
|
180
180
|
writable: true,
|
|
181
|
-
value:
|
|
181
|
+
value: []
|
|
182
182
|
});
|
|
183
183
|
Object.defineProperty(this, "resizeObserver", {
|
|
184
184
|
enumerable: true,
|
|
@@ -253,8 +253,8 @@ export class ChartGroup {
|
|
|
253
253
|
return this;
|
|
254
254
|
}
|
|
255
255
|
addChild(component) {
|
|
256
|
-
if (component
|
|
257
|
-
this.
|
|
256
|
+
if (this.isTextComponent(component)) {
|
|
257
|
+
this.textComponents.push(component);
|
|
258
258
|
if (this.container) {
|
|
259
259
|
this.refresh();
|
|
260
260
|
}
|
|
@@ -272,7 +272,10 @@ export class ChartGroup {
|
|
|
272
272
|
}
|
|
273
273
|
return this;
|
|
274
274
|
}
|
|
275
|
-
throw new Error('ChartGroup only supports Title and Legend via addChild()');
|
|
275
|
+
throw new Error('ChartGroup only supports Text, Title, and Legend via addChild()');
|
|
276
|
+
}
|
|
277
|
+
isTextComponent(component) {
|
|
278
|
+
return component.type === 'text' || component.type === 'title';
|
|
276
279
|
}
|
|
277
280
|
render(target) {
|
|
278
281
|
this.container = this.resolveContainer(target);
|
|
@@ -285,17 +288,18 @@ export class ChartGroup {
|
|
|
285
288
|
return null;
|
|
286
289
|
}
|
|
287
290
|
const container = this.container;
|
|
288
|
-
const { width,
|
|
291
|
+
const { width, renderedTopText, renderedBottomText, renderedLegend, layout, totalHeight, } = this.prepareRenderState(container);
|
|
289
292
|
this.isRendering = true;
|
|
290
293
|
try {
|
|
291
294
|
this.applyScaleSyncOverrides(width);
|
|
292
295
|
container.innerHTML = '';
|
|
293
296
|
const { root, chartLayer } = this.createRenderHosts(totalHeight, layout.chartHeight);
|
|
294
|
-
this.
|
|
297
|
+
this.appendRenderedTextSections(root, renderedTopText);
|
|
295
298
|
root.appendChild(chartLayer);
|
|
296
299
|
container.appendChild(root);
|
|
297
300
|
this.renderLayoutItems(chartLayer, layout.items);
|
|
298
301
|
this.appendRenderedSection(root, 'chart-group__legend', renderedLegend);
|
|
302
|
+
this.appendRenderedTextSections(root, renderedBottomText);
|
|
299
303
|
this.readyPromise = this.createReadyPromise();
|
|
300
304
|
this.syncLegendStateFromChildren();
|
|
301
305
|
this.childYDomainSnapshot = this.serializeChildYDomains(width);
|
|
@@ -338,11 +342,20 @@ export class ChartGroup {
|
|
|
338
342
|
onLegendChange(callback) {
|
|
339
343
|
return this.legendState.subscribe(callback);
|
|
340
344
|
}
|
|
345
|
+
resolveAccessibleLabel() {
|
|
346
|
+
return (this.textComponents
|
|
347
|
+
.find((component) => {
|
|
348
|
+
return (component.display &&
|
|
349
|
+
(component.type === 'title' ||
|
|
350
|
+
component.variant === 'title'));
|
|
351
|
+
})
|
|
352
|
+
?.text.trim() || 'Chart group');
|
|
353
|
+
}
|
|
341
354
|
async export(format, options) {
|
|
342
355
|
const container = this.requireRenderedContainer(options);
|
|
343
356
|
await this.whenReady();
|
|
344
357
|
const width = options?.width ?? this.resolveContainerWidth(container);
|
|
345
|
-
const { svg, height } = await this.exportSVG(width, options);
|
|
358
|
+
const { svg, height } = await this.exportSVG(format, width, options);
|
|
346
359
|
const content = await this.resolveExportContent(format, svg, width, height, options);
|
|
347
360
|
if (options?.download) {
|
|
348
361
|
this.downloadContent(content, format, options);
|
|
@@ -701,11 +714,11 @@ export class ChartGroup {
|
|
|
701
714
|
legendHost.appendChild(renderedLegend.svg);
|
|
702
715
|
root.appendChild(legendHost);
|
|
703
716
|
}
|
|
704
|
-
|
|
705
|
-
if (!
|
|
717
|
+
renderTextSvg(width, component) {
|
|
718
|
+
if (!component.display) {
|
|
706
719
|
return null;
|
|
707
720
|
}
|
|
708
|
-
const height = Math.max(1,
|
|
721
|
+
const height = Math.max(1, component.getRequiredSpace(this.theme).height);
|
|
709
722
|
const svg = create('svg')
|
|
710
723
|
.attr('width', width)
|
|
711
724
|
.attr('height', height)
|
|
@@ -714,14 +727,27 @@ export class ChartGroup {
|
|
|
714
727
|
if (!svgNode) {
|
|
715
728
|
return null;
|
|
716
729
|
}
|
|
717
|
-
|
|
730
|
+
component.render(svg, this.theme, width);
|
|
718
731
|
return {
|
|
732
|
+
component,
|
|
719
733
|
height,
|
|
720
734
|
svg: svgNode,
|
|
721
735
|
};
|
|
722
736
|
}
|
|
723
|
-
|
|
724
|
-
|
|
737
|
+
renderTextSvgs(width, position, components = this.textComponents) {
|
|
738
|
+
return components
|
|
739
|
+
.filter((component) => {
|
|
740
|
+
return component.position === position;
|
|
741
|
+
})
|
|
742
|
+
.map((component) => {
|
|
743
|
+
return this.renderTextSvg(width, component);
|
|
744
|
+
})
|
|
745
|
+
.filter((section) => {
|
|
746
|
+
return section !== null;
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
renderLegendSvg(width, legend = this.legend) {
|
|
750
|
+
if (!legend || legend.mode === 'hidden') {
|
|
725
751
|
return null;
|
|
726
752
|
}
|
|
727
753
|
const items = this.getLegendItemsForWidth(width);
|
|
@@ -750,10 +776,10 @@ export class ChartGroup {
|
|
|
750
776
|
fill: item.color,
|
|
751
777
|
};
|
|
752
778
|
});
|
|
753
|
-
|
|
754
|
-
const height = Math.max(1,
|
|
779
|
+
legend.estimateLayoutSpace(series, this.theme, width, svgNode);
|
|
780
|
+
const height = Math.max(1, legend.getMeasuredHeight());
|
|
755
781
|
svg.attr('height', height);
|
|
756
|
-
|
|
782
|
+
legend.render(svg, series, this.theme, width);
|
|
757
783
|
return {
|
|
758
784
|
height,
|
|
759
785
|
svg: svgNode,
|
|
@@ -775,15 +801,23 @@ export class ChartGroup {
|
|
|
775
801
|
});
|
|
776
802
|
this.resizeObserver.observe(this.container);
|
|
777
803
|
}
|
|
778
|
-
async exportSVG(width, options) {
|
|
804
|
+
async exportSVG(format, width, options) {
|
|
779
805
|
this.applyScaleSyncOverrides(width);
|
|
780
|
-
const
|
|
806
|
+
const baseContext = this.createExportRenderContext(format, width, options);
|
|
807
|
+
const exportLayoutState = this.resolveExportLayoutState(width, baseContext);
|
|
781
808
|
const layout = this.calculateLayout(width, exportLayoutState.chartAreaHeight, exportLayoutState.defaultChartHeightOverride);
|
|
782
809
|
const childSvgs = await this.exportLayoutItems(layout.items, options);
|
|
783
|
-
const totalHeight = exportLayoutState.
|
|
810
|
+
const totalHeight = exportLayoutState.topTextHeight +
|
|
784
811
|
layout.chartHeight +
|
|
785
|
-
exportLayoutState.legendHeight
|
|
812
|
+
exportLayoutState.legendHeight +
|
|
813
|
+
exportLayoutState.bottomTextHeight;
|
|
786
814
|
const exportSvg = this.composeExportSvg(width, totalHeight, layout.chartHeight, exportLayoutState, childSvgs);
|
|
815
|
+
this.runExportHooks({
|
|
816
|
+
...baseContext,
|
|
817
|
+
height: totalHeight,
|
|
818
|
+
svg: exportSvg,
|
|
819
|
+
}, exportLayoutState);
|
|
820
|
+
this.syncAccessibleLabelFromSvg(exportSvg);
|
|
787
821
|
return {
|
|
788
822
|
svg: exportSvg.outerHTML,
|
|
789
823
|
height: totalHeight,
|
|
@@ -805,21 +839,31 @@ export class ChartGroup {
|
|
|
805
839
|
}
|
|
806
840
|
prepareRenderState(container) {
|
|
807
841
|
const width = this.resolveContainerWidth(container);
|
|
808
|
-
const
|
|
842
|
+
const renderedTopText = this.renderTextSvgs(width, 'top');
|
|
843
|
+
const renderedBottomText = this.renderTextSvgs(width, 'bottom');
|
|
809
844
|
const renderedLegend = this.renderLegendSvg(width);
|
|
810
|
-
const
|
|
845
|
+
const topTextHeight = this.sumRenderedTextHeight(renderedTopText);
|
|
846
|
+
const bottomTextHeight = this.sumRenderedTextHeight(renderedBottomText);
|
|
847
|
+
const chartAreaHeight = this.resolveChartAreaHeightConstraint(this.resolveTotalHeightConstraint(container.getBoundingClientRect()), topTextHeight, (renderedLegend?.height ?? 0) + bottomTextHeight);
|
|
811
848
|
const layout = this.calculateLayout(width, chartAreaHeight);
|
|
812
|
-
const totalHeight =
|
|
849
|
+
const totalHeight = topTextHeight +
|
|
813
850
|
layout.chartHeight +
|
|
814
|
-
(renderedLegend?.height ?? 0)
|
|
851
|
+
(renderedLegend?.height ?? 0) +
|
|
852
|
+
bottomTextHeight;
|
|
815
853
|
return {
|
|
816
854
|
width,
|
|
817
|
-
|
|
855
|
+
renderedTopText,
|
|
856
|
+
renderedBottomText,
|
|
818
857
|
renderedLegend,
|
|
819
858
|
layout,
|
|
820
859
|
totalHeight,
|
|
821
860
|
};
|
|
822
861
|
}
|
|
862
|
+
sumRenderedTextHeight(sections) {
|
|
863
|
+
return sections.reduce((total, section) => {
|
|
864
|
+
return total + section.height;
|
|
865
|
+
}, 0);
|
|
866
|
+
}
|
|
823
867
|
createRenderHosts(totalHeight, chartHeight) {
|
|
824
868
|
const root = document.createElement('div');
|
|
825
869
|
root.className = 'chart-group';
|
|
@@ -842,6 +886,16 @@ export class ChartGroup {
|
|
|
842
886
|
host.appendChild(section.svg);
|
|
843
887
|
root.appendChild(host);
|
|
844
888
|
}
|
|
889
|
+
appendRenderedTextSections(root, sections) {
|
|
890
|
+
sections.forEach((section) => {
|
|
891
|
+
this.appendRenderedSection(root, this.resolveTextSectionClassName(section.component), section);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
resolveTextSectionClassName(component) {
|
|
895
|
+
return component.type === 'title'
|
|
896
|
+
? 'chart-group__title'
|
|
897
|
+
: 'chart-group__text';
|
|
898
|
+
}
|
|
845
899
|
renderLayoutItems(chartLayer, items) {
|
|
846
900
|
items.forEach((item) => {
|
|
847
901
|
const chartHost = this.createChartHost(item);
|
|
@@ -922,37 +976,103 @@ export class ChartGroup {
|
|
|
922
976
|
margin: options?.pdfMargin ?? 0,
|
|
923
977
|
});
|
|
924
978
|
}
|
|
925
|
-
resolveExportLayoutState(width) {
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
979
|
+
resolveExportLayoutState(width, context) {
|
|
980
|
+
const textComponents = this.textComponents
|
|
981
|
+
.map((component) => {
|
|
982
|
+
return this.createExportComponent(component, context);
|
|
983
|
+
})
|
|
984
|
+
.filter((component) => {
|
|
985
|
+
return component !== null;
|
|
986
|
+
});
|
|
987
|
+
const legend = this.createExportComponent(this.legend, context);
|
|
988
|
+
const renderedTopText = this.renderTextSvgs(width, 'top', textComponents);
|
|
989
|
+
const renderedBottomText = this.renderTextSvgs(width, 'bottom', textComponents);
|
|
990
|
+
const renderedLegend = this.renderLegendSvg(width, legend);
|
|
991
|
+
return this.createExportLayoutState(renderedTopText, renderedBottomText, renderedLegend);
|
|
992
|
+
}
|
|
993
|
+
createExportLayoutState(renderedTopText, renderedBottomText, renderedLegend) {
|
|
994
|
+
const topTextHeight = this.sumRenderedTextHeight(renderedTopText);
|
|
995
|
+
const bottomTextHeight = this.sumRenderedTextHeight(renderedBottomText);
|
|
929
996
|
const legendHeight = renderedLegend?.height ?? 0;
|
|
930
997
|
if (this.configuredHeight !== undefined) {
|
|
931
998
|
return {
|
|
932
|
-
|
|
999
|
+
renderedTopText,
|
|
1000
|
+
renderedBottomText,
|
|
933
1001
|
renderedLegend,
|
|
934
|
-
|
|
1002
|
+
topTextHeight,
|
|
1003
|
+
bottomTextHeight,
|
|
935
1004
|
legendHeight,
|
|
936
|
-
chartAreaHeight: this.resolveChartAreaHeightConstraint(this.configuredHeight,
|
|
1005
|
+
chartAreaHeight: this.resolveChartAreaHeightConstraint(this.configuredHeight, topTextHeight, legendHeight + bottomTextHeight),
|
|
937
1006
|
};
|
|
938
1007
|
}
|
|
939
1008
|
const defaultChartHeightOverride = this.resolveLiveChartHeightOverride();
|
|
940
1009
|
return {
|
|
941
|
-
|
|
1010
|
+
renderedTopText,
|
|
1011
|
+
renderedBottomText,
|
|
942
1012
|
renderedLegend,
|
|
943
|
-
|
|
1013
|
+
topTextHeight,
|
|
1014
|
+
bottomTextHeight,
|
|
944
1015
|
legendHeight,
|
|
945
1016
|
defaultChartHeightOverride,
|
|
946
1017
|
};
|
|
947
1018
|
}
|
|
1019
|
+
createExportRenderContext(format, width, options) {
|
|
1020
|
+
return {
|
|
1021
|
+
format,
|
|
1022
|
+
options,
|
|
1023
|
+
width,
|
|
1024
|
+
height: this.resolveBaseExportHeight(width),
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
1027
|
+
resolveBaseExportHeight(width) {
|
|
1028
|
+
const state = this.createExportLayoutState(this.renderTextSvgs(width, 'top'), this.renderTextSvgs(width, 'bottom'), this.renderLegendSvg(width));
|
|
1029
|
+
const layout = this.calculateLayout(width, state.chartAreaHeight, state.defaultChartHeightOverride);
|
|
1030
|
+
return (state.topTextHeight +
|
|
1031
|
+
layout.chartHeight +
|
|
1032
|
+
state.legendHeight +
|
|
1033
|
+
state.bottomTextHeight);
|
|
1034
|
+
}
|
|
1035
|
+
createExportComponent(component, context) {
|
|
1036
|
+
if (!component) {
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
const exportable = component;
|
|
1040
|
+
const currentConfig = exportable.getExportConfig?.() ?? {};
|
|
1041
|
+
const result = exportable.exportHooks?.beforeRender?.call(component, context, currentConfig);
|
|
1042
|
+
if (!result ||
|
|
1043
|
+
typeof result !== 'object' ||
|
|
1044
|
+
!exportable.createExportComponent) {
|
|
1045
|
+
return component;
|
|
1046
|
+
}
|
|
1047
|
+
return exportable.createExportComponent(result);
|
|
1048
|
+
}
|
|
1049
|
+
runExportHooks(context, state) {
|
|
1050
|
+
[
|
|
1051
|
+
...state.renderedTopText.map((rendered) => {
|
|
1052
|
+
return { component: rendered.component, rendered };
|
|
1053
|
+
}),
|
|
1054
|
+
{ component: this.legend, rendered: state.renderedLegend },
|
|
1055
|
+
...state.renderedBottomText.map((rendered) => {
|
|
1056
|
+
return { component: rendered.component, rendered };
|
|
1057
|
+
}),
|
|
1058
|
+
].forEach(({ component, rendered }) => {
|
|
1059
|
+
if (!component || !rendered) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
const exportable = component;
|
|
1063
|
+
exportable.exportHooks?.before?.call(component, context);
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
948
1066
|
resolveLiveChartHeightOverride() {
|
|
949
1067
|
if (!this.container) {
|
|
950
1068
|
return undefined;
|
|
951
1069
|
}
|
|
952
1070
|
const liveWidth = this.resolveContainerWidth(this.container);
|
|
953
|
-
const
|
|
1071
|
+
const liveTopText = this.renderTextSvgs(liveWidth, 'top');
|
|
1072
|
+
const liveBottomText = this.renderTextSvgs(liveWidth, 'bottom');
|
|
954
1073
|
const liveLegend = this.renderLegendSvg(liveWidth);
|
|
955
|
-
const liveChartAreaHeight = this.resolveChartAreaHeightConstraint(this.resolveTotalHeightConstraint(this.container.getBoundingClientRect()),
|
|
1074
|
+
const liveChartAreaHeight = this.resolveChartAreaHeightConstraint(this.resolveTotalHeightConstraint(this.container.getBoundingClientRect()), this.sumRenderedTextHeight(liveTopText), (liveLegend?.height ?? 0) +
|
|
1075
|
+
this.sumRenderedTextHeight(liveBottomText));
|
|
956
1076
|
if (liveChartAreaHeight === undefined) {
|
|
957
1077
|
return undefined;
|
|
958
1078
|
}
|
|
@@ -982,28 +1102,44 @@ export class ChartGroup {
|
|
|
982
1102
|
exportSvg.setAttribute('width', String(width));
|
|
983
1103
|
exportSvg.setAttribute('height', String(totalHeight));
|
|
984
1104
|
exportSvg.setAttribute('role', 'img');
|
|
985
|
-
exportSvg.setAttribute('aria-label', this.
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
exportSvg.appendChild(document.importNode(state.renderedTitle.svg, true));
|
|
990
|
-
}
|
|
1105
|
+
exportSvg.setAttribute('aria-label', this.resolveAccessibleLabel());
|
|
1106
|
+
let yOffset = 0;
|
|
1107
|
+
this.appendExportTextSections(exportSvg, state.renderedTopText, yOffset);
|
|
1108
|
+
yOffset += state.topTextHeight;
|
|
991
1109
|
childSvgs.forEach((item) => {
|
|
992
1110
|
const parsed = parser.parseFromString(item.svg, 'image/svg+xml');
|
|
993
1111
|
const imported = document.importNode(parsed.documentElement, true);
|
|
994
1112
|
imported.setAttribute('x', String(item.x));
|
|
995
|
-
imported.setAttribute('y', String(
|
|
1113
|
+
imported.setAttribute('y', String(yOffset + item.y));
|
|
996
1114
|
imported.setAttribute('width', String(item.width));
|
|
997
1115
|
imported.setAttribute('height', String(item.height));
|
|
998
1116
|
exportSvg.appendChild(imported);
|
|
999
1117
|
});
|
|
1118
|
+
yOffset += chartHeight;
|
|
1000
1119
|
if (state.renderedLegend) {
|
|
1001
1120
|
state.renderedLegend.svg.setAttribute('x', '0');
|
|
1002
|
-
state.renderedLegend.svg.setAttribute('y', String(
|
|
1121
|
+
state.renderedLegend.svg.setAttribute('y', String(yOffset));
|
|
1003
1122
|
exportSvg.appendChild(document.importNode(state.renderedLegend.svg, true));
|
|
1123
|
+
yOffset += state.legendHeight;
|
|
1004
1124
|
}
|
|
1125
|
+
this.appendExportTextSections(exportSvg, state.renderedBottomText, yOffset);
|
|
1005
1126
|
return exportSvg;
|
|
1006
1127
|
}
|
|
1128
|
+
syncAccessibleLabelFromSvg(svg) {
|
|
1129
|
+
const titleText = svg
|
|
1130
|
+
.querySelector('.title text, .text--title text')
|
|
1131
|
+
?.textContent?.trim();
|
|
1132
|
+
svg.setAttribute('aria-label', titleText || this.resolveAccessibleLabel());
|
|
1133
|
+
}
|
|
1134
|
+
appendExportTextSections(exportSvg, sections, startY) {
|
|
1135
|
+
let yOffset = startY;
|
|
1136
|
+
sections.forEach((section) => {
|
|
1137
|
+
section.svg.setAttribute('x', '0');
|
|
1138
|
+
section.svg.setAttribute('y', String(yOffset));
|
|
1139
|
+
exportSvg.appendChild(document.importNode(section.svg, true));
|
|
1140
|
+
yOffset += section.height;
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1007
1143
|
validateResponsiveConfig(responsive) {
|
|
1008
1144
|
const breakpoints = responsive?.breakpoints;
|
|
1009
1145
|
if (!breakpoints) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ExportHooks } from './types.js';
|
|
2
|
-
export type ChartComponentType = 'line' | 'scatter' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'title' | 'donutCenterContent';
|
|
1
|
+
import type { ChartTheme, ExportHooks } from './types.js';
|
|
2
|
+
export type ChartComponentType = 'line' | 'scatter' | 'area' | 'bar' | 'xAxis' | 'yAxis' | 'grid' | 'tooltip' | 'legend' | 'text' | 'title' | 'donutCenterContent';
|
|
3
3
|
export interface ChartComponentBase {
|
|
4
4
|
type: ChartComponentType;
|
|
5
5
|
}
|
|
@@ -13,7 +13,7 @@ export type ComponentSpace = {
|
|
|
13
13
|
position: 'top' | 'bottom' | 'left' | 'right';
|
|
14
14
|
};
|
|
15
15
|
export interface LayoutAwareComponentBase extends ChartComponentBase {
|
|
16
|
-
getRequiredSpace(): ComponentSpace;
|
|
16
|
+
getRequiredSpace(theme?: ChartTheme): ComponentSpace;
|
|
17
17
|
}
|
|
18
18
|
export interface LayoutAwareComponent<TConfig = unknown> extends ChartComponent<TConfig>, LayoutAwareComponentBase {
|
|
19
19
|
}
|
package/dist/grouped-data.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type NormalizedChartData = {
|
|
|
13
13
|
data: DataItem[];
|
|
14
14
|
schema: DataSchema;
|
|
15
15
|
};
|
|
16
|
+
export declare function attachDataSchema<T extends ChartData>(data: T, schema: DataSchema): T;
|
|
16
17
|
export declare function hasEmptyGroupNames(data: GroupedDataGroup[]): boolean;
|
|
17
18
|
export declare function isGroupedData(data: unknown): data is GroupedDataGroup[];
|
|
18
19
|
export declare function resolveDataSchema(data: ChartData): DataSchema;
|
package/dist/grouped-data.js
CHANGED
|
@@ -3,9 +3,78 @@ export const GROUPED_CATEGORY_LABEL_KEY = '__iis_grouped_category_label__';
|
|
|
3
3
|
export const GROUPED_GROUP_LABEL_KEY = '__iis_grouped_group_label__';
|
|
4
4
|
export const GROUPED_GAP_TICK_PREFIX = '__iis_group_gap__';
|
|
5
5
|
export const GROUPED_EMPTY_GROUP_ERROR = 'Grouped data requires non-empty group names';
|
|
6
|
+
const DATA_SCHEMA_KEY = Symbol('iisChartDataSchema');
|
|
7
|
+
const CATEGORY_KEY_NAMES = new Set(['category', 'kategori', 'label', 'name']);
|
|
8
|
+
export function attachDataSchema(data, schema) {
|
|
9
|
+
Object.defineProperty(data, DATA_SCHEMA_KEY, {
|
|
10
|
+
value: schema,
|
|
11
|
+
enumerable: false,
|
|
12
|
+
configurable: true,
|
|
13
|
+
});
|
|
14
|
+
return data;
|
|
15
|
+
}
|
|
6
16
|
function isRecord(value) {
|
|
7
17
|
return typeof value === 'object' && value !== null;
|
|
8
18
|
}
|
|
19
|
+
function isBlankValue(value) {
|
|
20
|
+
if (value === null || value === undefined) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
return typeof value === 'string' && value.trim() === '';
|
|
24
|
+
}
|
|
25
|
+
function isNumericLikeValue(value) {
|
|
26
|
+
if (typeof value === 'number') {
|
|
27
|
+
return Number.isFinite(value);
|
|
28
|
+
}
|
|
29
|
+
if (typeof value !== 'string') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
const trimmed = value.trim();
|
|
33
|
+
return trimmed.length > 0 && Number.isFinite(parseFloat(trimmed));
|
|
34
|
+
}
|
|
35
|
+
function collectDataKeys(data) {
|
|
36
|
+
const seen = new Set();
|
|
37
|
+
const keys = [];
|
|
38
|
+
data.forEach((row) => {
|
|
39
|
+
Object.keys(row).forEach((key) => {
|
|
40
|
+
if (seen.has(key)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
seen.add(key);
|
|
44
|
+
keys.push(key);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return keys;
|
|
48
|
+
}
|
|
49
|
+
function hasOnlyNonNumericValues(data, key) {
|
|
50
|
+
let hasValue = false;
|
|
51
|
+
return (data.every((row) => {
|
|
52
|
+
const value = row[key];
|
|
53
|
+
if (isBlankValue(value)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
hasValue = true;
|
|
57
|
+
return !isNumericLikeValue(value);
|
|
58
|
+
}) && hasValue);
|
|
59
|
+
}
|
|
60
|
+
function isNamedCategoryKey(key) {
|
|
61
|
+
return CATEGORY_KEY_NAMES.has(key.trim().toLowerCase());
|
|
62
|
+
}
|
|
63
|
+
function resolveCategoryKey(data, fallback) {
|
|
64
|
+
const keys = collectDataKeys(data);
|
|
65
|
+
const namedCategoryKey = keys.find(isNamedCategoryKey);
|
|
66
|
+
if (namedCategoryKey) {
|
|
67
|
+
return namedCategoryKey;
|
|
68
|
+
}
|
|
69
|
+
const nonNumericCategoryKey = keys.find((key) => hasOnlyNonNumericValues(data, key));
|
|
70
|
+
return nonNumericCategoryKey ?? keys[0] ?? fallback;
|
|
71
|
+
}
|
|
72
|
+
function resolveMetricKeys(data, categoryKey) {
|
|
73
|
+
return collectDataKeys(data).filter((key) => key !== categoryKey);
|
|
74
|
+
}
|
|
75
|
+
function getAttachedDataSchema(data) {
|
|
76
|
+
return data[DATA_SCHEMA_KEY] ?? null;
|
|
77
|
+
}
|
|
9
78
|
function hasNonEmptyGroupName(value) {
|
|
10
79
|
return typeof value === 'string' && value.trim().length > 0;
|
|
11
80
|
}
|
|
@@ -38,56 +107,27 @@ function resolveFlatSchema(data) {
|
|
|
38
107
|
metricKeys: [],
|
|
39
108
|
};
|
|
40
109
|
}
|
|
41
|
-
const
|
|
42
|
-
const categoryKey = keys[0] ?? 'column';
|
|
43
|
-
const metricKeys = [];
|
|
44
|
-
const seen = new Set();
|
|
45
|
-
data.forEach((row) => {
|
|
46
|
-
Object.keys(row).forEach((key) => {
|
|
47
|
-
if (key === categoryKey || seen.has(key)) {
|
|
48
|
-
return;
|
|
49
|
-
}
|
|
50
|
-
seen.add(key);
|
|
51
|
-
metricKeys.push(key);
|
|
52
|
-
});
|
|
53
|
-
});
|
|
110
|
+
const categoryKey = resolveCategoryKey(data, 'column');
|
|
54
111
|
return {
|
|
55
112
|
grouped: false,
|
|
56
113
|
categoryKey,
|
|
57
|
-
metricKeys,
|
|
114
|
+
metricKeys: resolveMetricKeys(data, categoryKey),
|
|
58
115
|
};
|
|
59
116
|
}
|
|
60
117
|
function resolveGroupedSchema(data) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
if (group.data.length > 0) {
|
|
64
|
-
firstRow = group.data[0];
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
const categoryKey = firstRow
|
|
69
|
-
? (Object.keys(firstRow)[0] ?? 'category')
|
|
70
|
-
: 'category';
|
|
71
|
-
const metricKeys = [];
|
|
72
|
-
const seen = new Set();
|
|
73
|
-
data.forEach((group) => {
|
|
74
|
-
group.data.forEach((row) => {
|
|
75
|
-
Object.keys(row).forEach((key) => {
|
|
76
|
-
if (key === categoryKey || seen.has(key)) {
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
seen.add(key);
|
|
80
|
-
metricKeys.push(key);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
});
|
|
118
|
+
const rows = data.flatMap((group) => group.data);
|
|
119
|
+
const categoryKey = rows.length > 0 ? resolveCategoryKey(rows, 'category') : 'category';
|
|
84
120
|
return {
|
|
85
121
|
grouped: true,
|
|
86
122
|
categoryKey,
|
|
87
|
-
metricKeys,
|
|
123
|
+
metricKeys: resolveMetricKeys(rows, categoryKey),
|
|
88
124
|
};
|
|
89
125
|
}
|
|
90
126
|
export function resolveDataSchema(data) {
|
|
127
|
+
const attachedSchema = getAttachedDataSchema(data);
|
|
128
|
+
if (attachedSchema) {
|
|
129
|
+
return attachedSchema;
|
|
130
|
+
}
|
|
91
131
|
if (isGroupedData(data)) {
|
|
92
132
|
return resolveGroupedSchema(data);
|
|
93
133
|
}
|
|
@@ -95,7 +135,7 @@ export function resolveDataSchema(data) {
|
|
|
95
135
|
}
|
|
96
136
|
function normalizeGroupedData(data) {
|
|
97
137
|
assertNonEmptyGroupNames(data);
|
|
98
|
-
const schema =
|
|
138
|
+
const schema = resolveDataSchema(data);
|
|
99
139
|
const categoryKey = schema.categoryKey;
|
|
100
140
|
const normalizedRows = data.flatMap((group, groupIndex) => {
|
|
101
141
|
return group.data.map((row, rowIndex) => {
|
|
@@ -119,6 +159,6 @@ export function normalizeChartData(data) {
|
|
|
119
159
|
}
|
|
120
160
|
return {
|
|
121
161
|
data,
|
|
122
|
-
schema:
|
|
162
|
+
schema: resolveDataSchema(data),
|
|
123
163
|
};
|
|
124
164
|
}
|
package/dist/grouped-tabular.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { GROUPED_EMPTY_GROUP_ERROR, hasEmptyGroupNames, resolveDataSchema, } from './grouped-data.js';
|
|
1
|
+
import { GROUPED_EMPTY_GROUP_ERROR, attachDataSchema, hasEmptyGroupNames, resolveDataSchema, } from './grouped-data.js';
|
|
2
2
|
export const GROUPED_STRING_EMPTY_INPUT_ERROR = 'Input string is empty';
|
|
3
3
|
export const GROUPED_STRING_MISSING_HEADER_ERROR = 'Unable to resolve grouped header row from input string';
|
|
4
4
|
export const GROUPED_STRING_FIRST_ROW_BLANK_GROUP_ERROR = 'First data row cannot have a blank group label';
|
|
@@ -212,7 +212,11 @@ function parseGroupedRows(headerRow, dataRows, options) {
|
|
|
212
212
|
headers: ['', categoryKey, ...metricKeys],
|
|
213
213
|
categoryKey,
|
|
214
214
|
metricKeys,
|
|
215
|
-
data: rebuilt.data,
|
|
215
|
+
data: attachDataSchema(rebuilt.data, {
|
|
216
|
+
grouped: true,
|
|
217
|
+
categoryKey,
|
|
218
|
+
metricKeys,
|
|
219
|
+
}),
|
|
216
220
|
};
|
|
217
221
|
}
|
|
218
222
|
function rowHasCategoryOrMetricValues(row) {
|
|
@@ -258,7 +262,7 @@ function parseFlatRows(headerRow, dataRows, options) {
|
|
|
258
262
|
return '';
|
|
259
263
|
});
|
|
260
264
|
const columns = toUniqueNames(columnCandidates, 'column');
|
|
261
|
-
|
|
265
|
+
const rows = dataRows
|
|
262
266
|
.filter((row) => rowHasMetricContent(row))
|
|
263
267
|
.map((row) => {
|
|
264
268
|
const item = {};
|
|
@@ -267,6 +271,11 @@ function parseFlatRows(headerRow, dataRows, options) {
|
|
|
267
271
|
});
|
|
268
272
|
return item;
|
|
269
273
|
});
|
|
274
|
+
return attachDataSchema(rows, {
|
|
275
|
+
grouped: false,
|
|
276
|
+
categoryKey: columns[0] ?? options?.categoryKey?.trim() ?? 'category',
|
|
277
|
+
metricKeys: columns.slice(1),
|
|
278
|
+
});
|
|
270
279
|
}
|
|
271
280
|
export function parseGroupedTabularString(input, options) {
|
|
272
281
|
const { headerRow, dataRows } = parseStringTable(input, options);
|
package/dist/layout-manager.js
CHANGED
|
@@ -36,7 +36,7 @@ export class LayoutManager {
|
|
|
36
36
|
let marginLeft = this.theme.margins.left;
|
|
37
37
|
// Accumulate space requirements from components
|
|
38
38
|
for (const component of components) {
|
|
39
|
-
const space = component.getRequiredSpace();
|
|
39
|
+
const space = component.getRequiredSpace(this.theme);
|
|
40
40
|
switch (space.position) {
|
|
41
41
|
case 'top':
|
|
42
42
|
marginTop += space.height;
|
|
@@ -88,14 +88,14 @@ export class LayoutManager {
|
|
|
88
88
|
let leftOffset = 0;
|
|
89
89
|
let rightOffset = 0;
|
|
90
90
|
for (const component of components) {
|
|
91
|
-
const space = component.getRequiredSpace();
|
|
91
|
+
const space = component.getRequiredSpace(this.theme);
|
|
92
92
|
let x = 0;
|
|
93
93
|
let y = 0;
|
|
94
94
|
switch (space.position) {
|
|
95
95
|
case 'top':
|
|
96
|
-
// Position above plot area, stacking
|
|
96
|
+
// Position above plot area, stacking downward from the outer margin.
|
|
97
97
|
x = 0;
|
|
98
|
-
y = this.
|
|
98
|
+
y = this.theme.margins.top + topOffset;
|
|
99
99
|
topOffset += space.height;
|
|
100
100
|
break;
|
|
101
101
|
case 'bottom':
|