Skip to content

Positive and negative values ​

Values below zero need one thing decided: where bars grow from. Pin base to 0 and everything else (bar direction, label placement, caps, stacking) follows the sign on its own.

ts
// Without stacks the axis base defaults to the domain minimum, so mixed-sign
// bars would all grow up from the most negative value. Setting base: 0
// makes them grow out of zero instead. The outside labels flip on their
// own: above positive bars, below negative ones.
import type { MochartInputConfig } from '@mochart/core';

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Monthly Net Cash Flow' },
  categoryAxis: { property: 'month', type: 'string', scale: 'ordinal' },
  // The extra min margin leaves room for the lowest bar's outside label.
  valueAxes: [{ id: 'VA0', title: { text: 'Net cash flow ($k)' }, base: 0, minMarginFraction: 0.15 }],
  series: [
    {
      property: 'net',
      title: 'Net cash flow',
      renderer: 'bar',
      labelProperty: 'net',
      label: {
        position: 'outside',
        format: ',.0f'
      }
    }
  ]
};

export const data = [
  { month: 'Jan', net: 42 },
  { month: 'Feb', net: 28 },
  { month: 'Mar', net: -15 },
  { month: 'Apr', net: 33 },
  { month: 'May', net: -8 },
  { month: 'Jun', net: 51 },
  { month: 'Jul', net: 12 },
  { month: 'Aug', net: -24 }
];

How it works ​

  • Without stacks, base defaults to null (the axis domain minimum), so mixed-sign bars would all grow upward from the most negative value. base: 0 makes bars grow out of zero in both directions, and the divide is marked by the default base line (baseLine.visible, styled with baseLine.style).
  • The domain fits the data on both sides. When the values might sit all on one side, softMin / softMax 0 hold zero in view without ever clipping data, unlike min/max, which clip (see axis bounds).
  • Labels flip with the sign: an outside label.position sits above positive bars and below negative ones automatically. Each side can be tuned separately through label.aboveBase / label.belowBase (position, offset, and min/max position bounds), whose auto defaults follow the shared settings (the below-base offset mirrors label.offset).
  • The sign carries through the other bar features: caps point downward on negative bars, and colorScale.base.value: 0 colors gains and losses from two different ramps.

Stacking mixed signs ​

ts
// In a stack, each sign accumulates separately from the shared zero base:
// positive segments stack upward, negative segments stack downward. An axis
// with stacks defaults its base to 0, so no pinning is needed.
import type { MochartInputConfig } from '@mochart/core';

export const config: MochartInputConfig = {
  version: '1.0.0',
  title: { text: 'Quarterly Cash Flow' },
  categoryAxis: { property: 'quarter', type: 'string', scale: 'ordinal' },
  valueAxes: [{ id: 'VA0', title: { text: 'Cash flow ($k)' } }],
  seriesDefaults: { renderer: 'bar' },
  series: [
    { property: 'sales', title: 'Sales' },
    { property: 'services', title: 'Services' },
    { property: 'payroll', title: 'Payroll' },
    { property: 'infrastructure', title: 'Infrastructure' }
  ],
  seriesStacks: [{ id: 'cashflow' }]
};

export const data = [
  { quarter: 'Q1', sales: 68, services: 22, payroll: -48, infrastructure: -18 },
  { quarter: 'Q2', sales: 75, services: 26, payroll: -50, infrastructure: -21 },
  { quarter: 'Q3', sales: 71, services: 31, payroll: -52, infrastructure: -19 },
  { quarter: 'Q4', sales: 83, services: 34, payroll: -55, infrastructure: -23 }
];
  • Each sign accumulates separately from zero: a category's positive segments stack upward while its negative segments stack downward, so inflows and outflows read as two piles growing out of zero. An axis with stacks defaults its base to 0, so no pinning is needed.
  • Filtering a series in the legend re-stacks its own side and leaves the other untouched.
  • The stack's outer caps apply per sign: the topmost positive segment and the bottommost negative one each get one.
  • To show the net total across both piles, overlay a line series opted out of the stack with stack: null (see stacked bars); for a running total that crosses zero step by step, that's the waterfall.

Released under the MIT License.