Reputation: 180
I have the following plot, which uses two axes: one for the differences and another for the accumulative differences.
I would like to force that both y axis match on 0, scaling the second faster later (Note that now we have a 0:200 match)
I´ve been checking the documentation, and it seems to include some variables for this purpose: onZero and onZeroAxisIndex. Nevertheless, I didn´t find any successful solution. Currently, my axis code looks as follows:
xAxis: {
type: "category",
data: props.dates,
},
yAxis: [
{
type: 'value',
name: 'Discprepancia',
position: 'left',
axisLabel: {
formatter: '{value} °C'
},
},
{
type: 'value',
name: 'Discprepancia acumulada',
position: 'right',
alignTicks: true,
axisLabel: {
formatter: '{value} °C'
}
}
]
Edit: I found a similar question in this thread
Upvotes: 3
Views: 674
Reputation: 101
The above answer works, however is hard coded. This works (angular 19 with echarts) dynamically: The main idea is that just the min/max bounds and offset of the primary axis are calculated and then used to calculate the min/max for the secondary axis so that the x-axis intersection matches 0.
interface NumericAxisBounds {
min: number;
max: number;
zeroOffset: number; // Proportional position of the zero point
}
interface SecondaryAxisBounds {
min: number;
max: number;
}
export function calculatePrimaryAxisBounds(data: number[]): NumericAxisBounds {
if (!data.length) {
return { min: 0, max: 0, zeroOffset: 0 };
}
const minValue = Math.min(...data);
const maxValue = Math.max(...data);
if (minValue >= 0) {
// All positive - start at zero
const magnitude = Math.pow(10, Math.floor(Math.log10(maxValue)));
const roundedMax = Math.ceil(maxValue / (magnitude / 2)) * (magnitude / 2);
return {
min: 0,
max: roundedMax,
zeroOffset: 0, // Zero at the bottom
};
} else if (maxValue <= 0) {
// All negative - end at zero
const magnitude = Math.pow(10, Math.floor(Math.log10(-minValue)));
const roundedMin = Math.floor(minValue / (magnitude / 2)) * (magnitude / 2);
return {
min: roundedMin,
max: 0,
zeroOffset: 1, // Zero at the top
};
} else {
// Data crosses zero - dynamic scaling around the range
// First determine the appropriate magnitude for rounding
const maxMagnitude = Math.max(Math.abs(maxValue), Math.abs(minValue));
const roundingUnit = Math.pow(10, Math.floor(Math.log10(maxMagnitude)));
// Use finer granularity for the minimum value since it's smaller
const adjustedMin = Math.floor(minValue / (roundingUnit / 10)) * (roundingUnit / 10);
const adjustedMax = Math.ceil(maxValue / (roundingUnit / 2)) * (roundingUnit / 2);
const range = adjustedMax - adjustedMin;
const zeroOffset = Math.abs(adjustedMin) / range;
return {
min: adjustedMin,
max: adjustedMax,
zeroOffset,
};
}
}
/**
* Calculates axis bounds for secondary (percentage) axis that aligns with the primary axis zero point
* @param data - Array of percentage values (e.g., 1.534 = 153.4%)
* @param primaryBounds - Object containing the primary axis bounds and zero offset
*/
export function calculateSecondaryAxisBounds(
data: number[],
primaryBounds: { min: number; max: number; zeroOffset: number }
): SecondaryAxisBounds {
if (!data.length) {
return { min: 0, max: 0 };
}
const { zeroOffset } = primaryBounds;
const actualMin = Math.min(...data);
const actualMax = Math.max(...data);
const dataRange = actualMax - actualMin;
const padding = dataRange * 0.1; // 10% padding
// Case 1: Primary axis is all positive
if (primaryBounds.min >= 0) {
return {
min: 0,
max: actualMax + padding
};
}
// Case 2: Primary axis is all negative
if (primaryBounds.max <= 0) {
return {
min: actualMin - padding,
max: 0
};
}
// Case 3: Primary axis crosses zero
// For this case, we need to consider two sub-cases:
// Case 3a: Secondary data contains negative values
if (actualMin < 0) {
// For a zero position of 25.49%, we need:
// - Negative range to occupy 25.49% of total range
// - Positive range to occupy 74.51% of total range
const rangeFromPositive = actualMax / (1 - zeroOffset); // Positive values take (1-zeroOffset)
const rangeFromNegative = Math.abs(actualMin) / zeroOffset; // Negative values take zeroOffset
const totalRequiredRange = Math.max(rangeFromPositive, rangeFromNegative);
return {
min: -totalRequiredRange * zeroOffset,
max: totalRequiredRange * (1 - zeroOffset)
};
}
// Case 3b: Secondary data is all positive but primary crosses zero
// Here we need to position the zero point at the same relative position as the primary axis
const totalRequiredRange = actualMax / (1 - zeroOffset); // Scale relative to distance from top
return {
min: -(totalRequiredRange * zeroOffset), // This creates the negative range needed to position zero at zeroOffset from bottom
max: actualMax + padding
};
}
Upvotes: 0
Reputation: 180
In a React/Javascript ecosystem, I solved it by playing with the min and max inside yAxis as follows:
yAxis: [
// Make both y axis ticks to match
{
type: 'value',
name: 'Desviación',
// Title is shown on bottom of axis
interval: 3,
position: 'left',
// alignTicks: true,
axisLabel: {
formatter: '{value}°C'
},
min: function (value) {
const absValue = max_abs(value.min, value.max)
const absValueRounded = (absValue * -1).toFixed(0)
return absValueRounded - (absValueRounded % 3) - 3;
},
max: function (value) {
const absValue = max_abs(value.min, value.max)
const absValueRounded = (absValue).toFixed(0)
return absValueRounded - (absValueRounded % 3) + 3;
},
},
{
type: 'value',
name: 'Desv. media',
position: 'right',
alignTicks: true,
axisLabel: {
formatter: '{value}°C'
},
min: function (value) {
const absValue = max_abs(value.min, value.max)
const absValueRounded = (absValue * -1).toFixed(0)
return absValueRounded - (absValueRounded % 3) - 3;
},
max: function (value) {
const absValue = max_abs(value.min, value.max)
const absValueRounded = (absValue).toFixed(0)
return absValueRounded - (absValueRounded % 3) + 3;
},
}
]
As you can see, I played with the value 3, which matches all the plot variations (for each meteorology station). Depending on your needs, You can change it or define it dynamically.
I guess that a similar solution is adaptable to other tech stack.
Upvotes: 1