Related
I am using chartjs with the vue-chartjs wrapper and trying to create a Line Chart using data fetched from my api. The data prints to the console as expected and I do not get a error in the console. The resulting Line Chart does not display at all in the browser. There is, however a large amount of white space where the canvas tag is injected. I can create a doughnut chart fine, just not this line chart. Your help is greatly appreciated! I am using code from the Chart examples found at https://vue-chartjs.org/examples/ for the LineChart component
IndexView.vue
<script setup>
import axios from 'axios'
import { onMounted, reactive } from 'vue'
import LineChart from '#/components/LineChart.vue'
const data = reactive({
user: null,
totals: null,
checkins: null
})
const state = reactive({
loading: true
})
const charts = reactive({
doughnutConfig: null,
lineConfig: null
})
onMounted(async () => {
// load data from store and api
data.user = await userStore.fetchUser()
const user_resp = await axios.get(...)
data.totals = user_resp.data.totals
data.checkins = user_resp.data.check_ins
state.loading = false
// create line chart
var dates = []
var ratings = []
var length = data.checkins.length < 10 ? data.checkins.length : 10
for (var i = 0; i < length; i++) {
dates.push(data.checkins[i].date)
ratings.push(data.checkins[i].rating)
}
console.log(dates) // [ "2022-09-04T00:00:00", "2022-09-04T00:00:00", "2022-09-04T00:00:00", "2022-09-04T00:00:00", "2022-09-05T00:00:00" ]
console.log(ratings) // [ 5, 5, 3, 2, 4 ]
charts.lineConfig = {
data: {
labels: dates,
datasets: {
label: 'Ratings by date',
data: ratings,
backgroundColor: '#f87979'
}
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
}
}
}
})
</script>
<template>
<LineChart
v-if="charts.lineConfig"
:chart-options="charts.lineConfig.options"
:chart-data="charts.lineConfig.data"
:width="400"
:height="300"
/>
</template>
LineChart.vue
<script setup>
import { defineProps } from 'vue'
import { Line } from 'vue-chartjs'
import {
Chart as ChartJS,
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale
} from 'chart.js'
ChartJS.register(
Title,
Tooltip,
Legend,
LineElement,
LinearScale,
PointElement,
CategoryScale
)
const props = defineProps({
chartData: {
type: Object,
required: true
},
chartOptions: {
type: Object,
required: true
},
chartId: {
type: String,
default: 'line-chart'
},
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true
}
})
</script>
<template>
<Line
:chart-id="props.chartId"
:chart-data="props.chartData"
:chart-options="props.chartOptions"
:width="props.width"
:height="props.height"
/>
</template>
Usually when a chart is not shown, there is an issue on the data configuration.
In your data config, the datasets option seems to be defined as an object but instead should be an array.
data: {
labels: dates,
datasets: { // <--- should be an array.
label: 'Ratings by date',
data: ratings,
backgroundColor: '#f87979'
}
},
It should be:
data: {
labels: dates,
datasets: [{
label: 'Ratings by date',
data: ratings,
backgroundColor: '#f87979'
}]
},
I am building a react app using ChartJs and customizing each point on the line graph with a custom image. The image is accessed through a web address as shown below.
Essentially the getTopPlayersImages function returns an array of Image objects containing the src of the image.
const getTopPlayersImages = async (labels) => {
let images = await Promise.all(
labels.map(async (year, index) => {
let playerId = await getPlayerId(graphData[3][index].player);
let img = new Image(82.11, 60);
img.src = `https://ak-static.cms.nba.com/wp-content/uploads/headshots/nba/latest/260x190/${playerId}.png`;
img.onerror = ({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = require("../assets/logoman.png");
};
return img;
})
);
return images;
};
The issue is that sometimes when most or all of the images are found through the web address, the images are displayed on the line chart without issues. But if I select a category where the majority images are not found and have to use the local image ../assets/logoman.png, then I get the following error:
Uncaught DOMException: Failed to execute 'drawImage' on 'CanvasRenderingContext2D': The HTMLImageElement provided is in the 'broken' state.
at drawPoint (http://localhost:3000/static/js/bundle.js:17504:11)
at PointElement.draw (http://localhost:3000/static/js/bundle.js:10609:67)
at LineController.draw (http://localhost:3000/static/js/bundle.js:2967:17)
at LineController.draw (http://localhost:3000/static/js/bundle.js:4509:11)
at Chart._drawDataset (http://localhost:3000/static/js/bundle.js:9363:21)
at Chart._drawDatasets (http://localhost:3000/static/js/bundle.js:9333:12)
at Chart.draw (http://localhost:3000/static/js/bundle.js:9294:10)
at http://localhost:3000/static/js/bundle.js:1860:15
at Map.forEach (<anonymous>)
at Animator._update (http://localhost:3000/static/js/bundle.js:1833:18)
The head-scratching part is that this error doesn't happen 100% of the time. Sometimes everything works as expected, but more often than not, this error completely causes the app to crash and the chart to be removed from the DOM.
I think what is happening is that the chart is trying to display the images before they are fully loaded. How can I prevent ChartJs from rendering the images on the graph before they have had a chance to load?
Here is how I am rendering the images in chartJs.
const data = {
labels,
datasets: [
{
fill: "-1",
backgroundColor: "rgba(10, 162, 235, 0.5)",
label: "100th percentile",
data: graphData[3]?.map((item) => item[props.statSelection]),
playerName: graphData[3]?.map((item) => item.player),
pointStyle: topPlayerImages,
},
],
};
I am using pointStyle and the topPlayerImages is a state within my component which gets updated in a useEffect hook like so:
useEffect(() => {
const updateImages = async () => {
setTopPlayerImages([]);
let images = await getTopPlayersImages(labels);
setTopPlayerImages(images);
};
updateImages();
}, [graphData]);
Here is the full code for this component if that makes it a little bit clearer.
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend,
} from "chart.js";
import { Line } from "react-chartjs-2";
import { useEffect, useState } from "react";
import { main } from "../utils/statCalcs";
import { getPlayerId } from "../utils/getPlayerId";
const Chart = (props) => {
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Filler,
Legend
);
const getTopPlayersImages = async (labels) => {
let images = await Promise.all(
labels.map(async (year, index) => {
let playerId = await getPlayerId(graphData[3][index].player);
let img = new Image(82.11, 60);
img.src = `https://ak-static.cms.nba.com/wp-content/uploads/headshots/nba/latest/260x190/${playerId}.png`;
img.onerror = ({ currentTarget }) => {
currentTarget.onerror = null;
currentTarget.src = require("../assets/logoman.png");
};
return img;
})
);
return images;
};
const options = {
responsive: true,
plugins: {
tooltip: {
callbacks: {
label: function (context) {
let label = context.dataset.label || "";
if (context.dataset.label === "100th percentile") {
label += ": ";
label += context.dataset.playerName[context.dataIndex];
}
return label;
},
},
},
legend: {
position: "top",
},
title: {
display: true,
text: "Chart.js Line Chart",
},
},
};
const labels = props.playerData.map((item) => {
return item.Year;
});
let [graphData, setGraphData] = useState([]);
let [topPlayerImages, setTopPlayerImages] = useState([]);
useEffect(() => {
let getData = async (arrayOfPercentiles) => {
// Clear state on re-render
setGraphData([]);
// Loop through the percentiles and fetch the results and save in state.
let percentileDataArray = [];
for (let percentile of arrayOfPercentiles) {
let data = await Promise.all(
labels.map((year) => {
return main(year, props.statSelection, percentile);
})
);
percentileDataArray.push(data);
}
setGraphData(percentileDataArray);
};
getData([25, 50, 75, 100]);
}, [props.statSelection, props.playerData]);
useEffect(() => {
const updateImages = async () => {
setTopPlayerImages([]);
let images = await getTopPlayersImages(labels);
setTopPlayerImages(images);
};
updateImages();
}, [graphData]);
const data = {
labels,
datasets: [
{
label: "Dataset 1",
data: props.playerData.map((item) =>
parseFloat(item.Data[0][props.statSelection])
),
borderColor: "rgb(255, 99, 132)",
backgroundColor: "rgba(255, 99, 132, 0.5)",
},
{
fill: true,
backgroundColor: "rgba(53, 162, 235, 0.5)",
label: "25th percentile",
data: graphData[0]?.map((item) => item[props.statSelection]),
},
{
fill: "-1",
label: "50th percentile",
data: graphData[1]?.map((item) => item[props.statSelection]),
},
{
fill: "-1",
backgroundColor: "rgba(53, 162, 235, 0.5)",
label: "75th percentile",
data: graphData[2]?.map((item) => item[props.statSelection]),
},
{
fill: "-1",
backgroundColor: "rgba(10, 162, 235, 0.5)",
label: "100th percentile",
data: graphData[3]?.map((item) => item[props.statSelection]),
playerName: graphData[3]?.map((item) => item.player),
pointStyle: topPlayerImages,
},
],
};
return (
<div id="chart">
<Line options={options} data={data} />
</div>
);
};
export default Chart;
Any help is greatly appreciated!
I made a chart using ChartJS that would fetch data from a server being hosted on a microcontroller connected to a few devices streaming data into a CSV every seconds. The chart works fine and shows the correct data except that it doesn't update/animate the graph unless you refreshed the page. I'd like to make it so that the chart pushes and animates the new data without the refreshing.
I've found a couple of answers but none would work for me. I found this video as well
drawChart();
// setup
async function drawChart() {
const datapoints = await getData();
const data = {
labels: datapoints.labels,
datasets: [{
label: 'Solar Voltage',
data: datapoints.solar.voltage,
borderColor: [
'rgba(255, 159, 28, 1)'
],
tension: 0.15,
yAxisID: 'y'
},
{
label: 'Solar Current',
data: datapoints.solar.current,
borderColor: [
'rgba(254, 215, 102, 1)'
],
tension: 0.15,
yAxisID: 'y1'
}
]
};
Chart.defaults.font.family = "Verdana";
// config
const config = {
type: 'line',
data,
options: {
scales: {
y: {
beginAtZero: true,
display: true,
position: 'left',
title: {
display: true,
text: 'voltage'
}
},
y1: {
beginAtZero: true,
display: true,
position: 'right',
grid: {
drawOnChartArea: false
},
title: {
display: true,
text: 'current'
}
}
}
}
};
// render init block
const myChart = new Chart(
document.getElementById('solar-graph'),
config
);
}
async function getData() {
//arrays to store data
const labels = [];
const solar = {
voltage: [],
current: []
};
const battery = {
voltage: []
};
//fetch csv
const url = '/data';
const response = await fetch(url, {
'content-type': 'text/csv;charset=UTF-8'
});
const tabledata = await response.text();
//parse csv
//split by row
const table = tabledata.split('\n').slice(1);
//split by column
table.forEach(row => {
const column = row.split(',');
labels.push(column[1]);
solar.voltage.push(column[2]);
solar.current.push(column[3]);
battery.voltage.push(column[4]);
});
return {
labels,
solar,
battery
};
}
edit: i fixed it
updateChart();
let labels = [];
let solar = {
voltage: [],
current: []
};
let battery = {
voltage: []
};
//push new values every 1.5 seconds
function updateChart() {
setInterval(getData, 1500);
}
async function getData() {
//fetch csv from url
const url = '/data';
fetch(url, {
'content-type': 'text/csv;charset=UTF-8'
})
.then(data => data.text(), error => console.warn("Failed to fetch data"))
.then(tabledata => {
//split by row
const table = tabledata.split('\n').slice(1);
//get last row
const row = table[table.length - 1].split(',');
//display 30 data points on the graph at anytime
if (myChart.data.labels.length > 30) {
myChart.data.labels.shift();
myChart.data.datasets[0].data.shift();
myChart.data.datasets[1].data.shift();
}
//update chart
myChart.data.labels.push(row[1]);
myChart.data.datasets[0].data.push(row[2]);
myChart.data.datasets[1].data.push(row[3]);
//update array
labels.push(row[1]);
solar.voltage.push(row[2]);
solar.current.push(row[3]);
battery.voltage.push(row[4]);
myChart.update();
Whenever you change the data, you need to update your chart manually
myChart.update()
Are you watched this video you mentioned in your question? Because at 14:12 he also wrote myChart.update()
I am trying to hide the legend of my chart created with Chart.js.
According to the official documentation (https://www.chartjs.org/docs/latest/configuration/legend.html), to hide the legend, the display property of the options.display object must be set to false.
I have tried to do it in the following way:
const options = {
legend: {
display: false,
}
};
But it doesn't work, my legend is still there. I even tried this other way, but unfortunately, without success.
const options = {
legend: {
display: false,
labels: {
display: false
}
}
}
};
This is my full code.
import React, { useEffect, useState } from 'react';
import { Line } from "react-chartjs-2";
import numeral from 'numeral';
const options = {
legend: {
display: false,
},
elements: {
point: {
radius: 1,
},
},
maintainAspectRatio: false,
tooltips: {
mode: "index",
intersect: false,
callbacks: {
label: function (tooltipItem, data) {
return numeral(tooltipItem.value).format("+0,000");
},
},
},
scales: {
xAxes: [
{
type: "time",
time: {
format: "DD/MM/YY",
tooltipFormat: "ll",
},
},
],
yAxes: [
{
gridLines: {
display: false,
},
ticks: {
callback: function(value, index, values) {
return numeral(value).format("0a");
},
},
},
],
},
};
const buildChartData = (data, casesType = "cases") => {
let chartData = [];
let lastDataPoint;
for(let date in data.cases) {
if (lastDataPoint) {
let newDataPoint = {
x: date,
y: data[casesType][date] - lastDataPoint
}
chartData.push(newDataPoint);
}
lastDataPoint = data[casesType][date];
}
return chartData;
};
function LineGraph({ casesType }) {
const [data, setData] = useState({});
useEffect(() => {
const fetchData = async() => {
await fetch("https://disease.sh/v3/covid-19/historical/all?lastdays=120")
.then ((response) => {
return response.json();
})
.then((data) => {
let chartData = buildChartData(data, casesType);
setData(chartData);
});
};
fetchData();
}, [casesType]);
return (
<div>
{data?.length > 0 && (
<Line
data={{
datasets: [
{
backgroundColor: "rgba(204, 16, 52, 0.5)",
borderColor: "#CC1034",
data: data
},
],
}}
options={options}
/>
)}
</div>
);
}
export default LineGraph;
Could someone help me? Thank you in advance!
PD: Maybe is useful to try to find a solution, but I get 'undefined' in the text of my legend and when I try to change the text like this, the text legend still appearing as 'Undefindex'.
const options = {
legend: {
display: true,
text: 'Hello!'
}
};
As described in the documentation you linked the namespace where the legend is configured is: options.plugins.legend, if you put it there it will work:
var options = {
type: 'line',
data: {
labels: ["Red", "Blue", "Yellow", "Green", "Purple", "Orange"],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'pink'
}
]
},
options: {
plugins: {
legend: {
display: false
}
}
}
}
var ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.0/chart.js"></script>
</body>
On another note, a big part of your options object is wrong, its in V2 syntax while you are using v3, please take a look at the migration guide
Reason why you get undefined as text in your legend is, is because you dont supply any label argument in your dataset.
in the newest versions this code works fine
const options = {
plugins: {
legend: {
display: false,
},
},
};
return <Doughnut data={data} options={options} />;
Import your options value inside the charts component like so:
const options = {
legend: {
display: false
}
};
<Line data={data} options={options} />
I am working on building a React app that contains a line chart representing the timeline of data recordings from a certain point in time to present day. I am using the JS library Chart js. Specifically the 'react-chartjs-2' plugin. I am using the '/historical/all' path of the following API:
[https://corona.lmao.ninja/docs/?urls.primaryName=version%202.0.0#/][1]
I am successfully retrieving a response from the Axios GET request. But the structuring of the JSON being sent back to me is confusing me when it comes to the implementation I have been following to build the application. I have a lot of the scaffolding in place, but I am struggling to get the data from GET request function to the line chart.
The data is being sent in the following structure:
{
"cases": {
"3/21/20": 304507,
"3/22/20": 336953,
"3/23/20": 378231,
"3/24/20": 418041,
"3/25/20": 467653,
"3/26/20": 529591,
"3/27/20": 593291,
"3/28/20": 660693,
"3/29/20": 720140,
"3/30/20": 782389,
"3/31/20": 857487,
"4/1/20": 932605,
"4/2/20": 1013466,
"4/3/20": 1095917,
"4/4/20": 1176060,
"4/5/20": 1249754,
"4/6/20": 1321481,
"4/7/20": 1396476,
"4/8/20": 1480202,
"4/9/20": 1565278,
"4/10/20": 1657526,
"4/11/20": 1735650,
"4/12/20": 1834721,
"4/13/20": 1904838,
"4/14/20": 1976191,
"4/15/20": 2056054,
"4/16/20": 2152437,
"4/17/20": 2240190,
"4/18/20": 2317758,
"4/19/20": 2401378
},
"deaths": {
"3/21/20": 12973,
"3/22/20": 14651,
"3/23/20": 16505,
"3/24/20": 18625,
"3/25/20": 21181,
"3/26/20": 23970,
"3/27/20": 27198,
"3/28/20": 30652,
"3/29/20": 33925,
"3/30/20": 37582,
"3/31/20": 42107,
"4/1/20": 47180,
"4/2/20": 52983,
"4/3/20": 58787,
"4/4/20": 64606,
"4/5/20": 69374,
"4/6/20": 74565,
"4/7/20": 81937,
"4/8/20": 88338,
"4/9/20": 95521,
"4/10/20": 102525,
"4/11/20": 108502,
"4/12/20": 114090,
"4/13/20": 119481,
"4/14/20": 125983,
"4/15/20": 134176,
"4/16/20": 143800,
"4/17/20": 153821,
"4/18/20": 159509,
"4/19/20": 165043
},
"recovered": {
"3/21/20": 91682,
"3/22/20": 97889,
"3/23/20": 98341,
"3/24/20": 107890,
"3/25/20": 113604,
"3/26/20": 121966,
"3/27/20": 130659,
"3/28/20": 138949,
"3/29/20": 148616,
"3/30/20": 164100,
"3/31/20": 176442,
"4/1/20": 191853,
"4/2/20": 208528,
"4/3/20": 223621,
"4/4/20": 243575,
"4/5/20": 257000,
"4/6/20": 273259,
"4/7/20": 296263,
"4/8/20": 324507,
"4/9/20": 348813,
"4/10/20": 370241,
"4/11/20": 395521,
"4/12/20": 414599,
"4/13/20": 440897,
"4/14/20": 466051,
"4/15/20": 502053,
"4/16/20": 532409,
"4/17/20": 557798,
"4/18/20": 581355,
"4/19/20": 612056
}
}
I would like for each category (cases, deaths, recovered) to represent a line on the chart showing the progression over time, but I'm unsure how to handle it. With the intention being that the dates would be the X-axis and the points would be plotted in a different colored line for each.
Would anyone be able to help with this at all? I would greatly appreciate some guidance. Below are the two primary files corresponding to the Chart and API handling:
index.js
export const fetchDailyData = async () => {
try {
const { data } = await axios.get(`${url}/historical/all`);
const labels = Object.keys(data.cases);
const cases = labels.map((name) => data.cases[name]);
const deaths = labels.map((name) => data.deaths[name]);
const recovered = labels.map((name) => data.recovered[name]);
return {labels, cases, deaths, recovered};
} catch (error) {
return error;
}
}
Chart.jsx
import React, { useState, useEffect } from 'react';
import { Line, Bar } from 'react-chartjs-2';
import { fetchDailyData } from '../../api';
import styles from './Chart.module.css';
const Chart = ({data: { labels, cases, deaths, recovered }}) => {
const [dailyData, setDailyData] = useState([]);
useEffect(() => {
const fetchAPI = async () => {
setDailyData(await fetchDailyData());
}
fetchAPI();
}, []);
const lineChart = (
<Line data = {
{
labels,
datasets: [{
data:cases,
label: 'Infected',
borderColor: '#3333ff',
fill: true,
}, {
data: deaths,
label: 'Deaths',
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.5)',
fill: true,
},
{
data: recovered,
label: 'Recovered',
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.5)',
fill: true,
}],
}
}
/>
);
return (
<div className = { styles.container }>
{lineChart}
</div>
);
};
export default Chart;
Here is a complete solution:
// Create the chartData state
const [chartData, setChartData] = useState();
// in your fetch function, convert the data to chartData
const labels = Object.keys(data.cases);
const cases = labels.map((name) => data.cases[name]);
const deaths = labels.map((name) => data.deaths[name]);
const recovered = labels.map((name) => data.recovered[name]);
setChartData({
labels,
datasets: [{
data: cases,
label: 'Infected',
borderColor: '#3333ff',
fill: true
}, {
data: deaths,
label: 'Deaths',
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.5)',
fill: true
}, {
data: recovered,
label: 'Recovered',
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.5)',
fill: true
}]
})
// Now render your chart (but make sure chartData exists)
return (
<div className = { styles.container }>
{chartData &&
<Line data={chartData} />
}
</div>
);
First extract the x labels:
const labels = Object.keys(dailyData.cases);
Then extract the y values for each serie:
const cases = Object.keys(dailyData.cases).map((name) => dailyData.cases[name]);
const deaths = Object.keys(dailyData.deaths).map((name) => dailyData.deaths[name]);
const recovered = Object.keys(dailyData.recovered).map((name) => dailyData.recovered[name]);
Then assemble everything into a chart js object:
<Line data = {
labels,
datasets: [{
data: cases,
label: 'Infected',
borderColor: '#3333ff',
fill: true
}, {
data: deaths,
label: 'Deaths',
borderColor: 'red',
backgroundColor: 'rgba(255, 0, 0, 0.5)',
fill: true
}, {
data: recovered,
label: 'Recovered',
borderColor: 'green',
backgroundColor: 'rgba(0, 255, 0, 0.5)',
fill: true
}]
}
/>