Compare commits

...

9 Commits

Author SHA1 Message Date
Paulus Schoutsen 854e56947f Update IFRAME_SANDBOX to remove 'allow-same-origin'
Removed 'allow-same-origin' from the IFRAME_SANDBOX settings.
2026-05-18 12:12:21 -04:00
Marcin Bauer 4728eb7231 Remove arrow icon from continue on error indicator (#52092)
The arrow-right icon next to the alert icon was decorative noise.
With automation comments (#52090) adding yet another icon, simplify
to a single mdiAlertCircleCheck indicator.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
2026-05-18 17:28:14 +03:00
renovate[bot] d02b92bd32 Update dependency @tsparticles/engine to v4 (#52091)
* Update dependency @tsparticles/engine to v4

* Bump @tsparticles/preset-links to v4 to match engine

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-05-18 14:15:54 +00:00
Wendelin 98525d23e6 Lovelace condition live test (#52027)
* Add lovelace condition live test

* Add live card status

* Add empty text
2026-05-18 15:01:56 +02:00
Petar Petrov ec98b21276 Highlight problematic devices in Energy Dashboard list (#52088) 2026-05-18 10:18:27 +01:00
Paulus Schoutsen defad3beca Treat media player unknown state like off instead of unavailable (#52080)
* Show both power buttons for assumed-state media players when unknown

Media players with assumed state report an unknown state when their
actual power state can't be determined. In that case the entity row and
more info should still expose both turn on and turn off controls so the
user can operate the device.

https://claude.ai/code/session_01JyZojNPCCY65HmRVQaASkG

* Treat media player unknown state like off instead of unavailable

The media player controls lumped the "unknown" state in with
"unavailable" and hid all controls. An unknown state is closer to "off":
the device exists but its power state isn't reported, which is common
for assumed-state players. Only "unavailable" should hide the controls,
so an unknown-state player now shows the turn on button (and both power
buttons when it has an assumed state) in the entity row and more info.

https://claude.ai/code/session_01JyZojNPCCY65HmRVQaASkG

* Adjust comments and variable placement for media player state check

https://claude.ai/code/session_01JyZojNPCCY65HmRVQaASkG

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-18 09:58:01 +03:00
Petar Petrov 635d61256b Fix Y-axis label precision in statistics and history charts (#52038) 2026-05-18 08:17:43 +02:00
renovate[bot] 60c5bea6e0 Update formatjs monorepo (#52085)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 07:03:08 +02:00
renovate[bot] aed83ccc07 Update dependency @codemirror/view to v6.43.0 (#52081)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-18 07:02:53 +02:00
30 changed files with 1031 additions and 468 deletions
+13 -13
View File
@@ -37,18 +37,18 @@
"@codemirror/lint": "6.9.6",
"@codemirror/search": "6.7.0",
"@codemirror/state": "6.6.0",
"@codemirror/view": "6.42.1",
"@codemirror/view": "6.43.0",
"@date-fns/tz": "1.4.1",
"@egjs/hammerjs": "2.0.17",
"@formatjs/intl-datetimeformat": "7.4.4",
"@formatjs/intl-displaynames": "7.3.6",
"@formatjs/intl-durationformat": "0.10.10",
"@formatjs/intl-getcanonicallocales": "3.2.7",
"@formatjs/intl-listformat": "8.3.6",
"@formatjs/intl-locale": "5.3.6",
"@formatjs/intl-numberformat": "9.3.7",
"@formatjs/intl-pluralrules": "6.3.6",
"@formatjs/intl-relativetimeformat": "12.3.6",
"@formatjs/intl-datetimeformat": "7.4.5",
"@formatjs/intl-displaynames": "7.3.7",
"@formatjs/intl-durationformat": "0.10.11",
"@formatjs/intl-getcanonicallocales": "3.2.8",
"@formatjs/intl-listformat": "8.3.7",
"@formatjs/intl-locale": "5.3.7",
"@formatjs/intl-numberformat": "9.3.8",
"@formatjs/intl-pluralrules": "6.3.7",
"@formatjs/intl-relativetimeformat": "12.3.7",
"@fullcalendar/core": "6.1.20",
"@fullcalendar/daygrid": "6.1.20",
"@fullcalendar/interaction": "6.1.20",
@@ -75,8 +75,8 @@
"@replit/codemirror-indentation-markers": "6.5.3",
"@swc/helpers": "0.5.21",
"@thomasloven/round-slider": "0.6.0",
"@tsparticles/engine": "3.9.1",
"@tsparticles/preset-links": "3.2.0",
"@tsparticles/engine": "4.0.0",
"@tsparticles/preset-links": "4.0.0",
"@vibrant/color": "4.0.4",
"@webcomponents/scoped-custom-element-registry": "0.0.10",
"@webcomponents/webcomponentsjs": "2.8.0",
@@ -99,7 +99,7 @@
"hls.js": "1.6.16",
"home-assistant-js-websocket": "9.6.0",
"idb-keyval": "6.2.2",
"intl-messageformat": "11.2.5",
"intl-messageformat": "11.2.6",
"js-yaml": "4.1.1",
"leaflet": "1.9.4",
"leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
@@ -0,0 +1,59 @@
import type {
ReactiveController,
ReactiveControllerHost,
} from "@lit/reactive-element/reactive-controller";
import type {
Condition,
ConditionContext,
} from "../../panels/lovelace/common/validate-condition";
import type { HomeAssistant } from "../../types";
import { setupConditionListeners } from "../condition/listeners";
/**
* Reactive controller that manages the media-query and time-based listeners
* needed to keep a set of lovelace visibility conditions evaluated live.
*
* The host is responsible for the actual evaluation (e.g. computing visible /
* hidden / invalid state); the controller only triggers it via the supplied
* `onUpdate` callback when something the conditions depend on changes. Call
* `setup()` whenever the conditions change; the controller clears previous
* listeners and re-subscribes. Listeners are automatically released when the
* host disconnects.
*/
export class ConditionListenersController implements ReactiveController {
private _unsubs: (() => void)[] = [];
constructor(host: ReactiveControllerHost) {
host.addController(this);
}
public hostDisconnected(): void {
this.clear();
}
public setup(
conditions: Condition[],
hass: HomeAssistant,
onUpdate: () => void,
getContext?: () => ConditionContext
): void {
this.clear();
if (!conditions.length) {
return;
}
setupConditionListeners(
conditions,
hass,
(unsub) => this._unsubs.push(unsub),
() => onUpdate(),
getContext
);
}
public clear(): void {
for (const unsub of this._unsubs) {
unsub();
}
this._unsubs = [];
}
}
@@ -0,0 +1,91 @@
import { LitElement, css, html, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import "../ha-tooltip";
export type LiveTestState = "pass" | "fail" | "invalid" | "unknown";
/**
* @element ha-automation-row-live-test
*
* @summary
* Small status indicator dot used in automation/condition rows to surface the
* live evaluation result. Renders an optional tooltip with details on hover.
*
* @attr {"pass"|"fail"|"invalid"|"unknown"} state - The current live-test state. Defaults to `unknown`.
* @attr {string} label - Accessible label announced by assistive technology.
* @attr {string} message - Optional tooltip body shown on hover/focus.
*/
@customElement("ha-automation-row-live-test")
export class HaAutomationRowLiveTest extends LitElement {
@property({ reflect: true }) public state: LiveTestState = "unknown";
@property() public label = "";
@property() public message?: string;
protected render() {
return html`
<div
id="indicator"
role="status"
tabindex="0"
aria-label=${this.label}
></div>
${this.message
? html`<ha-tooltip for="indicator">${this.message}</ha-tooltip>`
: nothing}
`;
}
static styles = css`
:host {
position: absolute;
inset-inline-end: -6px;
display: inline-block;
}
#indicator {
width: 12px;
height: 12px;
border-radius: var(--ha-border-radius-circle);
border: 3px solid;
box-sizing: border-box;
background-color: var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
:host([state="pass"]) #indicator {
background-color: var(--ha-color-fill-success-loud-resting);
border-color: var(--ha-color-fill-success-loud-resting);
}
:host([state="pass"]) #indicator:hover {
background-color: var(--ha-color-fill-success-loud-hover);
border-color: var(--ha-color-fill-success-loud-hover);
}
:host([state="fail"]) #indicator {
border-color: var(--ha-color-fill-warning-loud-resting);
}
:host([state="fail"]) #indicator:hover {
background-color: var(--ha-color-fill-warning-loud-hover);
border-color: var(--ha-color-fill-warning-loud-hover);
}
:host([state="invalid"]) #indicator {
border-color: var(--ha-color-fill-danger-loud-resting);
}
:host([state="invalid"]) #indicator:hover {
background-color: var(--ha-color-fill-danger-loud-hover);
border-color: var(--ha-color-fill-danger-loud-hover);
}
:host([state="unknown"]) #indicator {
border-color: var(--ha-color-fill-neutral-loud-resting);
}
:host([state="unknown"]) #indicator:hover {
background-color: var(--ha-color-fill-neutral-loud-hover);
border-color: var(--ha-color-fill-neutral-loud-hover);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-row-live-test": HaAutomationRowLiveTest;
}
}
@@ -12,6 +12,7 @@ import type { LineChartEntity, LineChartState } from "../../data/history";
import type { HomeAssistant } from "../../types";
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
import type { ECOption } from "../../resources/echarts/echarts";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import {
@@ -117,9 +118,7 @@ export class StateHistoryChartLine extends LitElement {
private _chartTime: Date = new Date();
private _previousYAxisLabelValue = 0;
private _yAxisMaximumFractionDigits = 0;
private _yAxisFractionDigits = 1;
protected render() {
return html`
@@ -436,6 +435,14 @@ export class StateHistoryChartLine extends LitElement {
const datasets: LineSeriesOption[] = [];
const entityIds: string[] = [];
const datasetToDataIndex: number[] = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number | null | undefined) => {
if (typeof v === "number" && Number.isFinite(v)) {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
}
};
if (entityStates.length === 0) {
return;
}
@@ -471,6 +478,7 @@ export class StateHistoryChartLine extends LitElement {
d.data!.push([timestamp, prevValues[i]]);
}
d.data!.push([timestamp, datavalues[i]]);
trackY(datavalues[i]);
});
prevValues = datavalues;
};
@@ -821,6 +829,7 @@ export class StateHistoryChartLine extends LitElement {
const currentValue = stateObj ? safeParseFloat(stateObj.state) : null;
if (currentValue !== null) {
data[0].data!.push([now, currentValue]);
trackY(currentValue);
}
}
@@ -828,6 +837,7 @@ export class StateHistoryChartLine extends LitElement {
Array.prototype.push.apply(datasets, data);
});
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._entityIds = entityIds;
this._datasetToDataIndex = datasetToDataIndex;
@@ -861,20 +871,8 @@ export class StateHistoryChartLine extends LitElement {
}
private _formatYAxisLabel = (value: number) => {
// show the first significant digit for tiny values
const maximumFractionDigits = Math.max(
1,
// use the difference to the previous value to determine the number of significant digits #25526
-Math.floor(
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
)
);
this._yAxisMaximumFractionDigits = Math.max(
this._yAxisMaximumFractionDigits,
maximumFractionDigits
);
const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits: this._yAxisMaximumFractionDigits,
maximumFractionDigits: this._yAxisFractionDigits,
});
const width = measureTextWidth(label, 12) + 5;
if (width > this._yWidth) {
@@ -884,7 +882,6 @@ export class StateHistoryChartLine extends LitElement {
chartIndex: this.chartIndex,
});
}
this._previousYAxisLabelValue = value;
return label;
};
+19 -15
View File
@@ -41,6 +41,7 @@ import type { CustomLegendOption } from "./ha-chart-base";
import "./ha-chart-base";
import { sideTooltipPosition } from "./chart-tooltip-position";
import { fillDataGapsAndRoundCaps } from "./round-caps";
import { computeYAxisFractionDigits } from "./y-axis-fraction-digits";
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
mean: "mean",
@@ -131,7 +132,7 @@ export class StatisticsChart extends LitElement {
private _computedStyle?: CSSStyleDeclaration;
private _previousYAxisLabelValue = 0;
private _yAxisFractionDigits = 1;
protected shouldUpdate(changedProps: PropertyValues<this>): boolean {
return changedProps.size > 1 || !changedProps.has("hass");
@@ -496,6 +497,14 @@ export class StatisticsChart extends LitElement {
const chartStacked = this.chartType.endsWith("stack");
const statisticsData = Object.entries(this.statisticsData);
const totalDataSets: typeof this._chartData = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number | null | undefined) => {
if (typeof v === "number" && Number.isFinite(v)) {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
}
};
const legendData: {
id: string;
name: string;
@@ -601,6 +610,9 @@ export class StatisticsChart extends LitElement {
d.data!.push([prevEndTime, null]);
}
d.data!.push([start, ...dataValues[i]!]);
// For band-top rows dataValues[i] is [diff, top]; the actual Y is
// the last element. For regular rows it's [value]. Same call works.
trackY(dataValues[i][dataValues[i].length - 1]);
} else {
let time = start;
if (centerBars) {
@@ -611,6 +623,7 @@ export class StatisticsChart extends LitElement {
// Data value should always be a scalar for bar charts. Pass in
// real start time as extra value to allow formatting tooltip.
d.data!.push([time, dataValues[i][0]!, start, end]);
trackY(dataValues[i][0]);
}
});
prevValues = dataValues;
@@ -823,6 +836,7 @@ export class StatisticsChart extends LitElement {
val.push(currentValue);
}
statDataSets[i].data!.push([now, ...val]);
trackY(val[val.length - 1]);
});
}
}
@@ -856,6 +870,7 @@ export class StatisticsChart extends LitElement {
});
});
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = totalDataSets;
if (legendData.length !== this._legendData?.length) {
// only update the legend if it has changed or it will trigger options update
@@ -889,21 +904,10 @@ export class StatisticsChart extends LitElement {
return Math.abs(value) < 1 ? value : roundingFn(value);
}
private _formatYAxisLabel = (value: number) => {
// show the first significant digit for tiny values
const maximumFractionDigits = Math.max(
1,
// use the difference to the previous value to determine the number of significant digits #25526
-Math.floor(
Math.log10(Math.abs(value - this._previousYAxisLabelValue || 1))
)
);
const label = formatNumber(value, this.hass.locale, {
maximumFractionDigits,
private _formatYAxisLabel = (value: number) =>
formatNumber(value, this.hass.locale, {
maximumFractionDigits: this._yAxisFractionDigits,
});
this._previousYAxisLabelValue = value;
return label;
};
static styles = css`
:host {
@@ -0,0 +1,9 @@
// Derive the number of decimal digits to use for Y-axis labels from the
// observed data range. We estimate the tick interval as `range / 10` (twice
// ECharts' default splitNumber of 5, as a safety margin against finer "nice"
// intervals), then derive `ceil(-log10(interval))`.
export function computeYAxisFractionDigits(min: number, max: number): number {
const range = max - min;
if (!Number.isFinite(range) || range <= 0) return 1;
return Math.max(0, Math.ceil(-Math.log10(range / 10)));
}
+3 -2
View File
@@ -38,7 +38,7 @@ import { stateActive } from "../common/entity/state_active";
import { supportsFeature } from "../common/entity/supports-feature";
import type { MediaPlayerItemId } from "../components/media-player/ha-media-player-browse";
import type { HomeAssistant, TranslationDict } from "../types";
import { isUnavailableState } from "./entity/entity";
import { UNAVAILABLE } from "./entity/entity";
import { isTTSMediaSource } from "./tts";
interface MediaPlayerEntityAttributes extends HassEntityAttributeBase {
@@ -284,7 +284,8 @@ export const computeMediaControls = (
const state = stateObj.state;
if (isUnavailableState(state)) {
// We only filter out `unavailable`, not `unknown`
if (state === UNAVAILABLE) {
return undefined;
}
@@ -37,7 +37,7 @@ import "../../../components/ha-svg-icon";
import "../../../components/ha-tooltip";
import { showJoinMediaPlayersDialog } from "../../../components/media-player/show-join-media-players-dialog";
import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type {
MediaPickedEvent,
MediaPlayerEntity,
@@ -275,7 +275,8 @@ class MoreInfoMediaPlayer extends LitElement {
protected _renderGrouping() {
if (
!this.stateObj ||
isUnavailableState(this.stateObj.state) ||
// Compare against `unavailable` so we allow `unknown`
this.stateObj.state === UNAVAILABLE ||
!supportsFeature(this.stateObj, MediaPlayerEntityFeature.GROUPING)
) {
return nothing;
@@ -315,7 +316,7 @@ class MoreInfoMediaPlayer extends LitElement {
return nothing;
}
if (isUnavailableState(this.stateObj.state)) {
if (this.stateObj.state === UNAVAILABLE) {
return this._renderEmptyCover(this.hass.formatEntityState(this.stateObj));
}
@@ -461,7 +462,7 @@ class MoreInfoMediaPlayer extends LitElement {
: nothing}
${this._renderVolumeControl()}
<div class="controls-row">
${!isUnavailableState(stateObj.state) &&
${stateObj.state !== UNAVAILABLE &&
supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)
? this._renderControlButton(
"browse_media",
@@ -4,7 +4,6 @@ import {
mdiAlertCircleCheck,
mdiAppleKeyboardCommand,
mdiArrowDown,
mdiArrowRightThin,
mdiArrowUp,
mdiCheckboxBlankOutline,
mdiCheckboxOutline,
@@ -333,10 +332,6 @@ export default class HaAutomationActionRow extends LitElement {
${type !== "condition" &&
(this.action as NonConditionAction).continue_on_error === true
? html`<ha-svg-icon
class="arrow-right"
.path=${mdiArrowRightThin}
></ha-svg-icon
><ha-svg-icon
id="svg-icon"
.path=${mdiAlertCircleCheck}
></ha-svg-icon>
@@ -1163,9 +1158,6 @@ export default class HaAutomationActionRow extends LitElement {
rowStyles,
overflowStyles,
css`
ha-svg-icon.arrow-right {
--icon-primary-color: var(--ha-color-fill-neutral-loud-resting);
}
ha-svg-icon#svg-icon {
--icon-primary-color: var(--ha-color-fill-neutral-loud-active);
}
@@ -23,7 +23,7 @@ import type {
} from "home-assistant-js-websocket";
import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
@@ -39,6 +39,7 @@ import { debounce } from "../../../../common/util/debounce";
import "../../../../components/automation/ha-automation-row";
import type { HaAutomationRow } from "../../../../components/automation/ha-automation-row";
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/automation/ha-automation-row-live-test";
import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-dropdown";
@@ -46,7 +47,6 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-tooltip";
import type {
AutomationClipboard,
Condition,
@@ -498,23 +498,15 @@ export default class HaAutomationConditionRow extends LitElement {
@click=${this._toggleSidebar}
@toggle-collapsed=${this._toggleCollapse}
>${this._renderRow()}
<div
<ha-automation-row-live-test
slot="icons"
id="live-test"
class=${this._liveTestResult.state}
role="status"
tabindex="0"
aria-label=${this.hass.localize(
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult.state}`
)}
>
${this._liveTestResult.message
? html`<ha-tooltip for="live-test">
${this._liveTestResult.message}
</ha-tooltip>`
: nothing}
</div></ha-automation-row
>`
.message=${this._liveTestResult.message}
></ha-automation-row-live-test
></ha-automation-row>`
: html`
<ha-expansion-panel
left-chevron
@@ -1064,52 +1056,7 @@ export default class HaAutomationConditionRow extends LitElement {
}
static get styles(): CSSResultGroup {
return [
rowStyles,
overflowStyles,
css`
#live-test {
position: absolute;
inset-inline-end: -6px;
width: 12px;
height: 12px;
border-radius: var(--ha-border-radius-circle);
border: 3px solid;
box-sizing: border-box;
background-color: var(--card-background-color);
transition: all var(--ha-animation-duration-normal) ease-in-out;
}
#live-test.pass {
background-color: var(--ha-color-fill-success-loud-resting);
border-color: var(--ha-color-fill-success-loud-resting);
}
#live-test.pass:hover {
background-color: var(--ha-color-fill-success-loud-hover);
border-color: var(--ha-color-fill-success-loud-hover);
}
#live-test.fail {
border-color: var(--ha-color-fill-warning-loud-resting);
}
#live-test.fail:hover {
background-color: var(--ha-color-fill-warning-loud-hover);
border-color: var(--ha-color-fill-warning-loud-hover);
}
#live-test.invalid {
border-color: var(--ha-color-fill-danger-loud-resting);
}
#live-test.invalid:hover {
background-color: var(--ha-color-fill-danger-loud-hover);
border-color: var(--ha-color-fill-danger-loud-hover);
}
#live-test.unknown {
border-color: var(--ha-color-fill-neutral-loud-resting);
}
#live-test.unknown:hover {
background-color: var(--ha-color-fill-neutral-loud-hover);
border-color: var(--ha-color-fill-neutral-loud-hover);
}
`,
];
return [rowStyles, overflowStyles];
}
}
@@ -1,4 +1,5 @@
import {
mdiAlertCircle,
mdiDelete,
mdiWater,
mdiDragHorizontalVariant,
@@ -6,7 +7,7 @@ import {
mdiPlus,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { repeat } from "lit/directives/repeat";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -15,10 +16,12 @@ import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type {
DeviceConsumptionEnergyPreference,
EnergyPreferences,
EnergyPreferencesValidation,
EnergyValidationIssue,
} from "../../../../data/energy";
import { saveEnergyPreferences } from "../../../../data/energy";
import type { StatisticsMetaData } from "../../../../data/recorder";
@@ -93,7 +96,7 @@ export class EnergyDeviceSettingsWater extends LitElement {
${repeat(
this.preferences.device_consumption_water,
(device) => device.stat_consumption,
(device) => html`
(device, index) => html`
<div class="row" .device=${device}>
<div class="handle">
<ha-svg-icon
@@ -108,6 +111,12 @@ export class EnergyDeviceSettingsWater extends LitElement {
this.statsMetadata?.[device.stat_consumption]
)}</span
>
${this._renderIssueIndicator(
this.validationResult?.device_consumption_water[
index
],
index
)}
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editDevice}
@@ -144,6 +153,31 @@ export class EnergyDeviceSettingsWater extends LitElement {
`;
}
private _renderIssueIndicator(
issues: EnergyValidationIssue[] | undefined,
index: number
) {
if (!issues?.length) {
return nothing;
}
const titles = issues.map(
(issue) =>
this.hass.localize(`component.energy.issues.${issue.type}.title`) ||
issue.type
);
const label = titles.join("\n");
const id = `issue-icon-${index}`;
return html`
<ha-svg-icon
id=${id}
class="issue-icon"
.path=${mdiAlertCircle}
aria-label=${label}
></ha-svg-icon>
<ha-tooltip .for=${id} placement="top">${label}</ha-tooltip>
`;
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
@@ -248,6 +282,9 @@ export class EnergyDeviceSettingsWater extends LitElement {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.issue-icon {
color: var(--warning-color);
}
`,
];
}
@@ -1,4 +1,5 @@
import {
mdiAlertCircle,
mdiDelete,
mdiDevices,
mdiDragHorizontalVariant,
@@ -6,7 +7,7 @@ import {
mdiPlus,
} from "@mdi/js";
import type { CSSResultGroup, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { repeat } from "lit/directives/repeat";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
@@ -15,10 +16,12 @@ import "../../../../components/ha-button";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-sortable";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type {
DeviceConsumptionEnergyPreference,
EnergyPreferences,
EnergyPreferencesValidation,
EnergyValidationIssue,
} from "../../../../data/energy";
import { saveEnergyPreferences } from "../../../../data/energy";
import type { StatisticsMetaData } from "../../../../data/recorder";
@@ -93,7 +96,7 @@ export class EnergyDeviceSettings extends LitElement {
${repeat(
this.preferences.device_consumption,
(device) => device.stat_consumption,
(device) => html`
(device, index) => html`
<div class="row" .device=${device}>
<div class="handle">
<ha-svg-icon
@@ -108,6 +111,10 @@ export class EnergyDeviceSettings extends LitElement {
this.statsMetadata?.[device.stat_consumption]
)}</span
>
${this._renderIssueIndicator(
this.validationResult?.device_consumption[index],
index
)}
<ha-icon-button
.label=${this.hass.localize("ui.common.edit")}
@click=${this._editDevice}
@@ -144,6 +151,31 @@ export class EnergyDeviceSettings extends LitElement {
`;
}
private _renderIssueIndicator(
issues: EnergyValidationIssue[] | undefined,
index: number
) {
if (!issues?.length) {
return nothing;
}
const titles = issues.map(
(issue) =>
this.hass.localize(`component.energy.issues.${issue.type}.title`) ||
issue.type
);
const label = titles.join("\n");
const id = `issue-icon-${index}`;
return html`
<ha-svg-icon
id=${id}
class="issue-icon"
.path=${mdiAlertCircle}
aria-label=${label}
></ha-svg-icon>
<ha-tooltip .for=${id} placement="top">${label}</ha-tooltip>
`;
}
private _itemMoved(ev: CustomEvent): void {
ev.stopPropagation();
const { oldIndex, newIndex } = ev.detail;
@@ -244,6 +276,9 @@ export class EnergyDeviceSettings extends LitElement {
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
}
.issue-icon {
color: var(--warning-color);
}
`,
];
}
@@ -91,17 +91,12 @@ export function getSuggestedMax(
return suggestedMax;
}
function createYAxisLabelFormatter(locale: FrontendLocaleData) {
let previousValue: number | undefined;
return (value: number): string => {
const maximumFractionDigits = Math.max(
1,
-Math.floor(Math.log10(Math.abs(value - (previousValue ?? value) || 1)))
);
previousValue = value;
return formatNumber(value, locale, { maximumFractionDigits });
};
function createYAxisLabelFormatter(
locale: FrontendLocaleData,
fractionDigits: number
) {
return (value: number): string =>
formatNumber(value, locale, { maximumFractionDigits: fractionDigits });
}
export function getCommonOptions(
@@ -113,7 +108,8 @@ export function getCommonOptions(
compareStart?: Date,
compareEnd?: Date,
formatTotal?: (total: number) => string,
detailedDailyData = false
detailedDailyData = false,
yAxisFractionDigits = 1
): ECOption {
const suggestedPeriod = getSuggestedPeriod(start, end, detailedDailyData);
const suggestedMax = getSuggestedMax(suggestedPeriod, end, detailedDailyData);
@@ -152,7 +148,7 @@ export function getCommonOptions(
align: "left",
},
axisLabel: {
formatter: createYAxisLabelFormatter(locale),
formatter: createYAxisLabelFormatter(locale, yAxisFractionDigits),
},
splitLine: {
show: true,
@@ -10,6 +10,7 @@ import { getGraphColorByIndex } from "../../../../common/color/colors";
import { getEnergyColor } from "./common/color";
import "../../../../components/ha-card";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import type {
DeviceConsumptionEnergyPreference,
EnergyData,
@@ -75,6 +76,8 @@ export class HuiEnergyDevicesDetailGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _data?: EnergyData;
@state() private _legendData?: CustomLegendOption["data"];
@@ -157,7 +160,8 @@ export class HuiEnergyDevicesDetailGraphCard
this.hass.config,
UNIT,
this._compareStart,
this._compareEnd
this._compareEnd,
this._yAxisFractionDigits
)}
click-label-for-more-info
@dataset-hidden=${this._datasetHidden}
@@ -208,9 +212,10 @@ export class HuiEnergyDevicesDetailGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit?: string,
compareStart?: Date,
compareEnd?: Date
unit: string | undefined,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
): ECOption => {
const commonOptions = getCommonOptions(
start,
@@ -220,7 +225,9 @@ export class HuiEnergyDevicesDetailGraphCard
unit,
compareStart,
compareEnd,
this._formatTotal
this._formatTotal,
false,
yAxisFractionDigits
);
const selected = this._legendData
@@ -309,6 +316,13 @@ export class HuiEnergyDevicesDetailGraphCard
const datasets: BarSeriesOption[] = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
const { summedData, compareSummedData } = getSummedData(energyData);
const showUntracked =
@@ -331,6 +345,7 @@ export class HuiEnergyDevicesDetailGraphCard
energyData.prefs.device_consumption,
sorted_devices,
childMap,
trackY,
true
);
@@ -341,6 +356,7 @@ export class HuiEnergyDevicesDetailGraphCard
computedStyle,
processedCompareData,
consumptionCompareData,
trackY,
true
);
datasets.push(untrackedCompareData);
@@ -362,7 +378,8 @@ export class HuiEnergyDevicesDetailGraphCard
energyData.statsMetadata,
energyData.prefs.device_consumption,
sorted_devices,
childMap
childMap,
trackY
);
datasets.push(...processedData);
@@ -385,6 +402,7 @@ export class HuiEnergyDevicesDetailGraphCard
computedStyle,
processedData,
consumptionData,
trackY,
false
);
datasets.push(untrackedData);
@@ -401,6 +419,7 @@ export class HuiEnergyDevicesDetailGraphCard
}
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
}
@@ -408,6 +427,7 @@ export class HuiEnergyDevicesDetailGraphCard
computedStyle: CSSStyleDeclaration,
processedData,
consumptionData,
trackY: (v: number) => void,
compare: boolean
): BarSeriesOption {
const totalDeviceConsumption: Record<number, number> = {};
@@ -443,6 +463,7 @@ export class HuiEnergyDevicesDetailGraphCard
dataPoint[0] = compareTransform(new Date(ts)).getTime() + periodOffset;
}
untrackedConsumption.push(dataPoint);
trackY(value);
});
// random id to always add untracked at the end
const order = Date.now();
@@ -483,6 +504,7 @@ export class HuiEnergyDevicesDetailGraphCard
devices: DeviceConsumptionEnergyPreference[],
sorted_devices: string[],
childMap: Record<string, string[]>,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -530,6 +552,7 @@ export class HuiEnergyDevicesDetailGraphCard
cStats?.find((cStat) => cStat.start === point.start)?.change || 0;
});
const y = point.change - sumChildren;
const dataPoint: EnergyDataPoint = [
computeStatMidpoint(
point.start,
@@ -537,10 +560,11 @@ export class HuiEnergyDevicesDetailGraphCard
period,
compare ? compareTransform : undefined
),
point.change - sumChildren,
y,
point.start,
];
consumptionData.push(dataPoint);
trackY(y);
prevStart = point.start;
}
}
@@ -9,6 +9,7 @@ import type { BarSeriesOption } from "echarts/charts";
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type {
EnergyData,
@@ -64,6 +65,8 @@ export class HuiEnergyGasGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -139,7 +142,8 @@ export class HuiEnergyGasGraphCard
this.hass.config,
this._unit,
this._compareStart,
this._compareEnd
this._compareEnd,
this._yAxisFractionDigits
)}
chart-type="bar"
></ha-chart-base>
@@ -169,9 +173,10 @@ export class HuiEnergyGasGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit?: string,
compareStart?: Date,
compareEnd?: Date
unit: string | undefined,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
): ECOption =>
getCommonOptions(
start,
@@ -181,7 +186,9 @@ export class HuiEnergyGasGraphCard
unit,
compareStart,
compareEnd,
this._formatTotal
this._formatTotal,
false,
yAxisFractionDigits
)
);
@@ -203,6 +210,13 @@ export class HuiEnergyGasGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
if (energyData.statsCompare) {
datasets.push(
...this._processDataSet(
@@ -210,6 +224,7 @@ export class HuiEnergyGasGraphCard
energyData.statsMetadata,
gasSources,
computedStyles,
trackY,
true
)
);
@@ -230,11 +245,13 @@ export class HuiEnergyGasGraphCard
energyData.stats,
energyData.statsMetadata,
gasSources,
computedStyles
computedStyles,
trackY
)
);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, gasSources);
}
@@ -261,6 +278,7 @@ export class HuiEnergyGasGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
gasSources: GasSourceTypeEnergyPreference[],
computedStyles: CSSStyleDeclaration,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -300,6 +318,7 @@ export class HuiEnergyGasGraphCard
point.start,
];
gasConsumptionData.push(dataPoint);
trackY(point.change);
prevStart = point.start;
}
}
@@ -9,6 +9,7 @@ import type { BarSeriesOption, LineSeriesOption } from "echarts/charts";
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type {
EnergyData,
@@ -66,6 +67,8 @@ export class HuiEnergySolarGraphCard
@state() private _chartData: ECOption["series"][] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -138,7 +141,8 @@ export class HuiEnergySolarGraphCard
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd
this._compareEnd,
this._yAxisFractionDigits
)}
chart-type="bar"
></ha-chart-base>
@@ -168,8 +172,9 @@ export class HuiEnergySolarGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart?: Date,
compareEnd?: Date
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
): ECOption =>
getCommonOptions(
start,
@@ -179,7 +184,9 @@ export class HuiEnergySolarGraphCard
"kWh",
compareStart,
compareEnd,
this._formatTotal
this._formatTotal,
false,
yAxisFractionDigits
)
);
@@ -210,6 +217,13 @@ export class HuiEnergySolarGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
if (energyData.statsCompare) {
datasets.push(
...this._processDataSet(
@@ -217,6 +231,7 @@ export class HuiEnergySolarGraphCard
energyData.statsMetadata,
solarSources,
computedStyles,
trackY,
true
)
);
@@ -237,7 +252,8 @@ export class HuiEnergySolarGraphCard
energyData.stats,
energyData.statsMetadata,
solarSources,
computedStyles
computedStyles,
trackY
)
);
@@ -251,11 +267,13 @@ export class HuiEnergySolarGraphCard
solarSources,
computedStyles.getPropertyValue("--primary-text-color"),
energyData.start,
energyData.end
energyData.end,
trackY
)
);
}
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, solarSources);
}
@@ -282,6 +300,7 @@ export class HuiEnergySolarGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
solarSources: SolarSourceTypeEnergyPreference[],
computedStyles: CSSStyleDeclaration,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -322,6 +341,7 @@ export class HuiEnergySolarGraphCard
point.start,
];
solarProductionData.push(dataPoint);
trackY(point.change);
prevStart = point.start;
}
}
@@ -375,7 +395,8 @@ export class HuiEnergySolarGraphCard
solarSources: SolarSourceTypeEnergyPreference[],
borderColor: string,
start: Date,
end?: Date
end: Date | undefined,
trackY: (v: number) => void
) {
const data: LineSeriesOption[] = [];
@@ -429,10 +450,9 @@ export class HuiEnergySolarGraphCard
: 0;
}
for (const [time, value] of Object.entries(forecastsData)) {
solarForecastData.push([
Number(time) + forecastOffset,
value / 1000,
]);
const kWh = value / 1000;
solarForecastData.push([Number(time) + forecastOffset, kWh]);
trackY(kWh);
}
if (solarForecastData.length) {
@@ -13,6 +13,7 @@ import type {
import { getEnergyColor } from "./common/color";
import { formatNumber } from "../../../../common/number/format_number";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import "./common/hui-energy-graph-chip";
import type {
@@ -79,6 +80,8 @@ export class HuiEnergyUsageGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -154,7 +157,8 @@ export class HuiEnergyUsageGraphCard
this.hass.locale,
this.hass.config,
this._compareStart,
this._compareEnd
this._compareEnd,
this._yAxisFractionDigits
)}
chart-type="bar"
></ha-chart-base>
@@ -189,8 +193,9 @@ export class HuiEnergyUsageGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart?: Date,
compareEnd?: Date
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
): ECOption => {
const commonOptions = getCommonOptions(
start,
@@ -200,7 +205,9 @@ export class HuiEnergyUsageGraphCard
"kWh",
compareStart,
compareEnd,
this._formatTotal
this._formatTotal,
false,
yAxisFractionDigits
);
const options: ECOption = {
...commonOptions,
@@ -237,6 +244,13 @@ export class HuiEnergyUsageGraphCard
private async _getStatistics(energyData: EnergyData): Promise<void> {
const datasets: BarSeriesOption[] = [];
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
const statIds: {
to_grid?: string[];
from_grid?: string[];
@@ -341,6 +355,7 @@ export class HuiEnergyUsageGraphCard
colorIndices,
computedStyles,
labels,
trackY,
true
)
);
@@ -367,6 +382,7 @@ export class HuiEnergyUsageGraphCard
colorIndices,
computedStyles,
labels,
trackY,
false
)
);
@@ -374,6 +390,7 @@ export class HuiEnergyUsageGraphCard
// @ts-expect-error
datasets.sort((a, b) => a.order - b.order);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(consumption);
}
@@ -403,6 +420,7 @@ export class HuiEnergyUsageGraphCard
used_solar: string;
used_battery: string;
},
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -504,18 +522,17 @@ export class HuiEnergyUsageGraphCard
// Process chart data.
for (const key of uniqueKeys) {
const value = source[key] || 0;
const dataPoint: EnergyDataPoint = [
key + periodOffset,
const y =
value && ["to_grid", "to_battery"].includes(type)
? -1 * value
: value,
key,
];
: value;
const dataPoint: EnergyDataPoint = [key + periodOffset, y, key];
if (compare) {
dataPoint[0] =
compareTransform(new Date(key)).getTime() + periodOffset;
}
points.push(dataPoint);
trackY(y);
}
data.push({
@@ -8,6 +8,7 @@ import memoizeOne from "memoize-one";
import type { BarSeriesOption } from "echarts/charts";
import { getEnergyColor } from "./common/color";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type {
EnergyData,
@@ -64,6 +65,8 @@ export class HuiEnergyWaterGraphCard
@state() private _chartData: BarSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _start = startOfToday();
@state() private _end = endOfToday();
@@ -139,7 +142,8 @@ export class HuiEnergyWaterGraphCard
this.hass.config,
this._unit,
this._compareStart,
this._compareEnd
this._compareEnd,
this._yAxisFractionDigits
)}
chart-type="bar"
></ha-chart-base>
@@ -169,9 +173,10 @@ export class HuiEnergyWaterGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
unit?: string,
compareStart?: Date,
compareEnd?: Date
unit: string | undefined,
compareStart: Date | undefined,
compareEnd: Date | undefined,
yAxisFractionDigits: number
): ECOption =>
getCommonOptions(
start,
@@ -181,7 +186,9 @@ export class HuiEnergyWaterGraphCard
unit,
compareStart,
compareEnd,
this._formatTotal
this._formatTotal,
false,
yAxisFractionDigits
)
);
@@ -203,6 +210,13 @@ export class HuiEnergyWaterGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
if (energyData.statsCompare) {
datasets.push(
...this._processDataSet(
@@ -210,6 +224,7 @@ export class HuiEnergyWaterGraphCard
energyData.statsMetadata,
waterSources,
computedStyles,
trackY,
true
)
);
@@ -230,11 +245,13 @@ export class HuiEnergyWaterGraphCard
energyData.stats,
energyData.statsMetadata,
waterSources,
computedStyles
computedStyles,
trackY
)
);
fillDataGapsAndRoundCaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
this._chartData = datasets;
this._total = this._processTotal(energyData.stats, waterSources);
}
@@ -261,6 +278,7 @@ export class HuiEnergyWaterGraphCard
statisticsMetaData: Record<string, StatisticsMetaData>,
waterSources: WaterSourceTypeEnergyPreference[],
computedStyles: CSSStyleDeclaration,
trackY: (v: number) => void,
compare = false
) {
const data: BarSeriesOption[] = [];
@@ -300,6 +318,7 @@ export class HuiEnergyWaterGraphCard
point.start,
];
waterConsumptionData.push(dataPoint);
trackY(point.change);
prevStart = point.start;
}
}
@@ -8,6 +8,7 @@ import memoizeOne from "memoize-one";
import type { LineSeriesOption } from "echarts/charts";
import { LinearGradient } from "../../../../resources/echarts/echarts";
import "../../../../components/chart/ha-chart-base";
import { computeYAxisFractionDigits } from "../../../../components/chart/y-axis-fraction-digits";
import "../../../../components/ha-card";
import type { EnergyData } from "../../../../data/energy";
import {
@@ -53,6 +54,8 @@ export class HuiPowerSourcesGraphCard
@state() private _chartData: LineSeriesOption[] = [];
@state() private _yAxisFractionDigits = 1;
@state() private _legendData?: CustomLegendOption["data"];
@state() private _start = startOfToday();
@@ -117,7 +120,8 @@ export class HuiPowerSourcesGraphCard
this.hass.config,
this._compareStart,
this._compareEnd,
this._legendData
this._legendData,
this._yAxisFractionDigits
)}
></ha-chart-base>
${!this._chartData.some((dataset) => dataset.data!.length)
@@ -140,9 +144,10 @@ export class HuiPowerSourcesGraphCard
end: Date,
locale: FrontendLocaleData,
config: HassConfig,
compareStart?: Date,
compareEnd?: Date,
legendData?: CustomLegendOption["data"]
compareStart: Date | undefined,
compareEnd: Date | undefined,
legendData: CustomLegendOption["data"] | undefined,
yAxisFractionDigits: number
): ECOption => ({
...getCommonOptions(
start,
@@ -153,7 +158,8 @@ export class HuiPowerSourcesGraphCard
compareStart,
compareEnd,
undefined,
true
true,
yAxisFractionDigits
),
legend: {
show: this._config?.show_legend !== false,
@@ -193,6 +199,13 @@ export class HuiPowerSourcesGraphCard
const computedStyles = getComputedStyle(this);
let yMin = Infinity;
let yMax = -Infinity;
const trackY = (v: number) => {
if (v < yMin) yMin = v;
if (v > yMax) yMax = v;
};
for (const source of energyData.prefs.energy_sources) {
if (source.type === "solar") {
if (source.stat_rate) {
@@ -245,7 +258,8 @@ export class HuiPowerSourcesGraphCard
}
}
return stats;
})
}),
trackY
);
datasets.push({
...commonSeriesOptions,
@@ -307,6 +321,7 @@ export class HuiPowerSourcesGraphCard
this._end = energyData.end || endOfToday();
this._chartData = fillLineGaps(datasets);
this._yAxisFractionDigits = computeYAxisFractionDigits(yMin, yMax);
const usageData: NonNullable<LineSeriesOption["data"]> = [];
this._chartData[0]?.data!.forEach((item, i) => {
@@ -353,7 +368,7 @@ export class HuiPowerSourcesGraphCard
});
}
private _processData(stats: StatisticValue[][]) {
private _processData(stats: StatisticValue[][], trackY: (v: number) => void) {
const data: Record<number, number[]> = {};
stats.forEach((statSet) => {
statSet.forEach((point) => {
@@ -369,8 +384,12 @@ export class HuiPowerSourcesGraphCard
Object.entries(data).forEach(([x, y]) => {
const ts = Number(x);
const sumY = y.reduce((a, b) => a + b, 0);
positive.push([ts, Math.max(0, sumY)]);
negative.push([ts, Math.min(0, sumY)]);
const pos = Math.max(0, sumY);
const neg = Math.min(0, sumY);
positive.push([ts, pos]);
negative.push([ts, neg]);
trackY(pos);
trackY(neg);
});
return { positive, negative };
}
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../../types";
import type { Condition } from "../../common/validate-condition";
import { conditionsEntityContext } from "../conditions/context";
import "../conditions/ha-card-conditions-editor";
import "../conditions/ha-visibility-status";
@customElement("hui-badge-visibility-editor")
export class HuiBadgeVisibilityEditor extends LitElement {
@@ -34,11 +34,10 @@ export class HuiBadgeVisibilityEditor extends LitElement {
render() {
const conditions = this.config.visibility ?? [];
return html`
<p class="intro">
${this.hass.localize(
`ui.panel.lovelace.editor.edit_badge.visibility.explanation`
)}
</p>
<ha-visibility-status
.hass=${this.hass}
.conditions=${conditions}
></ha-visibility-status>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${conditions}
@@ -62,10 +61,8 @@ export class HuiBadgeVisibilityEditor extends LitElement {
}
static styles = css`
.intro {
margin: 0;
color: var(--secondary-text-color);
margin-bottom: 8px;
ha-visibility-status {
margin-bottom: var(--ha-space-3);
}
`;
}
@@ -3,12 +3,12 @@ import type { PropertyValues } from "lit";
import { LitElement, css, html } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import type { LovelaceCardConfig } from "../../../../data/lovelace/config/card";
import type { HomeAssistant } from "../../../../types";
import type { Condition } from "../../common/validate-condition";
import { conditionsEntityContext } from "../conditions/context";
import "../conditions/ha-card-conditions-editor";
import "../conditions/ha-visibility-status";
@customElement("hui-card-visibility-editor")
export class HuiCardVisibilityEditor extends LitElement {
@@ -34,11 +34,10 @@ export class HuiCardVisibilityEditor extends LitElement {
render() {
const conditions = this.config.visibility ?? [];
return html`
<p class="intro">
${this.hass.localize(
`ui.panel.lovelace.editor.edit_card.visibility.explanation`
)}
</p>
<ha-visibility-status
.hass=${this.hass}
.conditions=${conditions}
></ha-visibility-status>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${conditions}
@@ -62,10 +61,8 @@ export class HuiCardVisibilityEditor extends LitElement {
}
static styles = css`
.intro {
margin: 0;
color: var(--secondary-text-color);
margin-bottom: 8px;
ha-visibility-status {
margin-bottom: var(--ha-space-3);
}
`;
}
@@ -13,12 +13,14 @@ import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/automation/ha-automation-row-live-test";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-dropdown";
@@ -33,11 +35,11 @@ import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import { ICON_CONDITION } from "../../common/icon-condition";
import type {
AndCondition,
Condition,
LegacyCondition,
OrCondition,
AndCondition,
NotCondition,
OrCondition,
} from "../../common/validate-condition";
import {
checkConditionsMet,
@@ -103,6 +105,13 @@ export class HaCardConditionEditor extends LitElement {
@state() private _testingResult?: boolean;
@state() private _liveTestResult: {
state: "pass" | "fail" | "invalid" | "unknown";
message?: string;
} = { state: "unknown" };
private _listeners = new ConditionListenersController(this);
private get _editor() {
if (!this._condition) return undefined;
return customElements.get(
@@ -116,6 +125,14 @@ export class HaCardConditionEditor extends LitElement {
});
}
private _setupConditionListeners() {
this._listeners.setup(
this.condition ? [this.condition as Condition] : [],
this.hass,
() => this._evaluateLiveTest()
);
}
protected willUpdate(changedProperties: PropertyValues<this>): void {
if (changedProperties.has("condition")) {
this._condition = {
@@ -143,7 +160,61 @@ export class HaCardConditionEditor extends LitElement {
if (!this._uiAvailable && !this._yamlMode) {
this._yamlMode = true;
}
this._setupConditionListeners();
}
if (changedProperties.has("condition") || changedProperties.has("hass")) {
this._evaluateLiveTest();
}
}
protected updated(changedProperties: PropertyValues<this>): void {
if ((changedProperties as Map<string, unknown>).has("_entityContext")) {
this._evaluateLiveTest();
}
}
private _evaluateLiveTest() {
if (!this.condition || !this._condition) {
this._liveTestResult = { state: "unknown" };
return;
}
if (
isNoEntityCondition(this._condition.condition, this._noEntity) ||
containsNoEntityCondition(this._condition, this._noEntity)
) {
this._liveTestResult = {
state: "unknown",
message: this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.live_test_state.unknown"
),
};
return;
}
if (!validateConditionalConfig([this.condition])) {
this._liveTestResult = {
state: "invalid",
message: this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.live_test_state.invalid"
),
};
return;
}
const testContext =
this._entityContext?.mode === "current"
? { entity_id: this._entityContext.entityId }
: {};
const pass = checkConditionsMet([this.condition], this.hass, testContext);
this._liveTestResult = {
state: pass ? "pass" : "fail",
message: this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${pass ? "pass" : "fail"}`
),
};
}
protected render() {
@@ -151,6 +222,10 @@ export class HaCardConditionEditor extends LitElement {
if (!condition) return nothing;
const hideLiveTest =
isNoEntityCondition(condition.condition, this._noEntity) ||
containsNoEntityCondition(condition, this._noEntity);
return html`
<div class="container">
<ha-expansion-panel left-chevron>
@@ -164,6 +239,33 @@ export class HaCardConditionEditor extends LitElement {
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
) || condition.condition}
</h3>
<ha-automation-row-event-chip
.show=${this._testingResult !== undefined}
.variant=${this._testingResult ? "success" : "warning"}
slot="event"
class="event-chip"
aria-live="polite"
>
${this._testingResult
? this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_pass"
)
: this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_error"
)}
</ha-automation-row-event-chip>
${hideLiveTest
? nothing
: html`
<ha-automation-row-live-test
slot="icons"
.state=${this._liveTestResult.state}
.label=${this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.live_test_state.${this._liveTestResult.state}`
)}
.message=${this._liveTestResult.message}
></ha-automation-row-live-test>
`}
<ha-dropdown
slot="icons"
@wa-select=${this._handleAction}
@@ -267,23 +369,6 @@ export class HaCardConditionEditor extends LitElement {
`}
</div>
</ha-expansion-panel>
<div
class="testing ${classMap({
active: this._testingResult !== undefined,
pass: this._testingResult === true,
error: this._testingResult === false,
})}"
>
${this._testingResult
? this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_pass"
)
: this._testingResult === false
? this.hass.localize(
"ui.panel.lovelace.editor.condition-editor.testing_error"
)
: nothing}
</div>
</div>
`;
}
@@ -418,41 +503,9 @@ export class HaCardConditionEditor extends LitElement {
opacity: 0.5;
pointer-events: none;
}
.testing {
.event-chip {
position: absolute;
top: 0px;
right: 0px;
left: 0px;
text-transform: uppercase;
font-size: var(--ha-font-size-m);
font-weight: var(--ha-font-weight-bold);
background-color: var(--divider-color, #e0e0e0);
color: var(--text-primary-color);
max-height: 0px;
overflow: hidden;
transition: max-height 0.3s;
text-align: center;
border-top-right-radius: calc(
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - var(
--ha-card-border-width,
1px
)
);
border-top-left-radius: calc(
var(--ha-card-border-radius, var(--ha-border-radius-lg)) - var(
--ha-card-border-width,
1px
)
);
}
.testing.active {
max-height: 100px;
}
.testing.error {
background-color: var(--accent-color);
}
.testing.pass {
background-color: var(--success-color);
inset-inline-end: 40px;
}
.container {
position: relative;
@@ -0,0 +1,160 @@
import { consume } from "@lit/context";
import { mdiAlertCircle, mdiEye, mdiEyeOff } from "@mdi/js";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
import "../../../../components/ha-svg-icon";
import { HaRowItem } from "../../../../components/item/ha-row-item";
import type { HomeAssistant } from "../../../../types";
import type {
Condition,
LegacyCondition,
} from "../../common/validate-condition";
import {
checkConditionsMet,
validateConditionalConfig,
} from "../../common/validate-condition";
import type { ConditionsEntityContext } from "./context";
import { conditionsEntityContext } from "./context";
type VisibilityState = "visible" | "hidden" | "invalid";
const STATE_ICONS: Record<VisibilityState, string> = {
visible: mdiEye,
hidden: mdiEyeOff,
invalid: mdiAlertCircle,
};
/**
* @element ha-visibility-status
* @extends {HaRowItem}
*
* @summary
* Row-style banner that surfaces the live visibility result for a set of
* lovelace conditions. Replaces the static explanation alert at the top of
* card / section / badge / conditional-card visibility editors.
*
* @attr {"visible"|"hidden"|"invalid"} state - Computed visibility state (reflected for styling).
*/
@customElement("ha-visibility-status")
export class HaVisibilityStatus extends HaRowItem {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public conditions: (Condition | LegacyCondition)[] = [];
@state()
@consume({ context: conditionsEntityContext, subscribe: true })
private _entityContext?: ConditionsEntityContext;
@property({ reflect: true })
public state: VisibilityState = "visible";
private _listeners = new ConditionListenersController(this);
protected willUpdate(changedProperties: PropertyValues<this>): void {
super.willUpdate(changedProperties);
if (changedProperties.has("conditions") || changedProperties.has("hass")) {
this._listeners.setup(
(this.conditions ?? []) as Condition[],
this.hass,
() => this._evaluate()
);
}
if (
changedProperties.has("hass") ||
changedProperties.has("conditions") ||
(changedProperties as Map<string, unknown>).has("_entityContext")
) {
this._evaluate();
}
}
protected override _renderInner(): TemplateResult {
return html`
<div part="start" class="start">
<ha-svg-icon .path=${STATE_ICONS[this.state]}></ha-svg-icon>
</div>
<div part="content" class="content">
<div part="headline" class="headline">
${this.hass?.localize(
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.headline`
)}
</div>
<div part="supporting-text" class="supporting">
${this.hass?.localize(
`ui.panel.lovelace.editor.condition-editor.visibility_status.${this.state}.supporting${(this.conditions?.length ?? 0) === 0 ? "_empty" : ""}`
)}
</div>
</div>
`;
}
private _evaluate() {
const conditions = this.conditions ?? [];
let newState: VisibilityState;
if (conditions.length === 0) {
newState = "visible";
} else if (!validateConditionalConfig(conditions)) {
newState = "invalid";
} else {
const context =
this._entityContext?.mode === "current"
? { entity_id: this._entityContext.entityId }
: {};
newState = checkConditionsMet(conditions, this.hass, context)
? "visible"
: "hidden";
}
if (newState === this.state) {
return;
}
this.state = newState;
}
static styles: CSSResultGroup = [
HaRowItem.styles,
css`
:host {
display: block;
border-radius: var(--ha-border-radius-xl);
transition: background-color var(--ha-animation-duration-normal)
ease-in-out;
}
.base {
padding: var(--ha-space-4);
}
:host([state="visible"]) {
background-color: var(--ha-color-fill-success-quiet-resting);
--visibility-status-color: var(--ha-color-on-success-normal);
}
:host([state="hidden"]) {
background-color: var(--ha-color-fill-warning-quiet-resting);
--visibility-status-color: var(--ha-color-on-warning-normal);
}
:host([state="invalid"]) {
background-color: var(--ha-color-fill-danger-quiet-resting);
--visibility-status-color: var(--ha-color-on-danger-normal);
}
.start {
align-self: start;
}
.start ha-svg-icon {
color: var(--visibility-status-color);
--mdc-icon-size: 24px;
}
.headline {
font-weight: var(--ha-font-weight-medium);
white-space: normal;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"ha-visibility-status": HaVisibilityStatus;
}
}
@@ -7,7 +7,6 @@ import { any, array, assert, assign, object, optional } from "superstruct";
import { storage } from "../../../../common/decorators/storage";
import type { HASSDomEvent } from "../../../../common/dom/fire_event";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tab-group";
@@ -21,6 +20,7 @@ import "../card-editor/hui-card-element-editor";
import type { HuiCardElementEditor } from "../card-editor/hui-card-element-editor";
import "../card-editor/hui-card-picker";
import "../conditions/ha-card-conditions-editor";
import "../conditions/ha-visibility-status";
import "../hui-element-editor";
import type { ConfigChangedEvent } from "../hui-element-editor";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
@@ -147,11 +147,10 @@ export class HuiConditionalCardEditor
</div>
`
: html`
<ha-alert alert-type="info">
${this.hass!.localize(
"ui.panel.lovelace.editor.condition-editor.explanation"
)}
</ha-alert>
<ha-visibility-status
.hass=${this.hass}
.conditions=${this._config.conditions ?? []}
></ha-visibility-status>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${this._config.conditions}
@@ -246,9 +245,9 @@ export class HuiConditionalCardEditor
width: 100%;
justify-content: center;
}
ha-alert {
display: block;
margin-top: 12px;
ha-visibility-status {
margin-top: var(--ha-space-3);
margin-bottom: var(--ha-space-3);
}
.card {
margin-top: 8px;
@@ -1,11 +1,11 @@
import { LitElement, html } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-alert";
import type { LovelaceSectionRawConfig } from "../../../../data/lovelace/config/section";
import type { HomeAssistant } from "../../../../types";
import type { Condition } from "../../common/validate-condition";
import "../conditions/ha-card-conditions-editor";
import "../conditions/ha-visibility-status";
@customElement("hui-section-visibility-editor")
export class HuiDialogEditSection extends LitElement {
@@ -16,11 +16,10 @@ export class HuiDialogEditSection extends LitElement {
render() {
const conditions = this.config.visibility ?? [];
return html`
<ha-alert alert-type="info">
${this.hass.localize(
`ui.panel.lovelace.editor.edit_section.visibility.explanation`
)}
</ha-alert>
<ha-visibility-status
.hass=${this.hass}
.conditions=${conditions}
></ha-visibility-status>
<ha-card-conditions-editor
.hass=${this.hass}
.conditions=${conditions}
@@ -30,6 +29,12 @@ export class HuiDialogEditSection extends LitElement {
`;
}
static styles = css`
ha-visibility-status {
margin-bottom: var(--ha-space-3);
}
`;
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
const conditions = ev.detail.value as Condition[];
@@ -22,7 +22,7 @@ import { supportsFeature } from "../../../common/entity/supports-feature";
import { debounce } from "../../../common/util/debounce";
import "../../../components/ha-icon-button";
import "../../../components/ha-slider";
import { isUnavailableState } from "../../../data/entity/entity";
import { UNAVAILABLE } from "../../../data/entity/entity";
import type {
ControlButton,
MediaPlayerEntity,
@@ -195,7 +195,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
<div class="controls">
${supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON) &&
(!stateActive(stateObj) || assumedState) &&
!isUnavailableState(entityState)
entityState !== UNAVAILABLE
? html`
<ha-icon-button
.path=${assumedState ? mdiPowerOn : mdiPower}
@@ -209,7 +209,7 @@ class HuiMediaPlayerEntityRow extends LitElement implements LovelaceRow {
(stateActive(stateObj) ||
assumedState ||
!supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_ON) ||
isUnavailableState(entityState))
entityState === UNAVAILABLE)
? buttons
: ""}
${supportsFeature(stateObj, MediaPlayerEntityFeature.TURN_OFF) &&
+22 -11
View File
@@ -8929,9 +8929,6 @@
"tab_visibility": "Visibility",
"tab_layout": "Layout",
"paste_condition": "Paste condition",
"visibility": {
"explanation": "The card will be shown when ALL conditions below are fulfilled. If no conditions are set, the card will always be shown."
},
"layout": {
"full_width": "Full width",
"full_width_helper": "Take up the full width of the section whatever its size",
@@ -8962,10 +8959,7 @@
"cut": "[%key:ui::panel::lovelace::editor::edit_card::cut%]",
"duplicate": "[%key:ui::panel::lovelace::editor::edit_card::duplicate%]",
"tab_config": "[%key:ui::panel::lovelace::editor::edit_card::tab_config%]",
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]",
"visibility": {
"explanation": "The badge will be shown when ALL conditions below are fulfilled. If no conditions are set, the badge will always be shown."
}
"tab_visibility": "[%key:ui::panel::lovelace::editor::edit_card::tab_visibility%]"
},
"suggest_badge": {
"header": "[%key:ui::panel::lovelace::editor::suggest_card::header%]",
@@ -9037,9 +9031,6 @@
"background_opacity": "Opacity",
"theme": "Theme",
"theme_helper": "Apply a specific theme to this section, overriding the view theme"
},
"visibility": {
"explanation": "The section will be shown when ALL conditions below are fulfilled. If no conditions are set, the section will always be shown."
}
},
"suggest_card": {
@@ -9081,11 +9072,31 @@
}
},
"condition-editor": {
"explanation": "The card will be shown when ALL conditions below are fulfilled.",
"add": "Add condition",
"test": "[%key:ui::panel::config::automation::editor::conditions::test%]",
"testing_pass": "[%key:ui::panel::config::automation::editor::conditions::testing_pass%]",
"testing_error": "[%key:ui::panel::config::automation::editor::conditions::testing_error%]",
"live_test_state": {
"pass": "[%key:ui::panel::config::automation::editor::conditions::testing_pass%]",
"fail": "[%key:ui::panel::config::automation::editor::conditions::testing_error%]",
"invalid": "[%key:ui::panel::config::automation::editor::conditions::live_test_state::invalid%]",
"unknown": "[%key:ui::panel::config::automation::editor::conditions::live_test_state::unknown%]"
},
"visibility_status": {
"visible": {
"headline": "Current visibility: Visible",
"supporting": "All visibility conditions are met",
"supporting_empty": "No visibility conditions are set"
},
"hidden": {
"headline": "Current visibility: Hidden",
"supporting": "Not all visibility conditions are met"
},
"invalid": {
"headline": "Visibility status unknown",
"supporting": "One or more conditions have an invalid configuration"
}
},
"invalid_config_title": "Invalid configuration",
"invalid_config_text": "The condition cannot be tested because the configuration is not valid.",
"condition": {
+1 -1
View File
@@ -1,2 +1,2 @@
export const IFRAME_SANDBOX =
"allow-forms allow-popups allow-pointer-lock allow-same-origin allow-scripts allow-modals allow-downloads";
"allow-forms allow-popups allow-pointer-lock allow-scripts allow-modals allow-downloads";
@@ -0,0 +1,42 @@
import { describe, expect, it } from "vitest";
import { computeYAxisFractionDigits } from "../../../src/components/chart/y-axis-fraction-digits";
describe("computeYAxisFractionDigits", () => {
it("uses two decimals for a sub-unit range (e.g. gas prices around 1.85-2.00)", () => {
expect(computeYAxisFractionDigits(1.85, 2.0)).toBe(2);
});
it("uses no decimals for integer-scale ranges", () => {
expect(computeYAxisFractionDigits(0, 100)).toBe(0);
expect(computeYAxisFractionDigits(0, 1000)).toBe(0);
});
it("uses no decimals when the range covers an order of magnitude or more", () => {
expect(computeYAxisFractionDigits(0, 10)).toBe(0);
expect(computeYAxisFractionDigits(0, 50)).toBe(0);
});
it("uses one decimal for ranges around one", () => {
expect(computeYAxisFractionDigits(0, 1)).toBe(1);
expect(computeYAxisFractionDigits(0, 2)).toBe(1);
});
it("uses more decimals as the range shrinks", () => {
expect(computeYAxisFractionDigits(0, 0.05)).toBe(3);
expect(computeYAxisFractionDigits(0, 0.005)).toBe(4);
});
it("falls back to one decimal when min equals max", () => {
expect(computeYAxisFractionDigits(1.5, 1.5)).toBe(1);
});
it("falls back to one decimal when range is non-finite", () => {
expect(computeYAxisFractionDigits(Infinity, -Infinity)).toBe(1);
expect(computeYAxisFractionDigits(NaN, 1)).toBe(1);
});
it("handles negative-to-positive ranges by the magnitude of the range", () => {
expect(computeYAxisFractionDigits(-2, 2)).toBe(1);
expect(computeYAxisFractionDigits(-0.1, 0.1)).toBe(2);
});
});
+189 -177
View File
@@ -1384,15 +1384,15 @@ __metadata:
languageName: node
linkType: hard
"@codemirror/view@npm:6.42.1, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
version: 6.42.1
resolution: "@codemirror/view@npm:6.42.1"
"@codemirror/view@npm:6.43.0, @codemirror/view@npm:^6.0.0, @codemirror/view@npm:^6.17.0, @codemirror/view@npm:^6.23.0, @codemirror/view@npm:^6.27.0, @codemirror/view@npm:^6.37.0, @codemirror/view@npm:^6.42.0":
version: 6.43.0
resolution: "@codemirror/view@npm:6.43.0"
dependencies:
"@codemirror/state": "npm:^6.6.0"
crelt: "npm:^1.0.6"
style-mod: "npm:^4.1.0"
w3c-keyname: "npm:^2.2.4"
checksum: 10/5732718743c048d81d0f63b2c1804f7eb3ae61eac9442d0fee645214cdcca5bb2c5acceaa959c70602d7f1133b3fb150591c8e1c5e36ffbeaff6d6d252447fa2
checksum: 10/7cfeebe1507f71a960dfb2d5152400507d28ed5827680bc73e0a093bfba9a796c2e559c960fd2b046379fac31ff0b59663dfc481baadf1d6ececd71eb5b48014
languageName: node
linkType: hard
@@ -1666,10 +1666,10 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/bigdecimal@npm:0.2.4":
version: 0.2.4
resolution: "@formatjs/bigdecimal@npm:0.2.4"
checksum: 10/c945a25c8ef3dcdb31bbe7d829ef764e2006f9997eb275c2242d75b2094cd6bc53ecbf8bf9751b12f373c4aae0944cbeb7083be26f2386a197f1200918389bde
"@formatjs/bigdecimal@npm:0.2.5":
version: 0.2.5
resolution: "@formatjs/bigdecimal@npm:0.2.5"
checksum: 10/035a70be4175d47d82d81025ad4386d7c248ef7afb2676b4e0773595d01df1078f3d5224e2f7f17721c9169bbb77d4282898624bf0112d2e6350438b7032a1cb
languageName: node
linkType: hard
@@ -1680,121 +1680,121 @@ __metadata:
languageName: node
linkType: hard
"@formatjs/icu-messageformat-parser@npm:3.5.8":
version: 3.5.8
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.8"
"@formatjs/icu-messageformat-parser@npm:3.5.9":
version: 3.5.9
resolution: "@formatjs/icu-messageformat-parser@npm:3.5.9"
dependencies:
"@formatjs/icu-skeleton-parser": "npm:2.1.8"
checksum: 10/e082fe0c1abf0cbd5d65fb154710a894351ac7855e807ffb274e1dc7b3728bd6c8e0dc68cfdb88266e5554d31f0ca623514467d7d2c166698c17354415e7a073
"@formatjs/icu-skeleton-parser": "npm:2.1.9"
checksum: 10/b2543274b8359873ea279139c9da3ab0f42421651b28855c63d2ca7768a747e662f30ff3d296a1807425d08f1b3ae84376372289749da2fb17ba342e9686673a
languageName: node
linkType: hard
"@formatjs/icu-skeleton-parser@npm:2.1.8":
version: 2.1.8
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.8"
checksum: 10/004ea08c6106c4eb3b073f4d7e231232508817b9d081499ec338479f89e41b874e1b02dee951d9ca32b16692a208f9e6a1c38ca3d8160837206a002d024d3b50
"@formatjs/icu-skeleton-parser@npm:2.1.9":
version: 2.1.9
resolution: "@formatjs/icu-skeleton-parser@npm:2.1.9"
checksum: 10/eacb8acd60d487092fc1a6b7fdbac87dfc32475db7001562034a8ca7b0a4be7a35f95c30928ae8314f5e680f63302180bece9462c872a042a0302a5f4cf6a842
languageName: node
linkType: hard
"@formatjs/intl-datetimeformat@npm:7.4.4":
version: 7.4.4
resolution: "@formatjs/intl-datetimeformat@npm:7.4.4"
"@formatjs/intl-datetimeformat@npm:7.4.5":
version: 7.4.5
resolution: "@formatjs/intl-datetimeformat@npm:7.4.5"
dependencies:
"@formatjs/bigdecimal": "npm:0.2.4"
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/c731b19bdcdd5d407eaf51d5ea5ec6adc784e0ca0627c8bab93585577ade004b45c8158d69f11c7b2cfe5327f068c68e3df09148c955944288d5b25735b6fbdd
"@formatjs/bigdecimal": "npm:0.2.5"
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/e45b8dcb745016e705d2fd7078114ddaa04f9f5fbea7615c9155736633069d65039d41f7c41e492247b58d29d9828a3a9782b7de414d80492f221a1639944fab
languageName: node
linkType: hard
"@formatjs/intl-displaynames@npm:7.3.6":
version: 7.3.6
resolution: "@formatjs/intl-displaynames@npm:7.3.6"
"@formatjs/intl-displaynames@npm:7.3.7":
version: 7.3.7
resolution: "@formatjs/intl-displaynames@npm:7.3.7"
dependencies:
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/78bf1f5300fae292eeefcd4009d9836deec4f8970fed88f7cce8c2db3afb764bb62a6145cedf0744e4af29ca4bb223f829452d0b01307e940123ec53862f9f3c
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/1b7b38b3c45babd76b1b59656fa96a4f8cb27668a64e419231d04786b571546934d4e8706955ca6b16d0c8bb5bd2a5157a438c3081d43d491a7c65a141f84d2e
languageName: node
linkType: hard
"@formatjs/intl-durationformat@npm:0.10.10":
version: 0.10.10
resolution: "@formatjs/intl-durationformat@npm:0.10.10"
"@formatjs/intl-durationformat@npm:0.10.11":
version: 0.10.11
resolution: "@formatjs/intl-durationformat@npm:0.10.11"
dependencies:
"@formatjs/bigdecimal": "npm:0.2.4"
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/dbab9cb8410452d93287d9d7e26946533b82615aee973620ae828e0f55976421e9eb283781e6f20b28701e5fdaefaa6baf9e0dde02d9dd979c00c466d65eca5c
"@formatjs/bigdecimal": "npm:0.2.5"
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/df44f858d918ae25764aaee747d6e0679c9d5a3c5e09e3171ac991e72cd6f39f5863a5f92b66a2d19ce819bdad02ec4ad6e7434a84141af5eeb8e5fb82aa0595
languageName: node
linkType: hard
"@formatjs/intl-getcanonicallocales@npm:3.2.7":
version: 3.2.7
resolution: "@formatjs/intl-getcanonicallocales@npm:3.2.7"
checksum: 10/6e9ce95d71d07cd42b298ae1964e87ef205df8f698cf32e5945e6d31dffc970ca54d89334d13417388ae60b9c5178c314ee45f84c024dff19b7b7a9627945c64
"@formatjs/intl-getcanonicallocales@npm:3.2.8":
version: 3.2.8
resolution: "@formatjs/intl-getcanonicallocales@npm:3.2.8"
checksum: 10/d81f8752b118bf8b2de9049d482daba4c50c5b87c22ab6ed111499313d0007460d5d14d4909a5b3754d46b930b56e5fcf34b185098c04b826f1544b4acabe5e5
languageName: node
linkType: hard
"@formatjs/intl-listformat@npm:8.3.6":
version: 8.3.6
resolution: "@formatjs/intl-listformat@npm:8.3.6"
"@formatjs/intl-listformat@npm:8.3.7":
version: 8.3.7
resolution: "@formatjs/intl-listformat@npm:8.3.7"
dependencies:
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/9fac6ad108fe9e085b7f7cdd70b142e74299d5290918aba4b09a55c405824271cf8dcb329a632bc4897484e8468445bfd89c9f19cf702b66863bcb616f3f4972
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/01757f47172150d5eae7ce2cd47386dc6888b9c3d23305b9cc3f16b457e10cbdda7c5894629326532715d85fc04406a9844784d247994442dd2df9380d5cb53e
languageName: node
linkType: hard
"@formatjs/intl-locale@npm:5.3.6":
version: 5.3.6
resolution: "@formatjs/intl-locale@npm:5.3.6"
"@formatjs/intl-locale@npm:5.3.7":
version: 5.3.7
resolution: "@formatjs/intl-locale@npm:5.3.7"
dependencies:
"@formatjs/intl-getcanonicallocales": "npm:3.2.7"
"@formatjs/intl-supportedvaluesof": "npm:2.3.5"
checksum: 10/7102d69ca243464d1dc4a396fa620492fbc7e2e51c11ebcd8a30b226428f586d45fdec2779fb91344b4ee5e34e317bfe97f1c09f9c523ce4d51276f133fcef1a
"@formatjs/intl-getcanonicallocales": "npm:3.2.8"
"@formatjs/intl-supportedvaluesof": "npm:2.3.6"
checksum: 10/c601d1cabd96e96a08e94676b0d676f9f0bf348fca037e4390b9a55f22fa94fb9f2c72679a9be6ed74091e11f78a137ed4d2026889c43f2c45045967f18d6b3c
languageName: node
linkType: hard
"@formatjs/intl-localematcher@npm:0.8.7":
version: 0.8.7
resolution: "@formatjs/intl-localematcher@npm:0.8.7"
"@formatjs/intl-localematcher@npm:0.8.8":
version: 0.8.8
resolution: "@formatjs/intl-localematcher@npm:0.8.8"
dependencies:
"@formatjs/fast-memoize": "npm:3.1.5"
checksum: 10/f7fc35a24af76e2010a0f272356915857739dfb5e41e77a200ae6ae6be37c6b3ea4e01632382f63063918074b00095f56ad5a03495505eb9d8913d122886f9be
checksum: 10/21b02d3d5e40a9a3530f314e7ac2020c1caccc538baca120be6d37bf47ca3208ecf1a3d1fe1dde89d0d46382ec457c6c2714423dc7322ecbd1a3b60d553572a6
languageName: node
linkType: hard
"@formatjs/intl-numberformat@npm:9.3.7":
version: 9.3.7
resolution: "@formatjs/intl-numberformat@npm:9.3.7"
"@formatjs/intl-numberformat@npm:9.3.8":
version: 9.3.8
resolution: "@formatjs/intl-numberformat@npm:9.3.8"
dependencies:
"@formatjs/bigdecimal": "npm:0.2.4"
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/077fba3e9bcc15d3eac55407998026541a373350e7264f3f99f8d28743a3dd9e666e943b617ab7fadfeb8239cf79be7fe78e99f721539f89b4cb70912f1ac51c
"@formatjs/bigdecimal": "npm:0.2.5"
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/bb61e4489a75ad3e243e03fb6d2169549d90f088630b0300b3adc57e12882946f0280726b3bc8781b97ef1b22d95eac6b6346640e63fd77e89b1bf7303e02610
languageName: node
linkType: hard
"@formatjs/intl-pluralrules@npm:6.3.6":
version: 6.3.6
resolution: "@formatjs/intl-pluralrules@npm:6.3.6"
"@formatjs/intl-pluralrules@npm:6.3.7":
version: 6.3.7
resolution: "@formatjs/intl-pluralrules@npm:6.3.7"
dependencies:
"@formatjs/bigdecimal": "npm:0.2.4"
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/bfea3c0643b7d3a4b662d01e518c5204dc995ca3ada26646c804348bf2c6b3c7ecc78e0e550de8a3cdd08fc9e9724904e36a1160b9b67bd8f113e1a8537674e5
"@formatjs/bigdecimal": "npm:0.2.5"
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/0f98dcec85d7365988e5c0ed5d21f13f3b514e9fc1227b0d7d9fbef9ea71dd09427a672c80b06cf065b76b48d48fbed6ee1a687c1365b63260aab68cb5f3b478
languageName: node
linkType: hard
"@formatjs/intl-relativetimeformat@npm:12.3.6":
version: 12.3.6
resolution: "@formatjs/intl-relativetimeformat@npm:12.3.6"
"@formatjs/intl-relativetimeformat@npm:12.3.7":
version: 12.3.7
resolution: "@formatjs/intl-relativetimeformat@npm:12.3.7"
dependencies:
"@formatjs/intl-localematcher": "npm:0.8.7"
checksum: 10/0448dd9a94f768d764b15b4d348a04780fd0d85fbbb864f21fc5b4988d95980c8902fa00deb6aeea2a0f98001ec91ba7741379960b0fce0a585e6965009fa914
"@formatjs/intl-localematcher": "npm:0.8.8"
checksum: 10/d5dc0d3e6d87409deae64ca349010fba0d8c2b4d79fccf57f1cb05ed16732503d41b891997d2f6c7c46068ea7b3044b4e6adf9d632eab9a550272442d02490ce
languageName: node
linkType: hard
"@formatjs/intl-supportedvaluesof@npm:2.3.5":
version: 2.3.5
resolution: "@formatjs/intl-supportedvaluesof@npm:2.3.5"
"@formatjs/intl-supportedvaluesof@npm:2.3.6":
version: 2.3.6
resolution: "@formatjs/intl-supportedvaluesof@npm:2.3.6"
dependencies:
"@formatjs/fast-memoize": "npm:3.1.5"
checksum: 10/dd0423f4d69578b6bf81d91a94660a3eedabcd78563a3d46a41400b9fceb5919eb1a15169ac38e7f9d21380f3c1704a72258b57305e9481f5217a01609d7a3fc
checksum: 10/04f7bc6e256533e8eebf365bc986c17f5c3dd81b58d3452e26b032cab2cda2de68593376119913caf28c44afeb08760367e44de7efb6850d977b5bb3482e011b
languageName: node
linkType: hard
@@ -4335,129 +4335,141 @@ __metadata:
languageName: node
linkType: hard
"@tsparticles/basic@npm:^3.7.1":
version: 3.9.1
resolution: "@tsparticles/basic@npm:3.9.1"
"@tsparticles/basic@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/basic@npm:4.0.0"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
"@tsparticles/move-base": "npm:3.9.1"
"@tsparticles/plugin-hex-color": "npm:3.9.1"
"@tsparticles/plugin-hsl-color": "npm:3.9.1"
"@tsparticles/plugin-rgb-color": "npm:3.9.1"
"@tsparticles/shape-circle": "npm:3.9.1"
"@tsparticles/updater-color": "npm:3.9.1"
"@tsparticles/updater-opacity": "npm:3.9.1"
"@tsparticles/updater-out-modes": "npm:3.9.1"
"@tsparticles/updater-size": "npm:3.9.1"
checksum: 10/a3d0c926e5822931df9762b2038955093ecfb558715807482f691d54efb848286a5d78a55a184885b2d4f46a005bf52c3c54e0013e29e71158ae1ccb5dce08d3
"@tsparticles/engine": "npm:4.0.0"
"@tsparticles/plugin-hex-color": "npm:4.0.0"
"@tsparticles/plugin-hsl-color": "npm:4.0.0"
"@tsparticles/plugin-move": "npm:4.0.0"
"@tsparticles/plugin-rgb-color": "npm:4.0.0"
"@tsparticles/shape-circle": "npm:4.0.0"
"@tsparticles/updater-opacity": "npm:4.0.0"
"@tsparticles/updater-out-modes": "npm:4.0.0"
"@tsparticles/updater-paint": "npm:4.0.0"
"@tsparticles/updater-size": "npm:4.0.0"
checksum: 10/5e455beb0663019d719bc111928a9e22a0f471450414391eed4be2f00455019c59ec85e1e8b7fadf8da2dc258c803d555ebb9b2e0a497a8f53b5922fffab314e
languageName: node
linkType: hard
"@tsparticles/engine@npm:3.9.1, @tsparticles/engine@npm:^3.7.1":
version: 3.9.1
resolution: "@tsparticles/engine@npm:3.9.1"
checksum: 10/91e95f33d526558e0f7251a75dc2971873a7854bb903b61aab95d394c954d3d5f6c2429c151483ebe83445e14e2a7ed9ceadb0fd9c0b7e8c11ec316e4bfd04fa
"@tsparticles/engine@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/engine@npm:4.0.0"
checksum: 10/05fd84ad82f75c9a9d44280ed948d9340cbfb5b18a18f112ee08c0c7d70ba35687197c9ce7508f7c2600410f42075369f2708504485912eb57e5e6e1cf8394ef
languageName: node
linkType: hard
"@tsparticles/interaction-particles-links@npm:^3.7.1":
version: 3.9.1
resolution: "@tsparticles/interaction-particles-links@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/be3925f0892de0eb9a4bc35b1ad402a462874272174379625bccce4c162a528d4f2a4526398f1b4a8b53ff27dae95e2b42a799a9c81ab99e233a9d16c7123774
"@tsparticles/interaction-particles-links@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/interaction-particles-links@npm:4.0.0"
peerDependencies:
"@tsparticles/canvas-utils": 4.0.0
"@tsparticles/engine": 4.0.0
"@tsparticles/plugin-interactivity": 4.0.0
checksum: 10/98d7defc0af362775339f4d7016fa096d352065579036837c99c1a721d88f3207a074075d78130b95236e60a77f66dd29fe957b42e26b7421bdfb797e3271a28
languageName: node
linkType: hard
"@tsparticles/move-base@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/move-base@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/d03795bb4d789295ce4179e1b22d618658a15c31915cba5c8f137bf4a8f183186e3969145ef3951df07fddea0e9d1830a4e25a22baa70d904769c488041da40c
"@tsparticles/plugin-hex-color@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/plugin-hex-color@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/356310741b0019bcdd352e2affc9090662d7191aff932a1e2743c8d1911f87bd8a49a623a3733cf032911884d52b4e65a291267f37e28513c597d5cd4cadba1e
languageName: node
linkType: hard
"@tsparticles/plugin-hex-color@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/plugin-hex-color@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/726a2ae6182bc6e40ed443e1d664bae7ddb4e606a108e92a0c5fc50f0623105144672295720c06e915ef0e36c2a2455ed80dc335a850f11e3a1de1ad44e4ed08
"@tsparticles/plugin-hsl-color@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/plugin-hsl-color@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/cb04a297a40fdad04f25dfaa10d77a04c70dceded6c24e6f3ab766d5cc70514464011527d822fc883bb5da5003fdd5d8ab92b71046cb290a3c8ca60f7a40483f
languageName: node
linkType: hard
"@tsparticles/plugin-hsl-color@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/plugin-hsl-color@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/f81aaed365045e437c8f1627c03f3d255dd2bba2d5ff5231b5e90d576b24fbb5dc110f3b860e13d8696ef820feb465414c0e62962a59e55e6e4a86883cb0f003
"@tsparticles/plugin-interactivity@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/plugin-interactivity@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/23b089e537f5fed67fe7e3cf6b67aa639d9586ebda6838d349d9a02095449ca7bcfe164040a9fa08bdfd779f44ae5917434a335e5c67db5df6d6a6bfd521819e
languageName: node
linkType: hard
"@tsparticles/plugin-rgb-color@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/plugin-rgb-color@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/17352010973ad83e6c9292722896dd0eef0b9f4411684d3fbcf110363bd2aa41594a77b28709dbf1ee9945624567d945ef71900cb37630b0da68714375333c6f
"@tsparticles/plugin-move@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/plugin-move@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/abc5d175105243171ce440d88ae322f2412fb1c390bd39859fde1dc201b528e5d6099f38fa4a87a47ff110eaf65a930306922c409b1404271ba61d222ca9e48d
languageName: node
linkType: hard
"@tsparticles/preset-links@npm:3.2.0":
version: 3.2.0
resolution: "@tsparticles/preset-links@npm:3.2.0"
dependencies:
"@tsparticles/basic": "npm:^3.7.1"
"@tsparticles/engine": "npm:^3.7.1"
"@tsparticles/interaction-particles-links": "npm:^3.7.1"
checksum: 10/1cae6c097d3cac1ba210ed681a40626a79f8579a4e82b1827e0b5864b1cb1fb737471f699800447a7a2bd6e17c706b05db36f84741d9f0c9600bd638e7e29999
"@tsparticles/plugin-rgb-color@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/plugin-rgb-color@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/7d2255d5f428c56cd56fcf00839707ca3fa554a25fa59b2353e30c1c275ca24f06b9f2eee28a869633a740c56b3609cb6540135fb7297ec54d5fcc140e2ce575
languageName: node
linkType: hard
"@tsparticles/shape-circle@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/shape-circle@npm:3.9.1"
"@tsparticles/preset-links@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/preset-links@npm:4.0.0"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/1f0e5add252ee6e59b32b018b585106189a8938798e879a5a09b42434091c82f748b7656206d316705d65821f45e87bb9ef4c7a240c33be4384dbef0def1e2f5
"@tsparticles/basic": "npm:4.0.0"
"@tsparticles/engine": "npm:4.0.0"
"@tsparticles/interaction-particles-links": "npm:4.0.0"
"@tsparticles/plugin-interactivity": "npm:4.0.0"
checksum: 10/67d8c4c90c44a9f3f940aa249e2b86acef4485a54ac02005de5b947cc3c2d01d4b6b00695dc5ff47ab0a8594faa18ba2ccf05b7510f7fd84993939f7d137121e
languageName: node
linkType: hard
"@tsparticles/updater-color@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/updater-color@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/5c4cb7fc7f4767461abffd3ba90ee2c5dba8a7cd3a38d6278314bb9d074c7e5f4977844f4c84448af00e24c923e4635f9392b320fffbac644ae59f157c6ed5b0
"@tsparticles/shape-circle@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/shape-circle@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/37f010c44ba9b82712532da32c2cb6e482d5f5205aaf3bcd7fb04c87925218cd732a31d69ae424613810f11b2651a80fbbd9ec9d6b9d0e58884022ec68cc9d5b
languageName: node
linkType: hard
"@tsparticles/updater-opacity@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/updater-opacity@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/c0ecfd623bdb9cf6ece47098403cb19ce4d9c6204b19ca65357c106f400b4faf2e8f3a51a7ae518b9f708ba549073b4ea8ad8f2282dc298016437a62659298f4
"@tsparticles/updater-opacity@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/updater-opacity@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/b078a28175372246d6861562bcc013c03dff88f6aeaf9b16558e533477340e1b3cd07d57997bdf11b730636df26ef0117bef9ba4ab1b791a9b9a4cd1d2e6623e
languageName: node
linkType: hard
"@tsparticles/updater-out-modes@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/updater-out-modes@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/b74bb0987aacabaeabc981fbbeffb362263ad69cea2f1733b0f5b8753a5c7338ca51eb02164a41d0a70b75ae0bd6e2f2c16c0bc0b4805b367122536df110f21a
"@tsparticles/updater-out-modes@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/updater-out-modes@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/3b27af0a68a7f320ae2481142ff9d6e342b75f245b94e875ea2bdc3f9f47c0b8e79b7ddbf45fea5eeaa9e4610dbdb7130e10056b1f26618958e7e6c28e25dbbe
languageName: node
linkType: hard
"@tsparticles/updater-size@npm:3.9.1":
version: 3.9.1
resolution: "@tsparticles/updater-size@npm:3.9.1"
dependencies:
"@tsparticles/engine": "npm:3.9.1"
checksum: 10/211038b9cadd1df1a0fb747d2e1eeff9f1ce57fd4828e838ddfe6b5365feb3f625e7f713380bc819601d88c50ba4b122a401eae627449760d94c95cff85efdb3
"@tsparticles/updater-paint@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/updater-paint@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/22f9c275ee3eb1409923c74b63fc5984d978b68473bd0f58418d6c28a9be0e9e2e010e338adb7deb482021ab444fab85b962745c06d94066dd40265cf016395d
languageName: node
linkType: hard
"@tsparticles/updater-size@npm:4.0.0":
version: 4.0.0
resolution: "@tsparticles/updater-size@npm:4.0.0"
peerDependencies:
"@tsparticles/engine": 4.0.0
checksum: 10/4664fc5c4c961331d733d98287e9fef0079077def528b814034aea163eb95e2f6c0549e1b87e95eef5f376a7cd0983fbf18c2d77023b9f1f1b163698b03ec504
languageName: node
linkType: hard
@@ -8715,19 +8727,19 @@ __metadata:
"@codemirror/lint": "npm:6.9.6"
"@codemirror/search": "npm:6.7.0"
"@codemirror/state": "npm:6.6.0"
"@codemirror/view": "npm:6.42.1"
"@codemirror/view": "npm:6.43.0"
"@date-fns/tz": "npm:1.4.1"
"@egjs/hammerjs": "npm:2.0.17"
"@eslint/js": "npm:10.0.1"
"@formatjs/intl-datetimeformat": "npm:7.4.4"
"@formatjs/intl-displaynames": "npm:7.3.6"
"@formatjs/intl-durationformat": "npm:0.10.10"
"@formatjs/intl-getcanonicallocales": "npm:3.2.7"
"@formatjs/intl-listformat": "npm:8.3.6"
"@formatjs/intl-locale": "npm:5.3.6"
"@formatjs/intl-numberformat": "npm:9.3.7"
"@formatjs/intl-pluralrules": "npm:6.3.6"
"@formatjs/intl-relativetimeformat": "npm:12.3.6"
"@formatjs/intl-datetimeformat": "npm:7.4.5"
"@formatjs/intl-displaynames": "npm:7.3.7"
"@formatjs/intl-durationformat": "npm:0.10.11"
"@formatjs/intl-getcanonicallocales": "npm:3.2.8"
"@formatjs/intl-listformat": "npm:8.3.7"
"@formatjs/intl-locale": "npm:5.3.7"
"@formatjs/intl-numberformat": "npm:9.3.8"
"@formatjs/intl-pluralrules": "npm:6.3.7"
"@formatjs/intl-relativetimeformat": "npm:12.3.7"
"@fullcalendar/core": "npm:6.1.20"
"@fullcalendar/daygrid": "npm:6.1.20"
"@fullcalendar/interaction": "npm:6.1.20"
@@ -8762,8 +8774,8 @@ __metadata:
"@rspack/dev-server": "npm:2.0.1"
"@swc/helpers": "npm:0.5.21"
"@thomasloven/round-slider": "npm:0.6.0"
"@tsparticles/engine": "npm:3.9.1"
"@tsparticles/preset-links": "npm:3.2.0"
"@tsparticles/engine": "npm:4.0.0"
"@tsparticles/preset-links": "npm:4.0.0"
"@types/babel__plugin-transform-runtime": "npm:7.9.5"
"@types/chromecast-caf-receiver": "npm:6.0.26"
"@types/chromecast-caf-sender": "npm:1.0.11"
@@ -8827,7 +8839,7 @@ __metadata:
html-minifier-terser: "npm:7.2.0"
husky: "npm:9.1.7"
idb-keyval: "npm:6.2.2"
intl-messageformat: "npm:11.2.5"
intl-messageformat: "npm:11.2.6"
js-yaml: "npm:4.1.1"
jsdom: "npm:29.1.1"
jszip: "npm:3.10.1"
@@ -9163,13 +9175,13 @@ __metadata:
languageName: node
linkType: hard
"intl-messageformat@npm:11.2.5":
version: 11.2.5
resolution: "intl-messageformat@npm:11.2.5"
"intl-messageformat@npm:11.2.6":
version: 11.2.6
resolution: "intl-messageformat@npm:11.2.6"
dependencies:
"@formatjs/fast-memoize": "npm:3.1.5"
"@formatjs/icu-messageformat-parser": "npm:3.5.8"
checksum: 10/2064e4e4adf438b137241c34923fe9ae7017d0d569a7ea70e2538950d2c57e7b0621d234241abc75c74a657f9d8d13ed55d793d12008daa9422040535f848a6b
"@formatjs/icu-messageformat-parser": "npm:3.5.9"
checksum: 10/a93a33c607be110715d76f532f74c0f34f1a4e39e28822333d8dd801d0e5e3f4f9a82e2d88895c179b7583d4a9135b1e6bb4044a8ce84c17fd67f2d78cfd84e1
languageName: node
linkType: hard