@circutor/charts 0.2.0 → 0.3.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/dist/bundler/itic-chart.js +1 -1
- package/dist/charts/charts.esm.js +1 -1
- package/dist/charts/p-70293967.entry.js +25 -0
- package/dist/cjs/charts.cjs.js +1 -1
- package/dist/cjs/itic-chart.cjs.entry.js +452 -100
- package/dist/cjs/loader.cjs.js +1 -1
- package/dist/collection/components/itic-chart/index.js +59 -58
- package/dist/collection/components/itic-chart/itic-chart.i18n.js +26 -0
- package/dist/collection/components/itic-chart/itic-chart.options.js +100 -42
- package/dist/collection/components/itic-chart/itic-chart.phases.js +55 -0
- package/dist/collection/playground/itic-chart.scenarios.js +129 -0
- package/dist/collection/playground/playground.js +170 -0
- package/dist/components/itic-chart.js +2 -2
- package/dist/esm/charts.js +1 -1
- package/dist/esm/itic-chart.entry.js +452 -100
- package/dist/esm/loader.js +1 -1
- package/dist/types/components/itic-chart/index.d.ts +19 -15
- package/dist/types/components/itic-chart/itic-chart.i18n.d.ts +13 -0
- package/dist/types/components/itic-chart/itic-chart.options.d.ts +32 -17
- package/dist/types/components/itic-chart/itic-chart.phases.d.ts +17 -0
- package/dist/types/components.d.ts +19 -29
- package/dist/types/index.d.ts +1 -1
- package/package.json +2 -5
- package/readme.md +138 -73
- package/dist/charts/p-943aa343.entry.js +0 -25
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// Sample data for the itic-chart playground page. Each scenario is a value for the `groups` prop.
|
|
2
|
+
|
|
3
|
+
const NOMINAL_VOLTS = 230;
|
|
4
|
+
|
|
5
|
+
const point = (duration, voltage, ts, extra = {}) => ({
|
|
6
|
+
duration,
|
|
7
|
+
voltage,
|
|
8
|
+
ts,
|
|
9
|
+
eventType: voltage < 10 ? 'POWER_INTERRUPT' : voltage < 90 ? 'VOLTAGE_DIP' : voltage > 110 ? 'VOLTAGE_SWELL' : 'VOLTAGE_VARIATION',
|
|
10
|
+
status: 'CLOSED',
|
|
11
|
+
min: Math.round(NOMINAL_VOLTS * Math.min(voltage, 100)) / 100,
|
|
12
|
+
max: Math.round(NOMINAL_VOLTS * Math.max(voltage, 100)) / 100,
|
|
13
|
+
...extra,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const TYPICAL = [
|
|
17
|
+
{
|
|
18
|
+
phase: 'AN',
|
|
19
|
+
assetName: 'Main switchboard',
|
|
20
|
+
points: [
|
|
21
|
+
point(230, 93.46, '2024-06-26T20:23:45.220+02:00'),
|
|
22
|
+
point(12, 62, '2024-07-02T08:11:03.051+02:00'),
|
|
23
|
+
point(4000, 85, '2024-09-14T17:45:12.900+02:00', { status: 'OPEN' }),
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
phase: 'BN',
|
|
28
|
+
assetName: 'Main switchboard',
|
|
29
|
+
points: [point(20, 0.4, '2025-01-26T23:04:34.840+01:00'), point(3, 150, '2025-02-03T10:20:00.004+01:00')],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
phase: 'CN',
|
|
33
|
+
assetName: 'Main switchboard',
|
|
34
|
+
points: [point(800, 125, '2025-03-11T13:02:47.123+01:00'), point(30000, 88, '2025-04-01T06:30:00.000+02:00')],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
phase: 'AN,BN,CN',
|
|
38
|
+
assetName: 'Main switchboard',
|
|
39
|
+
points: [point(60, 45, '2025-05-20T19:59:59.999+02:00', { status: 'OPEN' })],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Every code of the phase catalog, plus one unknown code (shown as is, with an ECharts palette color)
|
|
44
|
+
const PHASE_CODES = ['AN', 'BN', 'CN', 'AB', 'BC', 'CA', 'A', 'B', 'C', 'AN,BN', 'AN,CN', 'BN,CN', 'AB,BC', 'AB,CA', 'BC,CA', 'AN,BN,CN', 'AB,BC,CA', 'ALL', 'NONE', 'XYZ'];
|
|
45
|
+
|
|
46
|
+
const PHASES = PHASE_CODES.map((phase, i) => ({
|
|
47
|
+
phase,
|
|
48
|
+
assetName: 'Main switchboard',
|
|
49
|
+
points: [point(2 * 1.8 ** i, 30 + i * 8, '2025-01-26T23:04:34.840+01:00')],
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Seeded random numbers, so the generated data is the same on every load
|
|
53
|
+
function random(seed) {
|
|
54
|
+
return () => {
|
|
55
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
56
|
+
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
57
|
+
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
|
58
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function manyPoints(count) {
|
|
63
|
+
const next = random(42);
|
|
64
|
+
const start = Date.parse('2025-01-01T00:00:00Z');
|
|
65
|
+
const year = 365 * 24 * 3600 * 1000;
|
|
66
|
+
|
|
67
|
+
return ['AN', 'BN', 'CN', 'AN,BN,CN'].map(phase => ({
|
|
68
|
+
phase,
|
|
69
|
+
assetName: 'Main switchboard',
|
|
70
|
+
points: Array.from({ length: count }, () => {
|
|
71
|
+
// Durations spread evenly on the log axis; most events are dips, some swells
|
|
72
|
+
const duration = Math.round(10 ** (next() * 5) * 100) / 100;
|
|
73
|
+
const voltage = Math.round((next() < 0.8 ? next() * 95 : 105 + next() * 90) * 100) / 100;
|
|
74
|
+
return point(duration, voltage, new Date(start + next() * year).toISOString());
|
|
75
|
+
}),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Corners of the axes, plus two points outside them that ECharts doesn't draw
|
|
80
|
+
const AXIS_LIMITS = [
|
|
81
|
+
{
|
|
82
|
+
phase: 'ALL',
|
|
83
|
+
points: [
|
|
84
|
+
point(1, 0, '2025-01-26T23:04:34.840+01:00'),
|
|
85
|
+
point(1, 200, '2025-01-26T23:04:34.840+01:00'),
|
|
86
|
+
point(100000, 0, '2025-01-26T23:04:34.840+01:00'),
|
|
87
|
+
point(100000, 200, '2025-01-26T23:04:34.840+01:00'),
|
|
88
|
+
point(0.5, 100, '2025-01-26T23:04:34.840+01:00'),
|
|
89
|
+
point(1000, 250, '2025-01-26T23:04:34.840+01:00'),
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
const NOT_CLICKABLE = [
|
|
95
|
+
{
|
|
96
|
+
phase: 'AN',
|
|
97
|
+
assetName: 'Main switchboard',
|
|
98
|
+
points: [
|
|
99
|
+
point(50, 40, '2025-01-26T23:04:34.840+01:00'),
|
|
100
|
+
point(500, 75, '2025-01-27T09:00:00.000+01:00', { clickable: false }),
|
|
101
|
+
point(5000, 130, '2025-01-28T15:30:00.000+01:00', { clickable: false }),
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
// Long names, unknown codes, HTML (must be shown as text) and missing optional fields
|
|
107
|
+
const EDGE_CASES = [
|
|
108
|
+
{
|
|
109
|
+
phase: 'AN',
|
|
110
|
+
assetName: 'Very long asset name of an industrial plant, building B, floor 3, switchboard 12 <b>not bold</b>',
|
|
111
|
+
points: [
|
|
112
|
+
point(100, 50, '2025-01-26T23:04:34.840+01:00', { eventType: 'UNKNOWN_EVENT_CODE', status: 'PENDING_REVIEW_BY_MAINTENANCE' }),
|
|
113
|
+
{ duration: 1000, voltage: 80, ts: '2025-01-26T23:04:34.840+01:00' },
|
|
114
|
+
point(10, 120, 'not a date', { min: 0, max: 0 }),
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
/** @type {import('./playground.js').Scenario[]} */
|
|
120
|
+
export const scenarios = [
|
|
121
|
+
{ id: 'typical', label: 'Typical installation', data: TYPICAL },
|
|
122
|
+
{ id: 'phases', label: 'Phase catalog (colors and legend)', data: PHASES },
|
|
123
|
+
{ id: 'many', label: 'Many events (4 × 250)', data: manyPoints(250) },
|
|
124
|
+
{ id: 'empty', label: 'No events', data: [] },
|
|
125
|
+
{ id: 'axis-limits', label: 'Axis limits and points outside them', data: AXIS_LIMITS },
|
|
126
|
+
{ id: 'not-clickable', label: 'Points that are not clickable', data: NOT_CLICKABLE },
|
|
127
|
+
{ id: 'edge-cases', label: 'Long texts, unknown codes, missing fields', data: EDGE_CASES },
|
|
128
|
+
{ id: 'invalid', label: 'Invalid data (not an array)', data: { phase: 'AN', points: [] } },
|
|
129
|
+
];
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// Shared UI of the playground pages: controls, data editor and event log for one component.
|
|
2
|
+
// Plain JS, copied as is to www/playground/ by the `www` output target, so it isn't compiled or bundled.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {{ label: string, value: string }} Option
|
|
6
|
+
* @typedef {{ id: string, label: string, prop?: string, cssVar?: string, options: Option[], value?: string }} Control
|
|
7
|
+
* @typedef {{ id: string, label: string, data: unknown }} Scenario
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const params = new URLSearchParams(location.search);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {{
|
|
14
|
+
* element: HTMLElement,
|
|
15
|
+
* dataProp: string,
|
|
16
|
+
* scenarios: Scenario[],
|
|
17
|
+
* controls?: Control[],
|
|
18
|
+
* events?: string[],
|
|
19
|
+
* }} config
|
|
20
|
+
*/
|
|
21
|
+
export function mountPlayground({ element, dataProp, scenarios, controls = [], events = [] }) {
|
|
22
|
+
const log = createLog(document.querySelector('#log'));
|
|
23
|
+
const editor = createDataEditor(document.querySelector('#data'), value => (element[dataProp] = value));
|
|
24
|
+
|
|
25
|
+
captureConsole(log);
|
|
26
|
+
events.forEach(name => element.addEventListener(name, event => log.add(name, event.detail)));
|
|
27
|
+
|
|
28
|
+
const panel = document.querySelector('#controls');
|
|
29
|
+
|
|
30
|
+
const scenarioSelect = addSelect(panel, {
|
|
31
|
+
id: 'scenario',
|
|
32
|
+
label: 'Data',
|
|
33
|
+
options: scenarios.map(({ id, label }) => ({ value: id, label })),
|
|
34
|
+
});
|
|
35
|
+
const loadScenario = id => editor.load((scenarios.find(scenario => scenario.id === id) ?? scenarios[0]).data);
|
|
36
|
+
scenarioSelect.addEventListener('change', () => loadScenario(scenarioSelect.value));
|
|
37
|
+
loadScenario(scenarioSelect.value);
|
|
38
|
+
|
|
39
|
+
controls.forEach(control => {
|
|
40
|
+
const select = addSelect(panel, control);
|
|
41
|
+
const apply = () => {
|
|
42
|
+
const value = select.value === '' ? undefined : select.value;
|
|
43
|
+
if (control.cssVar) {
|
|
44
|
+
value === undefined ? element.style.removeProperty(control.cssVar) : element.style.setProperty(control.cssVar, value);
|
|
45
|
+
} else {
|
|
46
|
+
element[control.prop ?? control.id] = value;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
select.addEventListener('change', apply);
|
|
50
|
+
apply();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
mountSizeControls(document.querySelector('#frame'), panel);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Adds a labelled `<select>` whose value is kept in the URL, so a link reproduces the same state. */
|
|
57
|
+
function addSelect(panel, { id, label, options, value }) {
|
|
58
|
+
const field = document.createElement('label');
|
|
59
|
+
field.className = 'pg-field';
|
|
60
|
+
field.textContent = label;
|
|
61
|
+
|
|
62
|
+
const select = document.createElement('select');
|
|
63
|
+
select.name = id;
|
|
64
|
+
options.forEach(option => select.add(new Option(option.label, option.value)));
|
|
65
|
+
const initial = params.get(id) ?? value;
|
|
66
|
+
if (initial !== undefined && options.some(option => option.value === initial)) {
|
|
67
|
+
select.value = initial;
|
|
68
|
+
}
|
|
69
|
+
select.addEventListener('change', () => {
|
|
70
|
+
params.set(id, select.value);
|
|
71
|
+
history.replaceState(null, '', `?${params}`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
field.append(select);
|
|
75
|
+
panel.append(field);
|
|
76
|
+
return select;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const WIDTHS = ['100%', '1024px', '800px', '480px', '320px'];
|
|
80
|
+
|
|
81
|
+
/** Width presets for the frame around the component, which can also be resized by dragging its corner. */
|
|
82
|
+
function mountSizeControls(frame, panel) {
|
|
83
|
+
const field = document.createElement('div');
|
|
84
|
+
field.className = 'pg-field';
|
|
85
|
+
field.textContent = 'Width';
|
|
86
|
+
|
|
87
|
+
const buttons = document.createElement('div');
|
|
88
|
+
buttons.className = 'pg-buttons';
|
|
89
|
+
WIDTHS.forEach(width => {
|
|
90
|
+
const button = document.createElement('button');
|
|
91
|
+
button.type = 'button';
|
|
92
|
+
button.textContent = width;
|
|
93
|
+
button.addEventListener('click', () => (frame.style.width = width));
|
|
94
|
+
buttons.append(button);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const size = document.createElement('output');
|
|
98
|
+
size.className = 'pg-size';
|
|
99
|
+
new ResizeObserver(([entry]) => {
|
|
100
|
+
const { width, height } = entry.contentRect;
|
|
101
|
+
size.textContent = `${Math.round(width)} × ${Math.round(height)} px · drag the corner to resize`;
|
|
102
|
+
}).observe(frame);
|
|
103
|
+
|
|
104
|
+
field.append(buttons, size);
|
|
105
|
+
panel.append(field);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** JSON editor for the component's data. Applied with the button or Ctrl/Cmd + Enter. */
|
|
109
|
+
function createDataEditor(root, onApply) {
|
|
110
|
+
const textarea = root.querySelector('textarea');
|
|
111
|
+
const error = root.querySelector('.pg-error');
|
|
112
|
+
|
|
113
|
+
const apply = () => {
|
|
114
|
+
try {
|
|
115
|
+
onApply(textarea.value.trim() === '' ? undefined : JSON.parse(textarea.value));
|
|
116
|
+
error.textContent = '';
|
|
117
|
+
} catch (e) {
|
|
118
|
+
error.textContent = `Not applied: ${e.message}`;
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
root.querySelector('button').addEventListener('click', apply);
|
|
123
|
+
textarea.addEventListener('keydown', event => {
|
|
124
|
+
if (event.key === 'Enter' && (event.ctrlKey || event.metaKey)) {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
apply();
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
load(data) {
|
|
132
|
+
textarea.value = JSON.stringify(data, null, 2);
|
|
133
|
+
apply();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Event log, newest first. Also shows the component's console errors, which designers wouldn't see otherwise. */
|
|
139
|
+
function createLog(root) {
|
|
140
|
+
const list = root.querySelector('ol');
|
|
141
|
+
root.querySelector('button').addEventListener('click', () => list.replaceChildren());
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
add(name, detail, type = 'event') {
|
|
145
|
+
const item = document.createElement('li');
|
|
146
|
+
item.className = `pg-log-${type}`;
|
|
147
|
+
|
|
148
|
+
const entry = document.createElement('details');
|
|
149
|
+
const summary = document.createElement('summary');
|
|
150
|
+
summary.textContent = `${new Date().toLocaleTimeString()} · ${name}`;
|
|
151
|
+
const body = document.createElement('pre');
|
|
152
|
+
body.textContent = typeof detail === 'string' ? detail : JSON.stringify(detail, null, 2);
|
|
153
|
+
|
|
154
|
+
entry.append(summary, body);
|
|
155
|
+
item.append(entry);
|
|
156
|
+
list.prepend(item);
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function captureConsole(log) {
|
|
162
|
+
['error', 'warn'].forEach(level => {
|
|
163
|
+
const original = console[level].bind(console);
|
|
164
|
+
console[level] = (...args) => {
|
|
165
|
+
original(...args);
|
|
166
|
+
const text = args.map(arg => (arg instanceof Error ? arg.message : typeof arg === 'string' ? arg : JSON.stringify(arg))).join(' ');
|
|
167
|
+
log.add(`console.${level}`, text, level);
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|