chartBarNegative
A Charts block/component from shadcn/ui — clean-room reimplemented for Domphy (see methodology). Call chartBarNegative() with no arguments for a working demo, or edit the code below live.
Props
| Prop | Type | Description |
|---|---|---|
data | ChartBarPoint[] | — |
seriesLabel | string | — |
positiveColor | ThemeColor | — |
negativeColor | ThemeColor | — |
title | string | — |
subtitle | string | — |
trendText | string | — |
trendDirection | ChartTrendDirection | — |
captionText | string | — |
height | number | — |
Implementation notes
Bars diverge above/below zero depending on sign; per-datapoint color is resolved via familyHex() (a theme-token-derived hex, matching the established chart-option-color idiom already used by chart-area-shared.ts's chartColorRgba — WebGL per-item itemStyle.color must be a literal hex, not a reactive theme function) and passed as {value, itemStyle:{color}} per bar. A markLine at yAxis draws the dashed zero baseline (dash style/color are the engine's own fixed markLine styling, not independently configurable). Each bar's month label is drawn just outside its own tip (above for positive, below for negative) via a dedicated SVG overlay (chartBarSignedLabelOverlay), since the engine's built-in bar-label renderer only supports a flat 'top'/'inside'/'bottom' position with no sign-aware branching. Tooltip cursor is axisPointer:'none' per spec (no separate value axis — the zero line is the only baseline).
Status: ported · Reference: shadcn/ui original
// shadcn/ui "chart-bar" (negative recipe) — clean-room reimplementation.
//
// A single-series bar chart whose bars diverge above or below a zero
// baseline depending on sign, colored differently for gains vs losses, with
// each month's label printed just outside its own bar's tip instead of on a
// conventional axis row.
//
// Implemented purely from the block's public functional/visual spec — no
// upstream shadcn/ui source was viewed or copied.
import type { ChartOption, TooltipParams } from "@domphy/chart";
import type { DomphyElement } from "@domphy/core";
import {
CHART_BAR_NEGATIVE_DATA,
type ChartBarGrid,
type ChartBarPoint,
type ChartTrendDirection,
chartBarCardShell,
chartBarColorHex,
chartBarFrame,
chartBarSeriesColor,
chartBarSignedDomain,
chartBarSignedLabelOverlay,
chartBarTooltipRow,
chartBarTrendFooter,
} from "./chart-bar-shared.js";
export interface ChartBarNegativeProps {
data?: ChartBarPoint[];
seriesLabel?: string;
/** Theme role (resolved at shift-9) or literal ramp hex. */
positiveColor?: string;
/** Theme role (resolved at shift-9) or literal ramp hex. */
negativeColor?: string;
title?: string;
subtitle?: string;
trendText?: string;
trendDirection?: ChartTrendDirection;
captionText?: string;
height?: number;
}
const GRID: ChartBarGrid = { left: 8, right: 8, top: 28, bottom: 28 };
/**
* shadcn/ui "chart-bar" negative recipe — bars diverge above/below a zero
* baseline, colored by sign. Call with no arguments for a working demo.
*/
function chartBarNegative(
props: ChartBarNegativeProps = {},
): DomphyElement<"div"> {
const {
data = CHART_BAR_NEGATIVE_DATA,
seriesLabel = "Visitors",
// Upstream fills positive bars with var(--chart-1) and negative bars with
// var(--chart-2) — two steps of the same monochrome blue ramp.
positiveColor = chartBarSeriesColor(0).hex,
negativeColor = chartBarSeriesColor(1).hex,
title = "Bar Chart - Negative",
subtitle = "January - June 2026",
trendText = "Trending up by 5.2% this month",
trendDirection = "up",
captionText = "Showing visitor change for the last 6 months",
height = 64,
} = props;
const categories = data.map((point) => point.label);
const values = data.map((point) => point.value);
const valueDomain = chartBarSignedDomain(values);
const positiveHex = chartBarColorHex(positiveColor);
const negativeHex = chartBarColorHex(negativeColor);
const option: ChartOption = {
tooltip: {
trigger: "axis",
axisPointer: { type: "none" },
// Upstream renders this recipe's tooltip with hideLabel + hideIndicator —
// the month name and color dot are both dropped, only the series name +
// value line shows.
formatter: chartBarNegativeTooltipFormatter(seriesLabel),
},
xAxis: {
type: "category",
data: categories,
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
},
yAxis: {
type: "value",
min: valueDomain[0],
max: valueDomain[1],
axisLine: { show: false },
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: true },
},
grid: GRID,
series: [
{
type: "bar",
name: seriesLabel,
data: values.map((value) => ({
value,
itemStyle: { color: value > 0 ? positiveHex : negativeHex },
})),
},
],
};
return chartBarCardShell({
title,
subtitle,
content: {
div: [
chartBarFrame(option, {
height,
overlays: [
chartBarSignedLabelOverlay({
categories,
values,
valueDomain,
grid: GRID,
}),
],
}),
],
},
footer: chartBarTrendFooter({
trendText,
direction: trendDirection,
captionText,
}),
});
}
function escapeTooltipHtml(text: string): string {
return text
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">");
}
function chartBarNegativeTooltipFormatter(
seriesLabel: string,
): (parametersInput: TooltipParams | TooltipParams[]) => string {
return (parametersInput) => {
const parameters = Array.isArray(parametersInput)
? parametersInput
: [parametersInput];
if (parameters.length === 0) return "";
const value = escapeTooltipHtml(String(parameters[0].value ?? ""));
// Upstream negative chart: <ChartTooltipContent hideLabel hideIndicator />
// — no swatch, name muted, value mono medium foreground.
return chartBarTooltipRow("", escapeTooltipHtml(seriesLabel), value);
};
}
export { chartBarNegative };