Skip to content

Commit dd5c8c6

Browse files
[ENHANCEMENT] GaugeChart: improve responsiveness (#399)
* Better text size Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * Better progress width Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * factorize common attributes Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * minor adjustments Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * improve text size & trackbar responsiveness Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * improve a bit progress width Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * rename param for clarity Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * better responsiveness for value text Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> * misc adjustments Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr> --------- Signed-off-by: Antoine THEBAUD <antoine.thebaud@yahoo.fr>
1 parent 4b9d46f commit dd5c8c6

2 files changed

Lines changed: 120 additions & 80 deletions

File tree

gaugechart/src/GaugeChartBase.tsx

Lines changed: 40 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import { ReactElement } from 'react';
2121

2222
use([EChartsGaugeChart, GridComponent, TitleComponent, TooltipComponent, CanvasRenderer]);
2323

24-
const PROGRESS_WIDTH = 16;
25-
2624
// adjusts when to show pointer icon
2725
const GAUGE_SMALL_BREAKPOINT = 170;
2826

@@ -40,18 +38,42 @@ export interface GaugeChartBaseProps {
4038
format: FormatOptions;
4139
axisLine: GaugeSeriesOption['axisLine'];
4240
max?: number;
41+
valueFontSize: string;
42+
progressWidth: number;
43+
titleFontSize: number;
4344
}
4445

4546
export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement {
46-
const { width, height, data, format, axisLine, max } = props;
47+
const { width, height, data, format, axisLine, max, valueFontSize, progressWidth, titleFontSize } = props;
4748
const chartsTheme = useChartsTheme();
4849

4950
// useDeepMemo ensures value size util does not rerun everytime you hover on the chart
5051
const option: EChartsCoreOption = useDeepMemo(() => {
5152
if (data.value === undefined) return chartsTheme.noDataOption;
5253

53-
// adjusts fontSize depending on number of characters
54-
const valueSizeClamp = getResponsiveValueSize(data.value, format, width, height);
54+
// Base configuration shared by both series (= progress & scale)
55+
const baseGaugeConfig = {
56+
type: 'gauge' as const,
57+
center: ['50%', '65%'] as [string, string],
58+
startAngle: 200,
59+
endAngle: -20,
60+
min: 0,
61+
max: max,
62+
axisTick: {
63+
show: false,
64+
},
65+
splitLine: {
66+
show: false,
67+
},
68+
axisLabel: {
69+
show: false,
70+
},
71+
data: [
72+
{
73+
value: data.value,
74+
},
75+
],
76+
};
5577

5678
return {
5779
title: {
@@ -61,43 +83,26 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement {
6183
show: false,
6284
},
6385
series: [
86+
// Inner gauge (progress)
6487
{
65-
type: 'gauge',
66-
center: ['50%', '65%'],
67-
radius: '86%',
68-
startAngle: 200,
69-
endAngle: -20,
70-
min: 0,
71-
max,
88+
...baseGaugeConfig,
89+
radius: '90%',
7290
silent: true,
7391
progress: {
7492
show: true,
75-
width: PROGRESS_WIDTH,
93+
width: progressWidth,
7694
itemStyle: {
7795
color: 'auto',
7896
},
7997
},
80-
pointer: {
81-
show: false,
82-
},
8398
axisLine: {
8499
lineStyle: {
85100
color: [[1, 'rgba(127,127,127,0.35)']], // TODO (sjcobb): use future chart theme colors
86-
width: PROGRESS_WIDTH,
101+
width: progressWidth,
87102
},
88103
},
89-
axisTick: {
90-
show: false,
91-
distance: 0,
92-
},
93-
splitLine: {
94-
show: false,
95-
},
96-
axisLabel: {
104+
pointer: {
97105
show: false,
98-
distance: -18,
99-
color: '#999',
100-
fontSize: 12,
101106
},
102107
anchor: {
103108
show: false,
@@ -108,20 +113,11 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement {
108113
detail: {
109114
show: false,
110115
},
111-
data: [
112-
{
113-
value: data.value,
114-
},
115-
],
116116
},
117+
// Outer gauge (scale & display)
117118
{
118-
type: 'gauge',
119-
center: ['50%', '65%'],
119+
...baseGaugeConfig,
120120
radius: '100%',
121-
startAngle: 200,
122-
endAngle: -20,
123-
min: 0,
124-
max,
125121
pointer: {
126122
show: true,
127123
// pointer hidden for small panels, path taken from ex: https://echarts.apache.org/examples/en/editor.html?c=gauge-grade
@@ -133,23 +129,15 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement {
133129
color: 'auto',
134130
},
135131
},
136-
axisLine,
137-
axisTick: {
138-
show: false,
139-
},
140-
splitLine: {
141-
show: false,
142-
},
143-
axisLabel: {
144-
show: false,
145-
},
132+
axisLine: axisLine,
133+
// `detail` is the text displayed in the middle
146134
detail: {
147135
show: true,
148136
width: '60%',
149137
borderRadius: 8,
150138
offsetCenter: [0, '-9%'],
151139
color: 'inherit', // allows value color to match active threshold color
152-
fontSize: valueSizeClamp,
140+
fontSize: valueFontSize,
153141
formatter:
154142
data.value === null
155143
? // We use a different function when we *know* the value is null
@@ -172,15 +160,15 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement {
172160
color: chartsTheme.echartsTheme.textStyle?.color ?? 'inherit', // series name font color
173161
offsetCenter: [0, '55%'],
174162
overflow: 'truncate',
175-
fontSize: 12,
163+
fontSize: titleFontSize,
176164
width: width * 0.8,
177165
},
178166
},
179167
],
180168
},
181169
],
182170
};
183-
}, [data, width, height, chartsTheme, format, axisLine, max]);
171+
}, [data, width, height, chartsTheme, format, axisLine, max, valueFontSize, progressWidth, titleFontSize]);
184172

185173
return (
186174
<EChart
@@ -194,22 +182,3 @@ export function GaugeChartBase(props: GaugeChartBaseProps): ReactElement {
194182
/>
195183
);
196184
}
197-
198-
/**
199-
* Responsive font size depending on number of characters, clamp used
200-
* to ensure size stays within given range
201-
*/
202-
export function getResponsiveValueSize(
203-
value: number | null,
204-
format: FormatOptions,
205-
width: number,
206-
height: number
207-
): string {
208-
const MIN_SIZE = 3;
209-
const MAX_SIZE = 24;
210-
const SIZE_MULTIPLIER = 0.7;
211-
const formattedValue = typeof value === 'number' ? formatValue(value, format) : `${value}`;
212-
const valueCharacters = formattedValue.length ?? 2;
213-
const valueSize = (Math.min(width, height) / valueCharacters) * SIZE_MULTIPLIER;
214-
return `clamp(${MIN_SIZE}px, ${valueSize}px, ${MAX_SIZE}px)`;
215-
}

gaugechart/src/GaugeChartPanel.tsx

Lines changed: 80 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
import { Box, Skeleton, Stack } from '@mui/material';
1515
import { useChartsTheme } from '@perses-dev/components';
16-
import { CalculationsMap, DEFAULT_CALCULATION, TimeSeriesData } from '@perses-dev/core';
16+
import { CalculationsMap, DEFAULT_CALCULATION, FormatOptions, formatValue, TimeSeriesData } from '@perses-dev/core';
1717
import { PanelProps } from '@perses-dev/plugin-system';
1818
import type { GaugeSeriesOption } from 'echarts';
1919
import merge from 'lodash/merge';
@@ -31,6 +31,55 @@ const EMPTY_GAUGE_SERIES: GaugeSeries = { label: '', value: undefined };
3131
const GAUGE_MIN_WIDTH = 90;
3232
const PANEL_PADDING_OFFSET = 20;
3333

34+
/**
35+
* Calculate responsive progress width based on panel dimensions
36+
*/
37+
function getResponsiveProgressWidth(width: number, height: number): number {
38+
const MIN_WIDTH = 10;
39+
const MAX_WIDTH = 48;
40+
const RATIO = 0.1; // 10% of the smaller dimension
41+
42+
const minSize = Math.min(width, height);
43+
// Use RATIO of the smaller dimension as base, with reasonable min/max bounds
44+
return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, Math.round(minSize * RATIO)));
45+
}
46+
47+
/**
48+
* Responsive font size depending on number of characters and panel dimensions.
49+
* Uses clamp to ensure the text never overflows and scales appropriately with panel size.
50+
* (Value refers to the main number value displayed inside the gauge)
51+
*/
52+
function getResponsiveValueFontSize(
53+
value: number | null,
54+
format: FormatOptions,
55+
width: number,
56+
height: number
57+
): string {
58+
const MIN_SIZE = 8;
59+
const MAX_SIZE = 64;
60+
const formattedValue = typeof value === 'number' ? formatValue(value, format) : `${value}`;
61+
62+
const valueTextLength = Math.max(formattedValue.length, 6); // Ensure a minimum length to avoid overly large text for short values
63+
const availableSpace = Math.min(width, height);
64+
const fontSize = availableSpace / valueTextLength;
65+
66+
return `clamp(${MIN_SIZE}px, ${fontSize}px, ${MAX_SIZE}px)`;
67+
}
68+
69+
/**
70+
* Calculate responsive title font size based on panel dimensions
71+
* (Title refers to the text displayed below the gauge as a legend)
72+
*/
73+
function getResponsiveTitleFontSize(width: number, height: number): number {
74+
const MIN_SIZE = 10;
75+
const MAX_SIZE = 16;
76+
const RATIO = 0.06; // Use 6% of the smaller dimension as base
77+
78+
const size = Math.round(Math.min(width, height) * RATIO);
79+
// Scale based on panel size, with reasonable min/max bounds
80+
return Math.max(MIN_SIZE, Math.min(MAX_SIZE, size));
81+
}
82+
3483
export type GaugeChartPanelProps = PanelProps<GaugeChartOptions, TimeSeriesData>;
3584

3685
export function GaugeChartPanel(props: GaugeChartPanelProps): ReactElement | null {
@@ -81,16 +130,37 @@ export function GaugeChartPanel(props: GaugeChartPanelProps): ReactElement | nul
81130
}
82131
const axisLineColors = convertThresholds(thresholds, format, thresholdMax, thresholdsColors);
83132

133+
// accounts for showing a separate chart for each time series
134+
let chartWidth = contentDimensions.width / gaugeData.length - PANEL_PADDING_OFFSET;
135+
if (chartWidth < GAUGE_MIN_WIDTH && gaugeData.length > 1) {
136+
// enables horizontal scroll when charts overflow outside of panel
137+
chartWidth = GAUGE_MIN_WIDTH;
138+
}
139+
140+
// Calculate responsive values based on chart dimensions
141+
const progressWidth = getResponsiveProgressWidth(chartWidth, contentDimensions.height);
142+
const axisLineWidth = Math.round(progressWidth * 0.2); // Axis line width is 20% of progress width
143+
const titleFontSize = getResponsiveTitleFontSize(chartWidth, contentDimensions.height);
144+
84145
const axisLine: GaugeSeriesOption['axisLine'] = {
85146
show: true,
86147
lineStyle: {
87-
width: 5,
148+
width: axisLineWidth,
88149
color: axisLineColors,
89150
},
90151
};
91152

92153
// no data message handled inside chart component
93154
if (!gaugeData.length) {
155+
const emptyValueFontSize = getResponsiveValueFontSize(
156+
null,
157+
format,
158+
contentDimensions.width,
159+
contentDimensions.height
160+
);
161+
const emptyProgressWidth = getResponsiveProgressWidth(contentDimensions.width, contentDimensions.height);
162+
const emptyTitleFontSize = getResponsiveTitleFontSize(contentDimensions.width, contentDimensions.height);
163+
94164
return (
95165
<GaugeChartBase
96166
width={contentDimensions.width}
@@ -99,17 +169,13 @@ export function GaugeChartPanel(props: GaugeChartPanelProps): ReactElement | nul
99169
format={format}
100170
axisLine={axisLine}
101171
max={thresholdMax}
172+
valueFontSize={emptyValueFontSize}
173+
progressWidth={emptyProgressWidth}
174+
titleFontSize={emptyTitleFontSize}
102175
/>
103176
);
104177
}
105178

106-
// accounts for showing a separate chart for each time series
107-
let chartWidth = contentDimensions.width / gaugeData.length - PANEL_PADDING_OFFSET;
108-
if (chartWidth < GAUGE_MIN_WIDTH && gaugeData.length > 1) {
109-
// enables horizontal scroll when charts overflow outside of panel
110-
chartWidth = GAUGE_MIN_WIDTH;
111-
}
112-
113179
const hasMultipleCharts = gaugeData.length > 1;
114180

115181
return (
@@ -124,6 +190,8 @@ export function GaugeChartPanel(props: GaugeChartPanelProps): ReactElement | nul
124190
}}
125191
>
126192
{gaugeData.map((series, seriesIndex) => {
193+
const fontSize = getResponsiveValueFontSize(series.value ?? null, format, chartWidth, contentDimensions.height);
194+
127195
return (
128196
<Box key={`gauge-series-${seriesIndex}`}>
129197
<GaugeChartBase
@@ -133,6 +201,9 @@ export function GaugeChartPanel(props: GaugeChartPanelProps): ReactElement | nul
133201
format={format}
134202
axisLine={axisLine}
135203
max={thresholdMax}
204+
valueFontSize={fontSize}
205+
progressWidth={progressWidth}
206+
titleFontSize={titleFontSize}
136207
/>
137208
</Box>
138209
);

0 commit comments

Comments
 (0)