Skip to content
KONIGI Patterns

Dashboards / Interaction / Compare periods

1 of 8 ›

Compare periods

People need to compare periods to judge whether a change is unusual.

Updated September 10, 2026

Problem

A current value does not show whether a change is unusual or part of a recurring pattern.

Solution

Show the current and comparison periods on the same scale, with the comparison line visually secondary. Label both periods clearly.

Choose a comparison that fits the metric’s rhythm. The previous period, the same weekday last week, and the same season last year answer different questions. Make the selected basis visible on the chart.

Handle incomplete periods, clock changes, and holidays explicitly. Do not let panels with different comparison settings appear to use the same basis.

Use when

People need to compare the shape of activity across periods, especially when the metric follows a cycle.

Don’t use when

The underlying population changed. Comparing this week against last week is meaningless if a marketing campaign doubled acquisition in between, and the chart will look like a product improvement. Also skip it where an anomaly band already encodes expected range more compactly.

Trade-offs

A second period adds visual clutter and more data to load. Small differences can look more meaningful than they are. Holidays and unequal periods can mislead unless the comparison is explained.

Checklist

  • What is the comparison base, and is it stated on the chart?
  • Does the base align day of week, or does it manufacture a weekly cycle?
  • Is the comparison rendered subordinate to the current period?
  • Is only some of the page shifted, and would anyone notice?
  • Did the underlying population change between the periods?
  • How are clock changes, leap days and moving holidays handled?
  • Is the difference also available as a number, not only as two lines?
  • Is a 4% gap distinguishable from normal variation, and does anything say so?
  • Does the comparison survive a time-range change, and does it rescale sensibly?
  • Would an anomaly band say the same thing with half the ink?

Compare

Grafana implements this as a per-panel time shift relative to the dashboard range, which is flexible and lets one panel silently compare a different window from its neighbours. Plausible and similar analytics tools make period comparison a page-level toggle, so every number and chart shifts together and the base is displayed once where it cannot be missed. Datadog offers explicit day-before, week-before and month-before overlays as graph options, naming the base in the UI rather than expressing it as an offset someone has to decode. Amplitude pushes past overlays into cohort comparison, which answers the population-changed objection this pattern otherwise has no response to. Cloudflare Radar draws the previous period as a dotted line on the same axis by default, with no control to turn it off. The comparison is how the chart is drawn rather than a feature a viewer has to find, which is the one arrangement where nobody forgets to switch it on.

Delta indicator reduces a period comparison to one value. Time-range picker sets the current window. Cohort grid compares groups at the same age.

Anomaly band summarizes an expected range. Time series displays both periods.

Compare periods anatomy One chart carrying this week solid and last week dashed and quieter, so the current period reads first. Below, the three honest comparison bases drawn against the same axis: the immediately previous period, the same period last week, and the same period last year. Same axes, same scale, offset in time this week same period last week 1 2 1 THE PRESENT Solid, full weight. It has to read first, every time. 2 THE COMPARISON Thinner, dashed, quieter. Two lines of equal weight make a chart the viewer has to keep re-checking. Three bases, against the same now NOW PREVIOUS LAST WEEK LAST YEAR a year ago a week ago yesterday now Previous suits things with no cycle. Last week aligns the two strongest cycles in product data. Last year aligns the season and misaligns the weekday, and that wobble is what people mistake for signal.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Last week's line under today's. The comparison series has to read as secondary or the chart looks like two equal metrics, and the label must say which period the ghost is.

shadcn
npx shadcn@latest add toggle-group
npm
recharts
Tokens
--foreground--card--muted--muted-foreground--border--chart-1
this weeksame period last week+10%

aligns day of week and time of day

ComparePeriods.tsxThree bases as a closed set, the past drawn quieter than the present, the base named in the legend and the gap given as a number.

import { Line, LineChart, XAxis, YAxis } from "recharts";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";

/**
 * The base is the whole design decision, and there are three honest ones.
 * Each aligns a different cycle, so the chart says which it is using and
 * what that aligns, rather than leaving "last period" to mean anything.
 */
export const BASES = {
  "1d": { pick: "yesterday",  legend: "same period yesterday",  aligns: "time of day; a Sunday against a Monday" },
  "7d": { pick: "a week ago", legend: "same period last week",  aligns: "day of week and time of day" },
  "1y": { pick: "a year ago", legend: "same period last year",  aligns: "the season; the weekday wobbles" },
} as const;
export type Base = keyof typeof BASES;

const PRESENT = "hsl(var(--chart-1))";
const PAST = "hsl(var(--muted-foreground))";

export function ComparePeriods({ label, current, comparison, base, onBaseChange, width = 400, height = 150 }: {
  /** The current period, as the legend names it. */
  label: string;
  current: number[];
  /** The same window shifted back by `base`, on the same scale. */
  comparison: number[];
  base: Base;
  onBaseChange: (base: Base) => void;
  width?: number;
  height?: number;
}) {
  const data = current.map((now, i) => ({ i, now, then: comparison[i] }));
  const sum = (xs: number[]) => xs.reduce((a, b) => a + b, 0);
  // The gap as a number. Two lines show a difference; only a figure says how big.
  const pct = Math.round(((sum(current) - sum(comparison)) / sum(comparison)) * 100);

  return (
    <div className="rounded-lg border bg-card p-4">
      <div className="overflow-x-auto">
        <LineChart width={width} height={height} data={data} margin={{ top: 8, right: 16, bottom: 0, left: 16 }}>
          <XAxis interval={0} dataKey="i" hide />
          <YAxis domain={[0, "auto"]} hide />
          {/* The past is thinner, dashed and grey so the present reads first.
              Equal weight makes a chart the viewer keeps re-checking. */}
          <Line type="linear" dataKey="then" stroke={PAST} strokeWidth={1.5} strokeDasharray="5 4" dot={false} isAnimationActive={false} />
          <Line type="linear" dataKey="now" stroke={PRESENT} strokeWidth={2.5} dot={false} isAnimationActive={false} />
        </LineChart>
      </div>

      <div className="mt-2 flex flex-wrap items-center gap-x-5 gap-y-2 text-xs">
        <span className="inline-flex items-center gap-1.5 text-foreground">
          <span className="h-0.5 w-5 bg-chart-1" />{label}
        </span>
        <span className="inline-flex items-center gap-1.5 text-muted-foreground">
          <span className="w-5 border-t border-dashed border-muted-foreground" />{BASES[base].legend}
        </span>
        <span className="tabular-nums text-foreground">{pct > 0 ? "+" : ""}{pct}%</span>

        <ToggleGroup type="single" value={base} onValueChange={(v) => v && onBaseChange(v as Base)}
          aria-label="Compare against" className="ml-auto gap-1">
          {(Object.keys(BASES) as Base[]).map((b) => (
            <ToggleGroupItem key={b} value={b} size="sm" className="h-6 px-2 text-xs data-[state=on]:bg-muted">
              {BASES[b].pick}
            </ToggleGroupItem>
          ))}
        </ToggleGroup>
      </div>
      <p className="mt-1 text-xs text-muted-foreground">aligns {BASES[base].aligns}</p>
    </div>
  );
}

demo.tsxHow it is called: this week against the same window a week back, with the other two bases a click away.

import { useState } from "react";
import { ComparePeriods, type Base } from "./ComparePeriods";

/**
 * This week against the same window shifted back. Last week is the default
 * because it aligns the weekday; the other two are there to show the same
 * chart re-based, with the gap recomputed.
 */
const THIS_WEEK = [32, 54, 44, 92, 82, 60, 76, 58, 68];
const SHIFTED: Record<Base, number[]> = {
  "1d": [30, 50, 48, 88, 78, 64, 70, 60, 62],
  "7d": [40, 58, 50, 84, 74, 52, 62, 44, 50],
  "1y": [28, 44, 40, 70, 66, 48, 58, 42, 46],
};

export default function Demo() {
  const [base, setBase] = useState<Base>("7d");
  return (
    <ComparePeriods
      label="this week"
      current={THIS_WEEK}
      comparison={SHIFTED[base]}
      base={base}
      onBaseChange={setBase}
    />
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Screenshots are grouped by product and include the capture date. Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.

Cloudflare Radar

Public internet-traffic dashboards with location and time controls and links from summaries to detailed views.

Worldwide Overview September 11, 2026 Public site, signed out; no version string exposed medium · light · desktop-web
Radar’s overview uses location and time controls to set the scope of the page. Each summary panel links to a fuller section, making the page a starting point for further reading. Values are printed above the charts, so people can read figures without estimating them from marks. The bot and human shares, for example, are both labeled beside their bar.
Show 2 more examples Hide the rest

Kibana

A query bar and filtering controls define the data shown in charts and document tables.

Dashboards / [Flights] Global Flight Dashboard September 10, 2026 Elastic demo environment, sample flight data (guest session) dense · light · desktop-web
The delay table shows percentages without the number of flights behind each one. A high rate based on very few flights would look as prominent as one based on many. A nearby comparison tile names the previous week but does not clearly explain what its percentage represents. The filtering controls are more explicit: a query bar sits above pickers and a price range, giving people both typed and guided ways to narrow the view.
  • Ratio and rate Delay percentages appear without their flight counts.
  • Compare periods The comparison period is named, but the meaning of the percentage is unclear.
  • Share and embed Sharing and full-screen controls are available in the header.
  • Filter bar Pickers and a range control provide alternatives to typing a query.
  • Panel grid Panels use different sizes within a grid.
  • Stacked composition A 100% stack shows the mix of delay types without total volume.
  • Annotation Grouped event markers share the chart’s time axis.
  • Header KPI strip Different tile sizes and arrangements make the summary row less consistent.

Netdata

A large collection of system charts with per-chart controls and anomaly-rate views.

Metrics / System September 10, 2026 Netdata Agent v2.10.0-686-nightly (public registry node, signed out) dense · dark · desktop-web
The navigation reports 720 charts organized by the metrics collected from the system. The main page follows the same nested structure, giving broad coverage but little indication of which charts matter most to a particular task. Search helps navigate that volume. The header also distinguishes live and stale nodes, making missing updates visible instead of presenting every node as current.