Back to Langfuse

ChartingPrinciples

web/src/features/widgets/chart-library/ChartingPrinciples.mdx

3.203.19.7 KB
Original Source

import { Meta, Canvas } from "@storybook/addon-docs/blocks"; import * as Principles from "./ChartingPrinciples.stories";

<Meta title="Design System/Charts/Principles" />

export const RED = "#E4002B"; export const FG = "hsl(var(--foreground))"; export const BG = "hsl(var(--background))"; export const MUTED = "hsl(var(--muted-foreground))"; export const BORDER = "hsl(var(--border))"; // RED is the one fixed accent (reads on both themes); FG/BG/MUTED/BORDER track // the app theme. The bold slabs (hero, principle numbers) stay a fixed // ink/paper in BOTH themes — flipping them to a light block via the theme vars // looked washed-out and wrong in dark. export const INK = "#0B0B0B"; export const PAPER = "#F3F1EA";

export const Page = ({ children }) => (

<div style={{ background: BG, color: FG, padding: "56px clamp(24px, 6vw, 88px)", fontFamily: "ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif", }} > {/* Storybook's docs theme paints the whole page (and every example card) light, and can't follow our per-toggle theme. Pull the page background, the side padding, and the example-card chrome onto the app's themed vars so the article is fully dark in dark mode — no white frame, no white padding. */} <style>{` .sbdocs-wrapper{background:hsl(var(--background)) !important;padding:0 !important;} .sbdocs-content{max-width:none !important;padding:0 !important;} .sbdocs.sbdocs-preview{background:transparent !important;border:0 !important;box-shadow:none !important;} `}</style> <div style={{ maxWidth: 880, margin: "0 auto" }}>{children}</div> </div> );

export const Hero = () => (

<div style={{ position: "relative", overflow: "hidden", background: INK, color: PAPER, border: `1px solid ${BORDER}`, padding: "72px 48px 64px", marginBottom: 64, }} > <div style={{ position: "absolute", right: -60, top: -60, width: 280, height: 280, background: RED, transform: "rotate(18deg)", }} /> <div style={{ position: "relative" }}> <div style={{ fontSize: 12, fontWeight: 800, letterSpacing: "0.4em" }}> LANGFUSE · CHART LIBRARY </div> <h1 style={{ color: PAPER, fontSize: "clamp(56px, 11vw, 104px)", lineHeight: 0.88, fontWeight: 900, letterSpacing: "-0.045em", margin: "16px 0 0", }} > DRAW
    ANYTHING.
  </h1>
  <p
    style={{
      color: PAPER,
      fontSize: 22,
      fontWeight: 600,
      maxWidth: 540,
      marginTop: 24,
      opacity: 0.85,
    }}
  >
    Eight moves for rendering any data — clean or noisy, sparse or
    overloaded — legibly, every time.
  </p>
</div>
</div> );

export const Lede = () => (

<div style={{ borderLeft: `10px solid ${RED}`, paddingLeft: 24, margin: "0 0 56px", fontSize: "clamp(24px, 4vw, 34px)", fontWeight: 900, lineHeight: 1.12, letterSpacing: "-0.02em", maxWidth: 680, }} > The data carries the weight.
The frame stays quiet.
</div> );

export const Slab = ({ n, t, lead, children }) => (

<section style={{ margin: "0 0 24px" }}> <div style={{ display: "flex", gap: 22, alignItems: "flex-start" }}> <div style={{ flex: "0 0 auto", width: 60, height: 60, background: INK, color: PAPER, border: `1px solid ${BORDER}`, fontSize: 28, fontWeight: 900, display: "flex", alignItems: "center", justifyContent: "center", }} > {n} </div> <div style={{ paddingTop: 2 }}> <h2 style={{ fontSize: "clamp(22px, 3.4vw, 30px)", fontWeight: 900, letterSpacing: "-0.02em", textTransform: "uppercase", margin: "0 0 8px", lineHeight: 1.02, }} > {t} </h2> {lead && ( <p style={{ fontSize: 18, fontWeight: 700, margin: "0 0 6px", color: RED, }} > {lead} </p> )} <div style={{ fontSize: 16, lineHeight: 1.55, maxWidth: 660 }}> {children} </div> </div> </div> </section> );

export const Plate = ({ children }) => (

<div style={{ margin: "20px 0 44px", paddingLeft: 82 }}>{children}</div> );

export const Rule = () => (

<div style={{ height: 6, background: FG, margin: "8px 0 40px" }} /> ); <Page> <Hero /> <Lede />

A chart here is not a picture we compose by hand. It is the last step of a pipeline — data → prepare → visualise → pixels — and the renderer decides nothing. These eight moves are what the prepare step decides so that any data, from one clean series to two hundred noisy ones, comes out legible. They are deliberately absolute. Read them as commandments, argue with them in the preparer, never in the chart component.

<Rule /> <Slab n="1" t="Draw what was measured" lead="No invented shape between points."> Straight segments are the honest default — they claim only the points we have. A smooth curve <strong>invents values that were never sampled</strong>; reach for it only when you mean to. Stepped lines are for data that holds until it changes (states, counters). The line is a record, not a decoration. </Slab> <Plate> <Canvas of={Principles.Interpolation} sourceState="none" /> </Plate>

<Slab n="2" t="Missing is a gap, not a zero" lead="A hole in the data is the truth."

A null breaks the line. Substituting <strong>0</strong> to "fill" the hole manufactures a measurement that never happened — the single most common way a chart lies. Bridge a gap only when the series genuinely continues across it; break the line when a gap runs longer than the sampling cadence. Zero is a value, not a synonym for absence. </Slab>

<Plate> <Canvas of={Principles.NullHandling} sourceState="none" /> </Plate>

<Slab n="3" t="Show certainty in the ink" lead="One grammar for less-sure data."

A still-aggregating final bucket and a previous-period comparison are real, but they are <strong>less certain</strong> than confirmed history. Don't drop them and don't draw them as solid fact — render them dotted and paled. One treatment, used everywhere, so a reader learns the grammar once and trusts it. </Slab>

<Plate> <Canvas of={Principles.Certainty} sourceState="none" /> </Plate>

<Slab n="4" t="Hover is a timeline, not a panel" lead="Snap to real samples; share the crosshair."

The cursor between two points must never conjure a reading that isn't there — the crosshair tracks the cursor, the value snaps to the nearest real sample, and near a gap the snap tightens so a tooltip never floats over emptiness. Charts on one time range share <strong>one</strong> vertical marker; only the chart under the cursor opens a tooltip. Emphasis lives in the tooltip and the legend — the canvas stays calm. </Slab>

<Plate> <Canvas of={Principles.HoverTimeline} sourceState="none" /> </Plate> <Slab n="5" t="Color is identity" lead="Assign once. Read it back."> A series' color belongs to the series, not to its position in a list. Derive it once and let the legend read it back, so swatch and line can never diverge and the same entity keeps its color across every chart on the board. The palette is bounded, and a colorblind-safe option always exists. </Slab> <Slab n="6" t="Trust the scale" lead="Type-driven, adaptive formatting."> Numbers, durations, bytes, percentages, currency, dates — each formats by its kind and by the magnitude the scale chose. One unit per axis. Tick precision follows the scale; tooltip precision is capped for reading; digits are tabular so they never jitter. Don't out-clever the axis engine — format the spacing it gives you. </Slab> <Plate> <Canvas of={Principles.QuietChrome} sourceState="none" /> </Plate> <Slab n="7" t="Spend ink on data" lead="Quiet the frame."> Faint horizontal gridlines, no axis spine, muted labels, no data-point dots by default, fills barely there, no animation on the series. Every pixel of chrome is a pixel stolen from the data. The grid is scaffolding — build the building, then take the scaffolding down. </Slab> <Slab n="8" t="Bound the frame, not the data" lead="Top-N, and say so."> Two hundred series is not a chart, it is a wall. Draw a legible top-N, rolled by magnitude, and state the truth in the open —{" "} <em>“showing top 25 of 487.”</em> Bound the legend, bound the density upstream (about one point per pixel). Never buy legibility by silently throwing data away where no one can see you do it. </Slab> <Plate> <Canvas of={Principles.BoundTheFrame} sourceState="none" /> </Plate> <Rule /> <div style={{ background: RED, color: "#fff", padding: "40px 44px", fontSize: "clamp(22px, 4vw, 32px)", fontWeight: 900, lineHeight: 1.15, letterSpacing: "-0.02em", }} > One adaptable pipeline. No special cases.

Teach the preparer a new shape — never the component a new exception.

</div> <p style={{ fontSize: 14, color: MUTED, marginTop: 28 }}> The full architecture and the one-way{" "} <strong>data → prepare → visualise</strong> rule live in{" "} <code>web/src/features/widgets/chart-library/ARCHITECTURE.md</code>. For how overlays escape the chart frame, see the layer system in{" "} <code>web/src/components/ui/layer.tsx</code> (Design System → Overlay Layers). </p> </Page>