From ce684290d73d7d657ddfbbc535ef14a78b93741a Mon Sep 17 00:00:00 2001 From: colivi Date: Thu, 1 Oct 2026 15:25:40 +0200 Subject: [PATCH 1/3] fix(statchart): size auto tiles from the panel height The 72px cap left the rest of the panel empty and gave the sparkline no room under the series name and value. Share the panel height across rows instead, keeping the 60px minimum. Signed-off-by: colivi --- statchart/src/StatChartPanel.tsx | 7 +++---- statchart/src/series-columns.test.ts | 19 ++++++++++++++++++- statchart/src/stat-chart-model.ts | 13 +++++++++++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/statchart/src/StatChartPanel.tsx b/statchart/src/StatChartPanel.tsx index 6c6a68a59..eaebe21aa 100644 --- a/statchart/src/StatChartPanel.tsx +++ b/statchart/src/StatChartPanel.tsx @@ -22,7 +22,7 @@ import type { FC } from 'react'; import { useMemo } from 'react'; import type { StatChartOptions } from './stat-chart-model'; -import { resolveAutoOrientationColumnsCount } from './stat-chart-model'; +import { resolveAutoOrientationColumnsCount, resolveAutoOrientationTileHeight } from './stat-chart-model'; import type { StatChartData } from './StatChartBase'; import { StatChartBase } from './StatChartBase'; import { measureTextWidth } from './utils/calculate-font-size'; @@ -34,7 +34,6 @@ import { getStatChartColor } from './utils/get-color'; const MIN_WIDTH = 100; const MIN_TILE_HEIGHT = 60; const SPACING = 2; -const AUTO_TILE_HEIGHT = 72; const MAX_VALUE_FONT_SIZE = 96; export type StatChartPanelProps = PanelProps; @@ -122,7 +121,7 @@ export const StatChartPanel: FC = (props) => { return Math.max(MIN_TILE_HEIGHT, Math.floor(panelHeight / Math.max(1, statChartData.length))); } if (isAutoWrapped) { - return Math.min(AUTO_TILE_HEIGHT, Math.max(MIN_TILE_HEIGHT, Math.floor(panelHeight / autoRowCount))); + return resolveAutoOrientationTileHeight(panelHeight, autoRowCount, SPACING, MIN_TILE_HEIGHT); } return panelHeight; }, [autoRowCount, panelHeight, isAutoWrapped, isVerticalLayout, statChartData.length]); @@ -142,7 +141,7 @@ export const StatChartPanel: FC = (props) => { { it('matches ceil(sqrt(n))', () => { @@ -47,3 +52,15 @@ describe('resolveAutoOrientationColumnsCount', () => { expect(resolveAutoOrientationColumnsCount(10, 2)).toBe(2); }); }); + +describe('resolveAutoOrientationTileHeight', () => { + it('shares the panel height so a sparkline fits under the value', () => { + // 2 rows, 2px gap: a 72px cap would hide the sparkline in a tall panel. + expect(resolveAutoOrientationTileHeight(240, 2, 2, 60)).toBe(119); + expect(resolveAutoOrientationTileHeight(300, 3, 2, 60)).toBe(98); + }); + + it('keeps a minimum tile height when the panel is shorter than the rows', () => { + expect(resolveAutoOrientationTileHeight(80, 2, 2, 60)).toBe(60); + }); +}); diff --git a/statchart/src/stat-chart-model.ts b/statchart/src/stat-chart-model.ts index b55343cdf..a4574f17b 100644 --- a/statchart/src/stat-chart-model.ts +++ b/statchart/src/stat-chart-model.ts @@ -103,6 +103,19 @@ export function resolveAutoOrientationColumnsCount( return Math.max(1, Math.min(seriesCount, width, ideal)); } +// Share the panel height across auto-orientation rows +export function resolveAutoOrientationTileHeight( + panelHeight: number, + rowCount: number, + spacing: number, + minTileHeight: number, +): number { + const rows = Math.max(1, Math.floor(rowCount)); + const gaps = Math.max(0, spacing) * Math.max(0, rows - 1); + const available = Math.max(0, panelHeight - gaps); + return Math.max(minTileHeight, Math.floor(available / rows)); +} + export interface StatChartSparklineOptions { color?: string; width?: number; From b5df6909d369062484908d10314816aa760bbfc9 Mon Sep 17 00:00:00 2001 From: colivi Date: Thu, 1 Oct 2026 16:25:53 +0200 Subject: [PATCH 2/3] fix(statchart): keep the value readable next to the sparkline Auto layout sized the number to 25% of the tile and gave the rest to the chart, so Traffic distribution stayed tiny while the panel grew. Cap the sparkline band and let the value use the remaining height, up to 96px. Series names scale with the tile instead of sticking at 14px. Signed-off-by: colivi --- statchart/src/StatChartBase.tsx | 18 +++++++++++------- statchart/src/StatChartPanel.tsx | 2 +- statchart/src/series-columns.test.ts | 13 +++++++++++++ statchart/src/stat-chart-model.ts | 12 ++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/statchart/src/StatChartBase.tsx b/statchart/src/StatChartBase.tsx index cf335c93f..b3c7ab682 100644 --- a/statchart/src/StatChartBase.tsx +++ b/statchart/src/StatChartBase.tsx @@ -26,6 +26,7 @@ import type { FC, ReactNode } from 'react'; import { useMemo } from 'react'; import type { ColorMode } from './stat-chart-model'; +import { resolveSparklineBandHeight } from './stat-chart-model'; import { useOptimalFontSize } from './utils/calculate-font-size'; import { formatStatChartValue } from './utils/format-stat-chart-value'; @@ -101,24 +102,27 @@ export const StatChartBase: FC = (props) => { if (legendFontSize !== undefined) { seriesNameFontSize = legendFontSize; } else if (alignmentSeriesName !== undefined) { - // multi-series: use 15% of cell height for legend, clamped between 14px and 30px - seriesNameFontSize = Math.max(14, Math.min((height * 0.15) / LINE_HEIGHT, SERIES_NAME_MAX_FONT_SIZE)); + // Multi-series names track the tile. Dividing by line-height pinned them at 14px + // until the tile was taller than ~110px. + seriesNameFontSize = Math.max(14, Math.min(height * 0.18, SERIES_NAME_MAX_FONT_SIZE)); } const seriesNameHeight = showSeriesName ? seriesNameFontSize * LINE_HEIGHT + containerPadding : 0; const availableHeight = height - seriesNameHeight; + // A sparkline used to claim everything under a value sized to 25% of the tile, + // so the number stayed tiny while the chart grew into empty space. + const sparklineBand = sparkline ? resolveSparklineBandHeight(availableHeight) : 0; + const valueAreaHeight = sparkline ? Math.max(0, availableHeight - sparklineBand) : availableHeight * 0.9; const optimalValueFontSize = useOptimalFontSize({ text: alignmentText || formattedValue, fontSizeOverride: valueFontSize, fontWeight: VALUE_FONT_WEIGHT, width: sparkline ? availableWidth : availableWidth * 0.5, - height: sparkline ? availableHeight * 0.25 : availableHeight * 0.9, + height: valueAreaHeight, lineHeight: LINE_HEIGHT, maxSize: maxValueFontSize, }); - const valueFontHeight = optimalValueFontSize * LINE_HEIGHT; - // single-series: keep legend smaller than value (unless explicitly set) if (alignmentSeriesName === undefined && legendFontSize === undefined) { seriesNameFontSize = Math.min(optimalValueFontSize * 0.7, seriesNameFontSize); @@ -157,7 +161,7 @@ export const StatChartBase: FC = (props) => { }, grid: { show: false, - top: '35%', // adds space above sparkline + top: 0, right: 0, bottom: 0, left: 0, @@ -267,7 +271,7 @@ export const StatChartBase: FC = (props) => { style={{ // ECharts rounds the height to the nearest integer by default. // This can cause unneccessary scrollbars when the total height of this chart exceeds the 'height' prop. - height: Math.floor(height - seriesNameHeight - valueFontHeight), + height: Math.floor(sparklineBand), }} option={option} theme={chartsTheme.echartsTheme} diff --git a/statchart/src/StatChartPanel.tsx b/statchart/src/StatChartPanel.tsx index eaebe21aa..d7fb15ff5 100644 --- a/statchart/src/StatChartPanel.tsx +++ b/statchart/src/StatChartPanel.tsx @@ -200,7 +200,7 @@ export const StatChartPanel: FC = (props) => { legendFontSize={legendFontSize} alignmentText={isAutoWrapped || !isVerticalLayout ? undefined : alignmentText} alignmentSeriesName={alignmentSeriesName} - maxValueFontSize={!isAutoWrapped && !isVerticalLayout ? MAX_VALUE_FONT_SIZE : undefined} + maxValueFontSize={MAX_VALUE_FONT_SIZE} /> ); }) diff --git a/statchart/src/series-columns.test.ts b/statchart/src/series-columns.test.ts index e6d3f8385..841433455 100644 --- a/statchart/src/series-columns.test.ts +++ b/statchart/src/series-columns.test.ts @@ -18,6 +18,7 @@ import { MAX_SERIES_COLUMNS, resolveAutoOrientationColumnsCount, resolveAutoOrientationTileHeight, + resolveSparklineBandHeight, } from './stat-chart-model'; describe('computeIdealSeriesColumns', () => { @@ -64,3 +65,15 @@ describe('resolveAutoOrientationTileHeight', () => { expect(resolveAutoOrientationTileHeight(80, 2, 2, 60)).toBe(60); }); }); + +describe('resolveSparklineBandHeight', () => { + it('leaves most of the tile to the value', () => { + expect(resolveSparklineBandHeight(47)).toBeCloseTo(18.8); + expect(resolveSparklineBandHeight(100)).toBe(30); + expect(resolveSparklineBandHeight(200)).toBe(60); + }); + + it('returns 0 when there is no room under the series name', () => { + expect(resolveSparklineBandHeight(0)).toBe(0); + }); +}); diff --git a/statchart/src/stat-chart-model.ts b/statchart/src/stat-chart-model.ts index a4574f17b..5ddeb386d 100644 --- a/statchart/src/stat-chart-model.ts +++ b/statchart/src/stat-chart-model.ts @@ -116,6 +116,18 @@ export function resolveAutoOrientationTileHeight( return Math.max(minTileHeight, Math.floor(available / rows)); } +const SPARKLINE_BAND_MIN_HEIGHT = 24; +const SPARKLINE_BAND_MAX_HEIGHT = 72; +// The sparkline keeps at most this share of the space under the series name. +// The value uses the rest, so the number stays readable when the panel grows. +const SPARKLINE_BAND_MAX_RATIO = 0.4; + +export function resolveSparklineBandHeight(availableHeight: number): number { + if (availableHeight <= 0) return 0; + const preferred = Math.min(SPARKLINE_BAND_MAX_HEIGHT, Math.max(SPARKLINE_BAND_MIN_HEIGHT, availableHeight * 0.3)); + return Math.min(availableHeight * SPARKLINE_BAND_MAX_RATIO, preferred); +} + export interface StatChartSparklineOptions { color?: string; width?: number; From e285e03d533dc2215ce90f8dc54d4b121de98bb0 Mon Sep 17 00:00:00 2001 From: colivi Date: Fri, 2 Oct 2026 16:23:48 +0200 Subject: [PATCH 3/3] fix(statchart): drop layout comments flagged in review Signed-off-by: colivi --- statchart/src/StatChartBase.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/statchart/src/StatChartBase.tsx b/statchart/src/StatChartBase.tsx index b3c7ab682..74ce1c70a 100644 --- a/statchart/src/StatChartBase.tsx +++ b/statchart/src/StatChartBase.tsx @@ -102,16 +102,12 @@ export const StatChartBase: FC = (props) => { if (legendFontSize !== undefined) { seriesNameFontSize = legendFontSize; } else if (alignmentSeriesName !== undefined) { - // Multi-series names track the tile. Dividing by line-height pinned them at 14px - // until the tile was taller than ~110px. seriesNameFontSize = Math.max(14, Math.min(height * 0.18, SERIES_NAME_MAX_FONT_SIZE)); } const seriesNameHeight = showSeriesName ? seriesNameFontSize * LINE_HEIGHT + containerPadding : 0; const availableHeight = height - seriesNameHeight; - // A sparkline used to claim everything under a value sized to 25% of the tile, - // so the number stayed tiny while the chart grew into empty space. const sparklineBand = sparkline ? resolveSparklineBandHeight(availableHeight) : 0; const valueAreaHeight = sparkline ? Math.max(0, availableHeight - sparklineBand) : availableHeight * 0.9; const optimalValueFontSize = useOptimalFontSize({