How to update chart when state changes in vue? - vue.js

I've mapped chartData to a state property using vuex. What I'd like to do is update the chart when a dataset is updated. I have heard that it can be done with mixins or watchers but I don't know how to implement it. I understand that mixins creates a watcher but I don't know how it is used within vuex.
Chartline.vue:
<script>
import { Line } from 'vue-chartjs'
import { mapState } from 'vuex'
export default {
name: 'ChartLine',
extends: Line,
computed:{
...mapState(['charData','options'])
},
methods:{
regraph: function(){
this.renderChart(this.charData,this.options);
}
},
mounted () {
this.regraph();
},
watch: {
}
}
</script>
Pension.vue:
<template>
<div id='pension' class="navbarPar">
<ChartLine/>
</div>
</template>
<script>
import ChartLine from '../components/ChartLine.vue';
import { mapState } from 'vuex'
//import { Line } from 'vue-chartjs'
export default {
name: 'Pension',
components: {
ChartLine,
},
data(){
return{
form: {
...
},
var:{
...
},
}
},
methods: {
calculate: function(indice){
...
//modify data of mapState
//after here, I want to rerender chart
}
},
computed:{
...mapState(['charData','options']),
},
}
</script>

Using a watcher like this should be enough:
<script>
import { Line } from "vue-chartjs";
import { mapState } from "vuex";
export default {
name: "ChartLine",
extends: Line,
computed: {
...mapState(["chartData", "options"])
},
methods: {
regraph() {
this.renderChart(this.chartData, this.options);
}
},
mounted() {
this.regraph();
},
watch: {
chartData: {
handler: this.regraph,
deep: true
}
}
};
</script>
Also having the explicit vuex state map inside the ChartLine component seems a bit wasteful - passing the vuex data through props would render the component more generic:
<template>
<div id='pension' class="navbarPar">
<ChartLine :options="options" :chart-data="chartData"/>
</div>
</template>
<script>...
Chartline.vue:
<script>
import { Line } from "vue-chartjs";
export default {
name: "ChartLine",
extends: Line,
props: {
options: {
type: Object,
default: () => ({})
},
chartData: {
type: Object /*is it?*/,
default: () => ({})
}
},
methods: {
regraph() {
this.renderChart(this.chartData, this.options);
}
},
mounted() {
this.regraph();
},
watch: {
chartData: {
handler: this.regraph,
deep: true
}
}
};
</script>

If you are using vue-chartjs, the library has its own way to handle reactive data in charts:
// ChartLine.js
import { Line, mixins } from 'vue-chartjs'
const { reactiveProp } = mixins
export default {
extends: Line,
mixins: [reactiveProp],
props: ['options'], // passed from the parent
mounted () {
// this.chartData is created in the mixin (pass it as any prop with :chart-data="data").
this.renderChart(this.chartData, this.options)
}
}
Now the Pension.vue file
// Pension.vue
<template>
<div id='pension' class="navbarPar">
<ChartLine :chart-data="charData" :options="options" />
</div>
</template>
<script>
import ChartLine from '../components/ChartLine';
import { mapState } from 'vuex'
export default {
name: 'Pension',
components: {
ChartLine,
},
data(){
return{
form: {
...
},
var:{
...
},
}
},
methods: {
calculate: function(indice){
...
//modify data of mapState
//after here, I want to rerender chart
}
},
computed:{
...mapState(['charData','options']),
},
}
</script>
You can read more about it here: https://vue-chartjs.org/guide/#updating-charts,
there are some caveats

Related

Attribute patch in pinia store is working from one component, but not from another (Nuxt.js)

I've those two components using Nuxt 3.
The setActive method in component 1 changes the state of activeColor, but the cancelEdit method in component 2 does not.
Any idea why this is the case?
Component 1
Here the setActive method changes activeColor:
<template>
<div class="f-button" #click="addColor">+ Add Color</div>
{{ activeColor }}
<div class="f-colors">
<Color v-for="color in colors" :color="color" :edit="activeColor === color" #click="setActive(color)"/>
</div>
</template>
<script>
import {useProjectStore} from "~/stores/projects";
import {storeToRefs} from "pinia";
import Color from "~/components/Color.vue";
export default defineComponent({
name: "ColorManagement",
components: {Color},
setup() {
const projectStore = useProjectStore()
const { getColors, getActiveColor } = storeToRefs(projectStore);
return {
projectStore,
colors: getColors,
activeColor: getActiveColor
};
},
methods: {
addColor() {
...
},
setActive(color) {
this.projectStore.$patch({ activeColor: color })
}
}
});
</script>
Component 2
Here the cancelEdit method doesn't change activeColor:
<div class="f-color">
<div class="f-color__actions" v-if="edit">
<div class="f-color__action" #click="cancelEdit">
<Cancel /><span>Cancel</span>
</div>
</div>
</div>
</template>
<script>
import Cancel from "~/components/icons/Cancel.vue";
import {useProjectStore} from "~/stores/projects";
import {storeToRefs} from "pinia";
export default defineComponent({
name: "Color",
components: {Cancel},
props: ["color","edit"],
setup() {
const projectStore = useProjectStore()
const { activeColor } = storeToRefs(projectStore);
return {
projectStore,
activeColor
};
},
methods: {
cancelEdit() {
this.projectStore.$patch({ activeColor: false })
}
}
});
</script>
nuxt.config.ts
export default defineNuxtConfig({
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData: '#use "#/assets/styles/_styles.scss" as *;'
}
}
}
},
modules: ['#pinia/nuxt']
})
Store
import { defineStore } from "pinia";
export const useProjectStore = defineStore({
id: 'project',
state: () => {
return {
project: {
colors: [{ color: false, name: '' }]
},
activeColor: null
}
},
getters: {
getColors(state){
return state.project.colors || [];
},
getActiveColor(state){
return state.activeColor;
}
}
});
Ok, if I got this correctly, the deal is this:
Your so called Component 2 is the <Color ... component being used in Component 1, right?
When you trigger cancelEdit inside Component 2 (aka Color) you are also triggering the logic from setActive due to this <Color ...#click="setActive(color)"...so your activeColor is set to false (from the cancelEdit method) but right after it is set to active again, got it?
To fix this (if you don't want to change your HTML structure) you can use events stopPropagation method inside the cancelEdit:
cancelEdit(e) {
e.stopPropagation()
this.projectStore.$patch({ activeColor: false })
}
Event.stopPropagation() reference

Vue received a Component which was made a reactive object

The problem I need to solve: I am writing a little vue-app based on VueJS3.
I got a lot of different sidebars and I need to prevent the case that more than one sidebar is open at the very same time.
To archive this I am following this article.
Now I got a problem:
Vue received a Component which was made a reactive object. This can lead to unnecessary performance overhead, and should be avoided by marking the component with markRaw or using shallowRef instead of ref. (6)
This is my code:
SlideOvers.vue
<template>
<component :is="component" :component="component" v-if="open"/>
</template>
<script>
export default {
name: 'SlideOvers',
computed: {
component() {
return this.$store.state.slideovers.sidebarComponent
},
open () {
return this.$store.state.slideovers.sidebarOpen
},
},
}
</script>
UserSlideOver.vue
<template>
<div>test</div>
</template>
<script>
export default {
name: 'UserSlideOver',
components: {},
computed: {
open () {
return this.$store.state.slideovers.sidebarOpen
},
component () {
return this.$store.state.slideovers.sidebarComponent
}
},
}
</script>
slideovers.js (vuex-store)
import * as types from '../mutation-types'
const state = {
sidebarOpen: false,
sidebarComponent: null
}
const getters = {
sidebarOpen: state => state.sidebarOpen,
sidebarComponent: state => state.sidebarComponent
}
const actions = {
toggleSidebar ({commit, state}, component) {
commit (types.TOGGLE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
},
closeSidebar ({commit, state}, component) {
commit (types.CLOSE_SIDEBAR)
commit (types.SET_SIDEBAR_COMPONENT, component)
}
}
const mutations = {
[types.TOGGLE_SIDEBAR] (state) {
state.sidebarOpen = !state.sidebarOpen
},
[types.CLOSE_SIDEBAR] (state) {
state.sidebarOpen = false
},
[types.SET_SIDEBAR_COMPONENT] (state, component) {
state.sidebarComponent = component
}
}
export default {
state,
getters,
actions,
mutations
}
App.vue
<template>
<SlideOvers/>
<router-view ref="routerView"/>
</template>
<script>
import SlideOvers from "./SlideOvers";
export default {
name: 'app',
components: {SlideOvers},
};
</script>
And this is how I try to toggle one slideover:
<template>
<router-link
v-slot="{ href, navigate }"
to="/">
<a :href="href"
#click="$store.dispatch ('toggleSidebar', userslideover)">
Test
</a>
</router-link>
</template>
<script>
import {defineAsyncComponent} from "vue";
export default {
components: {
},
data() {
return {
userslideover: defineAsyncComponent(() =>
import('../../UserSlideOver')
),
};
},
};
</script>
Following the recommendation of the warning, use markRaw on the value of usersslideover to resolve the warning:
export default {
data() {
return {
userslideover: markRaw(defineAsyncComponent(() => import('../../UserSlideOver.vue') )),
}
}
}
demo
You can use Object.freeze to get rid of the warning.
If you only use shallowRef f.e., the component will only be mounted once and is not usable in a dynamic component.
<script setup>
import InputField from "src/core/components/InputField.vue";
const inputField = Object.freeze(InputField);
const reactiveComponent = ref(undefined);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = undefined;
}, 5000);
setTimeout(function() => {
reactiveComponent.value = inputField;
}, 5000);
</script>
<template>
<component :is="reactiveComponent" />
</template>

Can't access store from Vuex [Quasar]

I'm trying Quasar for the first time and trying to use the Vuex with modules but I can't access the $store property nor with ...mapState. I get the following error 'Cannot read property 'logbook' of undefined' even though I can see that the promise logbook exists on Vue Devtools. Print from Devtools
Here is my store\index.js
import Vue from 'vue';
import Vuex from 'vuex';
import logbook from './logbook';
Vue.use(Vuex);
export default function (/* { ssrContext } */) {
const Store = new Vuex.Store({
modules: {
logbook,
},
strict: process.env.DEV,
});
return Store;
}
Here is the component
<template>
<div>
<div>
<h3>RFID</h3>
<q-btn #click="logData"
label="Save"
class="q-mt-md"
color="teal"
></q-btn>
<q-table
title="Logbook"
:data="data"
:columns="columns"
row-key="uid"
/>
</div>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
export default {
name: 'RFID',
mounted() {
this.getLogbookData();
},
methods: {
...mapActions('logbook', ['getLogbookData']),
...mapGetters('logbook', ['loadedLogbook']),
...mapState('logbook', ['logbookData']),
logData: () => {
console.log(this.loadedLogbook);
},
},
data() {
return {
};
},
};
</script>
<style scoped>
</style>
Here is the state.js
export default {
logbookData: [],
};
Error that I get on the console
Update: Solved the problem by refactoring the way I declared the function. I changed from:
logData: () => { console.log(this.loadedLogbook); }
to
logData () { console.log(this.loadedLogbook); }
Check the .quasar/app.js file. Is there a line similar to import createStore from 'app/src/store/index', and the store is later exported with the app in that same file?
I think you confused all the mapx functions.
...mapState and ...mapGetters provide computed properties and should be handled like this
export default {
name: 'RFID',
data() {
return {
};
},
mounted() {
this.getLogbookData();
},
computed: {
...mapGetters('logbook', ['loadedLogbook']),
...mapState('logbook', ['logbookData']),
}
methods: {
...mapActions('logbook', ['getLogbookData']),
logData: () => {
console.log(this.loadedLogbook);
},
}
};

Vue pie chart show after a while (like show console with a warning)

I am trying to fill a pie chart on my vue application, I can correctly fill data into it, but the page didn't show immediately the pie chart, but after a while (like if a show console), and I got a warning in console :
vue.esm.js?efeb:628 [Vue warn]: Invalid prop: type check failed for
prop "chartData". Expected Object, got Null
found in
--->
at src/components/StastCard.vue
at src/App.vue
Here my code (Maybe there was another way to fill data, but I only succesfully done it in this way):
StastCard.vue:
<template>
<div>
<div class="container">
<div class="row">
<div class="col-sm">
<pie-chart :chartData="dataChart"></pie-chart>
</div>
<div class="col-sm"></div>
<div class="col-sm"></div>
</div>
</div>
</div>
</template>
<script>
import DataService from '#/services/DataService'
import PieChart from "#/plugins/PieChart.js";
export default {
name: 'StastCard',
props: {
username: {
type: String
}
},
components: {
PieChart
},
data: function() {
return {
dataChart: {
labels: ["Km", "KJ", "HB"],
datasets: [
{
label: "Data One",
backgroundColor: ["#41B883", "#E46651", "#00D8FF"],
data: [1, 10, 5]
}
]
},
}
},
methods: {
async addData() {
this.firstValue=DataService.getFirstValue()
this.secondValue=DataService.getSecondValue()
this.thirdValue=DataService.getThirdValue()
this.dataChart.labels.pop()
this.dataChart.labels.pop()
this.dataChart.labels.pop()
this.dataChart.labels.push(["Km"])
this.dataChart.labels.push(["KJ"])
this.dataChart.labels.push(["HB"])
this.dataChart.datasets[0].data.pop()
this.dataChart.datasets[0].data.pop()
this.dataChart.datasets[0].data.pop()
this.dataChart.datasets[0].data.push(this.firstValue)
this.dataChart.datasets[0].data.push(this.secondValue)
this.dataChart.datasets[0].data.push(this.thirdValue)
},
},
mounted() {
this.addData()
}
}
</script>
And here my PieChart.js
import { Pie, mixins } from 'vue-chartjs'
export default {
extends: Pie,
props: ['chartData', 'options'],
mounted() {
this.renderChart(this.chartData, this.options)
}
}
What am I doing wrong? Why my pie chart is not immediately displayed? Thank you
First, I think you might want to use reactiveProp to make your chart reactive with data changes.
Secondly, because of vue-chartjs will render child component before parent component, so you will get the Invalid prop warning. To fix it, you can change from mounted to created hook. You can find more information here.
import { Pie, mixins } from 'vue-chartjs'
export default {
extends: Pie,
mixins: [mixins.reactiveProp],
created() {
this.renderChart(this.chartData, {})
}
}
Lastly, you should assign chartData object to a new reference to make Vue reactive. An easy way is using JSON.parse(JSON.stringify())
methods: {
async addData() {
this.firstValue=DataService.getFirstValue()
this.secondValue=DataService.getSecondValue()
this.thirdValue=DataService.getThirdValue()
this.dataChart.labels.pop()
this.dataChart.labels.pop()
this.dataChart.labels.pop()
this.dataChart.labels.push(["Km"])
this.dataChart.labels.push(["KJ"])
this.dataChart.labels.push(["HB"])
this.dataChart.datasets[0].data.pop()
this.dataChart.datasets[0].data.pop()
this.dataChart.datasets[0].data.pop()
this.dataChart.datasets[0].data.push(this.firstValue)
this.dataChart.datasets[0].data.push(this.secondValue)
this.dataChart.datasets[0].data.push(this.thirdValue)
this.dataChart = JSON.parse(JSON.stringify(this.dataChart))
},
},
I finally found a solution, I change the .js file with this:
import { Pie } from 'vue-chartjs'
export default {
extends: Pie,
props: {
chartdata: {
type: Object,
default: null
},
options: {
type: Object,
default: null
}
},
methods: {
renderpie() {
this.renderChart(this.chartdata, this.options)
}
},
mounted() {}
}
Here my view:
<template>
<div>
<div class="container">
<div class="row">
<div class="col-sm">
<pie-chart :chartData="dataChart"></pie-chart>
</div>
<div class="col-sm"></div>
<div class="col-sm"></div>
</div>
</div>
</div>
</template>
<script>
import DataService from '#/services/DataService'
import PieChart from "#/plugins/PieChart.js";
export default {
name: 'StastCard',
props: {
username: {
type: String
}
},
components: {
PieChart
},
data: function() {
return {
dataChart: {},
firstValue:'',
secondValue:'',
thirdValue:''
}
},
methods: {
addData() {
this.firstValue=DataService.getFirstValue()
this.secondValue=DataService.getSecondValue()
this.thirdValue=DataService.getThirdValue()
var hrate = []
this.heart_rate.forEach(el => {
hrate.push(el.rate)
})
this.dataChart = {
labels: ['Km', 'Kj', 'HB'],
datasets: [
{
label: 'Data One',
backgroundColor: ['#41B883', '#E46651', '#00D8FF'],
data: [this.firstValue,this.secondValue,this.thirdValue]
}
]
}
},
},
async created() {
await this.addData()
this.$refs.pie.renderpie()
}
}
</script>

Is it possible to dynamically add chart type in the extends: property, based on props from parent component?

I have a vue chartjs component which imports the whole vue-chartjs library. My idea is, is it possible to somehow pass the type of the chart which I want and add it to the 'extends: VueCharts.charttype?.' In the example I provide it extends the VueCharts.Line, I need this property to be dynamically interpolated, passed from props. Is it possible this charttype to come from a parent props dynamically and how?
<script>
import { VueCharts } from "vue-chartjs";
export default {
extends: VueCharts.Line,
props: ["chartdata", "options"],
mounted() {
this.renderChart(this.chartdata, this.options);
}
}
</script>
<style scoped>
</style>
since extends the same as mixins, you need to pass a dynamic mixin, in order to do that you need two components, imagine we have component ChartWrapper :
<template>
<div>
<div>{{ chartType }}</div>
<chart :chart-data="datacollection"/>
</div>
</template>
<script>
import Chart from "./Chart";
import { VueCharts, mixins } from "vue-chartjs";
const { reactiveProp } = mixins;
export default {
name: "ChartWrapper",
components: {
Chart
},
props: {
chartType: {
type: String,
required: true
}
},
data() {
return {
datacollection: {
labels: [this.getRandomInt(), this.getRandomInt()],
datasets: [
{
label: "Data One",
backgroundColor: "#f87979",
data: [this.getRandomInt(), this.getRandomInt()]
},
{
label: "Data One",
backgroundColor: "#f87979",
data: [this.getRandomInt(), this.getRandomInt()]
}
]
}
};
},
methods: {
getRandomInt() {
return Math.floor(Math.random() * (50 - 5 + 1)) + 5;
}
},
created() {
if (this.chartType) {
Chart.mixins = [reactiveProp,VueCharts[this.chartType]];
}
}
};
</script>
this component takes chartType as a prop, and I import all charts as VueCharts in top of the script ==> 1
second component:
<script>
export default {
props: ["options"],
mounted() {
// this.chartData is created in the mixin.
// If you want to pass options please create a local options object
this.renderChart(this.chartData, this.options);
}
};
</script>
the second component just has options props, and renderChart function invoked.
==> 2
What is happening?
the ChartWrapper component receives the chart type by chartType prop, in the created hook, if chartType exist, assign the chart(resolved by VueCharts[this.chartType]) to Chart component as a mixin in addition to reactiveProp,
I also pass the chart data to Chart component.
in the end, call the ChartWrapper component:
<ChartWrapper chartType="Bar"/>
Live example on code sandbox: https://codesandbox.io/s/vue-template-w9r8k
You can also choose for the option to just extend the Line chart and update the config of the chart with the chart type you want and give it an update so it changes type.
<script>
import { Line, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins;
export default {
extends: Line,
name: "LineChart",
mixins: [reactiveProp],
props: {
options: { type: Object },
chartType: { type: String }
},
mounted () {
this.renderChart(this.chartData, this.options);
},
watch: {
options: {
deep: true,
handler () {
this.$data._chart.options = this.options;
this.updateChart();
}
},
chartType (newVal) {
this.$data._chart.config.type = newVal;
this.updateChart()
}
},
methods: {
updateChart () {
this.$data._chart.update();
},
}
}
</script>