Compare commits

...

1 Commits

Author SHA1 Message Date
Petar Petrov b391d6d36d Fix Y-axis label precision in statistics and history charts 2026-05-14 10:01:21 +03:00
11 changed files with 259 additions and 93 deletions
@@ -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");
@@ -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);
});
});