#1471·chartist

[Bug]:

Author: codeCraft-RitikCreated Sep 10, 2026Updated Sep 10, 2026
Labelsbug

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

In Axis.createGridAndLabels, the available space for an axis label (labelLength) is calculated by measuring the distance to the next projected tick. However, when the next tick coordinate is 0, a truthiness check (if (projectedValues[index + 1])) evaluates to false, causing Chartist to incorrectly assume that no subsequent tick exists.

Location in Codebase

Root Cause Analysis

In src/axes/Axis.ts:

typescript
let labelLength;
if (projectedValues[index + 1]) {
  // If we still have one label ahead, we can calculate the distance to the next tick / label
  labelLength = projectedValues[index + 1] - projectedValue;
} else {
  // If we don't have a label ahead...
  labelLength = Math.max(
    this.axisLength - projectedValue,
    this.axisLength / this.ticks.length
  );
}

When an axis contains ticks where one tick projects to coordinate 0 (very common on bipolar scales or auto-scaled charts spanning negative to positive ranges, e.g. [-50, 0, 50]):

  • At index = 0 (tick -50), the next tick's projected coordinate is projectedValues[1] = 0.
  • In JavaScript, if (0) evaluates to false.
  • Instead of calculating the distance to the next tick (0 - (-50) = 50px), the code executes the else branch, setting labelLength to Math.max(axisLength - (-50), ...) (e.g. 450px).
  • This passes an excessively large dimension to createLabel(), setting an oversized width/height on the label <span> and <foreignObject>, which leads to text overlapping and distorted layout bounding boxes.

Expected vs Actual Behavior

  • Expected: When projectedValues = [-50, 0, 50] at index = 0, labelLength should be 0 - (-50) = 50.
  • Actual: if (0) evaluates to false, causing the fallback calculation to assign labelLength = Math.max(axisLength - (-50), ...).

Suggested Fix

Check for index / undefined existence instead of numeric truthiness:

typescript
if (projectedValues[index + 1] !== undefined) {
  labelLength = projectedValues[index + 1] - projectedValue;
} else {
  ...
}

Reproduction

typescript

### Chartist version

1.5.0 (latest / main)

### Possible solution

// ============================================================================
// 1. SOURCE CODE FIX: src/axes/Axis.ts
// ============================================================================

// ❌ BEFORE (BUG):
// let labelLength;
// if (projectedValues[index + 1]) {
//   labelLength = projectedValues[index + 1] - projectedValue;
// } else { ... }

// ✅ AFTER (FIX):
let labelLength;
if (projectedValues[index + 1] !== undefined) {
  labelLength = projectedValues[index + 1] - projectedValue;
} else {
  labelLength = Math.max(
    this.axisLength - projectedValue,
    this.axisLength / this.ticks.length
  );
}



// ============================================================================
// 2. UNIT TEST TO ADD: src/axes/Axis.spec.ts
// ============================================================================
// Add this test to `describe('Axis', () => { ... })` in `src/axes/Axis.spec.ts`:

it('should correctly calculate labelLength when next projected value is 0', () => {
  const chartRect = {
    x1: 50,
    x2: 450,
    y1: 300,
    y2: 50,
    padding: { top: 0, right: 0, bottom: 0, left: 0 },
    width() { return this.x2 - this.x1; },
    height() { return this.y1 - this.y2; }
  };

  class TestAxis extends Axis {
    projectValue(val: number) {
      return val; // Returns raw projected values: -50, 0, 50
    }
  }

  const axis = new TestAxis(axisUnits.x, chartRect as any, [-50, 0, 50]);
  const gridGroup = new Svg('g');
  const labelGroup = new Svg('g');
  const eventEmitter = new EventEmitter();

  const emittedLabels: any[] = [];
  eventEmitter.on('draw', data => {
    if (data.type === 'label') emittedLabels.push(data);
  });

  axis.createGridAndLabels(gridGroup, labelGroup, {
    axisX: {
      showGrid: true,
      showLabel: true,
      labelInterpolationFnc: (v: any) => v,
      offset: 30,
      position: 'end',
      labelOffset: { x: 0, y: 0 }
    },
    axisY: {} as any,
    classNames: { grid: 'ct-grid', label: 'ct-label', horizontal: 'ct-horizontal', end: 'ct-end' }
  } as any, eventEmitter);

  // For index 0 (value -50) where next tick is 0:
  // Distance should be 0 - (-50) = 50px, not the entire axis length fallback
  const firstLabelSpan = labelGroup.getNode().querySelectorAll('span')[0];
  expect(firstLabelSpan.style.width).toBe('50px');
});