Compare commits

...

4 Commits

Author SHA1 Message Date
Evan
013b38dea6 refactor(plugin-chart-echarts): reuse finite-number guards in forecast tooltip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 05:48:10 -07:00
Evan
b497dd6af4 fix(plugin-chart-echarts): narrow forecast tooltip values to satisfy tsc
Use a type-guard predicate so forecastLower/forecastUpper are narrowed to
number at the use sites, resolving TS18048.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 02:47:47 -07:00
Evan
44ce7e4d4b Use Number.isFinite for forecast tooltip guards to exclude NaN/Infinity
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 23:49:00 -07:00
Evan
40339257bd fix(plugin-chart-echarts): show forecast tooltip values when they equal zero
The forecast tooltip formatter used truthiness checks on the forecast trend
and confidence-bound values, so a legitimate value of exactly 0 was silently
dropped from the tooltip. This is what users saw as the confidence band
"disappearing" when a forecast crossed zero (issue #21734): a forecast trend
of 0, a lower bound of 0, or a zero-height band would render no interval at
all.

Switch the conditionals to explicit `typeof === 'number'` checks (matching the
existing pattern used for the observation value) so zero values are formatted
and shown like any other number.

Fixes #21734

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:04:46 -07:00
2 changed files with 81 additions and 3 deletions

View File

@@ -98,15 +98,24 @@ export const formatForecastTooltipSeries = ({
}): string[] => {
const name = `${marker}${sanitizeHtml(seriesName)}`;
let value = typeof observation === 'number' ? formatter(observation) : '';
if (forecastTrend || forecastLower || forecastUpper) {
// Use finite-number checks rather than truthiness so that legitimate
// zero values (e.g. a forecast that crosses zero, or a confidence bound of
// exactly 0) are not dropped from the tooltip, while non-finite values
// (NaN/Infinity) are still excluded.
const isFiniteNumber = (val: number | undefined): val is number =>
typeof val === 'number' && Number.isFinite(val);
const hasTrend = isFiniteNumber(forecastTrend);
const hasLower = isFiniteNumber(forecastLower);
const hasUpper = isFiniteNumber(forecastUpper);
if (hasTrend || hasLower || hasUpper) {
// forecast values take the form of "20, y = 30 (10, 40)"
// where the first part is the observation, the second part is the forecast trend
// and the third part is the lower and upper bounds
if (forecastTrend) {
if (hasTrend) {
if (value) value += ', ';
value += `ŷ = ${formatter(forecastTrend)}`;
}
if (forecastLower && forecastUpper) {
if (hasLower && hasUpper) {
if (value) value += ' ';
// the lower bound needs to be added to the upper bound
value += `(${formatter(forecastLower)}, ${formatter(

View File

@@ -342,3 +342,72 @@ test('formatForecastTooltipSeries should format forecast with only confidence ba
}),
).toEqual(['<img>qwerty', '(7, 15)']);
});
test('formatForecastTooltipSeries should show forecast trend equal to zero', () => {
expect(
formatForecastTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
observation: 10,
forecastTrend: 0,
forecastLower: 5,
forecastUpper: 7,
formatter,
}),
).toEqual(['<img>qwerty', '10, ŷ = 0 (5, 12)']);
});
test('formatForecastTooltipSeries should show confidence band when lower bound is zero', () => {
expect(
formatForecastTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
observation: 10,
forecastTrend: 5,
forecastLower: 0,
forecastUpper: 7,
formatter,
}),
).toEqual(['<img>qwerty', '10, ŷ = 5 (0, 7)']);
});
test('formatForecastTooltipSeries should show confidence band when band height is zero', () => {
expect(
formatForecastTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
observation: 10,
forecastTrend: 5,
forecastLower: 4,
forecastUpper: 0,
formatter,
}),
).toEqual(['<img>qwerty', '10, ŷ = 5 (4, 4)']);
});
test('formatForecastTooltipSeries should show forecast trend and band all at zero', () => {
expect(
formatForecastTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
forecastTrend: 0,
forecastLower: 0,
forecastUpper: 0,
formatter,
}),
).toEqual(['<img>qwerty', 'ŷ = 0 (0, 0)']);
});
test('formatForecastTooltipSeries should skip non-finite forecast values', () => {
expect(
formatForecastTooltipSeries({
seriesName: 'qwerty',
marker: '<img>',
observation: 10,
forecastTrend: NaN,
forecastLower: Infinity,
forecastUpper: -Infinity,
formatter,
}),
).toEqual(['<img>qwerty', '10']);
});