Skip to content

Commit

Permalink
Yaml editor
Browse files Browse the repository at this point in the history
  • Loading branch information
dbuezas committed Oct 13, 2024
1 parent 1479c3c commit 547a512
Show file tree
Hide file tree
Showing 20 changed files with 11,898 additions and 4,469 deletions.
384 changes: 251 additions & 133 deletions package-lock.json

Large diffs are not rendered by default.

9 changes: 4 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,12 @@
"main": "index.js",
"scripts": {
"start": "(node ./script/hot-reload.mjs & esbuild src/plotly-graph-card.ts --servedir=dist --outdir=dist --bundle --sourcemap=inline)",
"build": "esbuild src/plotly-graph-card.ts --outdir=dist --bundle --minify && npm run schema",
"build": "esbuild src/plotly-graph-card.ts --outdir=dist --bundle --minify",
"tsc": "tsc",
"test": "jest",
"test:watch": "jest --watchAll",
"version": "npm run build && git add .",
"npm-upgrade": "npx npm-upgrade",
"schema": "typescript-json-schema ./tsconfig.json JsonSchemaRoot > schema.json"
"npm-upgrade": "npx npm-upgrade"
},
"author": "",
"license": "ISC",
Expand Down Expand Up @@ -45,8 +44,8 @@
"ndarray": "^1.0.19",
"ndarray-fft": "^1.0.3",
"plotly.js": "^2.34.0",
"prettier": "^3.3.3",
"propose": "^0.0.5",
"typescript": "^5.6.3",
"typescript-json-schema": "^0.65.1"
"typescript": "^5.6.3"
}
}
13 changes: 11 additions & 2 deletions src/duration/duration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const parseTimeDuration = (str: TimeDurationStr | undefined): number => {
if (!str || !str.match)
throw new Error(`Cannot parse "${str}" as a duration`);
const match = str.match(
/^(?<sign>[+-])?(?<number>\d*(\.\d)?)(?<unit>(ms|s|m|h|d|w|M|y))$/
/^(?<sign>[+-])?(?<number>\d*(\.\d)?)(?<unit>(ms|s|m|h|d|w|M|y))$/,
);
if (!match || !match.groups)
throw new Error(`Cannot parse "${str}" as a duration`);
Expand Down Expand Up @@ -87,7 +87,16 @@ export const setDateFnDefaultOptions = (hass: HomeAssistant) => {
weekStartsOn,
});
};
export const parseRelativeTime = (str: string): [number, number] => {
export type RelativeTimeStr =
| "current_minute"
| "current_hour"
| "current_day"
| "current_week"
| "current_month"
| "current_quarter"
| "current_year";

export const parseRelativeTime = (str: RelativeTimeStr): [number, number] => {
const now = new Date();
switch (str) {
case "current_minute":
Expand Down
12 changes: 10 additions & 2 deletions src/filters/filters.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import filters from "./filters";
import filters, { FilterInput } from "./filters";

const RIGHT_1 = { integrate: { offset: "2d" } } satisfies FilterInput;
const RIGHT_2 = "integrate" satisfies FilterInput;
const RIGHT_3 = "delta" satisfies FilterInput;
const RIGHT_4 = "deduplicate_adjacent" satisfies FilterInput;
//@ts-expect-error
const WRONG_1 = "add" satisfies FilterInput;

const data = {
states: [],
statistics: [],
Expand Down Expand Up @@ -111,7 +119,7 @@ describe("filters", () => {
});
it("fn", () => {
expect(
filters.fn(`({xs,ys,...rest}) => ({xs:ys, ys:xs,...rest})`)(data)
filters.fn(`({xs,ys,...rest}) => ({xs:ys, ys:xs,...rest})`)(data),
).toEqual({
attributes: {
unit_of_measurement: "w",
Expand Down
68 changes: 47 additions & 21 deletions src/filters/filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,24 @@ type FilterData = {
};
export type FilterFn = (p: FilterData) => Partial<FilterData>;

type FilterParam<K extends keyof typeof filters> = Parameters<
(typeof filters)[K]
>[0];

export type FilterInput = {
[K in keyof typeof filters]: FilterParam<K> extends undefined
? // Case 1: Function accepts `undefined` only, return the key as a string literal
K
: undefined extends FilterParam<K>
? object extends FilterParam<K>
? // Case 2: Function accepts `object | undefined`, return union of the key or the object
{ [P in K]: Exclude<FilterParam<K>, undefined> } | K
: // Case 3: Function accepts only `undefined`, return the key as a string literal
K
: // Case 4: Function accepts only an `object`, return object type
{ [P in K]: FilterParam<K> };
}[keyof typeof filters];

const mapNumbers = (ys: YValue[], fn: (y: number, i: number) => number) =>
ys.map((y, i) => {
const n = castFloat(y);
Expand Down Expand Up @@ -79,7 +97,7 @@ const filters = {
const mapping = mappingStr.map((str) => str.split("->").map(parseFloat));
const regression = new LinearRegression(
mapping.map(([x, _y]) => x),
mapping.map(([_x, y]) => y)
mapping.map(([_x, y]) => y),
);
return {
ys: regression.predict(ys.map(castFloat)),
Expand Down Expand Up @@ -150,7 +168,7 @@ const filters = {
unit?: keyof typeof timeUnits;
reset_every?: TimeDurationStr;
offset?: TimeDurationStr;
} = "h"
} = "h",
) => {
const param =
typeof unitOrObject == "string" ? { unit: unitOrObject } : unitOrObject;
Expand Down Expand Up @@ -197,7 +215,11 @@ const filters = {
};
},
sliding_window_moving_average:
({ window_size = 10, extended = false, centered = true } = {}) =>
({
window_size = 10,
extended = false,
centered = true,
}: { window_size?: number; extended?: boolean; centered?: boolean } = {}) =>
(params) => {
const { xs, ys, ...rest } = force_numeric(params);
const ys2: number[] = [];
Expand Down Expand Up @@ -227,7 +249,11 @@ const filters = {
return { xs: xs2, ys: ys2, ...rest };
},
median:
({ window_size = 10, extended = false, centered = true } = {}) =>
({
window_size = 10,
extended = false,
centered = true,
}: { window_size?: number; extended?: boolean; centered?: boolean } = {}) =>
(params) => {
const { xs, ys, ...rest } = force_numeric(params);
const ys2: number[] = [];
Expand Down Expand Up @@ -257,7 +283,7 @@ const filters = {
return { ys: ys2, xs: xs2, ...rest };
},
exponential_moving_average:
({ alpha = 0.1 } = {}) =>
({ alpha = 0.1 }: { alpha?: number } = {}) =>
(params) => {
const { ys, ...rest } = force_numeric(params);
let last = ys[0];
Expand All @@ -268,37 +294,37 @@ const filters = {
},
map_y_numbers: (fnStr: string) => {
const fn = myEval(
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`,
);
return ({ xs, ys, states, statistics, meta, vars, hass }) => ({
xs,
ys: mapNumbers(ys, (_, i) =>
// prettier-ignore
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass)
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass),
),
});
},
map_y: (fnStr: string) => {
const fn = myEval(
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`,
);
return ({ xs, ys, states, statistics, meta, vars, hass }) => ({
xs,
ys: ys.map((_, i) =>
// prettier-ignore
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass)
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass),
),
});
},
map_x: (fnStr: string) => {
const fn = myEval(
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`,
);
return ({ xs, ys, states, statistics, meta, vars, hass }) => ({
ys,
xs: xs.map((_, i) =>
// prettier-ignore
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass)
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass),
),
});
},
Expand Down Expand Up @@ -357,31 +383,31 @@ const filters = {
throw new Error(
`Trendline '${p.type}' doesn't exist. Did you mean <b>${propose(
p.type,
Object.keys(trendlineTypes)
)}<b>?\nOthers: ${Object.keys(trendlineTypes)}`
Object.keys(trendlineTypes),
)}<b>?\nOthers: ${Object.keys(trendlineTypes)}`,
);
}
const regression: BaseRegression = new RegressionClass(
xs_numbers,
ys,
p.degree
p.degree,
);
let extras: string[] = [];
if (p.show_r2)
extras.push(
`r²=${maxDecimals(regression.score(xs_numbers, ys).r2, 2)}`
`r²=${maxDecimals(regression.score(xs_numbers, ys).r2, 2)}`,
);

if (forecast > 0) {
const N = Math.round(
(xs_numbers.length /
(xs_numbers[xs_numbers.length - 1] - xs_numbers[0])) *
forecast
forecast,
);
xs_numbers.push(
...Array.from({ length: N }).map(
(_, i) => t1 - t0 + (forecast / N) * i
)
(_, i) => t1 - t0 + (forecast / N) * i,
),
);
}
const ys_out = regression.predict(xs_numbers);
Expand All @@ -405,12 +431,12 @@ const filters = {
*/
filter: (fnStr: string) => {
const fn = myEval(
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`
`(i, x, y, state, statistic, xs, ys, states, statistics, meta, vars, hass) => ${fnStr}`,
);
return ({ xs, ys, states, statistics, meta, vars, hass }) => {
const mask = ys.map((_, i) =>
// prettier-ignore
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass)
fn(i, xs[i], ys[i], states[i], statistics[i], xs, ys, states, statistics, meta, vars, hass),
);
return {
ys: ys.filter((_, i) => mask[i]),
Expand All @@ -425,7 +451,7 @@ export default filters;
function checkTimeUnits(unit: string) {
if (!timeUnits[unit]) {
throw new Error(
`Unit '${unit}' is not valid, use ${Object.keys(timeUnits)}`
`Unit '${unit}' is not valid, use ${Object.keys(timeUnits)}`,
);
}
}
Expand Down
19 changes: 12 additions & 7 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import {
ColorSchemeArray,
ColorSchemeNames,
} from "./parse-config/parse-color-scheme";
import { TimeDurationStr } from "./duration/duration";

import { RelativeTimeStr, TimeDurationStr } from "./duration/duration";
import {
AutoPeriodConfig,
StatisticPeriod,
Expand All @@ -11,20 +12,24 @@ import {
} from "./recorder-types";

import { HassEntity } from "home-assistant-js-websocket";
import { FilterFn } from "./filters/filters";
import { FilterFn, FilterInput } from "./filters/filters";
import type filters from "./filters/filters";
import internal from "stream";

export { HassEntity } from "home-assistant-js-websocket";

export type YValue = number | string | null;

export type InputConfig = {
type: "custom:plotly-graph";
hours_to_show?: number;
hours_to_show?: number | TimeDurationStr | RelativeTimeStr;
refresh_interval?: number | "auto"; // in seconds
color_scheme?: ColorSchemeNames | ColorSchemeArray | number;
title?: string;
offset?: TimeDurationStr;
entities: ({
entity?: string;
name?: string;
attribute?: string;
statistic?: StatisticType;
period?: StatisticPeriod | "auto" | AutoPeriodConfig;
Expand All @@ -37,7 +42,7 @@ export type InputConfig = {
};
offset?: TimeDurationStr;
extend_to_present?: boolean;
filters?: (Record<string, any> | string)[];
filters?: FilterInput[];
on_legend_click?: Function;
on_legend_dblclick?: Function;
on_click?: Function;
Expand Down Expand Up @@ -108,20 +113,20 @@ export type EntityIdConfig =
| EntityIdStatisticsConfig;

export function isEntityIdStateConfig(
entityConfig: EntityIdConfig
entityConfig: EntityIdConfig,
): entityConfig is EntityIdStateConfig {
return !(
isEntityIdAttrConfig(entityConfig) ||
isEntityIdStatisticsConfig(entityConfig)
);
}
export function isEntityIdAttrConfig(
entityConfig: EntityIdConfig
entityConfig: EntityIdConfig,
): entityConfig is EntityIdAttrConfig {
return !!entityConfig["attribute"];
}
export function isEntityIdStatisticsConfig(
entityConfig: EntityIdConfig
entityConfig: EntityIdConfig,
): entityConfig is EntityIdStatisticsConfig {
return !!entityConfig["statistic"];
}
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
},
"exclude": ["./yaml-editor/**"]
}
1 change: 1 addition & 0 deletions yaml-editor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/dist
36 changes: 36 additions & 0 deletions yaml-editor/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Demo

This demo is deployed to [monaco-yaml.js.org](https://monaco-yaml.js.org). It shows how
`monaco-editor` and `monaco-yaml` can be used with
[Webpack 5](https://webpack.js.org/concepts/entry-points).

## Table of Contents

- [Prerequisites](#prerequisites)
- [Setup](#setup)
- [Running](#running)

## Prerequisites

- [NodeJS](https://nodejs.org) 16 or higher
- [npm](https://github.com/npm/cli) 8.1.2 or higher

## Setup

To run the project locally, clone the repository and set it up:

```sh
git clone https://github.com/remcohaszing/monaco-yaml
cd monaco-yaml
npm ci
```

## Running

To start it, simply run:

```sh
npm --workspace demo start
```

The demo will open in your browser.
Loading

0 comments on commit 547a512

Please sign in to comment.