Skip to content

Examples

Minimal Plot

0123423456
X value
Y value
vue
<template>
    <div class="example-frame" :style="{ height: `${size.height}px`, width: `${size.width}px` }">
        <TransformGroup :domain="domain" :size="size" :padding="{ top: 0, right: 0, left: 72, bottom: 60 }">
            <Grid :x-ticks="xTicks" :y-ticks="yTicks" stroke-color="#e2e8f0" />
            <Line :points="points" stroke-color="#2563eb" :stroke-width="2" />
            <Scatter fill-color="#0f172a" :points="points" :radius="4" />
            <Axis direction="horizontal" :ticks="xTicks" />
            <Axis direction="vertical" :ticks="yTicks" />
            <XLabel>X value</XLabel>
            <YLabel>Y value</YLabel>
        </TransformGroup>
    </div>
</template>

<script setup lang="ts">
import { Axis, Grid, Line, Scatter, TransformGroup, XLabel, YLabel, getDataDomain, getNiceTicks } from '@putianyi888/vue3-plots'
import type { PlotPoint, PlotSize } from '@putianyi888/vue3-plots'

const points: PlotPoint[] = [
    { x: 0, y: 2 },
    { x: 1, y: 4 },
    { x: 2, y: 3 },
    { x: 3, y: 6 },
    { x: 4, y: 5 },
]
const size: PlotSize = { width: 640, height: 360 }
const domain = getDataDomain(points, 0.1)
const xTicks = getNiceTicks(domain.xMin, domain.xMax)
const yTicks = getNiceTicks(domain.yMin, domain.yMax)
</script>

<style scoped>
.example-frame {
  overflow-x: auto;
  padding: 16px;
  position: relative;
}
</style>

Responsive Plot With ResizeObserver

This example uses DOM layout for XLabel and YLabel, then observes the remaining plot area with ResizeObserver and passes that measured size to the SVG layers.

Value
024101520
Sample
vue
<template>
    <div class="resize-frame">
        <YLabel class="chart-y-label">
            Value
        </YLabel>
        <div ref="plot" class="plot">
            <TransformGroup :domain="domain" :size="size">
                <Grid :x-ticks="xTicks" :y-ticks="yTicks" stroke-color="#e2e8f0" />
                <Line :points="points" stroke-color="#16a34a" :stroke-width="2" />
                <Scatter fill-color="#14532d" :points="points" :radius="4" />
                <Axis direction="horizontal" :ticks="xTicks" />
                <Axis direction="vertical" :ticks="yTicks" />
            </TransformGroup>
        </div>
        <XLabel class="chart-x-label">
            Sample
        </XLabel>
    </div>
</template>

<script setup lang="ts">
import { useResizeObserver } from '@vueuse/core'
import { computed, ref, useTemplateRef } from 'vue'
import { Axis, Grid, Line, Scatter, TransformGroup, XLabel, YLabel, getDataDomain, getNiceTicks } from '@putianyi888/vue3-plots'
import type { PlotPoint, PlotSize } from '@putianyi888/vue3-plots'

const plot = useTemplateRef<HTMLElement>('plot')
const size = ref<PlotSize>({ width: 640, height: 320 })
const points: PlotPoint[] = [
    { x: 0, y: 8 },
    { x: 1, y: 12 },
    { x: 2, y: 7 },
    { x: 3, y: 16 },
    { x: 4, y: 13 },
    { x: 5, y: 20 },
]
const domain = getDataDomain(points, 0.1)
const xTicks = computed(() => getNiceTicks(domain.xMin, domain.xMax))
const yTicks = computed(() => getNiceTicks(domain.yMin, domain.yMax))

useResizeObserver(plot, ([entry]) => {
    if (!entry) return

    size.value = {
        width: Math.max(220, Math.round(entry.contentRect.width)),
        height: Math.max(180, Math.round(entry.contentRect.height)),
    }
})
</script>

<style scoped>
.resize-frame {
  overflow: auto;
  min-width: 320px;
  max-width: 100%;
  min-height: 260px;
  height: 320px;
  resize: both;
  border: 1px solid #e2e8f0;
  display: grid;
  grid-template-columns: auto minmax(220px, 1fr);
  grid-template-rows: minmax(180px, 1fr) auto;
  height: 100%;
  width: 100%;
}

.plot {
  grid-column: 2;
  grid-row: 1;
  min-width: 0;
  min-height: 0;
  position: relative;
}

.chart-x-label,
.chart-y-label {
  position: static;
  height: auto !important;
  width: auto !important;
}

.chart-x-label {
  grid-column: 2;
  grid-row: 2;
  padding-top: 6px;
}

.chart-y-label {
  grid-column: 1;
  grid-row: 1;
  padding-right: 6px;
}
</style>

Mini Pie Scatter Points

vue
<template>
    <div class="example-frame" :style="{ height: `${size.height}px`, width: `${size.width}px` }">
        <TransformGroup :domain="domain" :size="size" :padding="padding">
            <Scatter :points="points">
                <template #point="{ point }">
                    <MiniPie
                        :data="point.data"
                        :radius="pieRadius"
                        :start-angle="-90"
                        :style="{ height: `${pieRadius * 2}px`, width: `${pieRadius * 2}px`, x: `${-pieRadius}px`, y: `${-pieRadius}px` }"
                    />
                </template>
            </Scatter>
        </TransformGroup>
    </div>
</template>

<script setup lang="ts">
import { MiniPie, Scatter, TransformGroup, getDataDomain } from '@putianyi888/vue3-plots'
import type { PlotPadding, PlotPoint, PlotSize } from '@putianyi888/vue3-plots'

type CategoryBreakdown = Array<{ value: number, color: string }>

const points: PlotPoint<CategoryBreakdown>[] = [
    { x: 1, y: 3, data: [{ value: 4, color: '#2563eb' }, { value: 2, color: '#f97316' }, { value: 1, color: '#16a34a' }] },
    { x: 2, y: 6, data: [{ value: 2, color: '#2563eb' }, { value: 5, color: '#f97316' }, { value: 3, color: '#16a34a' }] },
    { x: 3, y: 4, data: [{ value: 3, color: '#2563eb' }, { value: 1, color: '#f97316' }, { value: 5, color: '#16a34a' }] },
    { x: 4, y: 8, data: [{ value: 5, color: '#2563eb' }, { value: 3, color: '#f97316' }, { value: 2, color: '#16a34a' }] },
    { x: 5, y: 5, data: [{ value: 1, color: '#2563eb' }, { value: 4, color: '#f97316' }, { value: 4, color: '#16a34a' }] },
]
const size: PlotSize = { width: 640, height: 360 }
const padding: PlotPadding = { top: 24, right: 24, bottom: 40, left: 44 }
const domain = getDataDomain(points, 0.14)
const pieRadius = 12
</script>

<style scoped>
.example-frame {
  overflow-x: auto;
  padding: 16px;
  position: relative;
}
</style>

Scatter Tooltips

Vue3 Plots intentionally leaves tooltips to your application, so you can use the library that best fits it. Enable Scatter's interactive prop and use its point-enter and point-leave events to update the tooltip content. This example uses VueTippy to display that content and follow the cursor.

vue
<template>
    <Tippy :duration="0" sticky follow-cursor :arrow="false">
        <div class="plot" :style="{ height: `${size.height}px`, width: `${size.width}px` }">
            <Scatter
                :domain="domain"
                fill-color="#2563eb"
                :points="points"
                :radius="6"
                :size="size"
                interactive
                @point-enter="showPointTooltip"
                @point-leave="hideTooltip"
            />
        </div>
        <template #content>
            <div v-if="hoveredPoint">
                ({{ hoveredPoint.x }}, {{ hoveredPoint.y }})
            </div>
        </template>
    </Tippy>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Tippy } from 'vue-tippy'
import { Scatter } from '@putianyi888/vue3-plots'

const points = [
    { x: 1, y: 2 },
    { x: 2, y: 5 },
    { x: 3, y: 3 },
    { x: 4, y: 6 },
    { x: 5, y: 4 },
]

const size = { width: 640, height: 360 }
const domain = { xMin: 0, xMax: 6, yMin: 0, yMax: 7 }
const hoveredPoint = ref<{ x: number, y: number }>()

function showPointTooltip(point: { x: number, y: number }) {
    hoveredPoint.value = point
}

function hideTooltip() {
    hoveredPoint.value = undefined
}
</script>

<style scoped>
.plot {
  position: relative;
}
</style>

Box Axes

0246810024681005100510
vue
<template>
    <div style="position: relative; width: 640px; height: 360px;">
        <TransformGroup :domain="domain" :size="size" :padding="padding">
            <Axis direction="horizontal" :ticks="xTicks" :offset="12" :tick-size="-4" :position="0" />
            <Axis direction="horizontal" :ticks="xTicks" :offset="-12" :position="10" />
            <Axis direction="vertical" :ticks="yTicks" :offset="-12" :tick-size="-4" :position="0" />
            <Axis direction="vertical" :ticks="yTicks" :offset="12" :position="10" />
        </TransformGroup>
    </div>
</template>

<script setup lang="ts">
import { Axis, TransformGroup } from '@putianyi888/vue3-plots'

const size = { width: 640, height: 360 }
const padding = { top: 30, right: 30, bottom: 30, left: 30 }
const domain = { xMin: 0, xMax: 10, yMin: 0, yMax: 10 }
const xTicks = [0, 2, 4, 6, 8, 10]
const yTicks = [0, 5, 10]
</script>

Multiple Axes

051005100510152005101520
vue
<template>
    <div style="position: relative; width: 640px; height: 360px;">
        <TransformGroup :domain="domain1" :size="size" :padding="padding">
            <Axis direction="vertical" :ticks="yTicks1" :offset="12" :tick-size="-4" :position="0" />
            <Axis direction="vertical" :ticks="yTicks1" :offset="-12" :position="5" />
        </TransformGroup>
        <TransformGroup :domain="domain2" :size="size" :padding="padding">
            <Axis direction="vertical" :ticks="yTicks2" :offset="12" :tick-size="-4" :position="5" />
            <Axis direction="vertical" :ticks="yTicks2" :offset="-12" :position="10" />
        </TransformGroup>
    </div>
</template>

<script setup lang="ts">
import { Axis, TransformGroup } from '@putianyi888/vue3-plots'

const size = { width: 640, height: 360 }
const padding = { top: 30, right: 30, bottom: 30, left: 30 }
const domain1 = { xMin: 0, xMax: 10, yMin: 0, yMax: 10 }
const domain2 = { xMin: 0, xMax: 10, yMin: 0, yMax: 20 }
const yTicks1 = [0, 5, 10]
const yTicks2 = [0, 5, 10, 15, 20]
</script>

ImageGrid Complex Example

Move over the grid
vue
<template>
    <div class="example-frame">
        <div class="controls">
            <label>
                Rows
                <input v-model.number="rowCount" min="1" max="200" type="number">
            </label>
            <label>
                Columns
                <input v-model.number="columnCount" min="1" max="200" type="number">
            </label>
            <label>
                Cell
                <input v-model.number="cellSize" min="1" max="16" type="number">
            </label>
            <span class="status">
                {{ hoverText }}
            </span>
        </div>

        <div class="viewport">
            <ImageGrid
                ref="imageGrid"
                :cell-height="cellSize"
                :cell-width="cellSize"
                :cells="cells"
                :images="images"
            />
        </div>
    </div>
</template>

<script setup lang="ts">
import { computed, ref, useTemplateRef, watch } from 'vue'
import { ImageGrid } from '@putianyi888/vue3-plots'

type CellIndex = { rowIndex: number, columnIndex: number }
type TileKey = keyof typeof baseToHighlight

const rowCount = ref(75)
const columnCount = ref(100)
const cellSize = ref(3)
const imageGrid = useTemplateRef<InstanceType<typeof ImageGrid>>('imageGrid')
const baseToHighlight = {
    grass: 'grassHover',
    stone: 'stoneHover',
    water: 'waterHover',
} as const
const images = {
    grass: createTile('#86efac', '#16a34a'),
    grassHover: createTile('#16a34a', '#86efac'),
    stone: createTile('#d4d4d8', '#71717a'),
    stoneHover: createTile('#71717a', '#d4d4d8'),
    water: createTile('#93c5fd', '#2563eb'),
    waterHover: createTile('#2563eb', '#93c5fd'),
}
const keys = Object.keys(baseToHighlight) as TileKey[]
const cells = ref(createCells())
const lastHoveredCell = ref<CellIndex>()
const cellIndex = computed(() => imageGrid.value?.cellIndex)
const hoverText = computed(() => {
    const index = cellIndex.value

    if (index === undefined) return 'Move over the grid'

    return `row ${index.rowIndex}, column ${index.columnIndex}: ${getBaseKey(cells.value[index.rowIndex]?.[index.columnIndex])}`
})

watch([rowCount, columnCount], () => {
    cells.value = createCells()
    lastHoveredCell.value = undefined
})

watch(cellIndex, (index) => {
    restoreHoveredCell()

    if (index === undefined) return

    const key = cells.value[index.rowIndex]?.[index.columnIndex]
    const baseKey = getBaseKey(key)

    if (baseKey === undefined) return

    cells.value[index.rowIndex][index.columnIndex] = baseToHighlight[baseKey]
    lastHoveredCell.value = index
})

function createCells() {
    return Array.from({ length: rowCount.value }, (_, rowIndex) => {
        return Array.from({ length: columnCount.value }, (_, columnIndex) => {
            return keys[(rowIndex * 17 + columnIndex * 7) % keys.length]
        })
    })
}

function restoreHoveredCell() {
    const index = lastHoveredCell.value

    if (index === undefined) return

    const key = cells.value[index.rowIndex]?.[index.columnIndex]
    const baseKey = getBaseKey(key)

    if (baseKey !== undefined) {
        cells.value[index.rowIndex][index.columnIndex] = baseKey
    }

    lastHoveredCell.value = undefined
}

function getBaseKey(key: string | undefined) {
    if (key === undefined) return undefined
    if (key in baseToHighlight) return key as TileKey

    return keys.find((baseKey) => baseToHighlight[baseKey] === key)
}

function createTile(background: string, foreground: string) {
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><rect width="32" height="32" fill="${background}"/><circle cx="16" cy="16" r="8" fill="${foreground}"/></svg>`

    return `data:image/svg+xml,${encodeURIComponent(svg)}`
}
</script>

<style scoped>
.example-frame {
  display: grid;
  gap: 12px;
}

.controls {
  display: flex;
  align-items: end;
  flex-wrap: wrap;
  gap: 12px;
}

label {
  display: grid;
  gap: 4px;
  color: #475569;
  font-size: 12px;
}

input {
  width: 72px;
  box-sizing: border-box;
  border: 1px solid #cbd5e1;
  padding: 4px 6px;
}

.status {
  color: #334155;
  font-size: 14px;
}

.viewport {
  overflow: auto;
  max-width: 100%;
  border: 1px solid #e2e8f0;
}
</style>