mirror of
https://github.com/home-assistant/frontend.git
synced 2026-05-14 21:27:01 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b391d6d36d |
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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");
|
||||
@@ -495,6 +496,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;
|
||||
@@ -600,6 +609,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) {
|
||||
@@ -610,6 +622,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;
|
||||
@@ -822,6 +835,7 @@ export class StatisticsChart extends LitElement {
|
||||
val.push(currentValue);
|
||||
}
|
||||
statDataSets[i].data!.push([now, ...val]);
|
||||
trackY(val[val.length - 1]);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -855,6 +869,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
|
||||
@@ -888,21 +903,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)));
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user