Horizon Chart — Free HTML CSS JS Snippet

Horizon Chart · Charts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Folds each full-range time series into overlapping color bands via SVG clip-paths, not a naive height-scaled area chart
Fits six dense hourly time series (168 points each) into a fraction of the vertical space six full area charts would need
Realistic synthesized per-host CPU data: random-walk level, daily sinusoidal cycle, and occasional spikes, not flat noise
Configurable BANDS constant controls fold count and color granularity per row
Sequential color ramp legend shown once beneath the whole chart, shared by every row
Native SVG tooltips report each row's average and peak value on hover
Decreasing per-band opacity reinforces which layer is "on top" at any given point
Pure hand-written SVG paths and clip-paths — no charting library

About this UI Snippet

Horizon Chart — Folded Color-Band Time Series in Minimal Vertical Space

Screenshot of the Horizon Chart snippet rendered live

A horizon chart is a space-efficient technique for displaying many dense time series at once by "folding" each series' value range into a small number of overlapping color bands, instead of giving every series its own full-height area chart. The result looks unusual at first — thin colored stripes rather than familiar peaks and valleys — but it lets six, twelve, or dozens of series stack in the vertical space that a handful of ordinary Area Charts would need, which is exactly why monitoring dashboards (server metrics, sensor arrays, financial time series) use the technique when screen space is scarce and dozens of series must be compared at a glance.

The folding technique, concretely

For a row with BANDS = 4 color bands and a value range of 0-100, the value range is divided into four equal slices (0-25, 25-50, 50-75, 75-100). Rather than compressing the whole 0-100 range into the row's height (which would make small values invisible), each band redraws the *entire* series as its own area, but shifted so that band's floor value sits at the row's bottom — shifted = Math.max(0, v - bandFloor). A value of 60 therefore appears as "full height" in band 0 (0-25, clipped since it exceeds the band), still substantial in band 1 (25-50, also clipped), and correctly partial in band 2 (50-75) — every band below the value's actual band renders as a full, clipped block of its own color, and the topmost relevant band shows the "remainder." Layering these clipped bands with decreasing opacity per band is what produces the characteristic banded-color read: deeper/more saturated color visible through more layers signals a higher value.

Why SVG clip-paths do the heavy lifting

Each band is drawn as a full-height area path that would otherwise overflow the row (since a shifted value can go far above the row's actual pixel height), then constrained with clip-path: url(#clipId) referencing a <clipPath> containing a simple rect matching the row's exact bounds. This is what turns an oversized, overflowing shape into a correctly-cropped band — without the clip-path, higher bands would draw far outside the intended row and overlap neighboring rows.

Reading a horizon chart

Once the eye adjusts, the technique reads quickly: a row that stays a light, single color the whole width was consistently low; a row that frequently shows the darkest red band was frequently near its maximum; a sudden burst of dark color in an otherwise light row is a spike. The legend strip beneath the chart shows the same color ramp used per band, from the coolest (lowest) to the hottest (highest) fold.

Synthesizing realistic per-host CPU data

genSeries(seed) is not pure random noise — it layers a slow random-walk "level," a daily sinusoidal cycle (higher during work hours, matching real server load patterns), and occasional random spikes, clamped to a 0-100 range, so each of the six simulated hosts in the demo produces a visually distinct but plausible week of hourly CPU-load data.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the shifted-and-clipped area path in drawRow() produces the folded color-band effect, and why the clip-path is essential rather than optional for keeping each band confined to its row. It's also a good candidate for extension — ask it to add a mirrored negative-value band style (horizon charts conventionally use a second color ramp below zero for series that can go negative), add a synchronized crosshair that highlights the same time index across every row on hover, or make the row order sortable by average or peak value.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a horizon chart in plain HTML, CSS, and JavaScript using inline SVG created with createElementNS — no charting library, no canvas.

Requirements:
- Render several rows, one per named series (e.g. "web-01", "db-primary"), each a single thin horizontal strip (e.g. 30-40px tall) rather than a full-height area chart.
- For each row, divide the series' known value range (e.g. 0-100) into a configurable number of equal-sized "fold bands" (e.g. 4). For each band, draw the row's ENTIRE value series as its own filled area path, shifted vertically so that band's floor value sits at the bottom of the row, then constrain that (intentionally oversized) shape to the row's exact pixel bounds using an SVG clip-path referencing a clipPath element with a rect matching the row's width and height.
- Give each band a distinct color from an ordered low-to-high color ramp, with later (higher-value) bands drawn on top of earlier ones and with a slightly different opacity per band so the layered folding is visually legible.
- Include a data-generation function that produces a realistic multi-day or multi-week hourly time series per row — combine a slowly-drifting baseline, a repeating daily cycle, and occasional randomized spikes, clamped to the known value range, rather than pure uniform random noise.
- Label each row with its series name to the left of its band strip.
- Add a native tooltip (or equivalent) on each row reporting that series' average and peak value across the full period.
- Render a single shared legend strip beneath the whole chart showing the low-to-high color ramp used by every row's bands.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Read color depth as magnitudeA row showing only the lightest band color stayed low the whole period; darker, more saturated color means the value climbed into higher bands.
  2. 2
    Compare rows at a glanceBecause each row is compressed to a single thin strip, scanning down the list of hosts quickly reveals which ones ran consistently hot versus consistently light.
  3. 3
    Hover a row for exact numbersEach row carries a native tooltip reporting that host's average and peak value across the full period.
  4. 4
    Adjust the number of fold bandsChange the BANDS constant — more bands give finer color gradation per row at the cost of more subtle distinctions between adjacent bands.
  5. 5
    Swap in real time-series dataReplace genSeries() with your own array of numeric values per row; drawRow() works with any array of the same length across rows.
  6. 6
    Recolor the rampEdit the RAMP array to any ordered low-to-high color sequence — sequential ramps read best since each band must be visually orderable.

Real-world uses

Common Use Cases

Infrastructure and server monitoring dashboards
The canonical use case — compare CPU, memory, or request-rate load across dozens of hosts in the space a handful of line charts would otherwise need.
Financial time-series comparison
Stack many securities' price or volatility series compactly, a well-known application of horizon charts in trading dashboards.
Sensor and IoT fleet visualization
Compare readings (temperature, battery level, signal strength) across a large fleet of devices in one scrollable, compact view.
Teaching SVG clip-path techniques
A concrete, practical example of using clip-path to constrain an intentionally-oversized shape, alongside the Area Chart and Streamgraph Chart for comparing area-based chart techniques.
Reference for dense small-multiple charting
The row-per-series, folded-band pattern generalizes to any dashboard needing to compare many series' magnitude trends without needing to see their exact numeric shape.

Got questions?

Frequently Asked Questions

Instead of scaling a value's full 0-100 range down to fit the row's pixel height (which would make low values nearly invisible), the value range is divided into several equal bands. Each band redraws the entire series shifted so that band's floor value sits at the bottom, then clips the result to the row's bounds. A high value fills every band below its own fully and shows a partial remainder in its own band, so several bands of color stacked visually communicate the value that a single, tiny-scale line could not show clearly in the same space.

Each band's shifted area path is drawn at full, potentially oversized height — a value of 90 in the top band could draw far above the row's actual pixel bounds. The clip-path (a <clipPath> containing a rect matching the row's exact width and height) crops that oversized shape down to exactly the intended row, which is what keeps every band visually confined to its own row instead of overlapping neighboring rows.

genSeries(seed) combines three signals: a slowly random-walking baseline level, a daily sinusoidal cycle peaking during simulated work hours, and small-probability random spikes, all clamped to a 0-100 range — producing plausible, visually distinct per-host CPU load data across a simulated week of hourly samples.

Edit the BANDS constant in the JS panel. More bands give finer-grained color distinctions per unit of value but make adjacent bands more subtly different in opacity; fewer bands are bolder but coarser.

Yes — replace genSeries() with your own function or static array returning one numeric value per time step for each row. drawRow() only requires an array of numbers and works identically regardless of how that data was produced.

A thin line chart at a small fixed height loses most of its readability once compressed — you can barely tell peaks from noise. A horizon chart deliberately trades exact shape for a color-coded magnitude read, which stays legible even at a very small row height, making it the better choice specifically when you need to fit many series into limited vertical space.