Introduction:
I have a ApexCharts chart with data from an API. I'm using an API called finnhub to get the stock market data which is displayed on the chart. The data returned has an array of prices and a corresponding array of times, which are not at an equal interval (stock market is closed at certain times) (prices on the y-axis and time on the x-axis). The data I'm getting is quite high resolution, which means there are a LOT of labels on the x-axis, making it look really cluttered and unreadable. I'm using React Hooks.
The problem I'm getting:
X-axis labels are too close together (image)
As you can see, the labels on the on the x-axis which are displaying the time, are really close together. It looks too cluttered. I want to make it so that there are only about 4-5 labels on the chart which are spaced out evenly across.
Code:
import React, { useState, useEffect } from "react";
import Chart from "react-apexcharts";
import axios from "axios";
import dayjs from "dayjs";
function StockChart() {
const [options, setOptions] = useState({
chart: {
type: "area",
height: 350,
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: "straight"
},
xaxis: {
categories: [],
labels: {
formatter: function (val) {
return dayjs(val).format("DD. HH:mm");
},
hideOverlappingLabels: Boolean,
rotate: 0,
trim: false
},
axisTicks: {
autoskip: true,
maxTicksLimit: 4,
interval: 3
}
},
tooltip: {
x: {
format: "dd MMM yyyy"
}
},
fill: {
type: "gradient",
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.9,
stops: [0, 100]
}
}
});
const [series, setSeries] = useState([
{
name: "Closing Price",
data: []
}
]);
useEffect(() => {
axios
.get(
"https://finnhub.io/api/v1/stock/candle?symbol=AAPL&resolution=60&from=1572100000&to=1572910590"
)
.then(res => {
setSeries(prev => {
prev[0].data = res.data.c.map(val => val);
return prev;
});
setOptions(prev => {
prev.xaxis.categories = res.data.t.map(time => time * 1000);
return prev;
});
})
.catch(err => console.log(err));
});
// Ignore the below function and state, it's just for testing
const [disableChart, setDisableChart] = useState(false);
function toggleChart() {
setDisableChart(prev => !prev);
}
return (
<div className="chart-container">
<h1 onClick={toggleChart}>my chart</h1>
{disableChart ? null : (
<Chart options={options} series={series} type="area" width="50%" />
)}
</div>
);
}
export default StockChart;
What I've tried:
I've tried messing around with the ticks property. There was no effect. I've tried setting it to a type: "numeric" and type: "datetime" chart but that caused the following effect:
Irregular data intervals(image)
The x-axis labels are now spaced perfectly, but the problem now is that the data on the chart isn't evenly spaced. As you can see, the data interval between 5:50 and 5:55 is very large, unlike the data interval right above 5:55. I want the data interval of the chart to be equal in all places, like in the first image.
Code:
import React, { useState, useEffect } from "react";
import Chart from "react-apexcharts";
import axios from "axios";
import dayjs from "dayjs";
function StockChart() {
const [options, setOptions] = useState({
chart: {
type: "area",
height: 350,
zoom: {
enabled: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: "straight"
},
xaxis: {
type: "numeric",
labels: {
formatter: function (val) {
return dayjs(val).format("DD. HH:mm");
},
hideOverlappingLabels: Boolean,
rotate: 0,
trim: false
},
axisTicks: {
show: true
}
},
tooltip: {
x: {
format: "dd MMM yyyy"
}
},
fill: {
type: "gradient",
gradient: {
shadeIntensity: 1,
opacityFrom: 0.7,
opacityTo: 0.9,
stops: [0, 100]
}
}
});
const [series, setSeries] = useState([
{
name: "Closing Price",
data: []
}
]);
useEffect(() => {
axios
.get(
"https://finnhub.io/api/v1/stock/candle?symbol=AAPL&resolution=60&from=1572100000&to=1572910590"
)
.then(res => {
setSeries(prev => {
for (let i = 0; i < res.data.c.length; i++) {
console.log(res.data.t[i]);
prev[0].data[i] = [res.data.t[i], res.data.c[i]];
}
console.log(prev);
return prev;
});
})
.catch(err => console.log(err));
});
// Ignore the below function and state, it's just for testing
const [disableChart, setDisableChart] = useState(false);
function toggleChart() {
setDisableChart(prev => !prev);
}
return (
<div className="chart-container">
<h1 onClick={toggleChart}>my chart</h1>
{disableChart ? null : (
<Chart options={options} series={series} type="area" width="50%" />
)}
</div>
);
}
export default StockChart;
What I want to achieve:
I want to have the data labels on the x-axis be similar to the ones like in the second picture (not too cluttered, only about 4-5 labels per chart), while having the chart itself look like the second picture (distance between data changes is equal). Any help would be greatly appreaciated.
PS: This is my first StackOverflow question, sorry if I did something incorrectly.
Well, I faced the same problem but in vue-project. So this helped to me:
methods: {
onChartResize(event) {
this.$refs.oilProductionProfile.updateOptions({
xaxis: { tickAmount: Math.ceil(event.srcElement.innerWidth / 140) },
});
},
},
created() {
window.addEventListener("resize", this.onChartResize);
},
destroyed() {
window.removeEventListener("resize", this.onChartResize);
},
We subscribing on window event, recalculating tickAmount according to current window's innerwidth, and then calling "updateOptions" to refresh axis.
Related
this one is the screenshot of the error on console it shows the error which is occuring in three different places, and under this image i copied all the code which is causing the error so check out the code and see if anybody can help me out
this one is the screenshot of the error on console it shows the error which is occuring in three different places, and under this image i copied all the code which is causing the error so check out the code and see if anybody can help me out
[error message screenshot][1]
[1]: https://i.stack.imgur.com/sjjAC.png
Code:
import React, { useState, useEffect } from "react";
import { Line } from "react-chartjs-2";
import numeral from "numeral";
const options = {
legend: {
display: false,
},
elements: {
point: {
radius: 0,
},
},
maintainAspectRatio: false,
tooltips: {
mode: "index",
intersect: false,
callbacks: {
label: function (tooltipItem, data) {
return numeral(tooltipItem.value).format("+0,0");
},
},
},
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) => {
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);
console.log(chartData);
// buildChart(chartData);
});
};
fetchData();
}, [casesType]);
return (
<div>
<h1>Graph</h1>
{data?.length > 0 && (
<Line
data={{
datasets: [
{
backgroundColor: "rgba(204, 16, 52, 0.5)",
borderColor: "#CC1034",
data: data,
},
],
}}
options={options}
/>
)}
</div>
);
}
export default LineGraph;
Try to debug your caseType variable in buildChartData function, most likely it's either undefined or null or some error in a typo which is causing the issue. from my guess, its value must either be cases or deaths or recovered.
Also, from what I noticed you have missed some registrations so add the following code at the beginning of your program.
to avoid Error: "category" is not a registered scale
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend,
} from 'chart.js';
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Tooltip,
Legend
);
uplot is used to dislay timeseries data from VictoriaMetrics database.
For the backend Node-Red is used to forward and recieve the query with node-red-contrib-uibuilder.
It works basically and is very fast.
The problem is, when I try to zoom into the uplot graph, my browser (Chrome, Firefox, Edge) freezes. It seems to run out of memory.
Here are parts of my code, using svelte.
<script>
import { onMount } from "svelte";
import { query } from "../lib/uibStore";
import { transformToUplot } from "../lib/helper";
// import uPlot from "uplot";
import Browsebar from "../components/Browsebar.svelte";
import TimerangeSelect from "../components/TimerangeSelect.svelte";
let uplotdiv; //
let opts = {
title: "Temperaturen1",
id: "chart1",
class: "my-chart",
width: 1000,
height: 600,
series: [
{},
{
show: true, // initial toggled state (optional)
spanGaps: true,
label: "RT",
stroke: "red", // series style
scale: "Temperature",
value: (self, rawValue) => rawValue.toFixed(1) + "°C",
},
{
show: true,
spanGaps: true,
label: "KT",
stroke: "green",
scale: "Temperature",
value: (self, rawValue) => rawValue.toFixed(1) + "°C",
},
{
show: true,
spanGaps: true,
label: "VT",
stroke: "blue",
scale: "Temperature",
value: (self, rawValue) => rawValue.toFixed(1) + "°C",
},
],
scales: {
x: { time: true },
Temperature: {
auto: true,
// range: [-10, 20],
side: 3,
},
},
axes: [
{},
{
scale: "Temperature",
values: (self, ticks) => ticks.map((rawValue) => rawValue.toFixed(1) + "°C"),
},
],
cursor: { drag: { x: true, y: true } },
};
let plot; // = new uPlot(opts);
let uPlot;
let d = [[0], [0], [0], [0]];
let resolved = false;
$: uisend($query); //use uibilder.send, if query changes which occurs when timerange or nav index changes
//send a victoriametrics query to the backend, _q is part of query
function uisend(_q) {
// Example 'uvr_prozess_celsius{ort="1"}&start=-3d&step=60s'
uibuilder.send({ topic: "getVMetrics", payload: _q });
}
onMount(async () => {
uisend($query);
const uplotModule = await import("https://unpkg.com/uplot#1.6.22/dist/uPlot.esm.js");
uPlot = uplotModule.default;
plot = new uPlot(opts, [[0], [0], [0], [0]], uplotdiv);
});
uibuilder.onChange("msg", function (msg) {
// load Metrics via Node-Red's uibuilder, serverside
if (msg.topic === "getVMetrics") {
resolved = true;
if (msg.payload.data.result.length > 0) {
d = transformToUplot(msg.payload.data);
plot.setData(d);
}
}
});
</script>
<svelte:head>
<link rel="stylesheet" href="https://unpkg.com/uplot#1.6.22/dist/uPlot.min.css" />
</svelte:head>
<Browsebar>
<TimerangeSelect />
</Browsebar>
<hr />
<div bind:this={uplotdiv} />
{#if resolved}
<code>{$query}</code>
{:else}
<h4>lade Metriken... {$query}</h4>
{/if}
<hr />
Has anyone experienced freezing with uplot? What did you do?
Lucky me, I found the problem. It had to do with the way I transformed the victoriametrics data. On every timestamp I did Number(timestamp).toFixed(0). Without toFixed(0) it is working now. :)
//transform raw data from metrics query to the uplot format
export function transformToUplot(dt) {
let udata = []; //2d data array, conforming uPlot
let tsd = []; //timestamp array
//from first result take only the timestamps
for (let t of dt.result[0].values) {
// tsd.push(Number(t[0]).toFixed(0)); //this was bad!!!!, it lead to freezing
tsd.push(Number(t[0]));
}
udata.push(tsd);
//then the values
for (let r of dt.result) {
let sd = [];
for (let d of r.values) {
let val = Number(d[1]);
sd.push(val);
}
udata.push(sd);
}
return udata;
}
Thanks for your interest!
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 used react hooks useEffect for rendering a chart from chart.js to canvas. However, I found problem of old chart showing when I hover my mouse on the chart. From the sources I found online I realized the problem might be able to solve by chart.destroy(), but I just do not know where and how to use it in my case of code. I have attached a snip of my code here, hope anybody can help me out.
import React, { useEffect } from 'react';
import Chart from 'chart.js';
import { Card } from 'components/Card';
import { localDateTime, dateFilter } from 'helpers';
const red = '#644DFF';
const purple = '#F73F64';
const DailyTrafficCard = (props) => {
const { store, capacity, data, setVar} = props;
const lastSevenDays = Array(7)
.fill()
.map((_, i) => {
const localdate = localDateTime(store);
return localdate()
.subtract(i, 'day')
.format('YYYY-MM-DD[T]07:00:00[Z]');
});
useEffect(() => {
const ctx = document && document.querySelector('#daily-traffic-chart');
if (!ctx) {
return;
}
const bar = new Chart(ctx, {
type: 'bar',
data: {
labels: [],
datasets: [{
data: data[data.length-1],
barThickness: 13,
backgroundColor: (ctx) => {
const idx = ctx && ctx.dataIndex;
const val = ctx && ctx.dataset && ctx.dataset.data && ctx.dataset.data[idx];
return val < 40 ? purple : red;
}
}]
},
options: {
intersect: false,
legend: {
display: false,
},
scales: {
xAxes: [{
type: 'time',
offset: true,
time: {
unit: 'hour',
displayFormats: {
hour: 'HH',
},
},
ticks: {},
gridLines: {
display: false,
},
}],
yAxes: [{
gridLines: {
display: true,
},
ticks: {
beginAtZero: true,
min: 0,
max: capacity,
stepSize: 5
},
}],
},
}
});
}, [data,capacity]);
const handleOnChange = (event) => {
setVar(event.target.value);
}
return (
<Card
classname="DailyTrafficCard"
icon={<span><i className="icon-user"/></span>}
title={<h3>Daily Traffic Analytics</h3>}>
<div className="daily">
<div className="daily-head p-4 text-center">
<select className="py-2 px-3" onChange={handleOnChange}>
{lastSevenDays.map(date => (
<option key={date} value={date}>{dateFilter(date, 'dddd')}</option>
))}
</select>
</div>
<div className="px-8">
{data && data.length > 0 && (
<canvas width="250" id="daily-traffic-chart"></canvas>
)}
</div>
</div>
</Card>
)
}
export {
DailyTrafficCard
}
I'm using Nivo bar to represent a user's progress on a budget. I've normalized the data by dividing the category balance by the category goal. Example data.
[{
"category": "Gas",
"budget": 0.24,
"over_budget": 0.0
},
{
"category": "Groceries",
"budget": 1.0,
"over_budget": 0.26
}]
I don't want these values to be used as the label on the chart. I plan to use the actual balance value as the label. I have an endpoint that will return the balance for a category and have attempted the following to use that value:
<ResponsiveBar
...
label={d => this.getDollarAmount(d.value)}
...
>
With the function POC as:
getDollarAmount(value) {
console.log("hitting getDollarAmount")
return 1
};
The log message gets logged 500+ times. My expectation would be that the function would only be hit once for each bar in the chart.
I'm still learning react so this could be something obvious. Thanks in advance!
EDIT - Here's the entire BarChart component:
import axios from 'axios';
import React, { Component } from "react";
import { ResponsiveBar } from '#nivo/bar'
// Nivo theming
const theme = {
axis: {
ticks: {
line: {
stroke: "#e9ecee",
strokeWidth: 40
},
text: {
// fill: "#919eab",
fill: "black",
fontFamily: "BlinkMacSystemFont",
fontSize: 16
}
}
},
grid: {
line: {
stroke: "#e9ecee",
strokeWidth: 5
}
},
legends: {
text: {
fontFamily: "BlinkMacSystemFont"
}
}
};
let budgetStatusAPI = 'http://127.0.0.1:8000/api/budget_status/?auth_user=1&month=2020-02-01';
class BarChart extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
this.getDollarAmount = this.getDollarAmount.bind(this);
}
componentDidMount() {
console.log("component did mount")
axios.get(budgetStatusAPI).then(response => {
this.setState({
data: response.data
}, function () {
console.log(this.state.data);
})
});
}
componentDidUpdate() {
console.log("component did update")
}
getDollarAmount(value) {
console.log("hitting getDollarAmount")
console.log(value)
return 1
};
render() {
const hard_data = [
{
"category": "Groceries",
"budget_status": 1.0,
"over_budget": .26,
},
{
"category": "Gas",
"budget_status": .24,
"over_budget": 0.0,
}]
return(
<ResponsiveBar
maxValue={1.5}
markers={[
{
axis: 'x',
value: 1,
lineStyle: { stroke: 'rgba(0, 0, 0, .35)', strokeWidth: 2 },
legend: 'Goal',
legendOrientation: 'horizontal',
legendPosition: 'top'
},
]}
enableGridX={false}
gridXValues={[1]}
enableGridY={false}
data={this.state.data}
// data={hard_data}
keys={['budget_status', 'over_budget']}
indexBy="category"
margin={{ top: 25, right: 130, bottom: 50, left: 125 }}
padding={0.3}
layout="horizontal"
colors={{ scheme: 'set2' }}
theme={theme}
defs={[
{
id: 'dots',
type: 'patternDots',
background: 'inherit',
color: '#38bcb2',
size: 4,
padding: 1,
stagger: true
},
{
id: 'lines',
type: 'patternLines',
background: 'inherit',
color: '#eed312',
rotation: -45,
lineWidth: 6,
spacing: 10
}
]}
borderColor={{ from: 'color', modifiers: [ [ 'darker', 1.6 ] ] }}
axisBottom={null}
label={d => this.getDollarAmount(d.value)}
isInteractive={false}
animate={true}
motionStiffness={90}
motionDamping={15}
/>
)
}
}
export default BarChart;
Reproduced here: https://codesandbox.io/s/nivo-bar-label-issue-k4qek
The multiple calling is happening because the bar chart is calling label function for each animation tick/frame render. If we setup a counter, we'll see with animate prop set to true it will render from 450+ to 550+ times, but if we set the prop animate to false, we'll it renders 6 times which is exactly how many price values are > 0.0.
If you want to avoid all these renders, you'll have to disable animation using animate={false} prop like this:
getDollarAmount(value) {
// Remove every console.log inside this function
return `$${value}`
}
render() {
return (
<ResponsiveBar
animate={false}
label={d => this.getDollarAmount(d.value)}
...
);
}
You can check it running to your cloned CodeSandbox. I have set animate to false and the counter log inside getDollarAmount is calling 6 times. Try to change animate to true and you'll see the 500+- renders.
Also, you don't have to create a function for each label call, you can just pass the getDollarAmount function and let it handle the whole d parameter like this:
getDollarAmount(d) {
// Remove every console.log inside this function
return `$${d.value}`
}
render() {
return (
<ResponsiveBar
animate={false}
label={this.getDollarAmount}
...
);
}