@circutor/charts 0.3.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.
@@ -45408,7 +45408,9 @@ function parseGroups(groups) {
45408
45408
  }
45409
45409
  function isValidTimeZone(timeZone) {
45410
45410
  try {
45411
- return true;
45411
+ // The result must be used: Rollup and terser drop an unused `new Intl.DateTimeFormat()` as side-effect free,
45412
+ // which turned this check into `return true` in the built packages
45413
+ return Boolean(new Intl.DateTimeFormat('en', { timeZone }).resolvedOptions().timeZone);
45412
45414
  }
45413
45415
  catch (_a) {
45414
45416
  return false;
@@ -45741,7 +45743,7 @@ const IticChart = class {
45741
45743
  if (!this.timeZone) {
45742
45744
  return;
45743
45745
  }
45744
- if (isValidTimeZone()) {
45746
+ if (isValidTimeZone(this.timeZone)) {
45745
45747
  this.validTimeZone = this.timeZone;
45746
45748
  }
45747
45749
  else {
@@ -45783,7 +45785,7 @@ const IticChart = class {
45783
45785
  this.chart.setOption(options, { notMerge: true });
45784
45786
  }
45785
45787
  render() {
45786
- return index.h("div", { key: '2bb964d590a36c9c9501761c3f848ad364a29030', class: "chart", ref: el => (this.container = el) });
45788
+ return index.h("div", { key: 'e202bbf61ad19502fd76a42f37a8ea69f2d8c58b', class: "chart", ref: el => (this.container = el) });
45787
45789
  }
45788
45790
  get el() { return index.getElement(this); }
45789
45791
  static get watchers() { return {
@@ -105,7 +105,7 @@ export class IticChart {
105
105
  this.chart.setOption(options, { notMerge: true });
106
106
  }
107
107
  render() {
108
- return h("div", { key: '2bb964d590a36c9c9501761c3f848ad364a29030', class: "chart", ref: el => (this.container = el) });
108
+ return h("div", { key: 'e202bbf61ad19502fd76a42f37a8ea69f2d8c58b', class: "chart", ref: el => (this.container = el) });
109
109
  }
110
110
  static get is() { return "itic-chart"; }
111
111
  static get encapsulation() { return "shadow"; }
@@ -126,7 +126,7 @@ export class IticChart {
126
126
  "mutable": false,
127
127
  "complexType": {
128
128
  "original": "ITICChartMarks[] | string",
129
- "resolved": "ITICChartMarks[] | string",
129
+ "resolved": "ITICChartMarks[] | string | undefined",
130
130
  "references": {
131
131
  "ITICChartMarks": {
132
132
  "location": "import",
@@ -137,7 +137,7 @@ export class IticChart {
137
137
  }
138
138
  },
139
139
  "required": false,
140
- "optional": false,
140
+ "optional": true,
141
141
  "docs": {
142
142
  "tags": [],
143
143
  "text": "Point groups, as an array or a JSON string. Each group's `phase` code sets its name, color and order.\nExtra fields in groups and points are kept and passed back in `pointClick`."
@@ -172,7 +172,7 @@ export class IticChart {
172
172
  "mutable": false,
173
173
  "complexType": {
174
174
  "original": "string",
175
- "resolved": "string",
175
+ "resolved": "string | undefined",
176
176
  "references": {}
177
177
  },
178
178
  "required": false,
@@ -17,8 +17,9 @@ export function parseGroups(groups) {
17
17
  }
18
18
  export function isValidTimeZone(timeZone) {
19
19
  try {
20
- new Intl.DateTimeFormat('en', { timeZone });
21
- return true;
20
+ // The result must be used: Rollup and terser drop an unused `new Intl.DateTimeFormat()` as side-effect free,
21
+ // which turned this check into `return true` in the built packages
22
+ return Boolean(new Intl.DateTimeFormat('en', { timeZone }).resolvedOptions().timeZone);
22
23
  }
23
24
  catch (_a) {
24
25
  return false;
@@ -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
+ }