Skip to content

Custom Charts or Canvas Rendering

This example shows how to render a line chart with D3.js and sync it with the timeline.

The same approach can be used to render a canvas-based component to display large amounts of data without performance issues, as the timeline items are rendered as individual DOM elements (see Performance).

INFO

The basic principle is to render a custom component in a group (using the items-GROUPID slot) and pass the viewport range to the custom component. That component is then responsible for rendering the data points within this range.

Position the custom component over a timeline row with CSS.

Jan 1, 1970 00:00
02:00

Code

vue
<script setup lang="ts">
  import { ref } from 'vue';
  import LineChart from './components/LineChart.vue';

  const items = ref([
    { id: 1, group: 1, type: 'range', cssVariables: { '--item-background': 'var(--color-2)' }, start: 1000000, end: 4500000 },
    { id: 3, group: 1, type: 'range', start: 6000000, end: 8000000 },
  ]);

  const linechartData = [{ group: 'linechart', value: 1, type: 'point', start: 1000000 },
    { group: 'linechart', value: 1, type: 'point', start: 1500000 },
    { group: 'linechart', value: 0.7, type: 'point', start: 2000000 },
    { group: 'linechart', value: 0, type: 'point', start: 2500000 },
    { group: 'linechart', value: 1, type: 'point', start: 3000000 },
    { group: 'linechart', value: 0, type: 'point', start: 3500000 },
    { group: 'linechart', value: 0, type: 'point', start: 4000000 },
    { group: 'linechart', value: 0, type: 'point', start: 4500000 },
    { group: 'linechart', value: 1, type: 'point', start: 5000000 },
    { group: 'linechart', value: 0.5, type: 'point', start: 5500000 },
    { group: 'linechart', value: 1, type: 'point', start: 6000000 },
    { group: 'linechart', value: 1, type: 'point', start: 6500000 },
    { group: 'linechart', value: 0.6, type: 'point', start: 7000000 },
  ];

</script>

<template>
  <Timeline
    :items="items"
    :groups="[{id: 1},{id: 'linechart'}]"
    :viewportMin="0"
    :viewportMax="8000000"
  >

  <template #items-linechart="{ viewportStart, viewportEnd, group }">
    <LineChart
      :viewportStart="viewportStart"
      :viewportEnd="viewportEnd"
      :data="linechartData"
    />
    </template>
  </Timeline>
</template>

LineChart.vue (custom component utilizing D3.js)

vue
<template>
  <div id="chart" class="root"></div>
</template>

<script lang="ts" setup>
import { onMounted, watch } from 'vue';
// import * as d3 from 'd3'; (disabled due to vitepress)

const props = defineProps<{
  viewportStart: number;
  viewportEnd: number;
  data: { start: number; value: number; }[];
}>();

function initChart(start: number, end: number, data: typeof props.data) {
  if (!window.d3) {
      // ensure d3 is loaded (due to import within vitepress)
      setTimeout(() => initChart(start, end, data), 100);
      return;
    };

  const chart = document.getElementById('chart');
  if (!chart) return;

  d3.select('#chart').selectAll('svg').remove();

  const margin = { top: 2, right: 0, bottom: 2, left: 0 };
  const width = chart.clientWidth - margin.left - margin.right;
  const height = 32 - margin.top - margin.bottom;

  const svg = d3.select('#chart')
    .append('svg')
    .attr('width', width + margin.left + margin.right)
    .attr('height', height + margin.top + margin.bottom)
    .append('g')
    .attr('transform', `translate(${margin.left},${margin.top})`);

  const x = d3.scaleLinear().domain([start, end]).range([0, width]);
  const y = d3.scaleLinear().domain([0, 1]).range([height, 0]);

  const line = d3.line()
    .x(d => x(d.start))
    .y(d => y(d.value));

  svg.append('path')
    .datum(data)
    .attr('class', 'line')
    .attr('d', line);
}

// Re-render chart when viewport changes:
watch(() => [props.viewportStart, props.viewportEnd],
  () => initChart(props.viewportStart, props.viewportEnd, props.data)
);

onMounted(() => {
  initChart(props.viewportStart, props.viewportEnd, props.data);
});
</script>

<style lang="scss" scoped>
  .root {
    width: 100%;
    height: 100%;
    position: absolute;
    inset: 0;
  }

  :deep(.line) {
    fill: none;
    stroke: var(--color-1);
    stroke-width: 2px;
  }
</style>