Skip to content

Commit 83f3b24

Browse files
authored
Refactor/config parser (#163)
* Move to dedicated file * Further separated parsing components
1 parent 76f951a commit 83f3b24

File tree

3 files changed

+196
-154
lines changed

3 files changed

+196
-154
lines changed

src/parse-config.ts

+190
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { getIsPureObject } from "./utils";
2+
import {
3+
AutoPeriodConfig,
4+
StatisticPeriod,
5+
StatisticType,
6+
STATISTIC_PERIODS,
7+
STATISTIC_TYPES,
8+
} from "./recorder-types";
9+
import colorSchemes, {
10+
ColorSchemeArray,
11+
isColorSchemeArray,
12+
} from "./color-schemes";
13+
import { Config, EntityConfig, InputConfig } from "./types";
14+
import { parseTimeDuration } from "./duration/duration";
15+
import merge from "lodash/merge";
16+
17+
function parseColorScheme(config: InputConfig): ColorSchemeArray {
18+
const schemeName = config.color_scheme ?? "category10";
19+
const colorScheme = isColorSchemeArray(schemeName)
20+
? schemeName
21+
: colorSchemes[schemeName] ||
22+
colorSchemes[Object.keys(colorSchemes)[schemeName]] ||
23+
null;
24+
if (colorScheme === null) {
25+
throw new Error(
26+
`color_scheme: "${
27+
config.color_scheme
28+
}" is not valid. Valid are an array of colors (see readme) or ${Object.keys(
29+
colorSchemes
30+
)}`
31+
);
32+
}
33+
return colorScheme;
34+
}
35+
36+
function getIsAutoPeriodConfig(periodObj: any): periodObj is AutoPeriodConfig {
37+
if (!getIsPureObject(periodObj)) return false;
38+
let lastDuration = -1;
39+
for (const durationStr in periodObj) {
40+
const period = periodObj[durationStr];
41+
const duration = parseTimeDuration(durationStr as any); // will throw if not a valud duration
42+
if (!STATISTIC_PERIODS.includes(period as any)) {
43+
throw new Error(
44+
`Error parsing automatic period config: "${period}" not expected. Must be ${STATISTIC_PERIODS}`
45+
);
46+
}
47+
if (duration <= lastDuration) {
48+
throw new Error(
49+
`Error parsing automatic period config: ranges must be sorted in ascending order, "${durationStr}" not expected`
50+
);
51+
}
52+
lastDuration = duration;
53+
}
54+
return true;
55+
}
56+
function parseStatistics(entity: InputConfig["entities"][0]) {
57+
if (!("statistic" in entity || "period" in entity)) return {};
58+
const statistic: StatisticType = entity.statistic || "mean";
59+
60+
if (!STATISTIC_TYPES.includes(statistic))
61+
throw new Error(
62+
`statistic: "${entity.statistic}" is not valid. Use ${STATISTIC_TYPES}`
63+
);
64+
if (getIsAutoPeriodConfig(entity.period)) {
65+
return {
66+
statistic,
67+
autoPeriod: entity.period,
68+
period: undefined,
69+
};
70+
}
71+
if (entity.period === "auto") {
72+
return {
73+
statistic,
74+
autoPeriod: {
75+
"0s": "5minute",
76+
"1d": "hour",
77+
"7d": "day",
78+
"28d": "week",
79+
"12M": "month",
80+
},
81+
period: undefined,
82+
};
83+
}
84+
const period: StatisticPeriod = entity.period || "hour";
85+
if (!STATISTIC_PERIODS.includes(period))
86+
throw new Error(
87+
`period: "${entity.period}" is not valid. Use ${STATISTIC_PERIODS}`
88+
);
89+
return {
90+
statistic,
91+
period,
92+
autoPeriod: undefined,
93+
};
94+
}
95+
function parseEntities(config: InputConfig): EntityConfig[] {
96+
const colorScheme = parseColorScheme(config);
97+
return config.entities.map((entityIn, entityIdx) => {
98+
if (typeof entityIn === "string") entityIn = { entity: entityIn };
99+
const [oldAPI_entity, oldAPI_attribute] = entityIn.entity.split("::");
100+
if (oldAPI_attribute) {
101+
entityIn.entity = oldAPI_entity;
102+
entityIn.attribute = oldAPI_attribute;
103+
}
104+
entityIn = merge(
105+
{
106+
hovertemplate: `<b>%{customdata.name}</b><br><i>%{x}</i><br>%{y}%{customdata.unit_of_measurement}<extra></extra>`,
107+
mode: "lines",
108+
show_value: false,
109+
line: {
110+
width: 1,
111+
shape: "hv",
112+
color: colorScheme[entityIdx % colorScheme.length],
113+
},
114+
},
115+
config.defaults?.entity,
116+
entityIn
117+
);
118+
119+
const statisticConfig = parseStatistics(entityIn);
120+
return {
121+
...(entityIn as any), // ToDo: make this type safe
122+
offset: parseTimeDuration(entityIn.offset ?? "0s"),
123+
lambda: entityIn.lambda && window.eval(entityIn.lambda),
124+
...statisticConfig,
125+
extend_to_present:
126+
entityIn.extend_to_present ?? !statisticConfig.statistic,
127+
};
128+
});
129+
}
130+
131+
export default function parseConfig(config: InputConfig): Config {
132+
if (
133+
typeof config.refresh_interval !== "number" &&
134+
config.refresh_interval !== undefined &&
135+
config.refresh_interval !== "auto"
136+
) {
137+
throw new Error(
138+
`refresh_interval: "${config.refresh_interval}" is not valid. Must be either "auto" or a number (in seconds). `
139+
);
140+
}
141+
return {
142+
title: config.title,
143+
hours_to_show: config.hours_to_show ?? 1,
144+
refresh_interval: config.refresh_interval ?? "auto",
145+
offset: parseTimeDuration(config.offset ?? "0s"),
146+
entities: parseEntities(config),
147+
layout: merge(
148+
{
149+
yaxis: merge({}, config.defaults?.yaxes),
150+
yaxis2: merge({}, config.defaults?.yaxes),
151+
yaxis3: merge({}, config.defaults?.yaxes),
152+
yaxis4: merge({}, config.defaults?.yaxes),
153+
yaxis5: merge({}, config.defaults?.yaxes),
154+
yaxis6: merge({}, config.defaults?.yaxes),
155+
yaxis7: merge({}, config.defaults?.yaxes),
156+
yaxis8: merge({}, config.defaults?.yaxes),
157+
yaxis9: merge({}, config.defaults?.yaxes),
158+
yaxis10: merge({}, config.defaults?.yaxes),
159+
yaxis11: merge({}, config.defaults?.yaxes),
160+
yaxis12: merge({}, config.defaults?.yaxes),
161+
yaxis13: merge({}, config.defaults?.yaxes),
162+
yaxis14: merge({}, config.defaults?.yaxes),
163+
yaxis15: merge({}, config.defaults?.yaxes),
164+
yaxis16: merge({}, config.defaults?.yaxes),
165+
yaxis17: merge({}, config.defaults?.yaxes),
166+
yaxis18: merge({}, config.defaults?.yaxes),
167+
yaxis19: merge({}, config.defaults?.yaxes),
168+
yaxis20: merge({}, config.defaults?.yaxes),
169+
yaxis21: merge({}, config.defaults?.yaxes),
170+
yaxis22: merge({}, config.defaults?.yaxes),
171+
yaxis23: merge({}, config.defaults?.yaxes),
172+
yaxis24: merge({}, config.defaults?.yaxes),
173+
yaxis25: merge({}, config.defaults?.yaxes),
174+
yaxis26: merge({}, config.defaults?.yaxes),
175+
yaxis27: merge({}, config.defaults?.yaxes),
176+
yaxis28: merge({}, config.defaults?.yaxes),
177+
yaxis29: merge({}, config.defaults?.yaxes),
178+
yaxis30: merge({}, config.defaults?.yaxes),
179+
},
180+
config.layout
181+
),
182+
config: {
183+
...config.config,
184+
},
185+
no_theme: config.no_theme ?? false,
186+
no_default_layout: config.no_default_layout ?? false,
187+
significant_changes_only: config.significant_changes_only ?? false,
188+
minimal_response: config.minimal_response ?? true,
189+
};
190+
}

src/plotly-graph-card.ts

+5-154
Original file line numberDiff line numberDiff line change
@@ -17,16 +17,12 @@ import {
1717
import Cache from "./cache/Cache";
1818
import getThemedLayout from "./themed-layout";
1919
import isProduction from "./is-production";
20-
import { getIsPureObject, sleep } from "./utils";
20+
import { sleep } from "./utils";
2121
import { Datum } from "plotly.js";
22-
import colorSchemes, { isColorSchemeArray } from "./color-schemes";
2322
import { parseISO } from "date-fns";
24-
import {
25-
STATISTIC_PERIODS,
26-
STATISTIC_TYPES,
27-
StatisticPeriod,
28-
} from "./recorder-types";
23+
import { StatisticPeriod } from "./recorder-types";
2924
import { parseTimeDuration } from "./duration/duration";
25+
import parseConfig from "./parse-config";
3026

3127
const componentName = isProduction ? "plotly-graph" : "plotly-graph-dev";
3228

@@ -361,152 +357,7 @@ export class PlotlyGraph extends HTMLElement {
361357
async _setConfig(config: InputConfig) {
362358
config = JSON.parse(JSON.stringify(config));
363359
this.config = config;
364-
const schemeName = config.color_scheme ?? "category10";
365-
const colorScheme = isColorSchemeArray(schemeName)
366-
? schemeName
367-
: colorSchemes[schemeName] ||
368-
colorSchemes[Object.keys(colorSchemes)[schemeName]] ||
369-
null;
370-
if (colorScheme === null) {
371-
throw new Error(
372-
`color_scheme: "${config.color_scheme
373-
}" is not valid. Valid are an array of colors (see readme) or ${Object.keys(
374-
colorSchemes
375-
)}`
376-
);
377-
}
378-
if (
379-
typeof config.refresh_interval !== "number" &&
380-
config.refresh_interval !== undefined &&
381-
config.refresh_interval !== "auto"
382-
) {
383-
throw new Error(
384-
`refresh_interval: "${config.refresh_interval}" is not valid. Must be either "auto" or a number (in seconds). `
385-
);
386-
}
387-
const newConfig: Config = {
388-
title: config.title,
389-
hours_to_show: config.hours_to_show ?? 1,
390-
refresh_interval: config.refresh_interval ?? "auto",
391-
offset: parseTimeDuration(config.offset ?? "0s"),
392-
entities: config.entities.map((entityIn, entityIdx) => {
393-
if (typeof entityIn === "string") entityIn = { entity: entityIn };
394-
395-
// being lazy on types here. The merged object is temporarily not a real Config
396-
const entity: any = merge(
397-
{
398-
hovertemplate: `<b>%{customdata.name}</b><br><i>%{x}</i><br>%{y}%{customdata.unit_of_measurement}<extra></extra>`,
399-
mode: "lines",
400-
show_value: false,
401-
line: {
402-
width: 1,
403-
shape: "hv",
404-
color: colorScheme[entityIdx % colorScheme.length],
405-
},
406-
},
407-
config.defaults?.entity,
408-
entityIn
409-
);
410-
entity.offset = parseTimeDuration(entity.offset ?? "0s");
411-
if (entity.lambda) {
412-
entity.lambda = window.eval(entity.lambda);
413-
}
414-
if ("statistic" in entity || "period" in entity) {
415-
const validStatistic = STATISTIC_TYPES.includes(entity.statistic!);
416-
if (entity.statistic === undefined) entity.statistic = "mean";
417-
else if (!validStatistic)
418-
throw new Error(
419-
`statistic: "${entity.statistic}" is not valid. Use ${STATISTIC_TYPES}`
420-
);
421-
// @TODO: cleanup how this is done
422-
if (entity.period === "auto") {
423-
entity.period = {
424-
"0s": "5minute",
425-
"1d": "hour",
426-
"7d": "day",
427-
"28d": "week",
428-
"12M": "month",
429-
};
430-
}
431-
if (getIsPureObject(entity.period)) {
432-
entity.autoPeriod = entity.period;
433-
entity.period = undefined;
434-
let lastDuration = -1;
435-
for (const durationStr in entity.autoPeriod) {
436-
const period = entity.autoPeriod[durationStr];
437-
const duration = parseTimeDuration(durationStr as any); // will throw if not a valud duration
438-
if (!STATISTIC_PERIODS.includes(period as any)) {
439-
throw new Error(
440-
`Error parsing automatic period config: "${period}" not expected. Must be ${STATISTIC_PERIODS}`
441-
);
442-
}
443-
if (duration <= lastDuration) {
444-
throw new Error(
445-
`Error parsing automatic period config: ranges must be sorted in ascending order, "${durationStr}" not expected`
446-
);
447-
}
448-
lastDuration = duration;
449-
}
450-
}
451-
const validPeriod = STATISTIC_PERIODS.includes(entity.period);
452-
if (entity.period === undefined) entity.period = "hour";
453-
else if (!validPeriod)
454-
throw new Error(
455-
`period: "${entity.period}" is not valid. Use ${STATISTIC_PERIODS}`
456-
);
457-
}
458-
entity.extend_to_present ??= !entity.statistic;
459-
const [oldAPI_entity, oldAPI_attribute] = entity.entity.split("::");
460-
if (oldAPI_attribute) {
461-
entity.entity = oldAPI_entity;
462-
entity.attribute = oldAPI_attribute;
463-
}
464-
return entity as EntityConfig;
465-
}),
466-
layout: merge(
467-
{
468-
yaxis: merge({}, config.defaults?.yaxes),
469-
yaxis2: merge({}, config.defaults?.yaxes),
470-
yaxis3: merge({}, config.defaults?.yaxes),
471-
yaxis4: merge({}, config.defaults?.yaxes),
472-
yaxis5: merge({}, config.defaults?.yaxes),
473-
yaxis6: merge({}, config.defaults?.yaxes),
474-
yaxis7: merge({}, config.defaults?.yaxes),
475-
yaxis8: merge({}, config.defaults?.yaxes),
476-
yaxis9: merge({}, config.defaults?.yaxes),
477-
yaxis10: merge({}, config.defaults?.yaxes),
478-
yaxis11: merge({}, config.defaults?.yaxes),
479-
yaxis12: merge({}, config.defaults?.yaxes),
480-
yaxis13: merge({}, config.defaults?.yaxes),
481-
yaxis14: merge({}, config.defaults?.yaxes),
482-
yaxis15: merge({}, config.defaults?.yaxes),
483-
yaxis16: merge({}, config.defaults?.yaxes),
484-
yaxis17: merge({}, config.defaults?.yaxes),
485-
yaxis18: merge({}, config.defaults?.yaxes),
486-
yaxis19: merge({}, config.defaults?.yaxes),
487-
yaxis20: merge({}, config.defaults?.yaxes),
488-
yaxis21: merge({}, config.defaults?.yaxes),
489-
yaxis22: merge({}, config.defaults?.yaxes),
490-
yaxis23: merge({}, config.defaults?.yaxes),
491-
yaxis24: merge({}, config.defaults?.yaxes),
492-
yaxis25: merge({}, config.defaults?.yaxes),
493-
yaxis26: merge({}, config.defaults?.yaxes),
494-
yaxis27: merge({}, config.defaults?.yaxes),
495-
yaxis28: merge({}, config.defaults?.yaxes),
496-
yaxis29: merge({}, config.defaults?.yaxes),
497-
yaxis30: merge({}, config.defaults?.yaxes),
498-
},
499-
config.layout
500-
),
501-
config: {
502-
...config.config,
503-
},
504-
no_theme: config.no_theme ?? false,
505-
no_default_layout: config.no_default_layout ?? false,
506-
significant_changes_only: config.significant_changes_only ?? false,
507-
minimal_response: config.minimal_response ?? true,
508-
};
509-
360+
const newConfig = parseConfig(config);
510361
const was = this.parsed_config;
511362
this.parsed_config = newConfig;
512363
const is = this.parsed_config;
@@ -649,7 +500,7 @@ export class PlotlyGraph extends HTMLElement {
649500
ys = [null];
650501
}
651502
const customdatum = { unit_of_measurement: unit, name, attributes };
652-
const customdata = xs.map(() => customdatum);
503+
const customdata = xs.map((x, i) => ({ ...customdatum, x, y: ys[i] }));
653504
const mergedTrace = merge(
654505
{
655506
name,

src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type InputConfig = {
3030
right_margin: number;
3131
};
3232
offset?: TimeDurationStr;
33+
extend_to_present?: boolean;
3334
} & Partial<Plotly.PlotData>)[];
3435
defaults?: {
3536
entity?: Partial<Plotly.PlotData>;

0 commit comments

Comments
 (0)