Skip to content

Interactive Answers

Diverging Bars

Stacked daily columns with one series hanging below zero on its own scale — triaged vs escalated, solved vs handed off.

2 variantsMIT

Preview

Alerts by source

Security agent · last 30 days

34,728alerts triaged in 30 days

99.3% closed without a human
1.3k2.5k025Sep 12Sep 16Sep 20Sep 24Sep 28Oct 2Oct 6Oct 10
Alerts by source
LabelIdentityEndpointCloudEmailNetworkSaaSEscalatedTotal
Sep 12420233143113685361k
Sep 13251150998453445681
Sep 14347171926356394768
Sep 15548375178175817971.4k
Sep 16569243199122975991.3k
Sep 17481300205178726381.3k
Sep 18433247156127786271.1k
Sep 19610288146166855591.4k
Sep 20336158809147305742
Sep 213752021209254414884
Sep 225273511571531096391.4k
Sep 23565239196118945791.3k
Sep 24653335189137748791.5k
Sep 25430243152123756071.1k
Sep 26435249158129806471.1k
Sep 273131331096753325707
Sep 28334157799046295735
Sep 29422235145115695461k
Sep 305273511571531096391.4k
Oct 1439253162133836781.1k
Oct 28053621668957170252.4k
Oct 3612290148169875791.4k
Oct 43221431187761394760
Oct 53742011209253404880
Oct 66573401941417855101.5k
Oct 7568241198121965891.3k
Oct 8446261169140887291.2k
Oct 9603280234159808581.4k
Oct 105363611661627170101.4k
Oct 11339162849550335763

Installation

Copies the component into components/ui and installs its dependencies and theme tokens.

pnpm dlx shadcn@latest add https://tuffui.com/r/diverging-bars.json

Usage

Pass your own data and wire the callbacks to your agent runtime.

import { DivergingBars } from "@/components/ui/diverging-bars";

// A security agent's month: alerts by source, escalations hanging below.
export function TriageVolume({ report }: { report: TriageReport }) {
  return (
    <DivergingBars
      title="Alerts by source"
      subtitle="Security agent · last 30 days"
      labels={report.days.map((d) => d.label)}
      series={report.sources.map((s) => ({
        id: s.id,
        label: s.name,
        values: report.days.map((d) => d.bySource[s.id] ?? 0),
      }))}
      below={{
        id: "escalated",
        label: "Escalated",
        values: report.days.map((d) => d.escalated),
      }}
      headline={{
        value: report.total.toLocaleString("en-US"),
        caption: "alerts triaged in 30 days",
      }}
      aside={`${report.autoClosedPct}% closed without a human`}
      onSelect={(i) => openDay(report.days[i].date)}
    />
  );
}

Props

PropTypeDefaultDescription
labels*string[]—One label per column, e.g. a date. Every few are drawn under the chart.
series*DivergingSeries[]—Stacked upward as separate segments, first series at the bottom. Colors default to the palette by position.
belowDivergingSeries—One series that hangs below the zero line on its own smaller scale. Defaults to the negative color.
title*string—Header title.
subtitlestring—Secondary line under the title.
headline{ value: string; caption?: string }—Big number above the chart with a caption. Rolls when it changes.
asideReactNode—Right side of the headline row, e.g. a secondary stat.
format(value: number) => stringcompact (1.2k)Formats ticks, the tooltip and the hidden table.
onSelect(index: number) => void—Called when a column is hovered, clicked or reached with the arrow keys.
classNamestring—Extra classes for the root element.
type DivergingSeries = {
  id: string;
  label: string;
  values: number[]; // one per label
  color?: string; // any CSS color
};

// Legend chips toggle series; at least one stacked series stays on.
// The tooltip total is the stack; the series below is listed but not added in.