chartJS pie chart not updating after axios GET - vue.js

In my Vue app I'm loading a page and it grabs some data from my Flask backend. I draw a bunch of elements and I also draw a pie chart based on an array of 3 values returned from the backend. When I get the response I update this.pie_data and I thought this would update in my template to reflect the pie chart. It renders the elements that get set in this.nvrs so not sure why it doesn't work for my pie chart.
Appreciate the help.
Template
<div class="box">
<p class="title">System Overview</p>
<chart :type="'pie'": data=chartData></chart>
</div>
Script
import axios from 'axios'
import Chart from 'vue-bulma-chartjs'
export default {
name: 'NVR_Overview',
components: {
Chart,
},
data: () => ({
nvrs: [],
// Health, Down, Warn
pie_data: [],
}),
methods: {
goToNVR (nvrId)
{
let wpsId = this.$route.params['wpsId']
let path = '/wps/' + wpsId + '/nvr/' + nvrId
this.$router.push(path)
},
},
created ()
{
axios
.get('http://localhost:5000/wps/' + this.$route.params['wpsId'])
.then(response =>
{
this.nvrs = response.data['nvr_list']
this.pie_data = response.data['pie_data']
console.log(this.pie_data)
})
.catch(e =>
{
console.log(e)
})
},
computed: {
chartData ()
{
return {
labels: ['Healthy', 'Down', 'Warning'],
datasets: [
{
data: this.pie_data,
backgroundColor: ['#41B482', '#ff4853', '#FFCE56'],
},
],
}
},
},
}

Solution 1:
Copy old chart data reference when you make the change (old data have Observer, so don't make completed new object), instead of using computed value, use watch:
<template>
<div class="box">
<p class="title">System Overview</p>
<chart :type="'pie'" :data="chartData"></chart>
</div>
</template>
<script>
import axios from 'axios'
import Chart from 'vue-bulma-chartjs'
export default {
name: 'NVR_Overview',
components: {
Chart,
},
data: () => ({
nvrs: [],
// Health, Down, Warn
pie_data: [],
chartData: {
labels: ['Healthy', 'Down', 'Warning'],
datasets: [
{
data: [],
backgroundColor: ['#41B482', '#ff4853', '#FFCE56'],
},
]
}
}),
methods: {
goToNVR (nvrId)
{
let wpsId = this.$route.params['wpsId']
let path = '/wps/' + wpsId + '/nvr/' + nvrId
this.$router.push(path)
},
},
created ()
{
axios
.get('http://localhost:5000/wps/' + this.$route.params['wpsId'])
.then(response =>
{
this.nvrs = response.data['nvr_list']
this.pie_data = response.data['pie_data']
console.log(this.pie_data)
})
.catch(e =>
{
console.log(e)
})
},
watch: {
pie_data (newData) {
const data = this.chartData
data.datasets[0].data = newData
this.chartData = {...data}
}
},
}
</script>
You can check this problem on vue-bulma-chartjs repository https://github.com/vue-bulma/chartjs/pull/24
Solution 2:
Add ref to chart
<chart ref="chart" :type="'pie'" :data="data"</chart>
then in script after you assign data:
this.$nextTick(() => {
this.$refs.chart.resetChart();
})

Related

Problems with chart.js redrawing or not redrawing graphs

I am Japanese. Therefore, my sentences may be strange. Please keep that in mind.
I am writing code using vue.js, vuex, vue-chart.js and vue-chart.js to display the population of each prefecture of Japan when checked.I’m code is written to redraw the graph when the input element for each prefecture is checked.However, it does not redraw when checked.Also, it may redraw after half of the check.I believe this phenomenon can be confirmed from the following URL.
https://yumemi-coding.web.app/
※There are no errors.
Here's a question: what causes the graphs to redraw or not? Also, how can I code to remedy this?
What I have done to counteract the cause is as follows
I went to the official website and used the rendering process as a reference.
 URL:https://vue-chartjs.org/migration-guides/#new-reactivity-system
 => The way we did it was right.
We thought there was a problem with VueX and coded in a way that did not use it. => There was nothing wrong with vuex.
TopFroont.vue
<template>
<div class="Bar_area">
<Bar :options="chartOptions" :data="chartData" class="Bar_item" />
</div>
</template>
<script>
import { Bar } from "vue-chartjs"
import { Chart as ChartJS, registerables } from "chart.js"
ChartJS.register(...registerables)
export default {
name: "BarChart",
components: { Bar },
data() {
return {
chartOptions: {
responsive: true,
},
}
},
computed: {
chartData() {
return {
labels: this.$store.state.years,
datasets: this.$store.state.prefectures,
}
},
},
}
</script>
NaviBar.vue
<template>
<div class="navApp">
<ul>
<li v-for="(pref, index) in prefData" :key="index" class="pref_itemBox">
<label>
<input type="checkbox" #change="checkItem(pref)" />
<span class="pref_text">{{ pref.prefName }}</span>
</label>
</li>
</ul>
</div>
</template>
<script>
import resasInfo from "#/library/resas.js"
import axios from "axios"
export default {
data() {
return {
resasInfo: resasInfo,
url: resasInfo.url_prefectures,
api: resasInfo.api,
prefData: [],
prefectures: [],
}
},
async created() {
const request_Header = {
headers: { "X-API-KEY": this.api.key },
}
await axios.get(this.url, request_Header).then((res) => {
const value = res.data.result
this.prefData.push(...value)
})
},
methods: {
checkItem(pref) {
// チェックされてる都道府県のみを配列に入れる
const isExistencePref = this.prefectures.indexOf(pref)
isExistencePref === -1
? this.prefectures.push(pref)
: this.prefectures.splice(isExistencePref, 1)
this.$store.dispatch("getPrefectures", this.prefectures)
},
},
}
</script>
vuex => store/index.js
import axios from "axios"
import { createStore } from "vuex"
import createPersistedState from "vuex-persistedstate"
export default createStore({
state: {
prefectures: [],
years: [],
},
mutations: {
getPrefs(state, payload) {
state.prefectures = payload
},
getYears(state, payload) {
state.years = payload
},
},
actions: {
getPrefectures({ commit }, payload) {
// payload => 各都道府県のprefCode + prefName
const allPrefecture_Data = []
const result = payload.map(async (el) => {
const prefCode_data = el.prefCode
axios
.get(
`https://opendata.resas-portal.go.jp/api/v1/population/composition/perYear?prefCode=${prefCode_data}&cityCode=-`,
{
headers: {
"X-API-KEY": "5RDiLdZKag8c3NXpEMb1FcPQEIY3GVwgQwbLqFIx",
},
}
)
.then((res) => {
const value = res.data.result.data[0].data
const TotalPopulation_Year = []
const TotalPopulation_Data = []
// 都道府県の総人口データと年データを各配列に入れ込む
value.forEach((element) => {
TotalPopulation_Data.push(element.value)
TotalPopulation_Year.push(element.year)
})
// rgbaを自動生成する関数 => backgroundColor
const generateRGBA = () => {
const r = Math.floor(Math.random() * 256)
const g = Math.floor(Math.random() * 256)
const b = Math.floor(Math.random() * 256)
const a = 0.8
return `rgba(${r}, ${g}, ${b}, ${a})`
}
// chart.jsに入れ込むデータ
const prefData = {
label: el.prefName,
data: TotalPopulation_Data,
backgroundColor: generateRGBA(),
}
allPrefecture_Data.push(prefData)
commit("getPrefs", allPrefecture_Data)
commit("getYears", TotalPopulation_Year)
})
.catch((err) => {
console.log(err)
})
})
return result
},
},
plugins: [createPersistedState()],
getters: {},
modules: {},
})

Vue, Vue-chartjs, pass looped data from template to script tag

I am totally new to Vuejs and I used vue-chartjs.
So I am calling data from an API to a parent view and passing it into a component.
It generates charts based on how much chart I have which is good. However I can't figure how to change "chartData"'s data based on what I got from the API.
using: vue3 composition API and vue-chartjs
viewChart.vue
<template>
<div class="grid grid-rows-2 grid-flow-col gap-40">
<chart :charts="charts"/>
</div>
</template>
<script setup>
import chart from '../components/chart.vue'
import axios from 'axios'
import { nextTick, ref, reactive } from 'vue';
const charts = ref([])
const getCharts = async () => {
try {
const response = await axios.get('http://localhost:3000/api/charts/',{
headers: {
"Content-Type": "application/json",
}
})
charts.value = response.data
console.log(charts.notApplicable)
} catch (error) {
}
}
getCharts()
</script>
chart.vue
<template>
<div style="color:black;">
<h2 class="text-black">Title</h2>
<div v-for="chart in charts" :key="chart.id">
<!-- {{chart.chart}} -->
<Pie
:chart-options="chartOptions"
:chart-data="chartData"
:chart-id="chartId"
:dataset-id-key="datasetIdKey"
:plugins="plugins"
:css-classes="cssClasses"
:styles="styles"
:width="width"
:height="height"
/>
</div>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Pie } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, ArcElement, CategoryScale } from 'chart.js'
ChartJS.register(Title, Tooltip, Legend, ArcElement, CategoryScale)
const props = defineProps({
chartId:{
type: String,
default: 'pie-chart'
},
datasetIdKey:{
type: String,
default: 'label'
},
width: {
type: Number,
default: 300
},
height: {
type: Number,
default: 500
},
cssClasses: {
default: '',
type: String
},
styles: {
type: Object,
default: () => {}
},
plugins: {
type: Object,
default: () => {}
},
charts: {
type: Object,
default: () => {}
}
})
/* data */
const chartData = reactive({
labels: [ 'Implemented',
'Partically Implemented',
'Not Implemented',
'Not Applicable' ],
datasets: [ {
backgroundColor: [ 'Green', 'Orange', 'Red', 'Grey'],
data: [ 4,2,8,5]
} ]
})
const chartOptions = reactive({
responsive: true,
maintainAspectRatio: false,
plugins:{
legend:{
display:true,
position:'bottom',
}
}
})
</script>
tried watchers and emits based on what I saw on the internet but I do not actually get it
try putting you api request into a onMounted hook
import { onMounted } from 'vue'
async function getCharts() {
try {
const response = await axios.get('http://localhost:3000/api/charts/',{
headers: {
"Content-Type": "application/json",
}
})
charts.value = response.data
console.log(charts.notApplicable)
} catch (error) {
}
}
onMounted(() => {
getCharts() //<--------------- call your API here.
})
Update
const chartData = computed(() => {
return {
labels: [
'Implemented',
'Partically Implemented',
'Not Implemented',
'Not Applicable',
],
datasets: [
{
backgroundColor: ['Green', 'Orange', 'Red', 'Grey'],
data: props.charts.data, // ??? how does your response look like?
},
],
};
})
otherwise give me a model example of your chart api response

Testing vue watchers with vue-testing-library

Anyone know how I would test a watcher in a component with vue-testing-library?
Here is my component. I want to test that the method is called when the brand vuex state is updated. With vue test utils it would be easy but I have not found a good way to do this with vue testing library. Has anyone did this before using vue testing library.
<template>
<v-data-table
data-testid="builds-table"
:headers="headers"
:items="builds"
:items-per-page="10"
class="elevation-1"
:loading="loading"
>
<template v-slot:[getItemStatus]="{ item }">
<v-chip :color="getStatusColor(item.status)" dark>
{{ item.status }}
</v-chip>
</template>
</v-data-table>
</template>
<script>
import { mapState } from "vuex";
import { getScheduledBuilds } from "../services/buildActivationService";
import { getStatusColor } from "../utils/getStatusColor";
export default {
name: "BuildsTable",
data() {
return {
loading: false,
headers: [
{
text: "Activation Time",
align: "start",
value: "buildActivationTime",
},
{ text: "Build ID", value: "buildId" },
{ text: "Build Label", value: "buildLabel" },
{ text: "Status", value: "status" },
],
error: "",
};
},
async mounted() {
this.getBuilds();
},
computed: {
...mapState(["brand", "builds"]),
getItemStatus() {
return `item.status`;
},
},
watch: {
brand() {
this.getBuilds();
},
},
methods: {
getStatusColor(status) {
return getStatusColor(status);
},
async getBuilds() {
try {
this.loading = true;
const builds = await getScheduledBuilds(this.$store.getters.brand);
this.$store.dispatch("setBuilds", builds);
this.items = this.$store.getters.builds;
this.loading = false;
} catch (error) {
this.loading = false;
this.error = error.message;
this.$store.dispatch("setBuilds", []);
}
},
},
};
</script>
Vue Testing Library is just a wrapper for Vue Test Utils, so the same call verification techniques apply.
Here's how to verify the call with Jest and Vue Testing Library:
Spy on the component method definition before rendering the component:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
render(BuildsTable)
Render the component with a given store and a callback to capture the Vuex store instance under test:
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
Update the store's brand value, and wait a macro tick for the watcher to take effect, then verify the getBuilds spy is called twice (once in mounted() and again in the brand watcher):
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
The full test would look similar to this:
import { render } from '#testing-library/vue'
import BuildsTable from '#/components/BuildsTable.vue'
describe('BuildsTable.vue', () => {
it('calls getBuilds when brand changes', async() => {
const getBuilds = jest.spyOn(BuildsTable.methods, 'getBuilds')
let store = {
state: {
brand: '',
builds: [],
}
}
const storeCapture = (_, vuexStore) => store = vuexStore
render(BuildsTable, { store }, storeCapture)
store.state.brand = 'foo'
await new Promise(r => setTimeout(r)) // wait for effect
expect(getBuilds).toHaveBeenCalledTimes(2)
})
})

vueJS table with dynamic data

I am trying to populate a table using bootStrap and vueJS using an API return from harry potter API. I can see the data is being returned but it does not show up on my DOM. I have a component called HouseRoster which I would like to deploy to create the tables for different houses.
<template>
<div>
<HouseRoster
house="Gryffindor"
fields="fields"
/>
</div>
</template>
<script>
import { mapState } from 'vuex'
import HouseRoster from '#/components/HouseRoster.vue'
export default {
components: {
HouseRoster
},
data () {
return {
fields: [
'name',
'role',
'bloodStatus',
'species'
],
characters: []
}
},
mounted: function () {
const house = 'Gryffindor'
const url = 'https://www.potterapi.com/v1/characters?key=key&house=' + house
fetch(url, {
method: 'get'
})
.then((response) => {
return response.json()
})
.then((jsonData) => {
this.characters = jsonData
})
},
computed: {
...mapState([
'Gryffindor'
])
}
}
</script>

How to pass multiple mutations to update Vuex store?

If I mutate the state only once (by committing either of the two mutations shown below), there is no error and chart is updated correctly.
If I run both mutations:
commit('SET_COURSE_MATERIAL', data)
commit('SET_TOOLS_EQUIPMENT', data)
then I get Maximum call stack exceeded: RangeError.
If I comment out the code in the watch property of chart.vue, there are no errors and I can see the state with correct values in console.log
I am getting the error regarding maximum call stack only when I run "npm run dev". When I deploy it to Google Cloud, the site works as expected and I don't get any errors. I even re-checked this by editing some code and re-deploying it twice while also noticing the time in the build logs.
summary.vue
<v-card-text>
<chart
:chart-config.sync="this.$store.state.summary.courseMaterial"
/>
</v-card-text>
...
<v-card-text>
<chart
:chart-config.sync="this.$store.state.summary.toolsEquipment"
/>
</v-card-text>
chart.vue
<template>
<v-flex>
<no-ssr><vue-c3 :handler="handler"/></no-ssr>
</v-flex>
</template>
<script>
import Vue from 'vue'
export default {
name: 'chart',
props: ['chartConfig'],
data() {
return {
handler: new Vue()
}
},
watch: {
chartConfig: function(val) {
console.log('chart component > watch > chartConfig', val)
this.drawChart()
}
},
created() {
this.drawChart()
},
methods: {
drawChart() {
this.handler.$emit('init', this.chartConfig)
}
}
}
</script>
store/summary.js
import axios from 'axios'
import _ from 'underscore'
import Vue from 'vue'
import {
courseMaterialChartConfig,
toolsEquipmentChartConfig,
} from './helpers/summary.js'
axios.defaults.baseURL = process.env.BASE_URL
Object.filter = (obj, predicate) =>
Object.assign(
...Object.keys(obj)
.filter(key => predicate(obj[key]))
.map(key => ({ [key]: obj[key] }))
)
export const state = () => ({
courseMaterial: '',
toolsEquipment: '',
})
export const getters = {
courseMaterial(state) {
return state.courseMaterial
},
toolsEquipment(state) {
return state.toolsEquipment
}
}
export const actions = {
async fetchData({ state, commit, rootState, dispatch }, payload) {
axios.defaults.baseURL = process.env.BASE_URL
let { data: initialData } = await axios.post(
'summary/fetchInitialData',
payload
)
console.log('initialData', initialData)
let [counterData, pieChartData, vtvcData, guestFieldData] = initialData
//dispatch('setCourseMaterial', pieChartData.coureMaterialStatus)
//dispatch('setToolsEquipment', pieChartData.toolsEquipmentStatus)
},
setCourseMaterial({ commit }, data) {
commit('SET_COURSE_MATERIAL', courseMaterialChartConfig(data))
},
setToolsEquipment({ commit }, data) {
commit('SET_TOOLS_EQUIPMENT', toolsEquipmentChartConfig(data))
}
}
export const mutations = {
// mutations to set user in state
SET_COURSE_MATERIAL(state, courseMaterial) {
console.log('[STORE MUTATIONS] - SET_COURSEMATERIAL:', courseMaterial)
state.courseMaterial = courseMaterial
},
SET_TOOLS_EQUIPMENT(state, toolsEquipment) {
console.log('[STORE MUTATIONS] - SET_TOOLSEQUIPMENT:', toolsEquipment)
state.toolsEquipment = toolsEquipment
},
}
helpers/summary.js
export const courseMaterialChartConfig = data => {
return {
data: {
type: 'pie',
json: data,
names: {
received: 'Received',
notReceived: 'Not Received',
notReported: 'Not Reported'
}
},
title: {
text: 'Classes',
position: 'right'
},
legend: {
position: 'right'
},
size: {
height: 200
}
}
}
export const toolsEquipmentChartConfig = data => {
return {
data: {
type: 'pie',
json: data,
names: {
received: 'Received',
notReceived: 'Not Received',
notReported: 'Not Reported'
}
},
title: {
text: 'Job Role Units',
position: 'right'
},
legend: {
position: 'right'
},
size: {
height: 200
}
}
}
Deep copy the chart config.
methods: {
drawChart() {
this.handler.$emit('init', {...this.chartConfig})
}
}