Line Chart

Compose line series with numeric or date domains.
alpha

Line charts are the default choice for showing change over an ordered domain. Use VisLine inside VisXYContainer; for categorical labels, map rows to numeric indexes and format the x-axis ticks back to text.

Multiple series

Pass an accessor array to y when the series share the same domain. Keep that array in the same order as the non-domain entries in ChartRoot.config.

Revenue
Costs
vue
<script setup lang="ts">
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import {
  ChartLegend,
  ChartRoot,
  ChartTooltip,
  ChartCrosshair,
  type ChartConfig,
} from '@typlog/ui/charts'

interface Point {
  month: string
  revenue: number
  costs: number
}

const data: Point[] = [
  { month: 'Jan', revenue: 48, costs: 31 },
  { month: 'Feb', revenue: 62, costs: 38 },
  { month: 'Mar', revenue: 57, costs: 42 },
  { month: 'Apr', revenue: 76, costs: 47 },
  { month: 'May', revenue: 88, costs: 53 },
]
const config = {
  month: { label: 'Month', role: 'x' },
  revenue: { label: 'Revenue' },
  costs: { label: 'Costs' },
} satisfies ChartConfig
const x = (_item: Point, index: number) => index
const y = [(item: Point) => item.revenue, (item: Point) => item.costs]
const xTickFormat = (value: number) => data[value]?.month ?? ''
</script>

<template>
  <ChartRoot v-slot="{ mounted }" :config="config">
    <VisXYContainer v-if="mounted" :data="data" :height="240">
      <VisLine :x="x" :y="y" :line-width="2" />
      <VisAxis
        type="x"
        :tick-format="xTickFormat"
        :tick-values="data.map((_item, index) => index)"
        :tick-line="false"
        :domain-line="false"
        :grid-line="false"
      />
      <VisAxis
        type="y"
        :tick-format="() => ''"
        :tick-line="false"
        :domain-line="false"
        :grid-line="true"
      />
      <ChartTooltip />
      <ChartCrosshair />
    </VisXYContainer>
    <ChartLegend class="mt-3" />
  </ChartRoot>
</template>

Single series

A single accessor keeps the chart focused and lets the series config provide a local palette override. A thicker line works well when there is no second series to distinguish.

vue
<script setup lang="ts">
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import {
  ChartCrosshair,
  ChartRoot,
  ChartTooltip,
  type ChartConfig,
} from '@typlog/ui/charts'

interface Point {
  day: string
  activeUsers: number
}

const data: Point[] = [
  { day: 'Mon', activeUsers: 1280 },
  { day: 'Tue', activeUsers: 1460 },
  { day: 'Wed', activeUsers: 1390 },
  { day: 'Thu', activeUsers: 1710 },
  { day: 'Fri', activeUsers: 1890 },
  { day: 'Sat', activeUsers: 1760 },
  { day: 'Sun', activeUsers: 2040 },
]
const config = {
  day: { label: 'Day', role: 'x' },
  activeUsers: { label: 'Active users', color: 'var(--cyan-9)' },
} satisfies ChartConfig
const x = (_item: Point, index: number) => index
const y = (item: Point) => item.activeUsers
const xTickFormat = (value: number) => data[value]?.day ?? ''
</script>

<template>
  <ChartRoot v-slot="{ mounted }" :config="config">
    <VisXYContainer v-if="mounted" :data="data" :height="220">
      <VisLine :x="x" :y="y" :line-width="3" />
      <VisAxis
        type="x"
        :tick-format="xTickFormat"
        :tick-values="data.map((_item, index) => index)"
        :tick-line="false"
        :domain-line="false"
        :grid-line="false"
      />
      <VisAxis type="y" :tick-format="() => ''" :tick-line="false" :domain-line="false" />
      <ChartTooltip />
      <ChartCrosshair />
    </VisXYContainer>
  </ChartRoot>
</template>

Dashed comparison

Use a dashed line for a target, forecast, or previous period rather than introducing another saturated color. The dash accessor receives the series index.

Actual
Target
vue
<script setup lang="ts">
import { VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
import {
  ChartCrosshair,
  ChartLegend,
  ChartRoot,
  ChartTooltip,
  type ChartConfig,
} from '@typlog/ui/charts'

interface Point {
  week: string
  actual: number
  target: number
}

const data: Point[] = [
  { week: 'W1', actual: 42, target: 48 },
  { week: 'W2', actual: 51, target: 54 },
  { week: 'W3', actual: 58, target: 60 },
  { week: 'W4', actual: 68, target: 66 },
  { week: 'W5', actual: 73, target: 72 },
  { week: 'W6', actual: 81, target: 78 },
]
const config = {
  week: { label: 'Week', role: 'x' },
  actual: { label: 'Actual' },
  target: { label: 'Target', color: 'var(--gray-9)' },
} satisfies ChartConfig
const x = (_item: Point, index: number) => index
const y = [(item: Point) => item.actual, (item: Point) => item.target]
const dash = (_data: Point[], index: number) => index === 1 ? [6, 5] : []
const xTickFormat = (value: number) => data[value]?.week ?? ''
</script>

<template>
  <ChartRoot v-slot="{ mounted }" :config="config">
    <VisXYContainer v-if="mounted" :data="data" :height="220">
      <VisLine :x="x" :y="y" :line-dash-array="dash" :line-width="2" />
      <VisAxis
        type="x"
        :tick-format="xTickFormat"
        :tick-values="data.map((_item, index) => index)"
        :tick-line="false"
        :domain-line="false"
        :grid-line="false"
      />
      <VisAxis type="y" :tick-format="() => ''" :tick-line="false" :domain-line="false" />
      <ChartTooltip />
      <ChartCrosshair />
    </VisXYContainer>
    <ChartLegend class="mt-3" align="center" />
  </ChartRoot>
</template>

Avoid placing unrelated metrics on the same line chart. If the series require different units or scales, separate them into charts or normalize the data explicitly.

Last updated Sep 6, 2026