[Bug]:
Would you like to work on a fix?
- Check this if you would like to implement a PR, we are more than happy to help you go through the process.
Current and expected behavior
Description
When initializing a BarChart with stackBars: true and stackMode: 'accumulate-relative', negative values in series data fail to stack below the zero line. Instead, they are incorrectly treated as positive values and placed into posStackedBarValues.
Location in Codebase
src/charts/BarChart/BarChart.ts(lines 438–439 and lines 514–518)
Root Cause Analysis
In BarChart.ts:
const valueX = safeHasProperty(value, 'x') && value.x;
const valueY = safeHasProperty(value, 'y') && value.y;When standard 1D series data is provided (such as series: [[4, -4], [3, -5]]), the values in a vertical bar chart are normalized as { x: undefined, y: -4 }.
Because value does not have an owned property 'x', safeHasProperty(value, 'x') returns false. As a result:
valueX = false;Later, the polarity of the bar is checked to select the stack container:
if (isAccumulateRelativeStackMode) {
stackedBarValues =
valueY >= 0 || valueX >= 0
? posStackedBarValues
: negStackedBarValues;
}In JavaScript, comparing a boolean to a number performs type coercion:
false >= 0; // evaluates to true (0 >= 0)Because valueX >= 0 (false >= 0) is always true, the expression valueY >= 0 || valueX >= 0 evaluates to true for every negative value.
- Negative bars are never placed into
negStackedBarValues. - Negative bars in vertical charts are drawn on top of positive stacks in the positive coordinate space (above zero) rather than accumulating downward into negative space.
- In horizontal bar charts (
horizontalBars: true),valueYevaluates tofalse, causing the identical bug in reverse.
Steps to Reproduce
import { BarChart } from 'chartist';
new BarChart(
'#chart',
{
labels: ['Category 1'],
series: [
[4],
[-4]
]
},
{
stackBars: true,
stackMode: 'accumulate-relative'
}
);Expected vs Actual Behavior
- Expected Behavior: The
4bar starts atzeroPointand extends into positive coordinate space. The-4bar starts atzeroPointand accumulates into negative coordinate space (below the zero line). - Actual Behavior: The
-4bar is placed inposStackedBarValuesand is drawn backwards in positive space fromy=80down toy=100, cancelling out the positive bar.
Suggested Fix
Extract the dimension value based on chart orientation (horizontalBars) and avoid boolean coercion:
const valueX = safeHasProperty(value, 'x') ? value.x : undefined;
const valueY = safeHasProperty(value, 'y') ? value.y : undefined;And in the stack selection logic:
if (isAccumulateRelativeStackMode) {
const activeValue = options.horizontalBars ? valueX : valueY;
stackedBarValues =
activeValue !== undefined && activeValue >= 0
? posStackedBarValues
: negStackedBarValues;
}Reproduction
This can be reproduced directly using the repository's own example at sandboxes/bar/stacked-accumulate-relative/index.ts or with the minimal code below: ```typescript import 'chartist/dist/index.css'; import { BarChart } from 'chartist'; new BarChart( '#chart', { labels: ['Day 1'], series: [ [5], // Positive bar (+5) [-5] // Negative bar (-5) ] }, { stackBars: true, stackMode: 'accumulate-relative' } );
Chartist version
1.5.0 (latest / main)
Possible solution
// ============================================================================ // 1. SOURCE CODE FIX: src/charts/BarChart/BarChart.ts // ============================================================================
// ---------------------------------------------------------------------------- // [Line ~438]: Safely resolve valueX and valueY to number | undefined (not false) // ----------------------------------------------------------------------------
// ❌ BEFORE (BUG): // const valueX = safeHasProperty(value, 'x') && value.x; // const valueY = safeHasProperty(value, 'y') && value.y;
// ✅ AFTER (FIX): const valueX = safeHasProperty(value, 'x') ? value.x : undefined; const valueY = safeHasProperty(value, 'y') ? value.y : undefined;
// ---------------------------------------------------------------------------- // [Line ~513]: Select active dimension based on chart orientation before check // ----------------------------------------------------------------------------
// ❌ BEFORE (BUG): // if (isAccumulateRelativeStackMode) { // stackedBarValues = // valueY >= 0 || valueX >= 0 // ? posStackedBarValues // : negStackedBarValues; // }
// ✅ AFTER (FIX): if (isAccumulateRelativeStackMode) { const activeValue = options.horizontalBars ? valueX : valueY; stackedBarValues = activeValue !== undefined && activeValue >= 0 ? posStackedBarValues : negStackedBarValues; }
// ============================================================================
// 2. UNIT TESTS TO ADD: src/charts/BarChart/BarChart.spec.ts
// ============================================================================
// Add this test block inside the describe('BarChart', () => { ... }) section:
describe('accumulate-relative stackMode', () => { it('should accumulate positive and negative values separately in vertical bar charts', async () => { data = { labels: ['Day 1'], series: [ [5], // Series 0 (Positive value) [-5] // Series 1 (Negative value) ] }; options = { stackBars: true, stackMode: 'accumulate-relative' }; await createChart();
const bars = fixture.wrapper.querySelectorAll('line.ct-bar');
expect(bars.length).toBe(2);
// In SVG coordinates, y values increase downwards:
// - Positive bar ends above zero (smaller y value).
// - Negative bar ends below zero (larger y value).
const posBarY2 = Number(bars[0].getAttribute('y2'));
const negBarY2 = Number(bars[1].getAttribute('y2'));
expect(posBarY2).toBeLessThan(negBarY2);
});
it('should accumulate positive and negative values separately in horizontal bar charts', async () => { data = { labels: ['Category 1'], series: [ [5], // Series 0 (Positive value) [-5] // Series 1 (Negative value) ] }; options = { stackBars: true, stackMode: 'accumulate-relative', horizontalBars: true }; await createChart();
const bars = fixture.wrapper.querySelectorAll('line.ct-bar');
expect(bars.length).toBe(2);
// In horizontal SVG coordinates, x values increase rightwards:
// - Positive bar extends to the right of zero (larger x value).
// - Negative bar extends to the left of zero (smaller x value).
const posBarX2 = Number(bars[0].getAttribute('x2'));
const negBarX2 = Number(bars[1].getAttribute('x2'));
expect(posBarX2).toBeGreaterThan(negBarX2);
}); });
Source: chartist-js/chartist