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 — 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: 'Net cash flow ($k)', base: 0, minMarginFraction: 0.15 }],
series: [
{
property: 'net',
title: 'Net cash flow',
renderer: 'bar',
labelProperty: 'net',
labelPosition: 'outside',
labelFormat: ',.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,
basedefaults tonull— the axis domain minimum — so mixed-sign bars would all grow upward from the most negative value.base: 0makes bars grow out of zero in both directions, and the divide is marked by the default base line (showBaseLine, styled withbaseLineStyle). - The domain fits the data on both sides. When the values might sit all on one side,
softMin/softMax0hold zero in view without ever clipping data — unlikemin/max, which clamp hard. - Labels flip with the sign: an
outsidelabelPositionsits above positive bars and below negative ones automatically. Each side can be tuned separately with thelabelAboveBase*/labelBelowBase*family — position, offset, and min/max position bounds — whoseautodefaults inherit the shared knobs. - The sign carries through the other bar features: caps point downward on negative bars, and
colorScale.base.value: 0colors 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 — no pinning 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: '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 the shared base: 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
baseto0— no pinning 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.