working on a covid app to get familiar with vue2js.
Am now trying to get a graph with vue-chartjs but am failing to pass the data to the graph/chart component.
I make an API request with vuex and passing the data to my component: CountryGraph.vue which contains a Graph.vue with the chart itself.
vuex -> CountryGraph.vue -> Graph.vue
Passing data into CountryGraph.vue works:
But when I try to pass my data (countryGraph) as props to my char/Graph.vue component, then it is not done and I get in Graph.vue only the value undefined:
Why?
Below my code, first the CountryGraph.vue:
<template>
<section class="countryGraph">
<LineChart
:chartdata="chartData"
:options="chartOptions"
/>
</section>
</template>
<script>
import { mapGetters, mapActions } from "vuex";
import LineChart from "../graph/Graph";
export default {
name: "CountryGraph",
components: { LineChart },
data: () => ({
chartData: {
labels: this.countryGraph.map((el) => el.date),
datasets: [
{
label: "Confirmed",
backgroundColor: "#f87979",
data: this.countryGraph.map(
(el) => el.confirmed
),
},
],
},
chartOptions: {
responsive: true,
maintainAspectRatio: false,
},
}),
methods: {
...mapActions(["selectCountryGraph"]),
},
computed: {
...mapGetters(["countryGraph"]),
},
};
</script>
<style></style>
And my chart/Graph.vue component which is made so, that I can reuse it (as stated in vue-chartjs guide):
<script>
import { Bar } from "vue-chartjs";
export default {
extends: Bar,
props: {
chartdata: {
type: Object,
default: null,
},
options: {
type: Object,
default: null,
},
},
mounted() {
this.renderChart(this.chartdata, this.options);
},
};
</script>
<style />
When I use mocked data, like instead of
labels: this.countryGraph.map((el) => el.data)
I do labels: ["q", "w", "e", "r", "t"]
and instead of
data: this.countryGraph.map(el => el.confirmed)
I do data: [0, 1, 2, 3, 4]
everything works fine.
Also, when I pass my variables directly into the component, like:
<LineChart
:chartdata="this.countryGraph.map((el) => el.data)"
:options="chartOptions"
/>
Then I can see the data as props in the child (Graph.vue) component.
But in this case I use v-bind: and in the earlier one not. Maybe that is the problem?
A couple issues to note:
It looks like you're mapping a nonexisting property (el.data should be el.date). Possibly just a typo in the question.
this.countryGraph.map((el) => el.data) ❌
^
data() is not reactive, and cannot rely on computed props, so the countryGraph computed prop will not be available in data() and will not update chartData with changes. One way to fix this is to make chartData a computed prop:
export default {
computed: {
...mapGetters(["countryGraph"]),
// don't use an arrow function here, as we need access to component instance (i.e., this.countryGraph)
chartData() {
return {
labels: this.countryGraph.map((el) => el.date),
datasets: [
{
label: "Confirmed",
backgroundColor: "#f87979",
data: this.countryGraph.map((el) => el.confirmed),
},
],
}
}
}
}
Related
I trying to make a COVID19 visualization site using Chart.js and VueJs
this is My App.vue that contains the API call and stores the data into arrays
<template>
<div id="app" class="container">
<div class="row mt-5" v-if="PositiveCases.length > 0">
<div class="col">
<h2>Positives</h2>
<lineChart :chartData="PositiveCases" :options="chartOptions" label="Positive" />
</div>
</div>
</div>
</template>
<script>
import axios from "axios";
import moment from 'moment'
import lineChart from "./components/lineChart.vue";
export default {
name: 'App',
components: {
lineChart
},
data(){
return{
PositiveCases : [],
Deaths: [],
Recoverd: [],
chartOptions: {
responsive: true,
maintainAspectRatio: false
}
}
},
async created(){
const {data} = await axios.get('https://api.covid19api.com/live/country/egypt')
//console.log(data);
data.forEach(d => {
const date = moment(d.Date,"YYYYMMDD").format("MM/DD")
const {Confirmed,Deaths,Recovered} = d
this.PositiveCases.push({date, total : Confirmed})
this.Deaths.push({date, total : Deaths})
this.Recoverd.push({date, total : Recovered})
// console.log("PositiveCases",this.PositiveCases);
// console.log("Deaths",this.Deaths);
// console.log("Recoverd",this.Recoverd);
});
}
}
</script>
and this is my lineChart.vue that contains the Line chart code the data stored correctly in both the dates and totals
<script>
import {Line} from 'vue-chartjs'
export default {
extends: Line,
props: {
label:{
type: String,
},
chartData:{
type: Array,
},
options:{
type: Object,
}
},
mounted(){
const dates = this.chartData.map(d => d.date).reverse()
const totals = this.chartData.map(d => d.total).reverse()
console.log("dates",dates);
console.log("totals",totals);
this.renderChart({
labels: dates,
datasets: [{
label: this.label,
data: totals,
}],
},this.options
)
}
}
</script>
the error in the console says
want to know what is the solution, all the data are stored correctly in both files
You are using V4 of vue-chart.js, the chart creation process has been changed as you can read here in the migration guide.
So instead of calling this.renderChart which was the old syntax you now have to use the actual component and pass the data to it like so:
<template>
<Bar :chart-data="chartData" />
</template>
<script>
// DataPage.vue
import { Bar } from 'vue-chartjs'
import { Chart, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale } from 'chart.js'
Chart.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale)
export default {
name: 'BarChart',
components: { Bar },
data() {
return {
chartData: {
labels: [ 'January', 'February', 'March'],
datasets: [
{
label: 'Data One',
backgroundColor: '#f87979',
data: [40, 20, 12]
}
]
}
}
}
}
</script>
My problem: I followed the doc from vue-chartjs but the chart is not rendering the data.
Here is my code:
My component Chart.vue
<script>
import { Line } from 'vue3-chart-v2'
export default {
extends: Line,
props: {
chartdata: {
type: Object,
default: null
},
options: {
type: Object,
default: null
}
},
mounted () {
this.renderChart(this.chartdata, this.options)
}
}
</script>
My Home.vue code:
<template>
<div class="container">
<line-chart
v-if="loaded"
:chartdata="chartdata"
:options="options"/>
</div>
</template>
<script>
import LineChart from '../components/Chart.vue'
export default {
name: 'LineChartContainer',
components: { LineChart },
data: () => ({
loaded: false,
chartdata: null,
options: {
responsive: true,
}
}),
async mounted () {
this.loaded = false
try {
const quotes = [
1, 2, 3, 4,
]
this.chartdata = quotes
this.loaded = true
} catch (e) {
console.error(e)
}
}
}
</script>
When I'm rendering the Home view, I see the axis of the chart, but no data inside...
In the console, I don't have any errors.
Later, I would like to fetch data from an API. Thus, the quotes would receive data from an external API. Here I just put some fake data for testing.
Does anyone can help me ? Thks
I was missing the import { defineComponent } from 'vue' in my component. Now it works using the below code.
<template>
<div class="container">
<line-chart
v-if="loaded"
:chartdata="chartdata"
:options="options"/>
</div>
</template>
<script>
import LineChart from '../components/Chart.vue'
import { defineComponent } from 'vue'
export default defineComponent({
name: 'LineChartContainer',
components: { LineChart },
data: () => ({
loaded: false,
chartdata: null,
options: {
responsive: true,
}
}),
async mounted () {
this.loaded = false
try {
const quotes = [
1, 2, 3, 4,
]
this.chartdata = quotes
this.loaded = true
} catch (e) {
console.error(e)
}
}
})
</script>
I currently set up a new playground with VueJS/Laravel/Spark and want to implement a tags input component.
I don't understand how to register those components correctly. I'm following the how-to-guides and official documentation, but the implementation just works so-so.
I want to implement the library from #johmun -> http://www.vue-tags-input.com which I installed via npm (npm install #johmun/vue-tags-input).
I created a single file component named VueTagsInput.vue that looks like this:
<template>
<div>
<vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="newTags => tags = newTags"
:autocomplete-items="filteredItems"
/>
</div>
</template>
<script>
import VueTagsInput from '#johmun/vue-tags-input';
export default {
components: {
VueTagsInput,
},
data() {
return {
tag: '',
tags: [],
autocompleteItems: [{
text: 'Spain',
}, {
text: 'France',
}, {
text: 'USA',
}, {
text: 'Germany',
}, {
text: 'China',
}],
};
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
};
</script>
I imported this single file component at resources/js/bootstrap.js like so:
import VueTagsInput from './VueTagsInput.vue'
And I'm using this component in the home.blade.php view like this:
<vue-tags-input v-model="tag"
autocomplete-always-open
add-from-paste
allow-edit-tags>
</vue-tags-input>
This renders an input with which I can interact as desired, but I can not use the autocomplete function with the countries entered above, and the console also throws the following error:
[Vue warn]: Property or method "tag" is not defined on the instance but referenced during render. Make sure that this property is reactive, either in the data option, or for class-based components, by initializing the property.
I don't know what I'm doing wrong.
So I stumbled across the solution by trial & error.
First I had to register the component the right way in resources/js/bootstrap.js like so:
import VueTagsInput from './VueTagsInput.vue'
Vue.component('vue-tags-input', VueTagsInput);
But this caused another error because I called the component within the component registration itself. I used the name option in the single file component in order to overcome this error. I gave my newly created component a different name like this:
<template>
<div>
<johmun-vue-tags-input
v-model="tag"
:tags="tags"
#tags-changed="newTags => tags = newTags"
:autocomplete-items="filteredItems"
/>
</div>
</template>
<script>
import JohmunVueTagsInput from '#johmun/vue-tags-input';
export default {
name: "VueTagsInput",
components: {
JohmunVueTagsInput,
},
data() {
return {
tag: '',
tags: [],
autocompleteItems: [{
text: 'Spain',
}, {
text: 'France',
}, {
text: 'USA',
}, {
text: 'Germany',
}, {
text: 'China',
}],
};
},
computed: {
filteredItems() {
return this.autocompleteItems.filter(i => {
return i.text.toLowerCase().indexOf(this.tag.toLowerCase()) !== -1;
});
},
},
};
</script>
I have a parent component with 2 child components that both inherit from the same base component. (This parent component is being created and used in a Vue Storybook). Both SiblingAComponent and SiblingBComponent inherit the same BaseComponent, and instantiate the same inherited data classInstance, which is a vanilla JS class instance from another library. I am trying to access this classInstance from the parent component to pass as data into the second sibling component (in this case, from SiblingAComponent to SiblingBComponent), by using an reference siblingARef. However, I get this error from the storybook compiler:
too much recursion
isArguments#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:49010:16
keys#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:49073:28
_traverse#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:119972:19
_traverse#http://localhost:6006/vendors~main.9107ef8d0bc0558399e1.bundle.js:119974:28
ParentComponent Story:
storiesOf("ParentComponent Story", module)
.addDecorator(
withKnobs({
escapeHTML: false
})
)
.add("Passing data from A to B", () => ({
name: 'ParentComponent',
components: {
SiblingAComponent,
SiblingBComponent,
},
data() {
return {
siblingAData: [....], // array of objects
siblingAOptions: {
axes: {},
height: "50px",
},
siblingBData: [...], // array of objects
siblingBOptions: null,
}
},
mounted() {
const siblingAInstance = this.$refs.siblingARef.classInstance;
const newOptions = {
legend: {
external: {
reference: siblingAInstance,
},
},
};
// this line is where I am getting an error
this.siblingBOptions = legendExternal;
},
template: `
<SiblingAComponent ref="siblingARef" :data="siblingAData" :options="siblingAOptions"/>
<SiblingBComponent v-if="siblingBData" :data="siblingBData" :options="siblingBOptions"/>
`,
}));
SiblingAComponent:
<template>
<div class="sibling-a-component"></div>
</template>
<script>
import { ComponentA } from '#libraryexample/components';
import BaseComponent from './base-component.vue';
export default {
name: 'SiblingAComponent',
extends: BaseComponent,
mounted() {
this.classInstance = new ComponentA(this.$el, {
data: this.data,
options: this.options,
});
},
};
</script>
SiblingBComponent:
<template>
<div class="sibling-b-component"></div>
</template>
<script>
import { ComponentB } from '#libraryexample/components';
import BaseComponent from './base-component.vue';
export default {
name: 'SiblingBComponent',
extends: BaseComponent,
mounted() {
this.classInstance = new ComponentB(this.$el, {
data: this.data,
options: this.options,
});
},
};
</script>
BaseComponent:
<script>
export default {
name: 'BaseComponent',
data() {
return {
classInstance: null,
};
},
props: {
data: { type: [Object, Array], required: true },
options: { type: Object, required: true },
},
};
</script>
Coming from the Angular and React worlds, using a reference to access a Vanilla class instance from another Component is nothing new, even if it's unconventional. I am new to Vue, so I am wondering why would trying to access a class instance fail (works fine for primitive data types) and give me such a weird error? Where is the recursion occurring?
I have created a doughnut chart using vue-chart.js, Chart.js and using some values which are within my vuex state. The chart works apart from when the vuex state is updated the chart doesn't updated too.
I have tried to use computed properties to try keep the chart up to date. But then I get the following errors:
Error in callback for watcher "chartData": "TypeError: Cannot set property 'data' of undefined"
TypeError: Cannot set property 'data' of undefined
DoughnutChart.vue:
<script>
import { Doughnut, mixins } from 'vue-chartjs';
const { reactiveProp } = mixins;
export default {
extends: Doughnut,
mixins: [reactiveProp],
props: ['chartData', 'options'],
mounted () {
this.renderChart(this.chartdata, this.options)
}
}
</script>
AppCharts.vue:
<template>
<div id="chart_section">
<h2 class="section_heading">Charts</h2>
<div id="charts">
<DoughnutChart :chart-data="datacollection" :options="chartOptions" class="chart"></DoughnutChart>
</div>
</div>
</template>
<script>
import DoughnutChart from './DoughnutChart';
import { mapGetters } from 'vuex';
export default {
components: {
DoughnutChart
},
computed: {
...mapGetters(['boardColumnData']),
datacollection() {
return {
datasets: [{
data: [this.boardColumnData[0].total.$numberDecimal, this.boardColumnData[1].total.$numberDecimal, this.boardColumnData[2].total.$numberDecimal, this.boardColumnData[3].total.$numberDecimal],
backgroundColor: [
'#83dd1a',
'#d5d814',
'#fdab2f',
'#1ad4dd'
],
borderColor: [
'#83dd1a',
'#d5d814',
'#fdab2f',
'#1ad4dd'
],
}]
}
},
},
data() {
return {
chartOptions: null
}
},
mounted () {
this.fillData();
},
methods: {
fillData() {
this.chartOptions = {
responsive: true,
maintainAspectRatio: false
}
}
}
}
</script>
this is what boardColumnData looks like after getting the state:
[
{
"name":"Opportunities",
"percentage":{
"$numberDecimal":"0"
},
"total":{
"$numberDecimal":70269
}
},
{
"name":"Prospects",
"percentage":{
"$numberDecimal":"0.25"
},
"total":{
"$numberDecimal":0
}
},
{
"name":"Proposals",
"percentage":{
"$numberDecimal":"0.5"
},
"total":{
"$numberDecimal":5376
}
},
{
"name":"Presentations",
"percentage":{
"$numberDecimal":"0.75"
},
"total":{
"$numberDecimal":21480
}
},
]
The $numberDecimal value is what is updated inside vuex when an event on another component happens. When these values are changed I want the chart to update with the new values.
You must pass the data in the prop. I.e. fastest solution for you would be to have fillData() return the datacollection in the correct format.
E.g.
fillData(){
return {
datasets: [
{
data: [
this.boardColumnData[0].total.$numberDecimal,
this.boardColumnData[1].total.$numberDecimal,
this.boardColumnData[2].total.$numberDecimal,
this.boardColumnData[3].total.$numberDecimal,
],
backgroundColor: ["#83dd1a", "#d5d814", "#fdab2f", "#1ad4dd"],
borderColor: ["#83dd1a", "#d5d814", "#fdab2f", "#1ad4dd"],
},
];
}
}
You need to do the same for the options, but pass them in a new options=yourOptions() prop.
This drove me absolutely bonkers. I hope this still helps you (or someone else).