why bad handling chartjs in a react component when render that component - reactjs

I am working on a digital currency personal project.
My problem is that when I click on any digital currency to show the details of that coin,
The new page must be refreshed to display the chart correctly, or use a tag,
but I use the Link (react-router-dom),
And before refreshing the page, my chart is shown as below.
(https://ibb.co/NFgNMmX)
And after refreshing the page, my chart is shown as below.
(https://ibb.co/DzWRXRY)
and this is my code
const CoinChart = () => {
let id = useParams();
// state
const [chartDay, setChartDay] = useState({
value: "1",
text: "1d"
})
// redux
const dispatch = useDispatch<any>();
const detail = useSelector((state: State) => state.coin_detail.chart);
useEffect(() => {
dispatch(coinChartFetchRequestFunc(id.coin_id, chartDay.value));
}, [chartDay]);
// chart config
const labels = detail.chart?.prices.map((item) => {
if (chartDay.value == "max") {
return (
new Date(item[0]).toLocaleDateString()
)
}
else {
return (
new Date(item[0]).getDate() +
"." +
new Date(item[0]).toDateString().split(/[0-9]/)[0].split(" ")[1] +
" " +
new Date(item[0]).getHours() +
":" +
new Date(item[0]).getMinutes()
);
}
});
const Data = {
labels,
datasets: [
{
fill: true,
drawActiveElementsOnTop: false,
data: detail.chart?.prices.map((item) => {
return item[1];
}),
label: "price(usd)",
borderColor: "#3861fb",
backgroundColor: "#3861fb10",
pointBorderWidth: 0,
borderWidth: 2.5,
},
],
};
return (
//some code
<Line data={Data} />
);
};
Is this problem solvable?
Also, I used translate in some part, and if there are any problems in the above texts, excuse me!

Related

ReactJS: TypeError: Cannot read properties of undefined (reading 'reduce')

I'm having a problem here when creating a date range picker with chartjs.
So the case is :
When the user has just logged in, the user opens the "HITAPI" page to view the chart by date.
Then the user filters the date according to his wishes, For example, between the 1st and 13th of February.
When finished selecting the date, on the web a white blank appears instead.
When it is reloaded again and opens the HITAPI page once again and selects a date, data will appear that corresponds to the selected date (and no white blanks appear again).
When I look at the console, it shows (TypeError: Cannot read properties of undefined (reading 'reduce')).
Guess what's the error? Can you guys help me? Thank you
My Code =
const [openModal, setOpenModal] = useState(false);
const defEndDate = new Date().toLocaleDateString("en-US");
const StartDate = useRecoilValue(startDate);
const EndDate = useRecoilValue(endDate);
const setStartDate = useSetRecoilState(startDate);
const setEndDate = useSetRecoilState(endDate);
const [dataKey, setDataKey] = useState([]);
const GetData = async () => {
try {
const result = await getHitApiTotal(StartDate, EndDate);
setDataKey(result.data[1]);
console.log(result.data[1]);
} catch (error) {
console.log(error);
}
};
const hasil = Object.values(
dataKey.reduce((acc, curr) => {
if (acc[curr["#timestamp"].split("T")[0]] == null) {
acc[curr["#timestamp"].split("T")[0]] = {
message: curr.message,
timestamp: curr["#timestamp"].split("T")[0],
count: 0,
};
}
acc[curr["#timestamp"].split("T")[0]].count++;
return acc;
}, {})
);
const labels = hasil.map((item) => item.timestamp);
const data = {
labels,
datasets: [
{
data: hasil.map((item) => item.count),
borderColor: "rgb(255, 99, 132)",
backgroundColor: "rgba(255, 99, 132, 0.5)",
pointRadius: 6,
xAxisID: "x1",
},
],
options: {
plugins: {
legend: {
display: false,
},
},
},
};
useEffect(() => {
if (defEndDate !== EndDate) {
GetData();
}
}, [EndDate, StartDate, defEndDate]);
for more details, you can see the video link that I wrote below:
https://va.media.tumblr.com/tumblr_rq0olbfygO1zb5h2t.mp4

ChartJS 3 Doesn't Show Data Until A Legend Is Clicked

I get some data from back-end to show. When I inspect element with React Developer Tools, I can see that data is there but not shown in production. ChartJS version is 3.8, not react-chartjs
I was having the same problem in development, too, but solved it by setting a unique key with key={Math.random()}. In development build, it works just fine. Problem occurs in production. I deploy my app on Firebase.
I wait for data before rendering:
{isAnyFetching ? "Loading..." : <BarChart01 data={chartData} width={595} height={248} key={Math.random()} />}
I tried giving an array of zeroes until data is loaded to be sure chartData changed to trigger re-render by changing the state of the chart component. I also tried giving an extraKey prop and change it with useEffect to re-render again.
The whole chart component is:
function BarChart01({
data,
width,
height
}) {
const canvas = useRef(null);
const legend = useRef(null);
useEffect(() => {
const ctx = canvas.current;
// eslint-disable-next-line no-unused-vars
const chart = new Chart(ctx, {
type: 'bar',
data: data,
options: {
layout: {
padding: {
top: 12,
bottom: 16,
left: 20,
right: 20,
},
},
scales: {
y: {
grid: {
drawBorder: false,
},
ticks: {
maxTicksLimit: 6,
callback: (value) => formatValue(value),
},
},
x: {
type: 'time',
time: {
parser: 'MM-DD-YYYY',
unit: 'month',
displayFormats: {
month: 'MMM YY',
},
},
grid: {
display: false,
drawBorder: false,
},
},
},
plugins: {
legend: {
display: true,
},
tooltip: {
callbacks: {
title: () => false, // Disable tooltip title
label: (context) => formatValue(context.parsed.y),
},
},
},
interaction: {
intersect: false,
mode: 'nearest',
},
animation: {
duration: 500,
},
maintainAspectRatio: false,
resizeDelay: 200,
},
plugins: [{
id: 'htmlLegend',
afterUpdate(c, args, options) {
const ul = legend.current;
if (!ul) return;
// Remove old legend items
while (ul.firstChild) {
ul.firstChild.remove();
}
// Reuse the built-in legendItems generator
const items = c.options.plugins.legend.labels.generateLabels(c);
items.forEach((item) => {
const li = document.createElement('li');
li.style.marginRight = tailwindConfig().theme.margin[4];
// Button element
const button = document.createElement('button');
button.style.display = 'inline-flex';
button.style.alignItems = 'center';
button.style.opacity = item.hidden ? '.3' : '';
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex, !c.isDatasetVisible(item.datasetIndex));
c.update();
};
// Color box
const box = document.createElement('span');
box.style.display = 'block';
box.style.width = tailwindConfig().theme.width[3];
box.style.height = tailwindConfig().theme.height[3];
box.style.borderRadius = tailwindConfig().theme.borderRadius.full;
box.style.marginRight = tailwindConfig().theme.margin[2];
box.style.borderWidth = '3px';
box.style.borderColor = item.fillStyle;
box.style.pointerEvents = 'none';
// Label
const labelContainer = document.createElement('span');
labelContainer.style.display = 'flex';
labelContainer.style.alignItems = 'center';
const value = document.createElement('span');
value.style.color = tailwindConfig().theme.colors.slate[800];
value.style.fontSize = tailwindConfig().theme.fontSize['3xl'][0];
value.style.lineHeight = tailwindConfig().theme.fontSize['3xl'][1].lineHeight;
value.style.fontWeight = tailwindConfig().theme.fontWeight.bold;
value.style.marginRight = tailwindConfig().theme.margin[2];
value.style.pointerEvents = 'none';
const label = document.createElement('span');
label.style.color = tailwindConfig().theme.colors.slate[500];
label.style.fontSize = tailwindConfig().theme.fontSize.sm[0];
label.style.lineHeight = tailwindConfig().theme.fontSize.sm[1].lineHeight;
const theValue = c.data.datasets[item.datasetIndex].data.reduce((a, b) => a + b, 0);
const valueText = document.createTextNode(formatValue(theValue));
const labelText = document.createTextNode(item.text);
value.appendChild(valueText);
label.appendChild(labelText);
li.appendChild(button);
button.appendChild(box);
button.appendChild(labelContainer);
labelContainer.appendChild(value);
labelContainer.appendChild(label);
ul.appendChild(li);
});
},
}],
});
chart.update();
return () => chart.destroy();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return (
<>
<div className="px-5 py-3">
<ul ref={legend} className="flex flex-wrap"></ul>
</div>
<div className="grow">
<canvas ref={canvas} width={width} height={height}></canvas>
</div>
</>
);
}
According to the code, it seems like
button.onclick = () => {
c.setDatasetVisibility(item.datasetIndex,!c.isDatasetVisible(item.datasetIndex));
c.update();
};
part in forEach loop is responsible of this update operation when I click on a label. So it somehow doesn't call update function in production as it should as useEffect listens to data prop.

Nivo line chart custom mesh layer

I have nivo line chart with gaps like this:
Gaps are covered by passing y/value: null in november and december in data series
Tooltip displays only on data points and this is correct, but I want add tooltip at November and December with explanation why there is no data.
The solution is to add custom layer 'mesh' which is responsible for displaying tooltips on line chart.
You have to declare custom layers in <ResponsiveLine component:
layers={[
'grid',
'markers',
'axes',
'areas',
'crosshair',
'lines',
'slices',
'points',
CustomMesh,
'legends',
]}
Create CustomMesh component:
const CustomMesh = (layerData: any) => {
const { showTooltipAt, hideTooltip } = useTooltip();
const handleMouseEnter = (point: any) => {
showTooltipAt(
layerData.tooltip({ point }),
[point.x + layerData.margin.left, point.y + layerData.margin.top],
'top'
);
};
const handleMouseMove = (point: any) => {
showTooltipAt(
layerData.tooltip({ point }),
[point.x + layerData.margin.left, point.y + layerData.margin.top],
'top'
);
};
const handleMouseLeave = (point: any) => {
hideTooltip();
};
const nullValuePoints = layerData.series.reduce((acc: any[], cur: any) => {
cur.data.forEach(({ data, position }: any) => {
if (data.y === null) {
const point = {
x: position.x,
y: 100, //whatever you want
data: {
x: data.x,
},
};
acc.push(point);
}
});
return acc;
}, []);
return (
<Mesh
nodes={[...layerData.points, ...nullValuePoints]}
width={layerData.width}
height={layerData.height}
onMouseEnter={handleMouseEnter}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
debug={layerData.debugMesh}
/>
);
};
When nullValuePoints are my custom points with no data
Import required packages:
import { Mesh } from '#nivo/voronoi';
import { useTooltip } from '#nivo/tooltip';
result:

Functional Component Not Re-rendering after data is updated

I am building a dashboard filled with Esri maps that are editable. The structure of the components is something like this:
<Dashboard>
<Visuals>
<EventsMap>
<PointsLayer/>
</EventsMap>
</Visuals>
</Dashboard>
When a user edits something inside of the Dashboards component(i.e. the color of the points on a map) the data does get passed through to PointsLayer which then should re-render and show the updated color, but only updates if I refresh the page. Is it because I don't have a render method? The PointsLayer component:
import {useState, useEffect} from 'react';
import {loadModules} from 'esri-loader';
import {getFormattedDate} from 'Lib/helpers';
import styles from './Summary.module.css';
import point from "#arcgis/core/geometry/Point";
const PointsLayer = (props) => {
const data = props.data;
const color = data?.color;
console.log(color)
const humanize = (str) =>{
let i, frags = str.split('_');
for (i=0; i<frags.length; i++) {
frags[i] = frags[i].charAt(0).toUpperCase() + frags[i].slice(1);
}
return frags.join(' ');
}
const pluralize = (val, word, plural = word + 's') => {
const _pluralize = (num, word, plural = word + 's') =>
[1, -1].includes(Number(num)) ? word : plural;
if (typeof val === 'object') return (num, word) => _pluralize(num, word, val[word]);
return _pluralize(val, word, plural);
};
const [graphic, setGraphic] = useState(null);
useEffect(() => {
loadModules(['esri/Graphic']).then(([Graphic]) => {
// Parse out the Lat-Long from each Event and the doc_count
for (let i = 0; i < data?.events.length; i++) {
const point = {
type: "point", // autocasts as new Point
longitude: data?.events[i]?.location.split(",")[1],
latitude: data?.events[i]?.location.split(",")[0]
};
// Create a symbol for rendering the graphic
const symbol = {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
style: "circle", color: color, // Color Selected on popup
size: "12px", outline: {
color: [255, 255, 255], // White
width: 1.5
}
};
// Create attributes for popup
const attributes = {
watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
eventCount: data?.events[i]?.doc_count,
plural: pluralize(data?.events[i]?.doc_count, 'Event'),
deviceName: data?.events[i]?.key,
lat: data?.events[i]?.location.split(",")[0],
long: data?.events[i]?.location.split(",")[1],
account: data?.events[i]?.doc_fields?.account_id,
address: data?.events[i]?.doc_fields['#service_address'],
meterId: data?.events[i]?.doc_fields?.meter_id,
lastEvent: getFormattedDate(data?.events[i]?.doc_fields['#time_raised_last'], '')
};
// Create popup template
const popupTemplate = {
title: "{eventCount} {watcherType} {plural}",
content:
"<ul><li><b>Address:</b> {address}</li>" +
"<li><b>Account ID:</b> {account}</li>" +
"<li><b>Meter ID:</b> {meterId}</li>" +
"<li><b>Last Event:</b> {lastEvent}</li>" +
"<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
};
// Add the multiPoints to a new graphic
const graphic = new Graphic({
geometry: point,
symbol: symbol,
attributes: attributes,
popupTemplate: popupTemplate
});
setGraphic(graphic);
props.view.graphics.add(graphic);
}
}).catch((err) => console.error(err));
return function cleanup() {
props.view.graphics.remove(graphic);
};
}, []);
return null;
}
export default PointsLayer
An image to visualize what I am working on:
Try adding the graphic state to the useEffect dependency array [graphic]
useEffect(function, [graphic]) or useEffect(function, [props]) but props may cause more re-renders than you may want
A more complete example would look like this.
const [graphic, setGraphic] = useState(null);
useEffect(() => {
loadModules(['esri/Graphic']).then(([Graphic]) => {
// Parse out the Lat-Long from each Event and the doc_count
for (let i = 0; i < data?.events.length; i++) {
const point = {
type: "point", // autocasts as new Point
longitude: data?.events[i]?.location.split(",")[1],
latitude: data?.events[i]?.location.split(",")[0]
};
// Create a symbol for rendering the graphic
const symbol = {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
style: "circle", color: color, // Color Selected on popup
size: "12px", outline: {
color: [255, 255, 255], // White
width: 1.5
}
};
// Create attributes for popup
const attributes = {
watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
eventCount: data?.events[i]?.doc_count,
plural: pluralize(data?.events[i]?.doc_count, 'Event'),
deviceName: data?.events[i]?.key,
lat: data?.events[i]?.location.split(",")[0],
long: data?.events[i]?.location.split(",")[1],
account: data?.events[i]?.doc_fields?.account_id,
address: data?.events[i]?.doc_fields['#service_address'],
meterId: data?.events[i]?.doc_fields?.meter_id,
lastEvent: getFormattedDate(data?.events[i]?.doc_fields['#time_raised_last'], '')
};
// Create popup template
const popupTemplate = {
title: "{eventCount} {watcherType} {plural}",
content:
"<ul><li><b>Address:</b> {address}</li>" +
"<li><b>Account ID:</b> {account}</li>" +
"<li><b>Meter ID:</b> {meterId}</li>" +
"<li><b>Last Event:</b> {lastEvent}</li>" +
"<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
};
// Add the multiPoints to a new graphic
const graphic = new Graphic({
geometry: point,
symbol: symbol,
attributes: attributes,
popupTemplate: popupTemplate
});
setGraphic(graphic);
props.view.graphics.add(graphic);
}
}).catch((err) => console.error(err));
return function cleanup() {
props.view.graphics.remove(graphic);
};
}, [graphic]);
or using props,
const [graphic, setGraphic] = useState(null);
useEffect(() => {
loadModules(['esri/Graphic']).then(([Graphic]) => {
// Parse out the Lat-Long from each Event and the doc_count
for (let i = 0; i < data?.events.length; i++) {
const point = {
type: "point", // autocasts as new Point
longitude: data?.events[i]?.location.split(",")[1],
latitude: data?.events[i]?.location.split(",")[0]
};
// Create a symbol for rendering the graphic
const symbol = {
type: "simple-marker", // autocasts as new SimpleMarkerSymbol()
style: "circle", color: color, // Color Selected on popup
size: "12px", outline: {
color: [255, 255, 255], // White
width: 1.5
}
};
// Create attributes for popup
const attributes = {
watcherType: humanize(data?.events[i]?.doc_fields?.watcher_type),
eventCount: data?.events[i]?.doc_count,
plural: pluralize(data?.events[i]?.doc_count, 'Event'),
deviceName: data?.events[i]?.key,
lat: data?.events[i]?.location.split(",")[0],
long: data?.events[i]?.location.split(",")[1],
account: data?.events[i]?.doc_fields?.account_id,
address: data?.events[i]?.doc_fields['#service_address'],
meterId: data?.events[i]?.doc_fields?.meter_id,
lastEvent: getFormattedDate(data?.events[i]?.doc_fields['#time_raised_last'], '')
};
// Create popup template
const popupTemplate = {
title: "{eventCount} {watcherType} {plural}",
content:
"<ul><li><b>Address:</b> {address}</li>" +
"<li><b>Account ID:</b> {account}</li>" +
"<li><b>Meter ID:</b> {meterId}</li>" +
"<li><b>Last Event:</b> {lastEvent}</li>" +
"<li><a href='https://maps.google.com/maps?q=&layer=c&cbll={lat},{long}&cbp='>Google Street View</a></li></ul>"
};
// Add the multiPoints to a new graphic
const graphic = new Graphic({
geometry: point,
symbol: symbol,
attributes: attributes,
popupTemplate: popupTemplate
});
setGraphic(graphic);
props.view.graphics.add(graphic);
}
}).catch((err) => console.error(err));
return function cleanup() {
props.view.graphics.remove(graphic);
};
}, [props]);
I would think just using the graphic state would be closer to what you are looking for. Leaving the dependency array empty causes the useEffect hook to only fire on the initial component mount.
Adding the graphic state to the array tells the useEffect to watch for changes in the graphic state, and if it changes, refire the useEffect again
This post may be helpful Hooks and Dependency Arrays

React hooks async useEffect to load database data

I'm using async useEffect in React because I need to do database requests. Then, add this data to my react-charts-2
const [ plSnapShot, setPlSnapShot ] = useState({
grossIncome: 0.00,
opeExpenses: 0.00,
nonOpeExpenses: 0.00,
netIncome: 0.00,
grossPotencialRent: 0.00,
lastMonthIncome: 0.00
});
const [ thisMonthPayment, setThisMonthPayments ] = useState({
labels: [],
data: [],
color: 'blue'
});
useEffect(() => {
async function fetchData() {
await axios.get(`${url.REQ_URL}/home/getUserFullName/${userID}`)
.then(async (res) => {
setUserFullName(res.data);
await axios.get(`${url.REQ_URL}/home/getThisMonthPayments/${propertyID}`)
.then(async (resMonthPay) => {
let total = 0;
let obj = {
labels: [],
data: [],
color: 'blue'
};
const data = resMonthPay.data;
for(const d of data) {
obj.labels.push(helper.formatDate(new Date(d.date)));
obj.data.push(d.amount);
total += d.amount;
}
setThisMonthPayments(obj);
setTotalEarnedMonth(parseFloat(total));
await axios.get(`${url.REQ_URL}/home/plSnapshot/${propertyID}`)
.then(async (resPL) => {
const data = resPL.data;
setPlSnapShot({
grossIncome: parseFloat(data.GrossIncome || 0).toFixed(2),
opeExpenses: parseFloat(data.OperatingExpenses || 0).toFixed(2),
nonOpeExpenses: parseFloat(data.NonOperatingExpenses || 0).toFixed(2),
netIncome: parseFloat(data.NetIncome || 0).toFixed(2),
grossPotencialRent: parseFloat(data.GrossPotencialRent || 0).toFixed(2),
lastMonthIncome: parseFloat(data.LastMonthIncome || 0).toFixed(2)
});
});
});
});
}
fetchData();
}, [propertyID, userID]);
const pieChart = {
chartData: {
labels: ['Gross Income', 'Operating Expenses', 'Non Operating Expenses'],
datasets: [{
data: [plSnapShot.grossIncome, plSnapShot.opeExpenses, plSnapShot.nonOpeExpenses],
backgroundColor: [
ChartConfig.color.primary,
ChartConfig.color.warning,
ChartConfig.color.info
],
hoverBackgroundColor: [
ChartConfig.color.primary,
ChartConfig.color.warning,
ChartConfig.color.info
]
}]
}
};
const horizontalChart = {
label: 'Last Month Income',
labels: ['Gross Potencial Rent', 'Last Month Income'],
chartdata: [plSnapShot.grossPotencialRent, plSnapShot.lastMonthIncome]
};
Here is an example of how I call the Chart component in my code in the render/return method.
<TinyPieChart
labels={pieChart.chartData.labels}
datasets={pieChart.chartData.datasets}
height={110}
width={100}
/>
And my Pie Chart component is just to display it
import React from 'react';
import { Pie } from 'react-chartjs-2';
// chart congig
import ChartConfig from '../../Constants/chart-config';
const options = {
legend: {
display: false,
labels: {
fontColor: ChartConfig.legendFontColor
}
}
};
const TinyPieChart = ({ labels, datasets, width, height }) => {
const data = {
labels,
datasets
};
return (
<Pie height={height} width={width} data={data} options={options} />
);
}
export default TinyPieChart;
Mostly of the times it works just fine, but sometimes the chart data is loaded and displayed in the screen real quick, then it disappear and the chart is displayed empty (no data). Am I loading it properly with the useEffect or should I use another method?
Thanks you.
The momentary flashing is likely due to the fact that the chart data is empty on first render. So depending on the time it take for your useEffect to fetch the data, that flashing may present a real problem.
One common solution is to use a state variable to indicate that the data is being loaded and either not display anything in place of the chart or display a loaded of some sort. So you can add something like you suggested in the comments const [ loader, setLoader ] = useState(true). Then once the data is loaded, togged it to false.
Meanwhile, inside your render function, you would do:
...
...
{loader ?
<div>Loading....</div>
:
<TinyPieChart
labels={pieChart.chartData.labels}
datasets={pieChart.chartData.datasets}
height={110}
width={100}
/>
}
loader can go from true to false or vise versa, depending on what make more intuitive sense to you.

Resources