-
Notifications
You must be signed in to change notification settings - Fork 6
[FEATURE] Enable multiple Y axis support for plugins #41
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
…n for regular single-Y-axis situations Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
…nent (UnitSelector) in the call hierarchy Signed-off-by: AntoineThebaud <antoine.thebaud@yahoo.fr>
| // Calculate cumulative offsets based on actual formatted label widths | ||
| let cumulativeOffset = 0; | ||
|
|
||
| // Additional Y axes (right side) for each unique format | ||
| additionalFormats.forEach((format, index) => { | ||
| // For subsequent axes, add the width of the previous axis's labels | ||
| if (index > 0 && maxValues) { | ||
| const prevMaxValue = maxValues[index - 1] ?? 1000; | ||
| cumulativeOffset += estimateLabelWidth(additionalFormats[index - 1], prevMaxValue); | ||
| } | ||
|
|
||
| const rightAxisConfig: YAXisComponentOption = { | ||
| type: 'value', | ||
| position: 'right', | ||
| // Dynamic offset based on cumulative width of preceding axis labels | ||
| offset: cumulativeOffset, | ||
| boundaryGap: [0, '10%'], | ||
| axisLabel: { | ||
| formatter: (value: number): string => { | ||
| return formatValue(value, format); | ||
| }, | ||
| }, | ||
| splitLine: { | ||
| show: false, // Hide grid lines for right-side axes to reduce visual noise | ||
| }, | ||
| show: baseAxis?.show, | ||
| }; | ||
| axes.push(rightAxisConfig); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think a "simpler" approach will be to calculate the width. This way the text can be truncated to the next line to avoid cutting it.
| // Calculate cumulative offsets based on actual formatted label widths | |
| let cumulativeOffset = 0; | |
| // Additional Y axes (right side) for each unique format | |
| additionalFormats.forEach((format, index) => { | |
| // For subsequent axes, add the width of the previous axis's labels | |
| if (index > 0 && maxValues) { | |
| const prevMaxValue = maxValues[index - 1] ?? 1000; | |
| cumulativeOffset += estimateLabelWidth(additionalFormats[index - 1], prevMaxValue); | |
| } | |
| const rightAxisConfig: YAXisComponentOption = { | |
| type: 'value', | |
| position: 'right', | |
| // Dynamic offset based on cumulative width of preceding axis labels | |
| offset: cumulativeOffset, | |
| boundaryGap: [0, '10%'], | |
| axisLabel: { | |
| formatter: (value: number): string => { | |
| return formatValue(value, format); | |
| }, | |
| }, | |
| splitLine: { | |
| show: false, // Hide grid lines for right-side axes to reduce visual noise | |
| }, | |
| show: baseAxis?.show, | |
| }; | |
| axes.push(rightAxisConfig); | |
| }); | |
| // Calculate cumulative offsets based on actual formatted label widths | |
| let width = 0; | |
| let cumulativeWidth = 0; | |
| // Additional Y axes (right side) for each unique format | |
| additionalFormats.forEach((format, index) => { | |
| // For subsequent axes, add the width of the previous axis's labels | |
| if (maxValues) { | |
| width = estimateLabelWidth(format, maxValues[index] ?? 1000); | |
| } | |
| const rightAxisConfig: YAXisComponentOption = { | |
| type: 'value', | |
| position: 'right', | |
| // Dynamic offset based on cumulative width of preceding axis labels | |
| offset: cumulativeWidth, | |
| boundaryGap: [0, '10%'], | |
| axisLabel: { | |
| formatter: (value: number): string => { | |
| return formatValue(value, format); | |
| }, | |
| width: width, | |
| overflow: 'breakAll', | |
| }, | |
| splitLine: { | |
| show: false, // Hide grid lines for right-side axes to reduce visual noise | |
| }, | |
| show: baseAxis?.show, | |
| }; | |
| axes.push(rightAxisConfig); | |
| cumulativeWidth += width + AXIS_LABEL_PADDING; | |
| }); |
| function estimateLabelWidth(format: FormatOptions | undefined, maxValue: number): number { | ||
| const formattedLabel = formatValue(maxValue, format); | ||
| return formattedLabel.length * AVG_CHAR_WIDTH + AXIS_LABEL_PADDING; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried a new approach that keeps character width variance in mind. It seems to be working much better.
// Character width multipliers (approximate for typical UI fonts)
const CHAR_WIDTH_BASE = 12;
const CHAR_WIDTH_MULTIPLIERS = {
dot: 0.5, // Dots and periods are very narrow
uppercase: 1.0,
lowercase: 0.55, // Lowercase letters slightly narrower
digit: 0.65,
symbol: 0.7, // Symbols like %, $, etc.
space: 0.5, // Spaces
};
const AXIS_LABEL_PADDING = 14;
/**
* Calculate the width of a single character based on its type
*/
function getCharWidth(char?: string): number {
if (!char || char.length === 0) {
return 0;
}
if (char === '.' || char === ',' || char === ':') {
return CHAR_WIDTH_BASE * CHAR_WIDTH_MULTIPLIERS.dot;
}
if (char === ' ') {
return CHAR_WIDTH_BASE * CHAR_WIDTH_MULTIPLIERS.space;
}
if (char >= 'A' && char <= 'Z') {
return CHAR_WIDTH_BASE * CHAR_WIDTH_MULTIPLIERS.uppercase;
}
if (char >= 'a' && char <= 'z') {
return CHAR_WIDTH_BASE * CHAR_WIDTH_MULTIPLIERS.lowercase;
}
if (char >= '0' && char <= '9') {
return CHAR_WIDTH_BASE * CHAR_WIDTH_MULTIPLIERS.digit;
}
// Symbols like %, $, -, +, etc.
return CHAR_WIDTH_BASE * CHAR_WIDTH_MULTIPLIERS.symbol;
}
/**
* Estimate the pixel width needed for an axis label based on the formatted max value.
* This provides dynamic spacing that adapts to the actual data scale.
*/
function estimateLabelWidth(format: FormatOptions | undefined, maxValue: number): number {
const formattedLabel = formatValue(maxValue, format);
// Calculate width based on individual character types
let totalWidth = 0;
for (let i = 0; i < formattedLabel.length; i++) {
totalWidth += getCharWidth(formattedLabel[i]);
}
return totalWidth;
}
| * This provides dynamic spacing that adapts to the actual data scale. | ||
| */ | ||
| function estimateLabelWidth(format: FormatOptions | undefined, maxValue: number): number { | ||
| const formattedLabel = formatValue(maxValue, format); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With some values there still a problem. IIUC this max value might not be the value with more text. e.g. 0.00001 vs 1
Description
Relates to perses/perses#2123.
Needed for perses/plugins#531.
This PR mainly updates TimeSeriesTooltip to be compatible with a multiple Y axis setup (e.g nearby series computation has to be made using pixel proximity in that case instead of series values, as the orders of magnitude between series can be very different). It also adds a new
getFormattedMultipleYAxesutility that plugins relying on ECharts can call in the same way than the existinggetFormattedAxisNB: TimeSeriesTooltip still has to be moved to TimeSeriesChart code as it's not really generic (ref), I really need to work on this 😅
Screenshots
See screenshots of perses/plugins#531.
Checklist
[<catalog_entry>] <commit message>naming convention using one of thefollowing
catalog_entryvalues:FEATURE,ENHANCEMENT,BUGFIX,BREAKINGCHANGE,DOC,IGNORE.UI Changes
See e2e docs for more details. Common issues include: