ChartJS 3.7 data not dynamically updating with data streaming into CSV - javascript

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()

Related

How can I use embedded twitter tweets as a tooltip for each plot on a scatter chart?

// both being used
// <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
[// <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>][1]
const externalTooltipHandler = (context) => {
//does not log anything?
console.log(context);
return context;
};
const chartData = {
datasets: [{
label: ['Tweets'],
data: [],
backgroundColor: 'rgb(255, 255, 255)'
}],
};
const config = {
type: 'scatter',
data: chartData,
options: {
scales: {
x: {
type: 'linear',
position: 'bottom'
}
},
plugins: {
tooltip: {
tooltip: {
enabled: false,
position: 'nearest',
external: externalTooltipHandler
}
}}}};
var output = false, finalData = [];
fetch("http://localhost:3000/getdata", {method: 'POST'})
.then(data => {return data.json()})
.then(data => {
//console.log(data);
const ctx = document.getElementById('myChart').getContext('2d');
const myChart = new Chart(ctx, config);
for (let i = 0; i<data.length; i++){
chartData.datasets[0].data.push({x:data[i].data.id, y: data[i].data.public_metrics.like_count})
myChart.update();
};
})
// Something along the lines of
//<blockquote class="twitter-tweet" data-theme="dark">
//
//</blockquote>
As the question says, I'd like to display the tooltip as the embedded tweet, showing when hovered over, I've tried reconfiguring the samples here https://www.chartjs.org/docs/latest/samples/tooltip/html.html and on other stackoverflow pages, though I can't get those to work.
what it would look like: https://i.stack.imgur.com/x79aT.png

Is there a better way to create an 'n' number of charts in ChartJS and ASP.NET C#?

EDIT: I have narrowed it down to something like this:
for (i = 0; i < data.length; i++) {
const newCanvas = document.createElement("canvas");
newCanvas.id = data[i].design_name;
const currentDiv = document.getElementById("chartSpace");
var parentDiv = document.getElementById("gridHere");
parentDiv.insertBefore(newCanvas, currentDiv);
createChart([data[i].design_name], [data[i].design_start, data[i].design_end]);
}
With the create chart making the chart id = to the array 'labels':
const myChart = new Chart(
document.getElementById(labels),
config
);
I am attempting to create a tool that creates an 'n' number of charts in ChartJS and save each of them as images. Currently, designButtonClick() sends the 'event_fky' value to
getDesigns(event_fky) in my controller. This method returns all designs with that foreign key. In turn, the chart plots each design on the chart. I need to evolve this into
something that can make a group individual charts for each design based on how many designs there are. My current solution, still conceptual, is to have methods in my controller
create chart variables 'chartData [data here]' and 'labels[datahere]' while looping through the designs returned from getDesigns, and sending those back to the JS script createChart
'n' number of times for each design. It would also send html chart/html element ids based on the design_name attribute to send back to createChart. This way, it is create a unique
chart 'n' number of times.
To save the charts as images, I would use the same set of element ids generated by getDesigns to send the charts to images using JS' toBase64Image() function and saving them to the
user's system.
Is this the best way of solving this problem? Or is this spaghetti, and is there a better method for this? My attempts to find better online answers have only resulted in docs on
updating one chart dynamically, not creating a dynamic number of charts. Much help is appreciated, code is below as well as a screenshot of the current chart output.
JavaScript:
var labels = [];
var cData = [];
function designButtonClick() {
var event_fky = 3;
$.ajax({
url: 'Tree/getDesigns',
type: 'POST',
data: { event_fky }
}).done(function (data) {
for (i = 0; i < data.length; i++) {
labels.push(data[i].design_name);
cData.push([data[i].design_start, data[i].design_end])
}
createChart(labels, cData);
});
}
function createChart(labels, cData) {
const data = {
labels: labels,
datasets: [{
barThickness: 2,
categoryPercentage: .5,
label: 'Design Time',
data: cData,
backgroundColor: [
'rgba(255, 26, 104, 0.2)'
],
borderColor: [
'rgba(255, 26, 104, 1)'
],
borderWidth: 1,
borderSkipped: false,
borderRadius: 20
}]
};
const config = {
type: 'bar',
data,
options: {
indexAxis: 'y',
scales: {
y: {
beginAtZero: true
},
x: {
min: 0,
max: 6000,
ticks: {
stepSize: 1000
}
}
}
}
};
const myChart = new Chart(
document.getElementById('myChart'),
config
);
}
C# Controller:
public ActionResult getDesigns(int? event_fky)
{
var designs = from e in _context.designs
where (event_fky.HasValue ? e.event_fky == event_fky : e.event_fky == null)
select new
{
design_pky = e.design_pky,
design_name = e.design_name,
design_start = e.design_start,
design_end = e.design_end
};
return this.Json(designs, JsonRequestBehavior.AllowGet);
}
Designs Table:
--------Design--------
design_pky |int
event_fky |int
design_name |varchar
design_start |number
design_end |number
Screenshot of Chart
This is a working answer for the javascript:
var eventList = function () {
var tmp = null;
$.ajax({
'async': false,
url: 'Tree/getEventIDs',
type: 'POST',
data: {},
'success': function (data) {
tmp = data;
}
});
return tmp;
}();
for (var i = 0; i < eventList.length; i++) {
event_fky = eventList[i].event_pky;
event_name = eventList[i].event_name;
event_length = eventList[i].event_end;
var designList = function () {
var tmpi = null;
$.ajax({
'async': false,
url: 'Tree/getDesigns',
type: 'POST',
data: {event_fky},
'success': function (data1) {
tmpi = data1;
}
});
console.log(event_fky);
console.log(tmpi);
return tmpi;
}();
var dLabels = [];
var dLengths = [];
for (var j = 0; j < designList.length; j++) {
dLabels.push(designList[j].design_name);
dLengths.push([designList[j].design_start, designList[j].design_end]);
}
const newCanvas = document.createElement("canvas");
newCanvas.id = event_name;
const currentDiv = document.getElementById("chartSpace");
var parentDiv = document.getElementById("gridHere");
parentDiv.insertBefore(newCanvas, currentDiv);
if (dLabels.length != 0) {
createChart(dLabels, dLengths, event_name, event_length);
}
}
}
function createChart(labels, cData, evName, evLen) {
// setup
const data = {
labels: labels,
datasets: [{
barThickness: 4,
categoryPercentage: .5,
label: evName,
data: cData,
backgroundColor: [
'rgba(' + Math.random() * 85 + ', ' + Math.random() * 170 + ', ' + Math.random() * 255 + ', 1)'
],
borderColor: [
'rgba(255, 26, 104, 1)'
],
borderWidth: 0,
borderSkipped: false,
borderRadius: 20
}]
};
// config
const config = {
type: 'bar',
data,
options: {
indexAxis: 'y',
scales: {
y: {
beginAtZero: true
},
x: {
min: 0,
max: evLen,
ticks: {
stepSize: 100
}
}
}
}
};
// render init block
const myChart = new Chart(
document.getElementById(evName),
config
);
return myChart;
}

ChartJS with React: Only one value showing on time chart

I am creating a cryptocurrency price tracker with React. I am using axios to make API requests and get information about the price history of different cryptocurrencies. This works fine, but when I am creating the chart with ChartJS, the chart only shows one value, even though I have thousands. Anyone know how to fix this? Big thanks in advance.
Here is my code for the chart component:
import Chartjs from "chart.js";
import axios from "axios";
import { historyOptions } from "../../chartConfigs/chartConfigs";
const HistoryChart = ({ coinId }) => {
const [timeLength, setTimeLength] = useState(30);
const chartRef = useRef();
const [timeSeriesData, setTimeSeriesData] = useState([]);
useEffect(() => {
const timeSeriesDataArray = [];
axios.get(`https://api.coingecko.com/api/v3/coins/${coinId}/market_chart?vs_currency=usd&days=${30}`)
.then(response => {
response.data.prices.forEach(element => {
timeSeriesDataArray.push(
{
t: element[0],
y: element[2].toFixed(2)
}
)
})
setTimeSeriesData(timeSeriesDataArray)
})
.catch(error => console.log(error))
}, [coinId, timeLength])
useEffect(() => {
if (chartRef && chartRef.current) {
const chartInstance = new Chartjs(chartRef.current,{
type: 'line',
data: {
datasets: [{
label: "Price",
data: timeSeriesData,
backgroundColor: "rgba(174, 374, 194, 0.5)",
borderColor: "rgba(174, 374, 194, 0.4)",
pointRadius: 0,
borderWidth: 1
}]
},
options: historyOptions
})
}
}, [timeSeriesData])
return (
<div className="history-chart-container">
<canvas ref={chartRef} id="history-chart" width={1200} height={500}></canvas>
</div>
)
}
export default HistoryChart
Here is my code for the chart options (historyOptions):
lineHeighAnnotation: {
always: true,
hover: false,
lineHeight: 1.5
},
animation: {
duration: 2000
},
maintainAspectRatio: false,
responsive: true,
scales: {
xAxes: {
type: "time",
distribution: "linear",
}
}
}```
[1]: https://i.stack.imgur.com/e7g4P.png
[2]: https://i.stack.imgur.com/t4YIa.png
This could be fixed by changing your line chart into a scatter chart.
const chartInstance = new Chartjs(chartRef.current,{
type: 'scatter',
...
To still get the lines drawn between the data points, you need to define the option showLine: true on the dataset.
datasets: [{
label: "Price",
showLine: true,
...
}]
UPDATE
You should also make sure to update the chart when new data is added and the chart already exists. Not knowing much about react.js, you could make chartInstance a global variable and proceed as follows:
if (chartRef && chartRef.current) {
chartInstance = new Chartjs(chartRef.current,{
type: 'line',
...
} else {
chartInstance.update();
}

attempting to destroy previous graph on canvas

I am creating multiple graphs on the same canvas but I am unable to successfully use the destroy() API to clean up the previous data.
HERE IS MY JS CODE FOR CREATING A CHART
const getCountryDataByMonth = async (country) => {
document.getElementById('casesGraphHeader').innerHTML = "Loading....";
const response = await fetch ('https://cors-anywhere.herokuapp.com/https://pomber.github.io/covid19/timeseries.json');
const data = await response.json();
const reports = await data[country];
var i;
var dateList = [];
var caseByDay = [];
var deathsByDay = [];
for(i = 0; i < reports.length; i++){
dateList.push(reports[i].date);
caseByDay.push(reports[i].confirmed);
deathsByDay.push(reports[i].deaths);
}
//GRAPH FOR TOTAL CASES
var casesOptions = {
type: 'bar',
data: {
labels: dateList,
datasets: [
{
label: 'Total Cases',
data: caseByDay,
backgroundColor: '#f49d12',
borderColor: '#f49d12',
fill: false,
borderWidth: 2
}
]
},
options: {
legend: {
labels: {
fontSize: 15
}
},
scales: {
yAxes: [{
ticks: {
reverse: false,
fontSize: 15
}
}],
xAxes: [{
ticks: {
fontSize: 15
}
}],
}
}
}
var totalCasesChart = document.getElementById('totalCasesContainer').getContext('2d');
new Chart(totalCasesChart, casesOptions);
document.getElementById('casesGraphHeader').innerHTML = "Total Cases for "+country;
//GRAPH FOR TOTAL Deaths
var deathOptions = {
type: 'bar',
data: {
labels: dateList,
datasets: [
{
label: 'Total Deaths',
data: deathsByDay,
backgroundColor: '#e84c3d',
borderColor: '#e84c3d',
fill: false,
borderWidth: 2
}
]
},
options: {
legend: {
labels: {
fontSize: 15
}
},
scales: {
yAxes: [{
ticks: {
reverse: false,
fontSize: 15
}
}],
xAxes: [{
ticks: {
fontSize: 15
}
}],
}
}
}
var totalCasesChart = document.getElementById('totalDeathsContainer').getContext('2d');
new Chart(totalDeathsContainer, deathOptions);
document.getElementById('deathsGraphHeader').innerHTML = "Total Deaths for "+country;
};
function renderChart(){
getCountryDataByMonth(document.getElementById('myInput').value);
}
function defaultChart() {
getCountryDataByMonth('US');
}
window.onload = defaultChart;
This is what I tried. I basically did
if(caseBar){
caseBar.destroy();
}
However, this does not work. In my FIDDLE you can try to type China first click to create the graph and then type Italy. Then HOVER over the Italy graph and you will see the stats from china appear on the graph.
Your code is riddle with issues, here is some of the stuff I see:
Look at what you are doing when you create the new charts:
var totalCasesChart = document.getElementById('totalCasesContainer').getContext('2d');
var caseBar = new Chart(totalCasesChart, casesOptions);
document.getElementById('casesGraphHeader').innerHTML = "Total Cases for " + country;
vs
var totalCasesChart = document.getElementById('totalDeathsContainer').getContext('2d');
new Chart(totalDeathsContainer, deathOptions);
document.getElementById('deathsGraphHeader').innerHTML = "Total Deaths for " + country;
You are calling the:
await fetch('https://cors-anywhere.herokuapp.com/https://pomber.github.io/...');
again and again when you should do it just once...
There are many variables that should be global to reduce what you do in getCountryDataByMonth, a perfect example are the totalCasesChart and caseBar
I made a few tweaks to your code here:
https://raw.githack.com/heldersepu/hs-scripts/master/HTML/chart_test.html

Getting Zoomdata data to work with echarts index.js stacked line chart

I am working with a echarts javascript chart and trying to get it to work with my data in Zoomdata. I have the data grouped by 20 different computers so I am looking to do a stacked line chart with 20 lines. I know how to hard code this but I would like to link the data in Zoomdata to the code to display in the chart. Right now it just plots all 20 computers on one line.
import echarts from 'echarts'; //
import styles from './index.css';
// create chart container
const chartContainer = document.createElement('div');
chartContainer.style.width = '100%';
chartContainer.style.height = '100%';
chartContainer.classList.add(styles.chartContainer);
controller.element.appendChild(chartContainer);
const groupAccessor = controller.dataAccessors['Group By'];
const metricAccessor = controller.dataAccessors.Size;
//Need help
//Part Im having trouble with linking data in zoomdata to this chart
const chart = echarts.init(chartContainer);
const option = {
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: [
{
name:'邮件营销',
type:'line',
stack: '总量',
data:[120, 132, 101, 134, 90, 230, 210]
},
{
name:'联盟广告',
type:'line',
stack: '总量',
data:[220, 182, 191, 234, 290, 330, 310]
},
{
name:'视频广告',
type:'line',
stack: '总量',
data:[150, 232, 201, 154, 190, 330, 410]
},
{
name:'直接访问',
type:'line',
stack: '总量',
data:[320, 332, 301, 334, 390, 330, 320]
},
{
name:'搜索引擎',
type:'line',
stack: '总量',
data:[820, 932, 901, 934, 1290, 1330, 1320]
}
]
};
//Rest of code
controller.update = data => {
// Called when new data arrives
option.series[0].data = reshapeData(data);
chart.setOption(option);
};
function reshapeData(data) {
return data.map(d => ({
name: groupAccessor.raw(d),
value: metricAccessor.raw(d),
datum: d,
itemStyle: { //tell the chart you would like to use the colors selected
color: groupAccessor.color(d),//tell the chart you would like to use the colors selected
}, //tell the chart you would like to use the colors selected
}));
}
chart.on('mousemove', param => {
controller.tooltip.show({
event: param.event.event,
data: () => param.data.datum,
});
});
chart.on('mouseout', param => {
controller.tooltip.hide();
});
chart.on('click', param => {
controller.menu.show({
event: param.event.event,
data: () => param.data.datum,
});
});
controller.createAxisLabel({
picks: 'Group By',
position: 'bottom',
orientation: 'horizontal',
});
controller.createAxisLabel({
picks: 'Size',
position: 'bottom',
orientation: 'horizontal',
});
The json looks like:
[
{
current: {
count: 1508,
metrics: null,
na: false
},
group: [
"Computer1"
]
},
{..},
{..}
]
Thanks for adding the json details. If I understood it good, the value you want to display on each line must be current.count, and the name of each series is given by the first item in the group array (I don't know why it's an array, though).
Here is the code I would write if I want to map your data on ECharts:
/*
* incremental update counter. This will be displayed
* on the xAxis by being pushed to options.xAxis.data array.
*/
let updateCount = 0,
// initialize series as empty
const options = {
xAxis: {
type: 'category',
data: []
},
yAxis: {
type: 'value'
},
series: []
}
controller.update = data => {
updateCount++
if (options.series.length > 0) {
// Called when new data arrives
options.xAxis.data.push('record ' + updateCount)
options.series = updateData(data)
} else {
// Called only once to initialize
options.xAxis.data.push('record ' + updateCount)
options.series = initData(data)
}
// you can remove the following line if your chart is already reactive.
chart.setOption(option)
}
// the init function.
const initData = data => {
// transform each item in the data array into a series entry.
data.map(item => {
return {
name: item.group[0],
type: 'line',
stack: 'defaultStack',
data: [item.current.count]
}
})
}
// the update function.
const updateData = newData => {
// push new data counts to its respective series data.
options.series.forEach((item, index) => {
item.data.push(newData[index].current.count)
}
}
It's a bit long but more secure way to parse your raw data into an ECharts option. Let me know if you have any issue with this, I haven't tested yet so it's only brain code.

Categories