I'm trying to use the PrimeVue Chart component in my VueJS app, but to do that I need to reference the underlying chart, which requires a template reference. Unfortunately, as soon as I add that, I get "Maximum recursive updates exceeded." Here's a super-simple component that causes it:
<script setup>
import { ref } from "vue"
const pvChart = ref(null)
let basicData = {
datasets: [{
label: "Setpoints",
data: [{ x: -10, y: 0 },
{ x: 0, y: 10 },
{ x: 10, y: 5 },
{ x: 0.5, y: 5.5 }],
showLine: true,
},
]
}
</script>
<template>
<div>
<div class="card">
<pv-chart ref="pvChart" type="scatter" :data="basicData" />
</div>
</div>
</template>
I don't even try to update anything (AFAICT).
I tried to follow the example in this SO question, but it's written in a slightly different style, and I'm not sure I didn't miss something in the translation.
Update:
Changing the name of the ref from pvChart to primeChart fixes all the issues. I just grepped my whole codebase and there is no other pvChart in it anywhere. I don’t know why this is an issue.
Related
Goal
I'm using Vue3 with the Composition API. I want to stream real-time data into a chart / graph (60 fps, possibly multiple streams / multiple updating arrays). To simplify for this question, I want to create a bar chart that updates it bars reactively to data changes by a button.
Attempts
I tried various JavaScript chart libraries like PrimeVue (Chart.js under the hood), vue-chartjs,Vue-ECharts, plotly.js. However, I'm still struggling either getting it to work or getting smooth animation. I thought real-time plotting would be a more common case, but I have a hard time finding examples (or my Google-foo fails me).
PrimeVue attempt
My best progress is with PrimeVue. I got the reactive part to work, but the issue is that the whole graph animates from scratch at each data update.
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import PrimeVue from 'primevue/config';
import Chart from 'primevue/chart';
const app = createApp(App);
app.use(PrimeVue);
app.component('Chart', Chart);
app.mount("#app");
<script setup lang="ts">
import { reactive, ref } from 'vue'
const chartValues = reactive([0.3, 1])
const basicData = ref({
labels: ['January', 'February'],
datasets: [
{
label: 'My First dataset',
backgroundColor: '#42A5F5',
data: chartValues
},
{
label: 'My Second dataset',
backgroundColor: '#FFA726',
data: [0.4, 0.1]
}
]
});
const horizontalOptions = ref(
{
// animation: {
// duration: 0
// }
}
);
function increment() {
chartValues[0] += 0.1
if (chartValues[0] > 1) {
chartValues[0] = 0
}
}
</script>
<template>
<h1>PrimeVue chart 2</h1>
<button #click="increment">count is: {{ chartValues[0] }}</button>
<div>
<div class="card">
<h5>Horizontal</h5>
<Chart type="bar" :data="basicData" :options="horizontalOptions" />
</div>
</div>
</template>
I can prevent the whole graph animation by setting animation: {duration: 0}, but this does not look dynamic/fluid. There is no transition between values.
Questions
It would be helpful if you could:
(best) Share a code snippet / link to a demo (any chart/graph library is okay) that only updates bars in a bar chart for which the data has changed (or line point in a line chart) with a smooth transition (or anything related to a single array) using Vue3.
Recommend a good JS chart library for smooth real-time plot animation that works with Vue3's reactive() / ref().
Any other advice that would help me
It depends on what kind of changes you want to make.
If you only want to change existing data, then it's relatively easy. If you want to add additional data to the chart it gets quite a bit harder, but based on your example, you're looking to mutate the a specific data inside an array so I won't cover the later.
The problem appears to be that these libraries don't handle the reactive data. Whether you have a reactive in ref or just using reactive The data passed to chart.js loses it's reactivity. I haven't looked into any of them to see why, but seems like prime-vue, vue-chartjs, and #j-t-mcc/vue3-chartjs all lose reactivity. I suspect they might be watching reactivity at a higher level, and when arrays and objects are mixed within reactive, it doesn't work well
to get around it, you can can manually call the update of the chartjs component. To do that pass the component a ref and then call the update() method when you are making an update (or using a watch)
<script>
import { reactive, ref, watch } from "vue";
export default {
setup() {
const barGraph = ref(null); // define the $ref
const basicData = reactive({
labels: ["January", "February"],
datasets: [
{
label: "My First dataset",
backgroundColor: "#42A5F5",
data: [0.3, 1],
},
{
label: "My Second dataset",
backgroundColor: "#FFA726",
data: [0.4, 0.1],
},
],
});
const horizontalOptions = {
animation: {
duration: 400,
},
};
function increment() {
let val = basicData.datasets[0].data[0];
basicData.datasets[0].data[0] = ((val * 10 + 1) / 10) % 1;
// when the data changes and the barGraph $ref is not null
if (barGraph.value) {
// call update method
barGraph.value.chart.update();
}
}
return {
basicData,
horizontalOptions,
increment,
barGraph,
};
},
};
</script>
<template>
<button #click="increment">
count is: {{ basicData.datasets[0].data[0] }}
</button>
<div>
<div class="card">
<h5>Horizontal</h5>
<Chart
type="bar"
:data="basicData"
:options="horizontalOptions"
ref="barGraph"
/>
</div>
</div>
</template>
With the Vue-ApexCharts library and learning from Daniel's answer, I was able to get a reactive bar chart to work.
Install Vue-ApexCharts in Vue3:
npm install --save apexcharts
npm install --save vue3-apexcharts
main.ts
import { createApp } from 'vue'
import App from './App.vue'
import VueApexCharts from "vue3-apexcharts";
const app = createApp(App);
app.use(VueApexCharts);
app.mount("#app");
App.vue
<template>
<div id="app">
<button #click="increment">Increment bar 1</button>
<button #click="addSeries">Add data series</button>
<button #click="incrementExtra">Increment appended series data</button>
<VueApexCharts :options="chartData.options" :series="chartData.series"/>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import VueApexCharts from 'vue3-apexcharts'
const seriesData1 = reactive([0.3, 1])
const seriesDataExtra = ref([0.1, 0.7])
const chartData = reactive({
series: [
{
name: 'My First series',
data: seriesData1
},
{
name: 'My Second series',
data: [0.4, 0.1]
}
],
options: {
chart: {
type: 'bar',
// https://apexcharts.com/docs/options/chart/animations/
animations: {
enabled: true,
easing: 'linear',
speed: 200,
animateGradually: {
enabled: false
},
dynamicAnimation: {
enabled: true,
speed: 150
}
}
},
xaxis: {
categories: ['January', 'February']
}
}
})
function increment() {
seriesData1[0] = ((seriesData1[0] * 10 + 1) / 10) % 1 // + 0.1
// chartData.series[0].data[0] = ((chartData.series[0].data[0] * 10 + 1) / 10) % 1
console.log(seriesData1)
}
function incrementExtra() {
seriesDataExtra.value = seriesDataExtra.value.map(element => ((element * 10 + 1) / 10) % 1)
console.log(seriesDataExtra)
}
function addSeries() {
console.log("Add extra series")
chartData.series.push({
name: 'My Next series',
data: seriesDataExtra
})
}
</script>
The above code can update a single bar's data, which triggers only an animation for that bar. This is done by:
Creating a reactive variable: const seriesData1 = reactive([0.3, 1])
With data: seriesData1 we assign this reactive array to the Chart
The first button triggers the function increment, which increments the first bar's data by 0.1: seriesData1[0] = ((seriesData1[0] * 10 + 1) / 10) % 1
Note that for step 3 we could have also directly updated the data in the Chart with chartData.series[0].data[0] = ((chartData.series[0].data[0] * 10 + 1) / 10) % 1 and this would give the same effect (this line of code also updates the variable seriesData1).
However, by updating the reactive variable seriesData1, we can separate data and the chart logic. You don't have to know how to assign data to the chart, but still get your reactive chart by just modifying the data.
As a bonus I also added a demonstration on how to:
Add an extra data stream / series to the plot with the function addSeries and
Reactively animate this new data by incrementing all values in a ref array using the function incrementExtra.
Modify the animation behavior so it updates all bars at the same time without a bouncy effect (default)
Demo video: https://i.imgur.com/e5a0y8Z.mp4
I am using vis.js timeline in a Vue.js project. Although it seems pretty easy to customize the locale using vanilla javascript (see codepen and documentation), I just can't get it done with Vue. I have already added moment.js and moment-with-locales-es6 to my project. Is this the right way to apply the locale to moment.js so it also applies to vis timeline?
This is my vue.js component:
<template>
<div className="wrapper">
<div id="visualization"></div>
</div>
</template>
<script>
import {Timeline} from 'vis-timeline/standalone';
import tr from 'moment/locale/tr'
import moment from "moment-with-locales-es6";
export default {
data() {
return {
items: [
{
id: 1,
content: "1",
start: new Date(2021, 2, 23),
group: 0
},
options: {
locale: "tr",
locales: {
tr: {
current: "geçerli",
time: "kere",
},
},
},
timeline: null
}
},
mounted() {
this.timeline = new Timeline(document.getElementById('visualization'));
this.timeline.setOptions(this.options);
this.timeline.setItems(this.items);
moment.locale('tr')
}
}
</script>
I managed to do it by adding this to my index.html file:
<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/moment.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.24.0/locale/tr.js'></script>
I am pretty sure there's a better solution for this...
I've some problems with vue-router and gsap's scrolltrigger plugin.
I have some vue components using scroltrigger and when I go to a different page and came back to the page having the scrolltrigger effect it doesn't trigger, but work if I manualy refresh the page.
I find this topic with people having the same problem with NuxtJs, but the ScrollTrigger.disable() and ScrollTrigger.kill() solutions not working for me :
https://greensock.com/forums/topic/24886-in-nuxt-when-using-scrolltriggerkill-how-can-it-run-again-when-page-is-viewed/
Here is a component I made with ScrollTrigger :
Template Part
<template>
<section class="marquee">
<div class="marquee__inner" aria-hidden="true" ref="inner">
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
<div class="marquee__part">food wine fish beef vegetables</div>
</div>
</section>
</template>
Script part
<script>
import gsap from "gsap"
import ScrollTrigger from 'gsap/ScrollTrigger'
gsap.registerPlugin(ScrollTrigger)
export default {
name: 'ServicesMarquee',
data() {
return {
currentScroll: 0,
isScrollingDown: true,
test: null,
}
},
methods: {
scrollAnim() {
gsap.to(this.$refs.inner, {
xPercent: -65,
scrollTrigger: {
trigger: ".marquee",
start: "top bottom",
end: "top top",
scrub: 0,
}
})
},
},
mounted() {
gsap.set(this.$refs.inner, {xPercent: -50});
let tween = gsap.to(this.$refs.inner.querySelectorAll('.marquee__part'), {xPercent: -100, repeat: -1, duration: 10, ease: "linear"}).totalProgress(0.5);
let self = this
window.addEventListener("scroll", function(){
if ( window.pageYOffset > self.currentScroll ) {
self.isScrollingDown = true;
} else {
self.isScrollingDown = false;
}
gsap.to(tween, {
timeScale: self.isScrollingDown ? 1 : -1
});
self.currentScroll = window.pageYOffset
});
gsap.to(this.$refs.inner, {xPercent: -65 });
this.scrollAnim()
}
}
</script>
I read all the articles on this and similar problems on Stackoverflow and on GSAP forums, and YES setTimeout working, but this is not the answer.
I was also confused that the problem does not affect a large number of people, but they describe almost the same situations, when vue + router + gsap + scrolltrigger stops working when you go through the pages (routes) and return to previous one with animations (animations don't work in this case).
So guys, first of all look at <transition> above <router-view>. This is the problem when you have components from previous page AND the next page at the same time, and ScrollTrigger calculates the values (offsetTop) in that time
I've created a component in vue which wraps a vue-apexchart donut graph. As soon as the page loads and this component is loaded, the vue-apexchart animates and displays a small graph.
Now I would like to instantiate multiple of these components from a dataset side by side. Instead of the components to all load an animate at the same time, I would like a small rendering delay to give it an overall nice effect. Something like this would be nice:
<donut :items="series1"></donut>
<donut :items="series2" delay=1500></donut>
The vue-apexchart doesent support initialization delays, and as far as I can see there isn't any vue-specific official solution to delay the rendering of components.
I've tried to put a setTimeout in any of the component hooks to stall the initialization,
I´ve also tried to inject the all the graph DOM in the template element on a v-html tag in a setTimeout, but apexchart doesent notice this new dom content, and vue doesent notice the html bindings either.
I´ve created this fiddle which loads two instances of a graph:
https://jsfiddle.net/4f2zkq5c/7/
Any creative suggestions?
There are several ways you can do this, and it depends on whether you can actually modify the <animated-component> logic yourself:
1. Use VueJS's built-in <transition-group> to handle list rendering
VueJS comes with a very handy support for transitions that you can use to sequentially show your <animated-component>. You will need to use a custom animation library (like VelocityJS) and simply store the delay in the element's dataset, e.g. v-bind:data-delay="500". VueJS docs has a very good example on how to introduce staggered transitions for <transition-group>, and the example below is largely adapted from it.
You then use the beforeAppear and appear hooks to set the opacity of the individual children of the <transition-group>.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
}
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
},
methods: {
beforeAppear: function(el) {
el.style.opacity = 0;
},
appear: function(el, done) {
var delay = +el.dataset.delay;
setTimeout(function() {
Velocity(
el, {
opacity: 1
}, {
complete: done
}
)
}, delay)
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<transition-group name="fade" v-on:before-appear="beforeAppear" v-on:appear="appear">
<animated-component v-bind:data="dataset.first" v-bind:key="0"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:key="1" v-bind:data-delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:key="2" v-bind:data-delay="1000"> </animated-component>
</transition-group>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div>
<h1>Animated Component</h1>
{{ data }}
</div>
</script>
2. Let <animated-component> handle its own rendering
In this example, you simply pass the a number to the delay property (remember to use v-bind:delay="<number>" so that you pass a number and not a string). Then, in the <animated-component>'s mounted lifecycle hook, you use a timer to toggle the visibility of the component itself.
The technique on how you want to show the initially hidden component is up to you, but here I simply apply an initial opacity of 0 and then transition it after a setTimeout.
Vue.component('animated-component', {
template: '#animatedComponentTemplate',
props: {
data: {
required: true
},
delay: {
type: Number,
default: 0
}
},
data: function() {
return {
isVisible: false
};
},
computed: {
styleObject: function() {
return {
opacity: this.isVisible ? 1 : 0
};
}
},
mounted: function() {
var that = this;
window.setTimeout(function() {
that.isVisible = true;
}, that.delay);
}
});
new Vue({
el: '#app',
data: {
dataset: {
first: 'Hello world',
second: 'Foo bar',
third: 'Lorem ipsum'
}
}
});
.animated-component {
transition: opacity 0.25s ease-in-out;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<animated-component v-bind:data="dataset.first"> </animated-component>
<animated-component v-bind:data="dataset.second" v-bind:delay="500"> </animated-component>
<animated-component v-bind:data="dataset.third" v-bind:delay="1000"> </animated-component>
</div>
<script type="text/x-template" id="animatedComponentTemplate">
<div class="animated-component" v-bind:style="styleObject">
<h1>Animated Component, delay: {{ delay }}</h1>
{{ data }}
</div>
</script>
If you have the possibility to reformat your data, you can build an array of series objects, add a show: true/false property and iterate it:
//template
<div v-for="serie in series">
<donut :items="serie.data" v-if="serie.show"></donut>
</div>
//script
data: function() {
return {
series: [
{ data: [44, 55, 41, 17, 15], show: false },
{ data: [10, 20, 30], show: false },
]
}
}
Now you can create a setTimeout function which will change the serie.show to true by incrementing the delay based on the serie index.
Then add the function on the mounted hook:
methods: {
delayedShow (serie, idx) {
let delay = 1500 * idx
setTimeout(() => {
serie.show = true
}, delay)
}
},
mounted () {
this.series.forEach((serie, idx) => {
this.delayedShow(serie, idx)
})
}
Live example
Faced the same problem with ApexCharts Pie Charts being redrawn rapidly in sequence due to data being pulled from a pinia store mutating too quickly for the chart to keep up, leading to ugly errors in the console.
I resolved the issue by using a boolean ref in a v-if="showChart" on the component and then using a setTimeout to trigger a delayed drawing of the chart:
import { ref } from "vue";
import useStore from "#/store/myChartStore";
const store = useStore();
const showChart = ref(false);
store.$subscribe((mutation, state) =>{
showChart.value = false;
setTimeout(()=> {
showChart.value = true;
}
, 100);
});
If you're not using a store, you may find another way to watch the initial availability of the chart data and then delay the rendering using that same approach.
I've got a working script that runs fine under Vue 1.x but it doesn't work with Vue 2.x even though I have replaced ready by mounted
<div id="app">
<div id="slider"></div>
<input id="slider-input" v-model="third" v-on:change="updateSlider"/>
<input id="slider-input" v-model="fourth" v-on:change="updateSlider"/>
</div>
Vue 1.x:
var vue = new Vue({
el: '#app',
data: {
first: 3,
second: 2,
third: 40,
fourth: 60,
slider: {
min: 0,
max: 100,
start: [50, 60],
step: 1
},
Slider: document.getElementById('slider')
},
computed: {
total: function total() {
return parseInt(this.first) * parseInt(this.second) * parseInt(this.third);
}
},
methods: {
updateSlider: function updateSlider() {
this.Slider.noUiSlider.set(this.third);
}
},
ready: function ready() {
noUiSlider.create(this.Slider, {
start: this.slider.start,
step: this.slider.step,
range: {
'min': this.slider.min,
'max': this.slider.max
}
});
}
});
Now I have replaced ready with mounted and it's still not working. Problem is, it doesn't even spit out an error message in the console.
My guess is that the <div id="slider"> in the template is being replaced with a different element instance once Vue has compiled the full template and rendered itself (after mounting). What I mean is, in the mounted hook, this.Slider and document.getElementById('slider') no longer refer to the same element (this.Slider is removed from the DOM).
There's probably no reason for Slider to be defined within the data block (it needn't be reactive), just initialize that in the mounted hook instead:
mounted() {
this.Slider = document.getElementById('slider')
noUiSlider.create(this.Slider, ...)
}
Actually, a better way would be to use ref to get an instance of the element instead of querying the DOM:
<div ref="slider"></div>
mounted() {
noUiSlider.create(this.$refs.slider, ...)
}
It was because mounted does not support functions, you had to create the functions in methods and just call it from mounted.