javascriptvuejs3vue-composition-apiapexchartspinia

How do I make my ApexCharts reactive with data in Pinia Store using Vue.js 3 and composition API?


I am building a dashboard using Vue.js 3 and ApexCharts with the composition API. The data for the charts is held in a Pinia data store.

I have a function that pulls data from a sample .json file (which models what an API will return once it is set up), processes, and returns the data as a multidimensional array for the charts that is stored in the Pinia store.

The problem I'm having is when I run the function to update the data, I can see it update in the Pinia store via vue devtools, but the chart does not change to reflect the new values like the rest of the app does.

At the advice of other stack overflow answers, I've wrapped chart options and series in ref(). I've also read the info on this GitHub repo, but as I'm fairly new to Vue and have only worked with the composition API and <script setup>, I'm unsure what exactly to implement in my code to make the charts reactive to Pinia data.

I have created a sample repo with a single chart and sample data on stackblitz showing the behavior I'm experiencing.

Chart component with options and series code:

<template>
  <VueApexCharts type="bar" height="360px" :options="chartOptions" :series="chartSeries">
  </VueApexCharts>
</template>

<script setup>
/*********
 Imports
 *********/
import VueApexCharts from "vue3-apexcharts";
import { ref } from "vue";
import { useDataStore } from "@/stores/dataStore.js";

const storeData = useDataStore();

/*********
 Apex Chart
 *********/
let chartOptions = ref({
  chart: {
    id: "grocerChart",
    type: "bar",
    // height: 350,
    stacked: true,
    stackType: "100%",
    toolbar: {
      show: true,
      tools: {
        download: true,
        zoom: false,
        zoomin: true,
        zoomout: true,
        reset: true,
      },
    },
  },
  title: {
    text: storeData.selectedRegion,
    align: "center",
  },
  plotOptions: {
    bar: {
      horizontal: true,
    },
  },
  grid: {
    padding: {
      bottom: 20,
      right: 20,
      top: 5,
    },
  },
  xaxis: {
    categories: storeData.chartData[0], // ['Fruit', "Meat", "Vegetables"],
    tickPlacement: "on",
    labels: {
      rotate: 0,
      style: {
        fontSize: 10,
      },
    },
  },
  fill: {
    opacity: 1,
  },
  legend: {
    fontSize: 10,
    offsetX: 0,
    offsetY: 0,
  },
  dataLabels: {
    enabled: false,
  },
  noData: {
    text: "No Data",
    style: {
      fontSize: "24px",
    },
  },
});

let chartSeries = ref([
  {
    name: storeData.chartData[1][0][0], // 'fruit',  
    data: storeData.chartData[1][0][1], // [10, 14, 10], 
  },
  {
    name: storeData.chartData[1][1][0], // 'meat', 
    data: storeData.chartData[1][1][1], // [10, 10, 4],
  },
  {
    name: storeData.chartData[1][2][0], // 'vegetable',
    data: storeData.chartData[1][2][1], // [9, 7, 12],
  },
]);

// console.log(storeData.chartDataTest[1][0][1])
</script>

<style scoped>
</style>

Pinia code:

import {ref} from 'vue'
import {defineStore} from 'pinia'

export const useDataStore = defineStore('data', () => {

    let selectedRegion = ref('No Region Selected');
    let chartData = ref([
        [],
        [
          [[], []],
          [[], []],
          [[], []]
        ]
    ]);

    return {
        selectedRegion,
        chartData
      }
})

App.vue with function to process model .json data and return multidimensional array for chart:

<template>
  <div class="w-100">
    <p>Select a region: </p>
    <v-btn class="ma-3" @click="storeData.selectedRegion = 'East'">East</v-btn>
    <v-btn class="ma-3" @click="storeData.selectedRegion = 'Midwest'">Midwest</v-btn>
    <p>Selected Region: {{ storeData.selectedRegion }}</p>
    <p class="mt-5">After selecting a region above, click "Update Chart" below:</p>
    <v-btn 
    class="ma-3" 
    @click="storeData.chartData = updateData(grocerData, storeData.selectedRegion)">
      Update Chart
    </v-btn>
    <v-card class="w-75 mt-5">
      <StoreInventoryChart/>
    </v-card>
  </div>

</template>

<script setup>
import {useDataStore} from "@/stores/dataStore.js";
import StoreInventoryChart from "@/components/storeInventoryChart.vue";
import grocerData from "./models/sample-api-data.json";

const storeData = useDataStore();

function updateData(data, selectedRegion) {
    // Filter data by selected state 
    const selRegionData = data.filter(item => item.state === selectedRegion);

    // Extract and sort store names
    const stores = [...new Set(selRegionData.map(item => item.store))].sort();

    // Extract and sort category values
    const categories = [...new Set(selRegionData.map(item => item.category))].sort();

    // Initialize the result array for categorized data
    const categorizedData = categories.map(category => [category, Array(stores.length).fill(0)]);

    // Populate the categorized data
    selRegionData.forEach(item => {
        const storeIndex = stores.indexOf(item.store);
        const categoryIndex = categories.indexOf(item.category);
        categorizedData[categoryIndex][1][storeIndex] += item.inventory;
    });

    return [stores, categorizedData];
}

</script>

<style scoped>

</style>

Any help is greatly appreciated!


Solution

  • You have to use computed instead of ref to receive all updates. Using ref only catches chart's data available at the component initialization ignoring further updates. The following code would work without extra effort:

    - import { ref } from 'vue';
    + import { computed } from 'vue';
    
    - let chartOptions = ref({
    + let chartOptions = computed(() => ({
    ...
    - let chartSeries = ref([
    + let chartSeries = computed(() => [
    

    The full working version:

    import VueApexCharts from 'vue3-apexcharts';
    import { computed } from 'vue';
    import { useDataStore } from '@/stores/dataStore.js';
    
    const storeData = useDataStore();
    
    /*********
     Apex Chart
     *********/
    let chartOptions = computed(() => ({
      chart: {
        id: 'grocerChart',
        type: 'bar',
        // height: 350,
        stacked: true,
        stackType: '100%',
        toolbar: {
          show: true,
          tools: {
            download: true,
            zoom: false,
            zoomin: true,
            zoomout: true,
            reset: true,
          },
        },
      },
      title: {
        text: storeData.selectedRegion,
        align: 'center',
      },
      plotOptions: {
        bar: {
          horizontal: true,
        },
      },
      grid: {
        padding: {
          bottom: 20,
          right: 20,
          top: 5,
        },
      },
      xaxis: {
        categories: storeData.chartData[0], // ['Fruit', "Meat", "Vegetables"],
        tickPlacement: 'on',
        labels: {
          rotate: 0,
          style: {
            fontSize: 10,
          },
        },
      },
      fill: {
        opacity: 1,
      },
      legend: {
        fontSize: 10,
        offsetX: 0,
        offsetY: 0,
      },
      dataLabels: {
        enabled: false,
      },
      noData: {
        text: 'No Data',
        style: {
          fontSize: '24px',
        },
      },
    }));
    
    let chartSeries = computed(() => [
      {
        name: storeData.chartData[1]?.[0]?.[0], // 'fruit',
        data: storeData.chartData[1]?.[0]?.[1], // [10, 14, 10],
      },
      {
        name: storeData.chartData[1]?.[1]?.[0], // 'meat',
        data: storeData.chartData[1]?.[1]?.[1], // [10, 10, 4],
      },
      {
        name: storeData.chartData[1]?.[2]?.[0], // 'vegetable',
        data: storeData.chartData[1]?.[2]?.[1], // [9, 7, 12],
      },
    ]);
    

    The computed properties would catch every changes and reflect them to the underlying component (VueApexCharts).

    In chartSeries computation, I've considered the nullable data using the null-coalescing operator (?.) preventing possible errors in absence of storeData.chartData.