highcharts / highcharts-vue

Other
684 stars 150 forks source link

How to replace series data completely after fetching new data? #265

Closed pablosettecase closed 1 week ago

pablosettecase commented 1 week ago

Intro

I have a simple Vue 3 app with a date picker and a Highcharts line plot. I'm using this library and was able to leverage the example code in these docs to create my line chart in Vue's composition API. My goal is to find the recommended way to update the entire series data. The example code in these docs only adds points to an existing ref array as far as I can tell.

Problem

After the user uses the date picker and a new data set is fetched, what is the recommended way to update the entire series data?

I have tried

My Current Solution

Pop all the elements out of my data ref object and push each new data point in.

My Code

My code below shows my current solution, not my attempts that I mentioned. Any help is appreciated. Thank you.

<template>
  <div class="py-5 px-10 border border-indigo-200 dark:border-slate-600 rounded-lg text-center bg-indigo-50 dark:bg-slate-700">
      <h1 class="text-3xl font-light sm:text-xl md:text-2xl p-5">
        My Daily Plot
      </h1>

    <!-- chart controls -->
    <div class="flex justify-center mx-auto p-5">
      <div class="flex-initial w-64">
        <VueDatePicker class="mb-1 dark:bg-slate-900" v-model="date" @update:model-value="handleDate" :enable-time-picker="false" utc position="left"></VueDatePicker>
      </div>
      <div class="flex-initial w-52">
        <select v-model="numDays" id="numDays" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500">
          <option >7</option>
          <option selected="14">14</option>
          <option value="30">30</option>
          <option value="90">90</option>
          <option value="365">365</option>
          <option value="All">All</option>
        </select>
        <label for="numDays" class="ml-2 mb-2 text-sm font-medium text-gray-900 dark:text-white">Days</label>
      </div>
      <div class="flex-initial w-52">
        <input v-model="yAxisMin" type="number" id="yAxisMin" class="w-20 bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="" required />
        <label for="yAxisMin" class="ml-2 mb-2 text-sm font-medium text-gray-900 dark:text-white">Y-Axis Min</label>
      </div>
      <div class="flex-initial w-52">
        <input v-model="yAxisMax" type="number" id="yAxisMax" class="w-20 bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 p-1.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="" required />
        <label for="yAxisMax" class="ml-2 mb-2 text-sm font-medium text-gray-900 dark:text-white">Y-Axis Max</label>
      </div>
      <div class="flex-initial w-52 py-2.5">
          <input v-model="autoCheck" @change="yAxisAuto()" id="autoAxis" type="checkbox" value="" class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600">
          <label for="autoAxis" class="ms-2 text-sm font-medium text-gray-900 dark:text-gray-300">Auto Y-Axis</label>
      </div>
    </div>

    <!-- Show loading spinner while loading is true -->
    <div v-if="state.isLoading" class="text-center text-gray-500 py-6">
    <PulseLoader />
    </div>

    <!-- highcharts -->
    <div v-else>
      <highcharts :options="chartOptions" :update="watchers"></highcharts>
    </div>
  </div>
</template>

<script setup>
  import { ref, onMounted, reactive } from 'vue';
  import VueDatePicker from '@vuepic/vue-datepicker';
  import '@vuepic/vue-datepicker/dist/main.css'
  import { dateToString1 } from '../../Utils/mydates.js';
  import PulseLoader from 'vue-spinner/src/PulseLoader.vue';
  import { useToast } from 'vue-toastification';
  import axios from 'axios';

  const watchers = ref([
    'options.series'
  ])
  const yAxisMinDefault = 0
  const yAxisMaxDefault = 0.5
  const yAxisMin = ref(yAxisMinDefault)
  const yAxisMax = ref(yAxisMaxDefault)
  const numDays = ref(14)
  const date = ref(new Date())
  const autoCheck = ref(false)

  const data = ref([[1724569232000, 0.2682],[1724655632000, 0.2661], [1724742032000, 0.2904]])
  const state = reactive({
    data: [],
    isLoading: true,
  });

  const toast = useToast();
  const chartOptions = ref({

    chart: {
      type: 'line',
        zooming: {
            type: 'x'
        },
    },
    plotOptions: {
      line: {
        dataLabels: {
          enabled: true,
        }
      }
    },
    title: {
        text: 'My Data'
    },
    xAxis: {
        type: 'datetime'
    },
    yAxis: {
        title: {
            text: 'My Data RMS (m)'
        },
        min: yAxisMin,
        max: yAxisMax
    },
    series: [{
      name: 'Daily Data',
      data: data
    }]
    })

  onMounted(() => {
    let dateStr = dateToString1(date.value)
    getData(dateStr)
  });

  const handleDate = (modelData) => {
    let dateStr = null;
    if (!modelData) {
      dateStr = dateToString1(date.value)
    } else {
      dateStr = modelData.split('T')[0]
    }

    getData(dateStr)
  }

  const getData = async function (dateStr) {
    // const baseurl = 'http://127.0.0.1:5000/api/v0/daily/'
    const baseurl = '/api/api/v0/daily'
    const url = `${baseurl}/${dateStr}`
    try {
      const response = await axios.get(url, {params: { report_date: dateStr }});
      const new_data = response.data

      while (data.value.length) {
        data.value.pop()
      }

      for (let i = 0; i < new_data.length; i++) {
        const a = new_data[i]
        var d1 = new Date(new_data[i][0]);
        data.value.push(a)
      }
      // toast.success(`Fetched daily data for ${date.value}`);
    } catch (error) {
      const msg = `Error fetching daily data for ${date.value}`
      console.error(msg, error);
      toast.error(msg);
    } finally {
      state.isLoading = false;
    }
  }

  const yAxisAuto = function () {
    if (autoCheck.value) {
      yAxisMin.value = null
      yAxisMax.value = null
    } else {
      yAxisMin.value = yAxisMinDefault
      yAxisMax.value = yAxisMaxDefault
    }
  }
</script>
jszuminski commented 1 week ago

Hi @pablosettecase, thanks for reporting!

I believe that this chartOptions.value.series[0].data = newdata is what you've tried out and it works as expected: https://stackblitz.com/edit/vue3-hightcharts-vue-b5qd87?file=src%2Fcomponents%2FMyChart.vue,src%2Fmain.js

By updating the chartOptions ref, you update the chart configuration and the chart gets redrawn properly.

This is currently the recommended approach.

As an alternative (though not officially supported and recommended), you could access the Highcharts.Chart instance (https://github.com/highcharts/highcharts-vue?tab=readme-ov-file#chart-object-reference) and perform all the methods listed here: https://api.highcharts.com/class-reference/Highcharts.Chart

But essentially, the exact same result can be achieved in 90+% of cases by just updating the chartOptions, just as you suggested.


I'll still be available to support you with this, but I'm closing this issue as it's not a bug.

pablosettecase commented 1 week ago

Thank you! Your code sample helped me update my code to clean it up. There is an important part that I missed in the docs in the npm page, that are also not in any of the sample code.

In the component I added:

import { Chart } from 'highcharts-vue'

and in the template I changed

<highcharts :options="chartOptions"></highcharts>

to

<Chart :options="chartOptions" ref="chart"></Chart>

When fetching data, I am now updating the series like you suggest:

chartOptions.value.series[0].data = newData