I'm having an issue while creating a custom tooltip using react-chartjs-2 library where my chart rerenders whenever I hover the chart and change the state of the tooltip's future data. (currently tooltip doesn't exist I'm simply logging some data which Ill use later)
I referenced this question while trying to create a tooltip however they are using a class component and I use functional component but it shouldn't really change anything but anyway. I'd be really grateful of someone could provide a working example of a custom tooltip with react-chartjs-2 because I'm still not sure whether tooltip should be a separate jsx component or what is the proper way to create a custom tooltip in React. Thanks in advance
My code
const GraphTooltip = ({ data }) => {
return (
<div
style={{
padding: 20,
position: 'absolute',
border: '1px solid',
borderColor: '#fff8f9',
backgroundColor: 'rgba(53,53,53,0.81)',
borderRadius: 4,
top: data.top,
left: data.left,
}}
></div>
);
};
const LineChart = ({ values, labels }) => {
const isSSR = useIsSSR();
const [tooltipData, setTooltipData] = useState(null);
console.log(tooltipData);
const chartRef = useRef(null);
const customTooltip = useCallback(tooltipModel => {
if (tooltipModel.tooltip.opacity == 0) {
setTooltipData(null);
console.log('Hide tooltip');
return;
}
console.log(tooltipModel);
const chart = chartRef.current;
const canvas = chart.canvas;
console.log(canvas);
if (canvas) {
const position = canvas.getBoundingClientRect();
// set position of tooltip
const left = tooltipModel.tooltip.x;
console.log(position.left);
console.log(tooltipModel.tooltip);
const top = tooltipModel.tooltip.y;
tooltipData?.top != top && setTooltipData({ top: top, left: left });
}
});
const options = useMemo(() => ({
scales: {
x: {
ticks: {
beginAtZero: true,
},
grid: {
color: '#EEF5FF',
},
},
y: {
grid: {
color: '#EEF5FF',
},
},
},
plugins: {
legend: {
display: false,
position: 'right',
},
tooltip: {
enabled: false,
external: customTooltip,
},
},
}));
const data = {
labels: labels,
datasets: [
{
data: values,
fill: false,
backgroundColor: '#88B1DD',
borderColor: '#88B1DD',
pointRadius: 6,
tension: 0.5,
},
],
};
if (isSSR) return null;
return (
<>
<div className="header"></div>
<div className="relative">
<Line data={data} options={options} ref={chartRef} />
{tooltipData ? <GraphTooltip data={tooltipData} /> : <div />}
</div>
</>
);
};
Using https://www.npmjs.com/package/test-react-chartjs-2 actually fixed this. Some problems in the package itself.
Related
My code this
import React, { useEffect, useRef, useState } from "react";
import * as Highcharts from "highcharts";
import HighchartsReact from "highcharts-react-official";
export const PieChart = (props: any) => {
const [chartIsLoaded, setChartIsLoaded] = useState(false);
const series: any = [{
innerSize: '80%',
name: props.name,
colorByPoint: true,
data: props.chartData
}]
useEffect(() => {
setChartIsLoaded(true);
}, [])
const chartComponentRef = useRef<HighchartsReact.RefObject>(null);
const options: Highcharts.Options = {
chart: {
backgroundColor: "#0c0c0c",
borderColor: "#0c0c0c",
plotBorderWidth: 0,
plotShadow: false,
type: 'pie',
height: "70%",
},
title: {
style : {
display : 'none'
}
},
tooltip: {
pointFormat: '<b>{point.percentage:.1f}%</b>',
backgroundColor: "#1B1B1B",
borderColor: "transparent",
valueDecimals: 2,
borderRadius: 0,
style: {
color: "#fff",
fontSize: "15px"
}
},
accessibility: {
point: {
valueSuffix: '%'
}
},
legend: {
itemStyle: {color: "#fff", fontWeight: "400", fontFamily: "teragon-sans"}
},
plotOptions: {
pie: {
borderColor: "#0c0c0c",
borderWidth: 6,
allowPointSelect: true,
color: "#fff",
cursor: 'pointer',
dataLabels: {
enabled: false,
},
showInLegend: true
}
},
series: series
};
return (
<div>
{chartIsLoaded &&
<HighchartsReact
highcharts={Highcharts}
options={options}
ref={chartComponentRef}
oneToOne={true}
/>
}
</div>
);
};
chartData coming from this code:
let data = sheetData[0].data;
let invesment = await groupData(data, "Investment Type");
Problem: Chart rendering multiple times. Also, I have Bar Chart it's happening on that. There is no problem with Line Chart. The data is preparing with reduce function but its async waiting awaits. Moreover I tried with promises. Unfortunately, It was rendered multiple times again. How can I fix this situation?
Your chart options are initiated after every component update which directs to the chart update on every component update. I recommend you to keep chart options in a stare or memorize them. For example:
const PieChart = ({ name, chartData }) => {
const [chartOptions, setChartOptions] = useState(false);
useEffect(() => {
const series = [
{
innerSize: "80%",
name,
colorByPoint: true,
data: chartData
}
];
const options = {
...,
series
};
if (chartData) {
setChartOptions(options);
}
}, [chartData]);
return (
<div>
{chartOptions && (
<HighchartsReact highcharts={Highcharts} options={chartOptions} />
)}
</div>
);
};
Live demo: https://codesandbox.io/s/highcharts-react-demo-fz6rr6?file=/demo.jsx
Docs: https://www.npmjs.com/package/highcharts-react-official#optimal-way-to-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 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 have a piechart made with chartjs. I then added a label on each pie thanks to chart-js plugin. Sometimes, the pie background is a bit light, so my white text is not visible enough. I've made a function that check the contrast and color it to black in such a case.
But when applied to chartjs-plugin's label color property, the function runs only once, and keep the same color for all label. How to apply a different color to each label based on its background?
Here is the code:
plugins: {
datalabels: {
color: function(ctx: any) {
[...Array(ctx.dataset.data.length)].map((_, i) => {
return transformColor(ctx.dataset.backgroundColor[i]);
});
},
Thanks!
EDIT:
here is the rendering of my current piechart:
you need to provide backgroundColor: [] in Pie Chart for background color. Here is the complete example:
import React from "react";
import {makeStyles} from "#material-ui/core/styles";
import {Pie} from "react-chartjs-2";
const useStyles = makeStyles(theme => ({
chart: {
marginLeft: theme.spacing(2)
}
}));
export default function PieChartExample(props) {
const classes = useStyles();
const [data, setdata] = React.useState({
labels: ["type1", "type2", "type3", "type4"],
datasets: [
{
label: "No. of registrations made",
backgroundColor: [
"#3e95cd",
"#8e5ea2",
"#3cba9f",
"#e8c3b9",
"#c45850"
],
barThickness: 80,
data: [50, 100, 75, 20, 0]
}
]
});
const getChartData = canvas => {
return data;
};
return (
<React.Fragment>
<div
className={classes.chart}
style={{position: "relative", width: 900, height: 450}}
>
<Pie
options={{
responsive: true,
maintainAspectRatio: true,
legend: {display: true},
title: {
display: true,
text: "Title for the graph"
}
}}
onElementsClick={(e) => { console.log(e, 'e')}}
data={getChartData}
/>
</div>
</React.Fragment>
);
}
I just need to align the Chart Legend so it don't look too messy as the default shows, here is an example what I'm trying to achieve:
Please give some code suggestions: https://jsfiddle.net/holp/68wf75r8/
new Chart(document.getElementById("field-0"), {
type: 'pie',
data: {
labels: ["Chat", "Prospeção", "Whatsapp", "Trial", "Site", "Telefone", "E-mail", "Evento"],
datasets: [{
data: [700, 400, 200, 150, 80, 50, 20, 10],
borderWidth: 2,
hoverBorderWidth: 10,
backgroundColor: pieColors,
hoverBackgroundColor: pieColors,
hoverBorderColor: pieColors,
borderColor: pieColors
}]
},
options: {
legend: {
labels: {
padding: 20
}
}
}
});
There is legend.labels.generateLabels hook you generally can use to customise your legend labels.
I found out, that you can put something like below to adjust Chart.js calculations.
generateLabels: function (chart) {
chart.legend.afterFit = function () {
var width = this.width; // guess you can play with this value to achieve needed layout
this.lineWidths = this.lineWidths.map(function(){return width;});
};
// here goes original or customized code of your generateLabels callback
}
Weird thing that there is no actual configuration option to achieve this.
Chartjs v2 creates an overhead to handle the legends. Basically what you are looking for is to leverage the usage of generateLabels.
The key point to bare in mind is that you need to return an valid array of legend objects.
This plunker describes the solution.
Main focus on this part:
generateLabels: (chart) => {
chart.legend.afterFit = function () {
var width = this.width;
console.log(this);
this.lineWidths = this.lineWidths.map( () => this.width-12 );
this.options.labels.padding = 30;
this.options.labels.boxWidth = 15;
};
var data = chart.data;
//https://github.com/chartjs/Chart.js/blob/1ef9fbf7a65763c13fa4bdf42bf4c68da852b1db/src/controllers/controller.doughnut.js
if (data.labels.length && data.datasets.length) {
return data.labels.map((label, i) => {
var meta = chart.getDatasetMeta(0);
var ds = data.datasets[0];
var arc = meta.data[i];
var custom = arc && arc.custom || {};
var getValueAtIndexOrDefault = this.getValueAtIndexOrDefault;
var arcOpts = chart.options.elements.arc;
var fill = custom.backgroundColor ? custom.backgroundColor : getValueAtIndexOrDefault(ds.backgroundColor, i, arcOpts.backgroundColor);
var stroke = custom.borderColor ? custom.borderColor : getValueAtIndexOrDefault(ds.borderColor, i, arcOpts.borderColor);
var bw = custom.borderWidth ? custom.borderWidth : getValueAtIndexOrDefault(ds.borderWidth, i, arcOpts.borderWidth);
return {
text: label,
fillStyle: fill,
strokeStyle: stroke,
lineWidth: bw,
hidden: isNaN(ds.data[i]) || meta.data[i].hidden,
// Extra data used for toggling the correct item
index: i
};
});
}
return [];
}
I tried to do as advised by the comments above. But to see that it is really difficult. It’s better and easier for me to set:
legend: {display: FALSE, ..} `, and then render the legend using html (angular, react, view .. another render template):
// part of angualr model class
public dataSets = [{
label: "New Deals",
backgroundColor: "#88B2FF",
data: [26, 15, 5],
},
{
label: "Active Deals",
backgroundColor: "#397FFF",
data: [7, 13, 22],
},
....
this.chart = new Chart(ctx, {
type: "roundedBar",
data: {
labels: this.xLabels,
datasets: this.dataSets,
},
<div style="width: 380px;height: 200px; display: inline-block;">
<canvas id="chart" aria-label="Hello ARIA World" role="img"></canvas>
</div>
<!-- This is angular template -->
<ul class="legend">
<li *ngFor="let set of dataSets">
<i [style.backgroundColor]="set.backgroundColor" class="icon"></i>
<label>
{{ set.label }}
</label>
</li>
</ul>
<style>
.legend {
display: flex;
text-align: center;
justify-content: space-between;
font-size: 10px;
line-height: 12px;
}
.icon {
width: 10px;
height: 10px;
border-radius: 50%;
display: inline-block;
}
</style>