I'm attempting make a chart with data that is being fetched from an API, but a problem i'm having is that i've been unable to properly render the chart due to an asynchronous call im making for the data. I believe the chart is executing before the data can be filled because upon loading the window nothing renders. However when I tried hard coding values in place of 'timestamps' and 'prices' in the data variable, the chart immediately renders. Does anyone know how I can format the rest of the code so that the chart renders only after the timestamp and price arrays have been filled?
import { useEffect } from 'react';
import { Line } from 'react-chartjs-2';
const MarketAPI = require('market-api');
const Client = new MarketAPI();
function GRAPH(){
const timestamps = [];
const prices = [];
var getData = async() => {
const fromTS = (Date.now() / 1000 | 0 ) - 86400; // 1D from current timestamp
const toTS = Date.now() / 1000 | 0; // current timestamp
let get = await Client.assets.fetchAssetHist('MSFT', {
from: fromTS,
to: toTS,
});
for(let i = 0; i < get.data.prices.length; i++){
timestamps.push(get.data.prices[i].[0]);
prices.push(get.data.prices[i].[1]);
}
console.log(timestamps);
console.log(prices);
}
const data = {
labels: timestamps,
datasets: [
{
data: prices,
fill: true,
backgroundColor: 'rgba(243, 230, 200, 0.2)',
borderColor: 'rgba(243, 210, 18)',
},
],
};
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
},
display:true
},
y: {
grid: {
display: false
},
display: false
}
},
elements: {
point:{
radius: 0
}
},
};
useEffect(()=>{
getData()
},[]);
return(
<div>
<Line data={data} options={options}/>
</div>
);
}
function App() {
return (
<GRAPH/>
);
}
Use the useState Hook to store your data instead.
import { useEffect, useState } from 'react';
import { Line } from 'react-chartjs-2';
const MarketAPI = require('market-api');
const Client = new MarketAPI();
function GRAPH(){
const timestamps = [];
const prices = [];
const [data, setData] = useState({})
var getData = async() => {
const fromTS = (Date.now() / 1000 | 0 ) - 86400; // 1D from current timestamp
const toTS = Date.now() / 1000 | 0; // current timestamp
let get = await Client.assets.fetchAssetHist('MSFT', {
from: fromTS,
to: toTS,
});
for(let i = 0; i < get.data.prices.length; i++){
timestamps.push(get.data.prices[i].[0]);
prices.push(get.data.prices[i].[1]);
}
console.log(timestamps);
console.log(prices);
}
setData({
labels: timestamps,
datasets: [
{
data: prices,
fill: true,
backgroundColor: 'rgba(243, 230, 200, 0.2)',
borderColor: 'rgba(243, 210, 18)',
},
],
})
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
}
},
scales: {
x: {
grid: {
display: false
},
display:true
},
y: {
grid: {
display: false
},
display: false
}
},
elements: {
point:{
radius: 0
}
},
};
useEffect(()=>{
getData()
},[]);
return(
<div>
<Line data={data} options={options}/>
</div>
);
}
function App() {
return (
<GRAPH/>
);
}
Related
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} />
**When i drawChartData the yAxis of chartjs is not displayed. What is the problem, can you help me **
Before
After
there is my script
<script lang="ts">
import { Vue, Component } from "nuxt-property-decorator";
import BarChartComponent from "#/components/chart/BarChartComponent";
import LineChartComponent from "#/components/chart/LineChartComponent";
import moment from "moment"
import traffic from '#/modules/traffic'
import { TrafficCarrierSummary, TrafficCarrierDetail } from "#/type/traffic-carrier-summary";
const FONT_COLOR = "rgba(255, 255, 255, 1)";
const GRID_LINES_SETTING = {
display: true,
drawOnChartArea: true,
color: "rgba(255, 255, 255, .5)",
zeroLineColor: "rgba(255, 255, 255, 1)"
};
#Component({
components: {
BarChartComponent,
LineChartComponent
},
asyncData: async () => {
let searchObject = {
from: moment().subtract(1, 'months').set('date', 1).format('Y-M-D'),
to: moment().subtract(1, 'months').endOf('month').format('Y-M-D'),
}
const response = await traffic.package(searchObject)
return {
searchObject: searchObject,
packageData: response.data,
}
}
})
export default class extends Vue {
width: number = 575
packageData!: Array<TrafficCarrierSummary>
packageDaliy: TrafficCarrierDetail | null = null
selectedPackage: string | null = null
isDayPackage: boolean = false
selectedDaliyDate: any
loading: boolean = false
lineColors = [
'rgba(120, 0, 0,1)',
'rgba(176, 0, 10,1)',
'rgba(234, 57, 51,1)',
'rgba(251,184, 43,1)',
'rgba(255,234, 97,1)',
'rgba( 92, 98, 91,1)',
'rgba( 35, 37, 35,1)',
]
searchObject = {
from: null,
to: null,
} as any
chartDataPackage: any = {};
chartDataCount: any = {};
chartPackageOptions: Chart.ChartOptions = {
responsive: true,
maintainAspectRatio: false,
title: {
display: false,
},
scales: {
yAxes: [
{
scaleLabel: {
display: true,
labelString: 'Data [GB]',
fontColor: "red",
fontSize: 16
},
ticks: {
suggestedMax: 1000,
suggestedMin: 0,
stepSize: 100,
callback: function(value, index, values) {
return value + "GB";
}
}
}
]
}
};
created() {
this.searchObject.from = new Date(moment().subtract(1, 'months').set('date', 1).toDate())
this.searchObject.to = new Date(moment().subtract(0, 'months').endOf('month').toDate())
this.selectedDaliyDate = this.searchObject.from
this.drawChartPackage()
}
async search() {
this.loading = true
let from = moment(this.searchObject.from).format('Y-MM-DD')
let to = moment(this.searchObject.to).format('Y-MM-DD')
const response = await traffic.package({
from: from,
to: to
})
this.packageData = response.data
this.drawChartPackage()
this.loading = false
}
drawChartPackage() {
let dataArr: Array<any> = []
let labels: Array<string> = []
let obj: any = {}
this.packageData.forEach(element => {
if(!labels.includes(element.target_date)){
labels.push(element.target_date)
}
if(!obj[element.package]){
obj[element.package] = []
}
obj[element.package].push(Number(element.data_total) / 1024 / 1024 / 1024)
})
let index = 0
Object.keys(obj).map(key => {
dataArr.push({
label: key,
data: obj[key],
borderColor: this.lineColors[index],
backgroundColor: 'rgba(0,0,0,0)',
type: 'line'
})
index++
})
this.chartDataPackage = {
labels: labels,
datasets: dataArr
}
}
}
</script>
i'm using react-chartjs-2 for customizing three donut pie charts. This library is amazing with many functionalities but i'm having a problem here. I dont know how to handle zero values. When all my values are zero not pie chart is drawn which is correct. Any ideas of how to handle zero values?? This is my code for drawing a doughnut pie chart:
const renderPortfolioSectorPie = (sectors, intl) => {
if (sectors.length > 0) {
const sectorsName = sectors
.map(sector => sector.name);
const sectorsValue = sectors
.map(sector => sector.subtotal);
const sectorsPercentage = sectors
.map(sector => sector.percentage);
const customeSectorsPercentage = sectorsPercentage.map(h =>
`(${h})`
);
let sectorsCounter = 0;
for (let i = 0; i < sectorsName.length; i += 1) {
if (sectorsName[i] !== sectorsName[i + 1]) {
sectorsCounter += 1;
}
}
const sectorsData = {
datasets: [{
data: sectorsValue,
backgroundColor: [
'#129CFF',
'#0c6db3',
'#4682B4',
'#00FFFF',
'#0099FF',
'#3E3BF5',
'#3366CC',
'#3399FF',
'#6600FF',
'#3366CC',
'#0099CC',
'#336699',
'#3333FF',
'#2178BA',
'#1F7AB8',
'#1C7DB5'
],
hoverBackgroundColor: [
'#129cff',
'#0c6db3',
'#4682B4',
'#00FFFF',
'#0099FF',
'#3E3BF5',
'#3366CC',
'#3399FF',
'#3366CC',
'#0099CC',
'#336699',
'#3333FF',
'#2178BA',
'#1F7AB8',
'#1C7DB5'
],
titles: sectorsName,
labels: sectorsValue,
afterLabels: customeSectorsPercentage,
}]
};
return (
<Doughnut
data={sectorsData}
width={250}
height={250}
redraw
options={{
legend: {
display: false
},
maintainAspectRatio: true,
responsive: true,
cutoutPercentage: 80,
animation: {
animateRotate: false,
animateScale: false
},
elements: {
center: {
textNumber: `${sectorsCounter}`,
text: intl.formatMessage({ id: 'pie.sectors' }),
fontColor: '#4a4a4a',
fontFamily: "'EurobankSans'",
fontStyle: 'normal',
minFontSize: 25,
maxFontSize: 25,
}
},
/*eslint-disable */
tooltips: {
custom: (tooltip) => {
tooltip.titleFontFamily = 'Helvetica';
tooltip.titleFontColor = 'rgb(0,255,255)';
},
/* eslint-enable */
callbacks: {
title: (tooltipItem, data) => {
const titles = data.datasets[tooltipItem[0]
.datasetIndex].titles[tooltipItem[0].index];
return (
titles
);
},
label: (tooltipItem, data) => {
const labels = data.datasets[tooltipItem.datasetIndex]
.labels[tooltipItem.index];
return (
labels
);
},
afterLabel: (tooltipItem, data) => {
const afterLabels = data.datasets[tooltipItem.datasetIndex]
.afterLabels[tooltipItem.index];
return (
afterLabels
);
},
},
},
}}
/>
);
}
I wanted to do the same thing, but I could not find a way to do it. So, that is what I found as a workaround:
After getting the data, you update your options:
this.setState({
doughnutOptions: {
tooltips: {
callbacks: {
label: function () {
let value = response.data.total;
let label = response.data.name;
return value === 0 ? label : label + ': ' + value;
}
}
}
}
});
My Doughnut looks like that:
<Doughnut data={this.state.myData} min-width={200} width={400}
height={400} options={this.state.doughnutOptions}/>
And initially, the doughnutOpitions are just one empty object defined in the state:
doughnutOptions: {}
That is how it will look when no data:
If you have found a better way of showing no data, please share. If not, I hope that workaround would be good enough for you!