Multiple Charts on One Page in Vue 3

Viewed 762

Desired Outcome

Display multiple instances of the same chart type on a single page. Example image contains additional data stripped from the code below.

Sample Output1

The initial chart canvas object is created with Chart.vue component:

<!-- Chart.vue -->
<template>
    <div :class="chartType">
        <canvas style="height: 100%; width: 100%;"></canvas>
    </div>
</template>

<script>
import Chart from "chart.js/auto";

Chart.defaults.elements.point.radius = 0;

export default {
    props:{
        chartType:String,
        chartData:Object,
        chartOptions:Object
    },
    methods: {

    chartConstructor(chartType, chartData, chartOptions) {
    const chartElement = document.querySelector(`.${this.chartType} canvas`);
    const chart = new Chart(chartElement, {
      type: chartType,
      data: chartData,
      options: chartOptions,
    });
        },
    },

    mounted(){
        let {chartType,chartData,chartOptions} = this;
        this.chartConstructor(chartType, chartData, chartOptions);
    }
};
</script>

The chart options and data are currently established in a "chart type" component, in this case a combined bar and line chart BarLine.vue (there are other components for other chart types). Although the data is provided here, the data will ultimately be sourced externally.

<!-- BarLine.vue -->
<template>
    <div class="chart">
        <Chart id="chartImage" :chartData="chartData" :chartOptions="chartOptions" :chartType="chartType" :style="{ width: chartWidth + 'px', height: chartHeight + 'px' }"/>
    </div>
</template>

<script>
import Chart from "@/components/Chart.vue";

export default {
    props:{
        chartWidth: {default: 500, type: Number},
        chartHeight: {default: 250, type: Number},
    },
    components: {
        Chart,
    },
    data() {
        return {
            chartType: "bar",
            chartData: {
        labels: ["T", "F", "S", "S", "M", "T", "W", "T"],
        datasets: [
          {
            type: 'line',
            backgroundColor: "rgba(128, 0, 0, 0.2)",
            borderColor: "rgba(128, 0, 0, 1)",
            borderRadius: 3,
            borderWidth: 1,
            data: [55, 43, 38, 38, 38, 53, 54, 42],
            hoverBackgroundColor: "rgba(128, 0, 0, 0.5)",
            label: "H",
            yAxisID: 'y',
          },
          {
            type: 'bar',
            backgroundColor: "rgba(0, 200, 255, 0.2)",
            borderColor: "rgb(0,200,255, 0.6)",
            borderRadius: 5,
            borderWidth: 1,
            data: [100, 50, 0, 40, 0, 0, 40, 0],
            hoverBackgroundColor: "rgba(0, 200, 255, 0.5)",
            label: "P",
            yAxisID: 'y1',
          }
        ]
      },
            chartOptions: {
        layout: {
          padding: {
            left: 5,
            right: 20,
            top: 15,
          }
        },
        plugins: {
          tooltip: {
            position: 'average',
            mode: 'index',
          },
          legend: {
            position: 'bottom',
            labels: {
              boxWidth: 20,
            },
          },
        },
        responsive: true,
        scales: {
          x: {
            barPercentage: 0.5,
            categoryPercentage: 0.5,
            stacked: false,
            fontSize: 5,
            grid: {  // x grid doesn't make much sense for this chart.
              color: "#333333",
              display: true,
              borderDash: [1, 2],
            },
          },
          y: {
            grid:{
              color: "#333333",
              display: true,
              borderDash: [1, 2],
            },
            ticks: {
              stepSize: 20,
            },
            barPercentage: 0,
            categoryPercentage: 0,
            fontSize: 5,
            stacked: false,
            position: 'left',
          },
          y1: {
            ticks: {
              stepSize: 25,
            },
            barPercentage: 0,
            categoryPercentage: 0,
            fontSize: 5,
            stacked: false,
            position: 'right',
            min: 0,
            max: 100,
          }
        },
        maintainAspectRatio: false,
        animation: {
          duration: 2000,
          easing:'easeInOutQuart'
        }
      },
    };
  },
};
</script>

Then the chart object is displayed on Home.vue (and ultimately App.vue):

<!-- Home.vue -->
<template>
  <div class="home">
    <TileFormat class="A" tileSize="tile-double" :showButton="false" :yOverflow="false" header="A">
      <BarLine class="ChartBarTile" :chartHeight="160" :chartWidth="375" style="display: flex; justify-content: center;"/>
    </TileFormat>

    <TileFormat class="B" tileSize="tile-double" :showButton="false" :yOverflow="false" header="B">
      This will be a second chart just as soon as I figure out how to do it.
    </TileFormat>

  </div>
</template>

<script>
import TileFormat from '@/components/Tile.vue'
import BarLine from '@/components/BarLine.vue'

export default {
  components: {
    TileFormat,
    BarLine,
  }
}
</script>

What I've tried:

Console reports "Canvas is already in use. Chart with ID '0' must be destroyed before the canvas can be reused". What I have tried:

  • Multiple different attempts to destroy the canvas between calls.
  • Creating unique chart ids for each component.
  • Duplicate chart type components.
  • Moving Chart.vue code into the chart type component Vue files.
  • Making the second chart object of a different type which does display.
3 Answers

The error stems from the fact that you are selecting your canvas element in a non unique way

const chartElement = document.querySelector(`.${this.chartType} canvas`);

It seems logical that if you display multiple times the same chart type, you will get the first canvas on the document instead of the one in your component, resulting in your error

Instead you should use a ref to target your canvas. You might also want to add a v-once directive to avoid a possible rerender destroying your canvas and thus your chart

In your template:

<canvas v-once ref="canvas" style="height: 100%; width: 100%;"></canvas>

And in your method:

const chartElement = this.$refs.canvas

What I can see in the code you posted: <Chart id="chartImage" ...>. So you have two times the same id in the DOM which sounds like the error you are getting. Try to add a generated unique id to this component.

I found a method that works, but it requires refactoring the above code. For this method to work, each chart must be in its own container like a <div> or, in my case, <TileFormat>. It's confirmed to work with Chartjs 3.6.0 and Vue 3.0.

Sample Output

ChartComponent.vue

<template>
    <canvas width="100%" height="100%" :id="id"/>
</template>

<script>
import Chart from "chart.js/auto";

    export default {
        props: ['backgroundColor', 'borderWidth', 'borderColor', 'data', 'fill', 'height', 'id', 'labels', 'title', 'type', 'width',],
        mounted() {
            let ctx = document.getElementById(this.id).getContext('2d');
            new Chart(ctx, {
                type: this.type ? this.type : 'bar',
                data: {
                    labels: this.labels,
                    datasets: [{
                        label: this.title,
                        data: this.data,
                        fill: this.fill,
                        backgroundColor: this.backgroundColor,
                        borderColor: this.borderColor,
                        borderWidth: this.borderWidth ? this.borderWidth : 1
                    }]
                }
            });
        }
    }
</script>

Home.vue

<template>
  <div class="home">

    <TileFormat class="A" tileSize="tile-double" :showButton="false" :yOverflow="false" header="Chart A" @click="wasClicked">
      <Chart id="firstChart"
             title="Chart A"
             :labels='["Red", "Blue", "Yellow", "Green","Purple", "Orange"]'
             :data='[1, 3, 5, 6, 4, 2]'
             :background-color='["red", "blue", "yellow", "green","purple", "orange"]'
             :border-color="'rgba(255, 255, 255, 1)'"
      />
    </TileFormat>

    <TileFormat class="B" tileSize="tile-double" :showButton="false" :yOverflow="false" header="Chart B" @click="wasClicked">
      <Chart id="secondChart"
             title="Chart B"
             :labels='["Orange", "Purple", "Green", "Yellow", "Blue", "Red"]'
             :data='[6, 4, 2, 1, 3, 5]'
             :background-color='["orange", "purple", "green", "yellow", "blue", "red"]'
             :border-color="'rgba(255, 255, 255, 1)'"
      />
    </TileFormat>

  </div>
</template>

<script>
import TileFormat from '@/components/Tile.vue'
import Chart from '@/components/ChartComponent.vue'

export default {
  components: {
    TileFormat,
    Chart
  }
}
</script>
Related