@circutor/charts 0.2.0 → 0.3.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.
@@ -6,7 +6,7 @@ var appGlobals = require('./app-globals-V2Kpy_OQ.js');
6
6
  const defineCustomElements = async (win, options) => {
7
7
  if (typeof window === 'undefined') return undefined;
8
8
  await appGlobals.globalScripts();
9
- return index.bootstrapLazy([["itic-chart.cjs",[[1,"itic-chart",{"groups":[1],"xAxisName":[1,"x-axis-name"],"yAxisName":[1,"y-axis-name"],"tooltipFormatter":[16]},null,{"groups":[{"onOptionsChange":0}],"xAxisName":[{"onOptionsChange":0}],"yAxisName":[{"onOptionsChange":0}],"tooltipFormatter":[{"onOptionsChange":0}]}]]]], options);
9
+ return index.bootstrapLazy([["itic-chart.cjs",[[1,"itic-chart",{"groups":[1],"locale":[1],"timeZone":[1,"time-zone"]},null,{"groups":[{"onGroupsChange":0}],"locale":[{"onLocaleChange":0}],"timeZone":[{"onTimeZoneChange":0}]}]]]], options);
10
10
  };
11
11
 
12
12
  exports.setNonce = index.setNonce;
@@ -3,22 +3,28 @@ import * as echarts from "echarts/core";
3
3
  import { LineChart, ScatterChart } from "echarts/charts";
4
4
  import { GridComponent, LegendComponent, TooltipComponent } from "echarts/components";
5
5
  import { CanvasRenderer } from "echarts/renderers";
6
- import { buildChartOptions, DEFAULT_SETTINGS, findClickedPoint, parseGroups, } from "./itic-chart.options";
6
+ import { getMessages, normalizeLocale } from "./itic-chart.i18n";
7
+ import { buildChartOptions, findClickedPoint, isValidTimeZone, parseGroups, } from "./itic-chart.options";
7
8
  // Register only what this chart uses, so bundles don't ship all of ECharts
8
9
  echarts.use([LineChart, ScatterChart, GridComponent, LegendComponent, TooltipComponent, CanvasRenderer]);
9
10
  export class IticChart {
10
11
  constructor() {
11
12
  /**
12
- * Name of the duration (X) axis.
13
+ * Language of the texts, dates and numbers: `ca`, `de`, `en`, `es`, `fr` or `pt`. Only the language
14
+ * is used (`es-ES` → `es`); any other value falls back to `en`.
13
15
  */
14
- this.xAxisName = DEFAULT_SETTINGS.xAxisName;
15
- /**
16
- * Name of the voltage (Y) axis.
17
- */
18
- this.yAxisName = DEFAULT_SETTINGS.yAxisName;
16
+ this.locale = 'en';
19
17
  this.parsedGroups = [];
20
18
  }
21
- onOptionsChange() {
19
+ onGroupsChange() {
20
+ this.updateChart();
21
+ }
22
+ onLocaleChange() {
23
+ this.updateAriaLabel();
24
+ this.updateChart();
25
+ }
26
+ onTimeZoneChange() {
27
+ this.validateTimeZone();
22
28
  this.updateChart();
23
29
  }
24
30
  connectedCallback() {
@@ -26,9 +32,13 @@ export class IticChart {
26
32
  if (!this.el.hasAttribute('role')) {
27
33
  this.el.setAttribute('role', 'img');
28
34
  }
35
+ this.updateAriaLabel();
29
36
  // componentDidLoad only fires once, so re-create the chart when the element is re-attached
30
37
  this.initChart();
31
38
  }
39
+ componentWillLoad() {
40
+ this.validateTimeZone();
41
+ }
32
42
  componentDidLoad() {
33
43
  this.initChart();
34
44
  }
@@ -39,6 +49,27 @@ export class IticChart {
39
49
  (_b = this.chart) === null || _b === void 0 ? void 0 : _b.dispose();
40
50
  this.chart = undefined;
41
51
  }
52
+ updateAriaLabel() {
53
+ const current = this.el.getAttribute('aria-label');
54
+ if (current !== null && current !== this.defaultAriaLabel) {
55
+ return;
56
+ }
57
+ this.defaultAriaLabel = getMessages(normalizeLocale(this.locale)).ariaLabel;
58
+ this.el.setAttribute('aria-label', this.defaultAriaLabel);
59
+ }
60
+ // Validated when it changes, not on every redraw, so an invalid value is only reported once
61
+ validateTimeZone() {
62
+ this.validTimeZone = undefined;
63
+ if (!this.timeZone) {
64
+ return;
65
+ }
66
+ if (isValidTimeZone(this.timeZone)) {
67
+ this.validTimeZone = this.timeZone;
68
+ }
69
+ else {
70
+ console.error(`[itic-chart] Invalid "timeZone" prop: "${this.timeZone}". Using the browser's time zone.`);
71
+ }
72
+ }
42
73
  initChart() {
43
74
  if (!this.container || this.chart) {
44
75
  return;
@@ -57,7 +88,6 @@ export class IticChart {
57
88
  this.updateChart();
58
89
  }
59
90
  updateChart() {
60
- var _a;
61
91
  if (!this.chart) {
62
92
  return;
63
93
  }
@@ -69,14 +99,13 @@ export class IticChart {
69
99
  console.error('[itic-chart] Invalid "groups" prop:', error);
70
100
  }
71
101
  const options = buildChartOptions(this.parsedGroups, {
72
- xAxisName: this.xAxisName,
73
- yAxisName: this.yAxisName,
74
- tooltipFormatter: (_a = this.tooltipFormatter) !== null && _a !== void 0 ? _a : DEFAULT_SETTINGS.tooltipFormatter,
102
+ locale: normalizeLocale(this.locale),
103
+ timeZone: this.validTimeZone,
75
104
  });
76
105
  this.chart.setOption(options, { notMerge: true });
77
106
  }
78
107
  render() {
79
- return h("div", { key: '53914c2232775d69a35b846005931c5e20f2eee2', class: "chart", ref: el => (this.container = el) });
108
+ return h("div", { key: '2bb964d590a36c9c9501761c3f848ad364a29030', class: "chart", ref: el => (this.container = el) });
80
109
  }
81
110
  static get is() { return "itic-chart"; }
82
111
  static get encapsulation() { return "shadow"; }
@@ -111,14 +140,14 @@ export class IticChart {
111
140
  "optional": false,
112
141
  "docs": {
113
142
  "tags": [],
114
- "text": "Point groups, as an array or a JSON string. Extra fields in groups and points are kept and passed\nback in `pointClick` and `tooltipFormatter`."
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`."
115
144
  },
116
145
  "getter": false,
117
146
  "setter": false,
118
147
  "reflect": false,
119
148
  "attribute": "groups"
120
149
  },
121
- "xAxisName": {
150
+ "locale": {
122
151
  "type": "string",
123
152
  "mutable": false,
124
153
  "complexType": {
@@ -130,15 +159,15 @@ export class IticChart {
130
159
  "optional": false,
131
160
  "docs": {
132
161
  "tags": [],
133
- "text": "Name of the duration (X) axis."
162
+ "text": "Language of the texts, dates and numbers: `ca`, `de`, `en`, `es`, `fr` or `pt`. Only the language\nis used (`es-ES` \u2192 `es`); any other value falls back to `en`."
134
163
  },
135
164
  "getter": false,
136
165
  "setter": false,
137
166
  "reflect": false,
138
- "attribute": "x-axis-name",
139
- "defaultValue": "'Duration (ms)'"
167
+ "attribute": "locale",
168
+ "defaultValue": "'en'"
140
169
  },
141
- "yAxisName": {
170
+ "timeZone": {
142
171
  "type": "string",
143
172
  "mutable": false,
144
173
  "complexType": {
@@ -147,40 +176,15 @@ export class IticChart {
147
176
  "references": {}
148
177
  },
149
178
  "required": false,
150
- "optional": false,
179
+ "optional": true,
151
180
  "docs": {
152
181
  "tags": [],
153
- "text": "Name of the voltage (Y) axis."
182
+ "text": "IANA time zone of the dates in the tooltip (e.g. `Europe/Madrid`). Defaults to the browser's,\nwhich is also used, with a console error, when the value isn't valid."
154
183
  },
155
184
  "getter": false,
156
185
  "setter": false,
157
186
  "reflect": false,
158
- "attribute": "y-axis-name",
159
- "defaultValue": "'Voltage (% of nominal voltage)'"
160
- },
161
- "tooltipFormatter": {
162
- "type": "unknown",
163
- "mutable": false,
164
- "complexType": {
165
- "original": "ITICTooltipFormatter",
166
- "resolved": "(point: ITICPoint, group: ITICChartMarks) => string",
167
- "references": {
168
- "ITICTooltipFormatter": {
169
- "location": "import",
170
- "path": "./itic-chart.options",
171
- "id": "src/components/itic-chart/itic-chart.options.ts::ITICTooltipFormatter",
172
- "referenceLocation": "ITICTooltipFormatter"
173
- }
174
- }
175
- },
176
- "required": false,
177
- "optional": true,
178
- "docs": {
179
- "tags": [],
180
- "text": "Returns the HTML of a point's tooltip. JS/React property only (not an HTML attribute).\nThe HTML is inserted as is: escaping the values is up to the formatter.\nDefaults to an English tooltip with phase, duration, voltage and timestamp."
181
- },
182
- "getter": false,
183
- "setter": false
187
+ "attribute": "time-zone"
184
188
  }
185
189
  };
186
190
  }
@@ -213,16 +217,13 @@ export class IticChart {
213
217
  static get watchers() {
214
218
  return [{
215
219
  "propName": "groups",
216
- "methodName": "onOptionsChange"
217
- }, {
218
- "propName": "xAxisName",
219
- "methodName": "onOptionsChange"
220
+ "methodName": "onGroupsChange"
220
221
  }, {
221
- "propName": "yAxisName",
222
- "methodName": "onOptionsChange"
222
+ "propName": "locale",
223
+ "methodName": "onLocaleChange"
223
224
  }, {
224
- "propName": "tooltipFormatter",
225
- "methodName": "onOptionsChange"
225
+ "propName": "timeZone",
226
+ "methodName": "onTimeZoneChange"
226
227
  }];
227
228
  }
228
229
  }
@@ -0,0 +1,26 @@
1
+ import ca from "./i18n/ca.json";
2
+ import de from "./i18n/de.json";
3
+ import en from "./i18n/en.json";
4
+ import es from "./i18n/es.json";
5
+ import fr from "./i18n/fr.json";
6
+ import pt from "./i18n/pt.json";
7
+ export const LOCALES = ['ca', 'de', 'en', 'es', 'fr', 'pt'];
8
+ export const DEFAULT_LOCALE = 'en';
9
+ const MESSAGES = { ca, de, en, es, fr, pt };
10
+ /** Keeps the language of a locale (`es-ES` → `es`), falling back to `en` when it isn't translated. */
11
+ export function normalizeLocale(locale) {
12
+ const language = String(locale !== null && locale !== void 0 ? locale : '')
13
+ .split(/[-_]/)[0]
14
+ .toLowerCase();
15
+ return LOCALES.includes(language) ? language : DEFAULT_LOCALE;
16
+ }
17
+ export const getMessages = (locale) => MESSAGES[locale];
18
+ const hasOwn = (object, key) => Object.prototype.hasOwnProperty.call(object, key);
19
+ /** Translated event type, or the code as is when it isn't in the catalog. */
20
+ export function translateEventType(messages, code) {
21
+ return hasOwn(messages.eventTypes, code) ? messages.eventTypes[code] : code;
22
+ }
23
+ /** Translated name of a phase code that has one (`ALL`, `NONE`), or `undefined`. */
24
+ export function translatePhase(messages, code) {
25
+ return hasOwn(messages.phases, code) ? messages.phases[code] : undefined;
26
+ }
@@ -1,4 +1,6 @@
1
- import dayjs from "dayjs";
1
+ import { DEFAULT_LOCALE, getMessages, translateEventType } from "./itic-chart.i18n";
2
+ import { getPhase, sortGroups } from "./itic-chart.phases";
3
+ export const DEFAULT_SETTINGS = { locale: DEFAULT_LOCALE };
2
4
  /**
3
5
  * Normalizes the `groups` prop, which arrives as an array from JS/React
4
6
  * or as a JSON string when set as an HTML attribute.
@@ -13,6 +15,15 @@ export function parseGroups(groups) {
13
15
  }
14
16
  return parsed;
15
17
  }
18
+ export function isValidTimeZone(timeZone) {
19
+ try {
20
+ new Intl.DateTimeFormat('en', { timeZone });
21
+ return true;
22
+ }
23
+ catch (_a) {
24
+ return false;
25
+ }
26
+ }
16
27
  export function escapeHtml(value) {
17
28
  return value
18
29
  .replace(/&/g, '&')
@@ -21,57 +32,95 @@ export function escapeHtml(value) {
21
32
  .replace(/"/g, '"')
22
33
  .replace(/'/g, ''');
23
34
  }
24
- /** Default tooltip, in English. Escapes every value, since they come from the consumer. */
25
- export const defaultTooltipFormatter = ({ duration, voltage, ts }, { groupName }) => {
26
- const rows = {
27
- phase: escapeHtml(String(groupName)),
28
- duration: `${escapeHtml(String(duration))} ms`,
29
- voltage: `${escapeHtml(String(Math.round(voltage * 10) / 10))}%`,
30
- ts: escapeHtml(dayjs(ts).locale('en').format('DD MMM, HH:mm:ss.SSS')),
35
+ const isBlank = (value) => value == null || value === '';
36
+ /**
37
+ * Returns the tooltip HTML of a point: one row per field, in the chart's language. Rows without a value
38
+ * are left out, and every value is escaped, since it comes from the consumer's data.
39
+ */
40
+ export function createTooltipFormatter({ locale, timeZone }) {
41
+ const messages = getMessages(locale);
42
+ const dateFormat = new Intl.DateTimeFormat(locale, {
43
+ day: '2-digit',
44
+ month: 'short',
45
+ hour: '2-digit',
46
+ minute: '2-digit',
47
+ second: '2-digit',
48
+ // Short events are milliseconds apart
49
+ fractionalSecondDigits: 3,
50
+ hourCycle: 'h23',
51
+ timeZone,
52
+ });
53
+ const durationFormat = new Intl.NumberFormat(locale, { style: 'unit', unit: 'millisecond', maximumFractionDigits: 3 });
54
+ const voltageFormat = new Intl.NumberFormat(locale, { style: 'percent', maximumFractionDigits: 1 });
55
+ // `volt` isn't a unit supported by Intl
56
+ const voltsFormat = new Intl.NumberFormat(locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
57
+ const formatDate = (ts) => {
58
+ const date = new Date(ts);
59
+ return Number.isNaN(date.getTime()) ? String(ts !== null && ts !== void 0 ? ts : '') : dateFormat.format(date);
31
60
  };
32
- return Object.entries(rows)
33
- .map(([label, value]) => ` <strong>${label}</strong>: ${value}`)
34
- .join('<br/>');
35
- };
36
- export const DEFAULT_SETTINGS = {
37
- xAxisName: 'Duration (ms)',
38
- yAxisName: 'Voltage (% of nominal voltage)',
39
- tooltipFormatter: defaultTooltipFormatter,
40
- };
61
+ const formatVolts = (value) => (Number(value) ? `${voltsFormat.format(Number(value))} V` : undefined);
62
+ return (point, group) => {
63
+ const rows = [
64
+ ['asset', isBlank(group.assetName) ? undefined : String(group.assetName)],
65
+ ['date', formatDate(point.ts)],
66
+ ['eventType', isBlank(point.eventType) ? undefined : translateEventType(messages, String(point.eventType))],
67
+ ['duration', durationFormat.format(Number(point.duration))],
68
+ // The value is already in %
69
+ ['voltage', voltageFormat.format(Number(point.voltage) / 100)],
70
+ ['status', isBlank(point.status) ? undefined : String(point.status)],
71
+ ['phase', getPhase(group.phase, messages).name || '-'],
72
+ ['min', formatVolts(point.min)],
73
+ ['max', formatVolts(point.max)],
74
+ ];
75
+ return rows
76
+ .filter((row) => row[1] !== undefined)
77
+ .map(([key, value]) => ` <strong>${messages.tooltip[key]}</strong>: ${escapeHtml(value)}`)
78
+ .join('<br/>');
79
+ };
80
+ }
81
+ /** Formats the axis labels in the chart's language; ECharts always uses English separators. */
82
+ export function createAxisLabelFormatter(locale) {
83
+ const numberFormat = new Intl.NumberFormat(locale);
84
+ return value => numberFormat.format(value);
85
+ }
41
86
  export const isClickable = (point) => point.clickable !== false;
42
87
  export function buildChartOptions(groups, settings = DEFAULT_SETTINGS) {
43
- return Object.assign(Object.assign({}, BASE_OPTIONS), { xAxis: Object.assign(Object.assign({}, BASE_OPTIONS.xAxis), { name: settings.xAxisName }), yAxis: Object.assign(Object.assign({}, BASE_OPTIONS.yAxis), { name: settings.yAxisName }), series: [
88
+ const messages = getMessages(settings.locale);
89
+ const formatTooltip = createTooltipFormatter(settings);
90
+ const axisLabel = { formatter: createAxisLabelFormatter(settings.locale) };
91
+ return Object.assign(Object.assign({}, BASE_OPTIONS), { xAxis: Object.assign(Object.assign({}, BASE_OPTIONS.xAxis), { name: messages.xAxisName, axisLabel }), yAxis: Object.assign(Object.assign({}, BASE_OPTIONS.yAxis), { name: messages.yAxisName, axisLabel }), series: [
44
92
  ...CURVE_SERIES,
45
- ...groups.map((group) => ({
46
- type: 'scatter',
47
- name: group.groupName,
48
- // ECharts 6 reads `cursor` per data item, although its types only declare it per series
49
- data: group.points.map(point => ({
50
- value: [point.duration, point.voltage],
51
- cursor: isClickable(point) ? 'pointer' : 'default',
52
- })),
53
- symbolSize: 12,
54
- z: 4,
55
- itemStyle: {
56
- color: group.color,
57
- opacity: 1,
58
- },
59
- tooltip: {
60
- formatter: params => settings.tooltipFormatter(group.points[params.dataIndex], group),
61
- },
62
- })),
93
+ ...sortGroups(groups).map((group) => {
94
+ const { name, color } = getPhase(group.phase, messages);
95
+ return {
96
+ type: 'scatter',
97
+ name,
98
+ // ECharts 6 reads `cursor` per data item, although its types only declare it per series
99
+ data: group.points.map(point => ({
100
+ value: [point.duration, point.voltage],
101
+ cursor: isClickable(point) ? 'pointer' : 'default',
102
+ })),
103
+ symbolSize: 12,
104
+ z: 4,
105
+ // Phases without a color take the next one of the ECharts palette
106
+ itemStyle: color ? { color, opacity: 1 } : { opacity: 1 },
107
+ tooltip: {
108
+ formatter: params => formatTooltip(group.points[params.dataIndex], group),
109
+ },
110
+ };
111
+ }),
63
112
  ] });
64
113
  }
65
114
  /**
66
- * Maps an ECharts click back to the original point and group, which `buildChartOptions` draws as
67
- * scatter series after the ITIC curve series, in the same order as `groups`.
115
+ * Maps an ECharts click back to the original point and group. `buildChartOptions` draws the groups as
116
+ * scatter series after the ITIC curve series, sorted with `sortGroups`.
68
117
  * Returns `undefined` for clicks outside the points and on points that aren't clickable.
69
118
  */
70
119
  export function findClickedPoint(groups, { componentType, seriesIndex, dataIndex }) {
71
120
  if (componentType !== 'series' || seriesIndex == null || dataIndex == null) {
72
121
  return undefined;
73
122
  }
74
- const group = groups[seriesIndex - CURVE_SERIES.length];
123
+ const group = sortGroups(groups)[seriesIndex - CURVE_SERIES.length];
75
124
  const point = group === null || group === void 0 ? void 0 : group.points[dataIndex];
76
125
  if (!point || !isClickable(point)) {
77
126
  return undefined;
@@ -96,10 +145,11 @@ const ITIC_CURVE_NODAMAGE_REGION = [
96
145
  [10000, 90],
97
146
  [100000, 90],
98
147
  ];
148
+ // Fixed ranges, as in Scout: ECharts clips points outside them (duration < 1 ms or voltage > 200%)
99
149
  const BASE_OPTIONS = {
100
150
  animation: false,
101
151
  grid: {
102
- right: '40px',
152
+ right: '20px',
103
153
  left: '60px',
104
154
  },
105
155
  // ECharts 6 moved the default legend to the bottom; keep it on top as in previous versions
@@ -111,7 +161,7 @@ const BASE_OPTIONS = {
111
161
  },
112
162
  xAxis: {
113
163
  type: 'log',
114
- min: 0.1,
164
+ min: 1,
115
165
  max: 100000,
116
166
  nameGap: 30,
117
167
  nameLocation: 'middle',
@@ -123,7 +173,7 @@ const BASE_OPTIONS = {
123
173
  yAxis: {
124
174
  type: 'value',
125
175
  min: 0,
126
- max: 500,
176
+ max: 200,
127
177
  nameTextStyle: {
128
178
  fontSize: 16,
129
179
  fontWeight: 'bold',
@@ -133,6 +183,7 @@ const BASE_OPTIONS = {
133
183
  nameLocation: 'middle',
134
184
  },
135
185
  };
186
+ // `silent`: the curve doesn't react to the mouse (no tooltip, cursor or click)
136
187
  const CURVE_SERIES = [
137
188
  {
138
189
  type: 'line',
@@ -144,6 +195,7 @@ const CURVE_SERIES = [
144
195
  lineStyle: {
145
196
  color: '#000',
146
197
  },
198
+ silent: true,
147
199
  showSymbol: false,
148
200
  emphasis: {
149
201
  disabled: true,
@@ -156,6 +208,7 @@ const CURVE_SERIES = [
156
208
  origin: 'start',
157
209
  color: '#FFF5DF',
158
210
  },
211
+ silent: true,
159
212
  showSymbol: false,
160
213
  lineStyle: {
161
214
  color: '#000',
@@ -171,6 +224,7 @@ const CURVE_SERIES = [
171
224
  origin: 100,
172
225
  color: '#DDFAF1',
173
226
  },
227
+ silent: true,
174
228
  showSymbol: false,
175
229
  lineStyle: {
176
230
  color: '#000',
@@ -186,6 +240,7 @@ const CURVE_SERIES = [
186
240
  origin: 100,
187
241
  color: '#DDFAF1',
188
242
  },
243
+ silent: true,
189
244
  showSymbol: false,
190
245
  lineStyle: {
191
246
  color: '#000',
@@ -204,6 +259,7 @@ const CURVE_SERIES = [
204
259
  origin: 100,
205
260
  color: '#DDFAF1',
206
261
  },
262
+ silent: true,
207
263
  showSymbol: false,
208
264
  lineStyle: {
209
265
  color: 'transparent',
@@ -222,6 +278,7 @@ const CURVE_SERIES = [
222
278
  origin: 100,
223
279
  color: '#DDFAF1',
224
280
  },
281
+ silent: true,
225
282
  showSymbol: false,
226
283
  lineStyle: {
227
284
  color: 'transparent',
@@ -0,0 +1,55 @@
1
+ import { translatePhase } from "./itic-chart.i18n";
2
+ const L1 = '#a67c52';
3
+ const L2 = '#20262C';
4
+ const L3 = '#B9BDC0';
5
+ const THREE_PHASE = '#00B388';
6
+ // Same catalog as Scout. Names that change with the language (`ALL`, `NONE`) are in the i18n JSON files.
7
+ const PHASES = {
8
+ AN: { name: 'L1', color: L1 },
9
+ BN: { name: 'L2', color: L2 },
10
+ CN: { name: 'L3', color: L3 },
11
+ AB: { name: 'L1', color: L1 },
12
+ BC: { name: 'L2', color: L2 },
13
+ CA: { name: 'L3', color: L3 },
14
+ 'AN,BN,CN': { name: 'L1,L2,L3', color: THREE_PHASE },
15
+ 'AB,BC,CA': { name: 'L1,L2,L3', color: THREE_PHASE },
16
+ ALL: { name: '', color: '#4C5CC5' },
17
+ A: { name: 'L1' },
18
+ B: { name: 'L2' },
19
+ C: { name: 'L3' },
20
+ 'AN,BN': { name: 'L1,L2' },
21
+ 'AN,CN': { name: 'L1,L3' },
22
+ 'BN,CN': { name: 'L2,L3' },
23
+ 'AB,BC': { name: 'L1,L2' },
24
+ 'AB,CA': { name: 'L1,L3' },
25
+ 'BC,CA': { name: 'L2,L3' },
26
+ NONE: { name: '' },
27
+ };
28
+ const phaseCode = (phase) => (phase == null ? '' : String(phase));
29
+ // Unknown codes keep the code as name and have no color
30
+ const catalogPhase = (code) => Object.prototype.hasOwnProperty.call(PHASES, code) ? PHASES[code] : { name: code };
31
+ /** Name, in the given language, and color of a phase code. */
32
+ export function getPhase(phase, messages) {
33
+ var _a;
34
+ const code = phaseCode(phase);
35
+ const known = catalogPhase(code);
36
+ return Object.assign(Object.assign({}, known), { name: (_a = translatePhase(messages, code)) !== null && _a !== void 0 ? _a : known.name });
37
+ }
38
+ // `NONE` (no name) goes after the named phases and `ALL` always last. Only these two names are translated,
39
+ // so ranking them by code keeps the order the same in every language.
40
+ const rank = (code) => (code === 'ALL' ? 2 : code === 'NONE' ? 1 : 0);
41
+ /**
42
+ * Sorts groups by phase name, then by code (e.g. `AN` and `AB` are both "L1"), with `NONE` and `ALL` last.
43
+ * Compares characters, not with a locale's rules, so the legend is the same in every language.
44
+ * Returns a new array with the same group objects; groups with the same code keep their order.
45
+ */
46
+ export function sortGroups(groups) {
47
+ const keyed = groups.map(group => {
48
+ const code = phaseCode(group.phase);
49
+ return { group, code, rank: rank(code), name: catalogPhase(code).name };
50
+ });
51
+ return keyed
52
+ .sort((a, b) => a.rank - b.rank || compare(a.name, b.name) || compare(a.code, b.code))
53
+ .map(({ group }) => group);
54
+ }
55
+ const compare = (a, b) => (a < b ? -1 : a > b ? 1 : 0);