---
title: Dashboard
description: A product analytics board with KPI cards, a trend chart, and a plugin table.
category: TemplateData
status: beta
since: 0.6.0
tags: [template, dashboard, analytics, charts, table]
syncStatus: reviewed
verified: false
---

## Usage

### Tuff Pulse board
:::TuffDemoWrapper{demo="TemplateDashboardDemo" code-lang="vue" description="Range, dates, brushing, and the legend recompute the whole board. Sample data."}
---
code: |
  <script setup lang="ts">
  import { ChartPalette } from '@talex-touch/tuffex/charts'
  import { computed, ref } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  const range = ref('30d')
  const zoom = ref<[number, number] | null>(null)
  const hidden = ref<string[]>([])

  // KPI cards, legend, share bar and plugin rows all come from one daily
  // history, cut by range + zoom
  const kpis = computed(() => buildKpis(range.value, zoom.value))

  // TxTimeseriesChart takes a pixel height, which no container query reaches,
  // so it is read off the stage
  function chartHeight(width: number, height: number) {
    if (width >= 960)
      return Math.min(320, Math.max(220, Math.round(height * 0.36)))
    return width >= 640 ? 240 : 176
  }
  </script>

  <template>
    <TemplateFrame title="Dashboard" @enter="entered = true">
      <template #default="{ width, height }">
        <div class="pulse">
          <header class="pulse__header">
            <TxFlatRadio v-model="range" size="lg">
              <TxFlatRadioItem value="7d" label="7d" />
              <TxFlatRadioItem value="30d" label="30d" />
              <TxFlatRadioItem value="90d" label="90d" />
            </TxFlatRadio>
            <TxDatePicker v-model="dates" variant="field" range />
          </header>

          <section class="pulse__kpis">
            <TxStatCard v-for="kpi in kpis" :key="kpi.key" :value="kpi.value" :label="kpi.label" :insight="kpi.insight">
              <template #value>
                <TxTextMorph :text="kpi.value" />
              </template>
            </TxStatCard>
          </section>

          <div class="pulse__main">
            <section class="pulse__trend">
              <TxChartLegendItem
                v-for="(item, index) in legend" :key="item.name"
                :name="item.name" :value="item.value" :color="ChartPalette.categoricalVar(index)"
                :inactive="hidden.includes(item.name)" @click="toggle(item.name)"
              />
              <TxTimeseriesChart
                v-model:hidden-series="hidden"
                :data="series" :markers="releases" :incomplete="{ after: yesterday }"
                :height="chartHeight(width, height)"
                @time-range-change="(from, to) => (zoom = [from, to])"
              />
            </section>
            <aside class="pulse__rail">
              <TxAllocationBar v-model="platform" :segments="share" />
              <TxProgressBar :segments="usage" height="8px" />
            </aside>
          </div>

          <TxDataTable v-model:sort="sort" :columns="columns" :data="plugins" row-key="id">
            <template #cell-trend="{ row }">
              <TxSparkChart :series="row.trend" :padding="{ top: 4, bottom: 4 }" :baseline="false" :interactive="false" />
            </template>
            <template #cell-status="{ row }">
              <TxStatusBadge size="sm" :status="row.tone" :text="row.status" />
            </template>
          </TxDataTable>
        </div>
      </template>
    </TemplateFrame>
  </template>

  <style scoped>
  .pulse__kpis { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); }
  @container template (max-width: 639.98px) { .pulse__kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
  @container template (min-width: 960px) { .pulse__kpis { grid-template-columns: repeat(5, minmax(0, 1fr)); } }
  </style>
---
:::

### Ops wall
A dark, real-time screen for watching rather than working: no filters or table, only the present, and no site theme.
:::TuffDemoWrapper{demo="TemplateDashboardOpsDemo" code-lang="vue" description="Goes live in view, and cloud sync in Tokyo degrades and recovers; pause it at any time. Sample data."}
---
code: |
  <script setup lang="ts">
  import { ChartPalette } from '@talex-touch/tuffex/charts'
  import { computed, ref, watch } from 'vue'
  import TemplateFrame from './TemplateFrame.vue'

  const sec = ref(0)
  const paused = ref(false)
  const hidden = ref(false) // IntersectionObserver (under 10% visible) and visibilitychange
  const live = computed(() => entered.value && !reduced && !paused.value && !hidden.value)

  // One heartbeat drives every cadence: KPIs 2s, events 3s, cities 4s, focus 8s, sankey window 12s
  watch(live, on => (on ? later(1000, beat) : clearTimers()))

  // Every middle node passes on exactly what it receives, or the sankey leaves a stub
  const flow = computed(() => flowAt(flowWindow.value))
  const bubbleColor = (row: City) => (row.id === 'tyo' && degraded.value ? orange : ChartPalette.categoricalVar(0))
  </script>

  <template>
    <TemplateFrame title="Ops wall" :height="600" @enter="onEnter">
      <template #default="{ width, height }">
        <!-- A local dark scope: the tuffex, BUI and chart dark blocks accept data-theme on any element -->
        <div class="ops" data-theme="dark" :style="{ gridTemplateRows: geometry(width, height).rows }">
          <header class="ops__head">
            <TxStatusBadge :status="healthy ? 'success' : 'warning'" :text="servicesText" />
            <TxModeChip :label="paused ? 'Paused' : 'Live'" :tone="paused ? 'warning' : 'success'" @click="paused = !paused" />
          </header>
          <div v-for="kpi in kpis" :key="kpi.key" class="ops-kpi">
            <TxTextMorph :text="kpi.value" :locale="localeTag" />
            <TxSparkChart :series="kpiSeries[kpi.key]" theme="dark" :animation="false" :interactive="false" />
          </div>
          <TxBubbleMap
            v-if="entered && world" :geo-json="world" :data="cities" lng="lng" lat="lat" value="online"
            :height="geometry(width, height).map" :bubble-color="bubbleColor" :bubble-border-width="ringWidth"
          />
          <TxInsightCards :active-index="focus" :pages="pages" @update:active-index="takeOverRotation" />
          <TxSankeyChart :nodes="nodes" :links="flow.links" :height="geometry(width, height).sankey" node-label-layout="inline">
            <template #tooltip="{ params }">
              {{ params.name }} · {{ count(params) }} / min
            </template>
          </TxSankeyChart>
          <TxTransition group preset="slide-fade" tag="ul">
            <li v-for="item in events" :key="item.id">{{ item.text }}</li>
          </TxTransition>
        </div>
      </template>
    </TemplateFrame>
  </template>

  <style scoped>
  .ops {
    color-scheme: dark; /* darkens the tooltips' canvas fallback and the scrollbars */
    --tx-skeleton-base-color: var(--tx-chart-semantic-skeleton); /* tokens :root derives with var() are redeclared here */
  }
  </style>
---
:::

## Overview

The analytics board a launcher team keeps open; every figure follows one time range, and changing the range, the dates, or the brushed span recomputes the page.

- Switch 7, 30, or 90 days or pick dates: KPI figures roll to new values, compared with the same-length period just before; up to seven days draws by the hour.
- Drag across the trend chart to zoom, and the whole page counts only that span; the Zoomed pill restores the full range.
- Click a legend item to hide or show a platform; sort the plugin table by installs or crash-free rate, and its trend column morphs when the range changes.
- Charts start drawing once in view, and nothing plays by itself; wide (≥ 960px) adds the crash-rate and insight cards, and narrow (< 640px) puts KPIs in two columns and trims the table to three.
- Ops wall: it goes live once in view, and cloud sync in Tokyo degrades at 18 seconds and recovers at 30; it stops updating when scrolled away or in a background tab.
- Ops wall: Live pauses every update, and the pointer on the focus card, sankey, or event list holds that part; reduced motion shows one still snapshot.
- Ops wall: always dark; in the docs column it fills the stage exactly, and narrow (< 640px) is one scrolling column.

## Customization

- To swap the data source, replace `daily()` with your API's per-day series; `total()`, `average()`, and `previousWindow()` already handle an unfinished today and the same-length comparison.
- Keep rates out of TxTimeseriesChart, whose y axis always includes 0; chart the counts, or use TxSparkChart with a `domain`.
- Pixel props such as chart heights or a table's `maxHeight` are out of reach of container queries; compute them from the stage size or your own ResizeObserver.
- To drill into a row, add `@row-click` to TxDataTable and swap the trend data for that plugin's installs.
- Ops wall: a local dark scope needs `data-theme="dark"` and `color-scheme: dark` on the root plus the few tokens `:root` derives; keep teleported overlays off the wall.

## Related components

| Region | Components | Role |
|---|---|---|
| Top bar | [TxFlatRadio](/docs/dev/components/flat-radio), [TxDatePicker](/docs/dev/components/date-picker), [TxStatusBadge](/docs/dev/components/status-badge) | Time range and degraded notice |
| KPI cards | [TxStatCard](/docs/dev/components/stat-card), [TxTextMorph](/docs/dev/components/text-morph) | KPIs against the previous period |
| Trend | [TxTimeseriesChart](/docs/dev/components/timeseries-chart), [TxChartLegendItem](/docs/dev/components/charts) | Per-platform trend with brush zoom |
| Side rail | [TxAllocationBar](/docs/dev/components/allocation-bar), [TxDotIndicator](/docs/dev/components/dot-indicator), [TxSignalMeter](/docs/dev/components/signal-meter), [TxProgressBar](/docs/dev/components/progress-bar) | Share, health, and AI usage |
| Insights (wide) | [TxInsightCards](/docs/dev/components/insight-cards), [TxSparkChart / TxChartScrubber](/docs/dev/components/spark-chart), [TxToastPanel](/docs/dev/components/toast-panel) | Pageable, scrubbable insights |
| Top plugins | [TxDataTable](/docs/dev/components/data-table), [TxSparkChart](/docs/dev/components/spark-chart), [TxSignalMeter](/docs/dev/components/signal-meter), [TxStatusBadge](/docs/dev/components/status-badge) | Sortable plugin table |
| Top bar (Ops wall) | [TxModeChip](/docs/dev/components/mode-chip) | Live mark and pause switch |
| Big KPIs (Ops wall) | [TxTextMorph](/docs/dev/components/text-morph), [TxSparkChart](/docs/dev/components/spark-chart) | Rolling figures and trend strips |
| City map (Ops wall) | [TxBubbleMap](/docs/dev/components/maps), [TxSkeleton](/docs/dev/components/skeleton) | Online bubbles in 14 cities |
| Focus rotation (Ops wall) | [TxInsightCards](/docs/dev/components/insight-cards), [TxAllocationBar](/docs/dev/components/allocation-bar) | Focus cards turning every 8 seconds |
| Search flow (Ops wall) | [TxSankeyChart](/docs/dev/components/sankey-chart) | Input → plugin → action |
| Health and events (Ops wall) | [TxDotIndicator](/docs/dev/components/dot-indicator), [TxSignalMeter](/docs/dev/components/signal-meter), [TxTransition](/docs/dev/components/transition) | Service health and event stream |
