@internetstiftelsen/charts 0.5.1 → 0.6.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 +4 -2
- package/area.d.ts +26 -0
- package/area.js +331 -0
- package/base-chart.d.ts +10 -3
- package/base-chart.js +33 -19
- package/chart-interface.d.ts +1 -1
- package/donut-center-content.d.ts +1 -1
- package/donut-center-content.js +7 -6
- package/donut-chart.d.ts +1 -0
- package/donut-chart.js +8 -1
- package/export-tabular.d.ts +4 -4
- package/export-tabular.js +8 -0
- package/export-xlsx.d.ts +2 -2
- package/gauge-chart.d.ts +138 -0
- package/gauge-chart.js +1041 -0
- package/grouped-data.d.ts +19 -0
- package/grouped-data.js +122 -0
- package/grouped-tabular.d.ts +26 -0
- package/grouped-tabular.js +149 -0
- package/package.json +1 -1
- package/pie-chart.d.ts +80 -0
- package/pie-chart.js +665 -0
- package/theme.d.ts +1 -0
- package/theme.js +25 -16
- package/tooltip.d.ts +3 -2
- package/tooltip.js +40 -39
- package/types.d.ts +46 -0
- package/validation.d.ts +4 -0
- package/validation.js +25 -0
- package/x-axis.d.ts +10 -2
- package/x-axis.js +205 -15
- package/xy-chart.d.ts +10 -1
- package/xy-chart.js +307 -90
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChartData, DataItem, GroupedDataGroup } from './types.js';
|
|
2
|
+
export declare const GROUPED_CATEGORY_ID_KEY = "__iis_grouped_category_id__";
|
|
3
|
+
export declare const GROUPED_CATEGORY_LABEL_KEY = "__iis_grouped_category_label__";
|
|
4
|
+
export declare const GROUPED_GROUP_LABEL_KEY = "__iis_grouped_group_label__";
|
|
5
|
+
export declare const GROUPED_GAP_TICK_PREFIX = "__iis_group_gap__";
|
|
6
|
+
export declare const GROUPED_EMPTY_GROUP_ERROR = "Grouped data requires non-empty group names";
|
|
7
|
+
export type DataSchema = {
|
|
8
|
+
grouped: boolean;
|
|
9
|
+
categoryKey: string;
|
|
10
|
+
metricKeys: string[];
|
|
11
|
+
};
|
|
12
|
+
export type NormalizedChartData = {
|
|
13
|
+
data: DataItem[];
|
|
14
|
+
schema: DataSchema;
|
|
15
|
+
};
|
|
16
|
+
export declare function hasEmptyGroupNames(data: GroupedDataGroup[]): boolean;
|
|
17
|
+
export declare function isGroupedData(data: unknown): data is GroupedDataGroup[];
|
|
18
|
+
export declare function resolveDataSchema(data: ChartData): DataSchema;
|
|
19
|
+
export declare function normalizeChartData(data: ChartData): NormalizedChartData;
|
package/grouped-data.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
export const GROUPED_CATEGORY_ID_KEY = '__iis_grouped_category_id__';
|
|
2
|
+
export const GROUPED_CATEGORY_LABEL_KEY = '__iis_grouped_category_label__';
|
|
3
|
+
export const GROUPED_GROUP_LABEL_KEY = '__iis_grouped_group_label__';
|
|
4
|
+
export const GROUPED_GAP_TICK_PREFIX = '__iis_group_gap__';
|
|
5
|
+
export const GROUPED_EMPTY_GROUP_ERROR = 'Grouped data requires non-empty group names';
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === 'object' && value !== null;
|
|
8
|
+
}
|
|
9
|
+
function hasNonEmptyGroupName(value) {
|
|
10
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
11
|
+
}
|
|
12
|
+
export function hasEmptyGroupNames(data) {
|
|
13
|
+
return data.some((group) => !hasNonEmptyGroupName(group.group));
|
|
14
|
+
}
|
|
15
|
+
function assertNonEmptyGroupNames(data) {
|
|
16
|
+
if (hasEmptyGroupNames(data)) {
|
|
17
|
+
throw new Error(GROUPED_EMPTY_GROUP_ERROR);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function isGroupedData(data) {
|
|
21
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
return data.every((item) => {
|
|
25
|
+
if (!isRecord(item) || !Array.isArray(item['data'])) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return item['data'].every((row) => {
|
|
29
|
+
return isRecord(row);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function resolveFlatSchema(data) {
|
|
34
|
+
if (data.length === 0) {
|
|
35
|
+
return {
|
|
36
|
+
grouped: false,
|
|
37
|
+
categoryKey: 'column',
|
|
38
|
+
metricKeys: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const keys = Object.keys(data[0]);
|
|
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
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
grouped: false,
|
|
56
|
+
categoryKey,
|
|
57
|
+
metricKeys,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function resolveGroupedSchema(data) {
|
|
61
|
+
let firstRow = null;
|
|
62
|
+
for (const group of data) {
|
|
63
|
+
if (group.data.length > 0) {
|
|
64
|
+
firstRow = group.data[0];
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const categoryKey = firstRow ? Object.keys(firstRow)[0] ?? 'category' : 'category';
|
|
69
|
+
const metricKeys = [];
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
data.forEach((group) => {
|
|
72
|
+
group.data.forEach((row) => {
|
|
73
|
+
Object.keys(row).forEach((key) => {
|
|
74
|
+
if (key === categoryKey || seen.has(key)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
seen.add(key);
|
|
78
|
+
metricKeys.push(key);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
grouped: true,
|
|
84
|
+
categoryKey,
|
|
85
|
+
metricKeys,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
export function resolveDataSchema(data) {
|
|
89
|
+
if (isGroupedData(data)) {
|
|
90
|
+
return resolveGroupedSchema(data);
|
|
91
|
+
}
|
|
92
|
+
return resolveFlatSchema(data);
|
|
93
|
+
}
|
|
94
|
+
function normalizeGroupedData(data) {
|
|
95
|
+
assertNonEmptyGroupNames(data);
|
|
96
|
+
const schema = resolveGroupedSchema(data);
|
|
97
|
+
const categoryKey = schema.categoryKey;
|
|
98
|
+
const normalizedRows = data.flatMap((group, groupIndex) => {
|
|
99
|
+
return group.data.map((row, rowIndex) => {
|
|
100
|
+
const categoryLabel = String(row[categoryKey] ?? '');
|
|
101
|
+
return {
|
|
102
|
+
[GROUPED_CATEGORY_ID_KEY]: `g${groupIndex}r${rowIndex}`,
|
|
103
|
+
[GROUPED_CATEGORY_LABEL_KEY]: categoryLabel,
|
|
104
|
+
[GROUPED_GROUP_LABEL_KEY]: String(group.group ?? ''),
|
|
105
|
+
...row,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
return {
|
|
110
|
+
data: normalizedRows,
|
|
111
|
+
schema,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export function normalizeChartData(data) {
|
|
115
|
+
if (isGroupedData(data)) {
|
|
116
|
+
return normalizeGroupedData(data);
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
data,
|
|
120
|
+
schema: resolveFlatSchema(data),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { GroupedDataGroup } from './types.js';
|
|
2
|
+
export type GroupedSheetRow = {
|
|
3
|
+
kind: 'data';
|
|
4
|
+
values: unknown[];
|
|
5
|
+
};
|
|
6
|
+
export type GroupedSheetModel = {
|
|
7
|
+
headers: string[];
|
|
8
|
+
metricKeys: string[];
|
|
9
|
+
categoryKey: string;
|
|
10
|
+
rows: GroupedSheetRow[];
|
|
11
|
+
matrix: unknown[][];
|
|
12
|
+
};
|
|
13
|
+
export type GroupedRebuildResult = {
|
|
14
|
+
ok: true;
|
|
15
|
+
data: GroupedDataGroup[];
|
|
16
|
+
} | {
|
|
17
|
+
ok: false;
|
|
18
|
+
error: 'FIRST_ROW_BLANK_GROUP';
|
|
19
|
+
};
|
|
20
|
+
export type GroupedTabularData = {
|
|
21
|
+
columns: string[];
|
|
22
|
+
rows: string[][];
|
|
23
|
+
};
|
|
24
|
+
export declare function buildGroupedSheetModel(data: GroupedDataGroup[], preferredMetricOrder?: string[]): GroupedSheetModel;
|
|
25
|
+
export declare function rebuildGroupedDataFromSheet(matrix: unknown[][], categoryKey: string, metricKeys: string[]): GroupedRebuildResult;
|
|
26
|
+
export declare function toGroupedTabularData(data: GroupedDataGroup[], preferredMetricOrder?: string[]): GroupedTabularData;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { GROUPED_EMPTY_GROUP_ERROR, hasEmptyGroupNames, resolveDataSchema, } from './grouped-data.js';
|
|
2
|
+
function isBlankValue(value) {
|
|
3
|
+
if (value === null || value === undefined) {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
if (typeof value === 'string') {
|
|
7
|
+
return value.trim() === '';
|
|
8
|
+
}
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
function asString(value) {
|
|
12
|
+
if (value === null || value === undefined) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
return String(value);
|
|
16
|
+
}
|
|
17
|
+
function rowHasMetricContent(values) {
|
|
18
|
+
return values.some((value) => !isBlankValue(value));
|
|
19
|
+
}
|
|
20
|
+
function applyMetricOrder(metricKeys, preferredOrder) {
|
|
21
|
+
if (!preferredOrder || preferredOrder.length === 0) {
|
|
22
|
+
return metricKeys;
|
|
23
|
+
}
|
|
24
|
+
const ordered = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
preferredOrder.forEach((key) => {
|
|
27
|
+
if (metricKeys.includes(key) && !seen.has(key)) {
|
|
28
|
+
seen.add(key);
|
|
29
|
+
ordered.push(key);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
metricKeys.forEach((key) => {
|
|
33
|
+
if (!seen.has(key)) {
|
|
34
|
+
seen.add(key);
|
|
35
|
+
ordered.push(key);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
return ordered;
|
|
39
|
+
}
|
|
40
|
+
export function buildGroupedSheetModel(data, preferredMetricOrder) {
|
|
41
|
+
if (hasEmptyGroupNames(data)) {
|
|
42
|
+
throw new Error(GROUPED_EMPTY_GROUP_ERROR);
|
|
43
|
+
}
|
|
44
|
+
const schema = resolveDataSchema(data);
|
|
45
|
+
const metricKeys = applyMetricOrder(schema.metricKeys, preferredMetricOrder);
|
|
46
|
+
const categoryKey = schema.categoryKey;
|
|
47
|
+
const headers = ['', '', ...metricKeys];
|
|
48
|
+
const rows = [];
|
|
49
|
+
data.forEach((group) => {
|
|
50
|
+
group.data.forEach((row, rowIndex) => {
|
|
51
|
+
const values = [
|
|
52
|
+
rowIndex === 0 ? group.group : '',
|
|
53
|
+
row[categoryKey] ?? '',
|
|
54
|
+
...metricKeys.map((metricKey) => row[metricKey] ?? ''),
|
|
55
|
+
];
|
|
56
|
+
rows.push({
|
|
57
|
+
kind: 'data',
|
|
58
|
+
values,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
headers,
|
|
64
|
+
metricKeys,
|
|
65
|
+
categoryKey,
|
|
66
|
+
rows,
|
|
67
|
+
matrix: rows.map((row) => row.values),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function rebuildGroupedDataFromSheet(matrix, categoryKey, metricKeys) {
|
|
71
|
+
const groups = [];
|
|
72
|
+
let currentGroup = '';
|
|
73
|
+
matrix.forEach((row) => {
|
|
74
|
+
const groupCell = row[0];
|
|
75
|
+
const categoryCell = row[1];
|
|
76
|
+
const metricCells = metricKeys.map((_, index) => row[index + 2]);
|
|
77
|
+
const hasCategory = !isBlankValue(categoryCell);
|
|
78
|
+
const hasMetrics = rowHasMetricContent(metricCells);
|
|
79
|
+
const hasGroup = !isBlankValue(groupCell);
|
|
80
|
+
if (!hasCategory && !hasMetrics) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (hasGroup) {
|
|
84
|
+
currentGroup = asString(groupCell);
|
|
85
|
+
}
|
|
86
|
+
else if (!currentGroup) {
|
|
87
|
+
groups.length = 0;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const groupName = currentGroup;
|
|
91
|
+
if (!groupName) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
let targetGroup = groups[groups.length - 1];
|
|
95
|
+
if (!targetGroup || targetGroup.group !== groupName) {
|
|
96
|
+
targetGroup = {
|
|
97
|
+
group: groupName,
|
|
98
|
+
data: [],
|
|
99
|
+
};
|
|
100
|
+
groups.push(targetGroup);
|
|
101
|
+
}
|
|
102
|
+
const rebuiltRow = {
|
|
103
|
+
[categoryKey]: categoryCell ?? '',
|
|
104
|
+
};
|
|
105
|
+
metricKeys.forEach((metricKey, index) => {
|
|
106
|
+
rebuiltRow[metricKey] = metricCells[index] ?? '';
|
|
107
|
+
});
|
|
108
|
+
targetGroup.data.push(rebuiltRow);
|
|
109
|
+
});
|
|
110
|
+
const firstDataRow = matrix.find((row) => {
|
|
111
|
+
const categoryCell = row[1];
|
|
112
|
+
const metricCells = metricKeys.map((_, index) => row[index + 2]);
|
|
113
|
+
return !isBlankValue(categoryCell) || rowHasMetricContent(metricCells);
|
|
114
|
+
});
|
|
115
|
+
if (firstDataRow && isBlankValue(firstDataRow[0])) {
|
|
116
|
+
return {
|
|
117
|
+
ok: false,
|
|
118
|
+
error: 'FIRST_ROW_BLANK_GROUP',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
data: groups,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function normalizeTableValue(value) {
|
|
127
|
+
if (value === null || value === undefined) {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
if (value instanceof Date) {
|
|
131
|
+
return value.toISOString();
|
|
132
|
+
}
|
|
133
|
+
if (typeof value === 'object') {
|
|
134
|
+
try {
|
|
135
|
+
return JSON.stringify(value);
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
return String(value);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return String(value);
|
|
142
|
+
}
|
|
143
|
+
export function toGroupedTabularData(data, preferredMetricOrder) {
|
|
144
|
+
const model = buildGroupedSheetModel(data, preferredMetricOrder);
|
|
145
|
+
return {
|
|
146
|
+
columns: model.headers,
|
|
147
|
+
rows: model.matrix.map((row) => row.map((value) => normalizeTableValue(value))),
|
|
148
|
+
};
|
|
149
|
+
}
|
package/package.json
CHANGED
package/pie-chart.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { DataItem } from './types.js';
|
|
2
|
+
import { BaseChart, type BaseChartConfig } from './base-chart.js';
|
|
3
|
+
import type { ChartComponent, LayoutAwareComponent } from './chart-interface.js';
|
|
4
|
+
export type PieSort = 'none' | 'ascending' | 'descending' | ((a: PieSegmentData, b: PieSegmentData) => number);
|
|
5
|
+
export type PieConfig = {
|
|
6
|
+
innerRadius?: number;
|
|
7
|
+
startAngle?: number;
|
|
8
|
+
endAngle?: number;
|
|
9
|
+
padAngle?: number;
|
|
10
|
+
cornerRadius?: number;
|
|
11
|
+
sort?: PieSort;
|
|
12
|
+
};
|
|
13
|
+
export type PieValueLabelPosition = 'inside' | 'outside' | 'auto';
|
|
14
|
+
export type PieValueLabelConfig = {
|
|
15
|
+
show?: boolean;
|
|
16
|
+
position?: PieValueLabelPosition;
|
|
17
|
+
minInsidePercentage?: number;
|
|
18
|
+
outsideOffset?: number;
|
|
19
|
+
insideMargin?: number;
|
|
20
|
+
minVerticalSpacing?: number;
|
|
21
|
+
};
|
|
22
|
+
export type PieLabelsConfig = {
|
|
23
|
+
show?: boolean;
|
|
24
|
+
mode?: 'adaptive' | 'inside' | 'outside';
|
|
25
|
+
minInsidePercentage?: number;
|
|
26
|
+
outsideOffset?: number;
|
|
27
|
+
minVerticalSpacing?: number;
|
|
28
|
+
};
|
|
29
|
+
export type PieChartConfig = BaseChartConfig & {
|
|
30
|
+
pie?: PieConfig;
|
|
31
|
+
valueLabel?: PieValueLabelConfig;
|
|
32
|
+
/** @deprecated Use valueLabel instead */
|
|
33
|
+
labels?: PieLabelsConfig;
|
|
34
|
+
valueKey?: string;
|
|
35
|
+
labelKey?: string;
|
|
36
|
+
};
|
|
37
|
+
type PieSegmentData = {
|
|
38
|
+
label: string;
|
|
39
|
+
value: number;
|
|
40
|
+
color: string;
|
|
41
|
+
source: DataItem;
|
|
42
|
+
};
|
|
43
|
+
export declare class PieChart extends BaseChart {
|
|
44
|
+
private readonly innerRadiusRatio;
|
|
45
|
+
private readonly startAngle;
|
|
46
|
+
private readonly endAngle;
|
|
47
|
+
private readonly padAngle;
|
|
48
|
+
private readonly cornerRadius;
|
|
49
|
+
private readonly sort;
|
|
50
|
+
private readonly valueKey;
|
|
51
|
+
private readonly labelKey;
|
|
52
|
+
private readonly valueLabel;
|
|
53
|
+
private segments;
|
|
54
|
+
constructor(config: PieChartConfig);
|
|
55
|
+
private validatePieData;
|
|
56
|
+
private prepareSegments;
|
|
57
|
+
private warnOnTinySlices;
|
|
58
|
+
addChild(component: ChartComponent): this;
|
|
59
|
+
protected getExportComponents(): ChartComponent[];
|
|
60
|
+
update(data: DataItem[]): void;
|
|
61
|
+
protected getLayoutComponents(): LayoutAwareComponent[];
|
|
62
|
+
protected createExportChart(): BaseChart;
|
|
63
|
+
protected renderChart(): void;
|
|
64
|
+
private resolveFontScale;
|
|
65
|
+
private resolveSortComparator;
|
|
66
|
+
private renderSegments;
|
|
67
|
+
private handleSegmentKeyNavigation;
|
|
68
|
+
private getRenderedSegments;
|
|
69
|
+
private buildAriaLabel;
|
|
70
|
+
private positionTooltip;
|
|
71
|
+
private positionTooltipAtElement;
|
|
72
|
+
private buildTooltipContent;
|
|
73
|
+
private getArcPoint;
|
|
74
|
+
private renderLabels;
|
|
75
|
+
private resolveValueLabelPlacement;
|
|
76
|
+
private canFitInsideLabel;
|
|
77
|
+
private resolveOutsideLabel;
|
|
78
|
+
private adjustOutsideLabelPositions;
|
|
79
|
+
}
|
|
80
|
+
export {};
|