Hacktron Design System

Charts

Visualizing trends over time with TrendChart.

Overview

Findings by severity New findings introduced per day, last 7 days

Use the Left and Right arrow keys to move between points. Escape hides the details.

126 findings introduced in the last 7 days
<template>  <Card title="Findings by severity" subtitle="New findings introduced per day, last 7 days">    <template #header-actions>      <DropdownMenu :items="groupByItems">        <Button variant="secondary" size="sm" trailing-icon="i-lucide-chevron-down">Group: {{ activeGroupBy }}</Button>      </DropdownMenu>      <DropdownMenu :items="rangeItems">        <Button variant="secondary" size="sm" trailing-icon="i-lucide-chevron-down">{{ activeRangeLabel }}</Button>      </DropdownMenu>    </template>    <TrendChart :data="data" :series="series" type="bar" :tick-format="tickFormat" :tooltip-heading="tooltipLabel" />    <template #footer>      <span class="body-05-regular text-text-tertiary">{{ total }} findings introduced in the last 7 days</span>    </template>  </Card></template>

Choosing a chart

@unovis/vue is the only charting library in this codebase. For a time-series chart, use TrendChart first: it already handles the axis, hover tooltip, legend, and the empty and loading states.

Line vs. bar

  • Use type="line" (the default) for rates and durations, where the trend between points is the point: SLA compliance, MTTR.
  • Use type="bar" for volumes and counts, where each point is its own discrete total: scan volume, findings by day.
  • Return null from a series' value accessor for "no data at this point" in either type. A real 0 (0% SLA compliance, a zero-day MTTR) is a value, not an absence.

Best practices

  • Use TrendChart before composing Unovis primitives. Composing VisLine or VisStackedBar yourself is only for a shape it does not cover (see below).
  • Use isLoading and let the empty state happen on its own. TrendChart renders both; do not add a skeleton or a "no data" message at the call site.
  • Keep call sites simple. Pass data and series straight through rather than wrapping TrendChart in another layer.

Color

Categorical slots

Six generic series colors, for series with no fixed meaning (PR review vs. whitebox scan). Assign them in order from chart-1; skipping or reordering slots breaks the hue relationship the palette was tuned for.

Severity

Use these instead of the generic slots whenever the series are severities, so a severity has the same color on every chart.

When TrendChart does not fit

Not every chart is a time series.

  • A part-to-whole split of one number is BreakdownBar: one bar sized by shares, the total beside it, a legend with counts.
  • A small inline chart is Sparkline, which fills the #chart slot of MetricCard.
  • There is no donut or pie fragment yet.
  • For a shape TrendChart does not cover, compose Unovis primitives directly but keep its conventions: hide the x-axis tick marks (:tick-line="false") and grid lines so only the tick labels remain, color tick labels and series from the design system tokens (--color-text-tertiary, --color-chart-chart-*), and use ChartTooltip for the hover panel.
  • Once a second use case needs that shape, promote it to a fragment.

Examples

Line

<template>  <TrendChart    :data="data"    :series="[      { key: 'critical', label: 'Critical', color: 'var(--color-chart-severity-critical)', value: (d) => d.critical },      { key: 'high', label: 'High', color: 'var(--color-chart-severity-high)', value: (d) => d.high },      { key: 'medium', label: 'Medium', color: 'var(--color-chart-severity-medium)', value: (d) => d.medium },      { key: 'low', label: 'Low', color: 'var(--color-chart-severity-low)', value: (d) => d.low },    ]"    :tick-format="tickFormat"    :tooltip-heading="tooltipLabel"    :value-format="(v) => pctFormat.format(v)"  /></template>

Bar

<template>  <TrendChart    :data="data"    :series="[      { key: 'pr', label: 'PR review', color: 'var(--color-chart-chart-2)', value: (d) => d.pr },      { key: 'whitebox', label: 'Whitebox scan', color: 'var(--color-chart-chart-3)', value: (d) => d.whitebox },    ]"    type="bar"    :tick-format="tickFormat"    :tooltip-heading="tooltipLabel"  /></template>

Loading

<template>  <TrendChart :data="data" :series="series" :tick-format="tickFormat" is-loading />  <TrendChart :data="data" :series="series" type="bar" :tick-format="tickFormat" is-loading /></template>

Empty

No data for this period

<template>  <TrendChart    :data="[]"    :series="series"    :tick-format="tickFormat"  /></template>

Error

Could not load this chart

<template>  <TrendChart    :data="data"    :series="series"    :tick-format="tickFormat"    :error="isError"    :error-label="t('dashboard.chart-error')"  /></template>

Axis options

<template>  <TrendChart    :data="data"    :series="series"    :tick-format="tickFormat"    :tooltip-heading="tooltipLabel"    :value-format="(v) => pctFormat.format(v)"    :num-ticks="4"    y-axis-position="right"  /></template>

Legend in the header

Findings by severity
<template>  <Card title="Findings by severity">    <template #header-actions>      <ChartLegend v-model:highlighted="highlighted" :items="items" />    </template>    <TrendChart      v-model:highlighted-series="highlighted"      :data="data"      :series="series"      type="bar"      :tick-format="tickFormat"      :tooltip-heading="tooltipLabel"      aria-label="Findings introduced over the selected period"      :show-legend="false"    />  </Card></template>

Single series

<template>  <TrendChart    :data="data"    :series="[{ key: 'pr', label: 'PR review', color: 'var(--color-chart-chart-2)', value: (d) => d.pr }]"    type="bar"    :tick-format="tickFormat"    :tooltip-heading="tooltipLabel"    legend-totals    show-legend  /></template>