Examples
Minimal Plot
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
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
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
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>