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

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.

Related

How to insert link for hashtags and mentions in react quill?

I am using react quill as rich text editor and I have used quill mention for adding hashtags and people mention in editor. I have went through the docs of quill mention but there is no example for adding links to inserted "hashtag" or "mention".
There is prop, "linkTarget" for adding link but there is no example for addition of link to hashtag and mention.
Hashvalues and atvalues from database:
hashvalues:[{
id:1,
value:"newHashtag"
}]
atvalues:[{
id:1,
value:"Jhon"
}]
So my expected output is:
for hashtag:
<a href:`/#/hashtags/${id}`>#{value}</a>
for people mention:
<a href:`/#/people/${id}`>#{value}</a>
Here's my code for text editor and mention module:
import React, { useEffect, useState } from "react";
import ReactQuill, { Quill } from "react-quill";
import * as Emoji from "quill-emoji";
import "react-quill/dist/quill.snow.css";
import "quill-emoji/dist/quill-emoji.css";
import "quill-mention/dist/quill.mention.css";
import "quill-mention";
//Add https to link if https is not present
const Link = Quill.import("formats/link");
Link.sanitize = function (url) {
// quill by default creates relative links if scheme is missing.
if (!url.startsWith("http://") && !url.startsWith("https://")) {
return `http://${url}`;
}
return url;
};
Quill.register(Link, true);
Quill.register("modules/emoji", Emoji);
// Add sizes to whitelist and register them
const Size = Quill.import("formats/size");
Size.whitelist = ["extra-small", "small", "medium", "large"];
Quill.register(Size, true);
// Add fonts to whitelist and register them
const Font = Quill.import("formats/font");
Font.whitelist = [
"arial",
"comic-sans",
"courier-new",
"georgia",
"helvetica",
"lucida",
];
Quill.register(Font, true);
let atValues = [];
let hashValues = [];
const mention = {
allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/,
mentionDenotationChars: ["#", "#"],
linkTarget:"https://www.google.com",
source: function (searchTerm, renderList, mentionChar, ) {
let values;
if (mentionChar === "#") {
values = atValues;
} else {
values = hashValues;
}
if (searchTerm.length === 0) {
renderList(values, searchTerm);
} else {
const matches = [];
for (let i = 0; i < values.length; i++)
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase()))
matches.push(values[i]);
renderList(matches, searchTerm);
}
},
};
function Editor(props) {
const [editorHtml, setEditorHtml] = useState("");
const handleChange = (html) => {
setEditorHtml(html);
props.changeHandler(html);
};
useEffect(() => {
if (props.value) {
setEditorHtml(props.value);
} else {
setEditorHtml("");
}
if(props.values){
let hash=props.values
hash.map((v) => {
v["value"] = v["display"]
})
hashValues=hash
}
if(props.people){
let peoples = props.people
peoples.map((v) => {
v["value"] = v["display"]
})
atValues=peoples
}
}, [props.value]);
return (
<div>
<ReactQuill
onChange={handleChange}
value={editorHtml}
modules={modules}
formats={formats}
bounds={".app"}
placeholder={props.placeholder}
/>
</div>
);
}
const modules = {
toolbar: [
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ list: "ordered" }, { list: "bullet" }],
["bold", "italic", "underline"],
[{ color: [] }, { background: [] }],
// [{ script: 'sub' }, { script: 'super' }],
[{ align: [] }],
["link", "blockquote", "emoji"],
["clean"],
],
clipboard: {
// toggle to add extra line breaks when pasting HTML:
matchVisual: false,
},
mention,
"emoji-toolbar": true,
"emoji-textarea": false,
"emoji-shortname": true,
};
const formats = [
"header",
"font",
"size",
"bold",
"italic",
"underline",
"strike",
"blockquote",
"list",
"bullet",
"indent",
"link",
"mention",
"emoji",
];
export default function EMTextArea({
placeHolder,
name,
value,
changeHandler,
hash,
peopleMention
}) {
return (
<div className="custom-toolbar-example">
<Editor
placeholder={placeHolder}
name={name}
value={value}
changeHandler={changeHandler}
values={hash}
people={peopleMention}
/>
</div>
);
}
How can I achieve this?
Thank You!
I solved it, I had to add "link" key in atvalues and hashvalues array of objects.
New hashvalues:
hashvalues:[{
id:1,
value:"hashtag",
link:"/#/users/hashtags/1"}]
And in mention module:
const mention = {
allowedChars: /^[A-Za-z\sÅÄÖåäö]*$/,
mentionDenotationChars: ["#", "#"],
linkTarget: '_self',
source: function (searchTerm, renderList, mentionChar, ) {
let values;
if (mentionChar === "#") {
values = atValues;
} else {
values = hashValues;
}
if (searchTerm.length === 0) {
renderList(values, searchTerm);
} else {
const matches = [];
for (let i = 0; i < values.length; i++)
if (~values[i].value.toLowerCase().indexOf(searchTerm.toLowerCase()))
matches.push(values[i]);
renderList(matches, searchTerm);
}
},
};
Thanks, anyway.

tinyMCE React loosing state value

I'm using the tinyMCE editor in my React project. I need a custom button based on number of additional users. If it has 3 additional users, I add 3 additional buttons in my dropdown.
import { Editor } from '#tinymce/tinymce-react';
...
const [ totalAdditionalUsers, setTotalAdditionalUsers] = useState(0);
// I get this data from NodeJS backend and set the value inside my useEffect
// I'll simplify the code here
useEffect(() => {
setTotalAdditionalUsers(myVariable); // The value here is 3, for example
});
console.log(totalAdditionalUsers); // it shows 3
return (
<>
<Editor
apiKey={TINYMCEKEY}
value={editorContent}
init={{
height: 600,
menubar: false,
branding: false,
plugins: [
"print"
],
setup: function (editor) {
editor.ui.registry.addMenuButton('addAllSignatures', {
text: "Users Signature",
fetch: function (callback) {
var items = [
{
type: 'menuitem',
text: 'Primary User Signature',
onAction: function () {
editor.insertContent(' <strong>#userSignature#</strong> ');
}
}, {
type: 'menuitem',
text: 'Primary User Signature Date',
onAction: function () {
editor.insertContent(' <strong>#userSignatureDate#</strong> ');
}
}
];
console.log(totalAdditionalUsers); // It is showing 0. Why??
for(let i=1; i<=totalAdditionalUsers; i++) {
let s = 'th';
if(i === 1) s = 'nd';
else if(i === 2) s = 'th';
const objSign = {
type: 'menuitem',
text: `${(i+1)}${s}User Signature`,
onAction: function () {
editor.insertContent(` <strong>#addUser${i}#</strong> `);
}
};
const objDate = {
type: 'menuitem',
text: `${(i+1)}${s}User Signature Date`,
onAction: function () {
editor.insertContent(` <strong>#addUser${i}SignatureDate#</strong> `);
}
};
items.push(objSign);
items.push(objDate);
}
callback(items);
}
})
},
toolbar1: "print | addAllSignatures"
}}
onEditorChange={handleEditorChange}
/>
</>
);
My issue, it that inside the TinyMCE editor, the totalAdditionalUsers is always 0. Looks like it is not updating.
Am I setting in wrong?
Thanks

display openlayers infowindow in React

I am trying to display an info window (overlay popup) when the user clicks on one of the markers displayed on the map. Here's the code:
export const Home = () => {
const { centerPoint, zoomValue, testSites } = useContext(AppContext);
const [layer, setLayer] = useState<VectorLayer>(new VectorLayer({}));
const [popup, setPopup] = useState<Overlay>(new Overlay({}));
const popupRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const [map] = useState(
new Map({
interactions: defaultInteractions().extend([
new DragRotateAndZoom()
]),
controls: defaultControls().extend([
new ScaleLine({
units: 'imperial'
})
]),
target: '',
layers: [new TileLayer({
source: new SourceOSM()
})],
view: new View({
center: fromLonLat([centerPoint.longitude, centerPoint.latitude]),
zoom: zoomValue
})
})
);
useEffect(() => {
map.setTarget('map');
map.on('click', (event) => {
const feature = map.forEachFeatureAtPixel(event.pixel, (feature) => {
return feature;
});
if (feature) {
popup.setPosition(event.coordinate);
if (contentRef.current) {
contentRef.current.innerHTML = '<p>' + feature.getProperties().name + '</p>';
}
}
});
map.on('pointermove', (event) => {
if (!event.dragging) {
map.getTargetElement().style.cursor = map.hasFeatureAtPixel(map.getEventPixel(event.originalEvent)) ? 'pointer' : '';
}
});
setPopup(new Overlay({
element: popupRef.current,
positioning: 'bottom-center' as OverlayPositioning,
stopEvent: false,
offset: [9, 9],
}));
}, [map]);
useEffect(() => {
map.addOverlay(popup);
}, [popup, map]);
useEffect(() => {
if (testSites.length) {
const features: Feature[] = [];
testSites.forEach((testSite: TestSite) => {
const feature = new Feature({
geometry: new Point(fromLonLat([testSite.longitude, testSite.latitude])),
name: testSite.name,
address: testSite.address,
city: testSite.city,
state: testSite.state,
notes: testSite.notes
});
feature.setStyle(new Style({
image: new Icon({
src: 'images/site.png'
})
}));
features.push(feature);
});
setLayer(new VectorLayer({
source: new VectorSource({
features: features
})
}));
if (layer.getProperties().source !== null) {
map.addLayer(layer);
}
}
map.getView().animate({zoom: zoomValue}, {center: fromLonLat([centerPoint.longitude, centerPoint.latitude])}, {duration: 1000});
}, [centerPoint, zoomValue, map, testSites]);
return (
<div className="map-wrapper">
<div id="map"></div>
<div id="popup" className="map-popup" ref={popupRef}>
<div id="popup-content" ref={contentRef}></div>
</div>
</div>
);
};
Pretty much everything works fine except for displaying the info window on feature icon click. From what I can tell the positioning on click is being applied to a different div, not the one that contains . See screenshot below. Any help would be appreciated. Thanks.
Ok, I solved it. I assigned an id to my overlay and then referenced it inside the on click event function.
setPopup(new Overlay({
id: 'info',
element: popupRef.current,
positioning: 'bottom-center' as OverlayPositioning,
stopEvent: false,
offset: [9, 9],
}));
......
if (feature) {
map.getOverlayById('info').setPosition(event.coordinate);
if (contentRef.current) {
contentRef.current.innerHTML = '<p>' + feature.getProperties().name + '</p>';
}
}

React useEffect doesn't change data displayed on map

I'm playing with uber's react-map-gl and deck-gl libraries to try to display data dynamically.I've got 2 components in my react little app.A navigation bar and a Mapbox map.So here is my pipeline.When the page first loads,only the map is displayed without any marker or visual data.But when i click on one of navigation bar link,an action creator gets called,when i make an ajax call to fetch some data,and from the action that is dispatched a pass my response data as payload and the reducer is reached so that i have a new version of the store.the data that i would like to visualize in they stor key : mainProjects that contains an array of geojson.My click on a navbar link succefully updates that value,and in the actual Map component,i can load the new values but useEffect does not update my localstate.Here is my map code:
import React, { useState, useEffect, useContext } from "react";
import { StaticMap } from "react-map-gl";
import { MapContext } from "./contexts/MapProvider";
import DeckGL, { GeoJsonLayer } from "deck.gl";
import chroma from "chroma-js";
import { connect } from "react-redux";
const MAPBOX_TOKEN =
"pk.mykey";
const mapStyle = "mapbox://mymapstyle";
function getEnergyFillColor(regime) {
const ENERGY_COLORS = {
naturalGaz: "#FF8500",
coal: "#99979A",
nuclear: "#E0399E",
hydroelectric: "#0082CB",
Wind: "#00B53B",
solar: "#DBCA00",
oil: "#FF0009",
other: "#FFEFD3"
};
let color;
switch (regime) {
case "Hydraulique":
color = ENERGY_COLORS.hydroelectric;
break;
case "Thermique":
color = ENERGY_COLORS.nuclear;
break;
default:
color = ENERGY_COLORS.other;
break;
}
color = chroma(color)
.alpha(0.667)
.rgba();
color[3] *= 255;
return color;
}
function _onClick(info) {
if (info.object) {
// eslint-disable-next-line
alert(
`${info.object.properties.NOM} (${info.object.properties.PROVINCE}) - ${
info.object.properties.PUISSANCE
}MW`
);
}
}
function Map({ mainProjects }) {
const { viewport, setViewport, onLoad } = useContext(MapContext);
const [airports, setAireports] = useState();
const [electricalEnergy, setElectricalEnergy] = useState();
const [hospitals, setHospitals] = useState();
const [roads, setRoads] = useState();
useEffect(() => {
if (mainProjects.length) {
setHospitals(mainProjects[0].hospitals);
setAireports(mainProjects[1].aeroports);
setElectricalEnergy(mainProjects[2].electricite);
setRoads(mainProjects[2].routes);
}
}, [airports, electricalEnergy, hospitals, roads]);
const layers = [
//ENERGIE ELECTRIQUE
new GeoJsonLayer({
id: "energy",
data: electricalEnergy,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.puissance * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.regime),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//AEROPORTS
new GeoJsonLayer({
id: "airports",
data: airports,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.PUISSANCE * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.REGIME),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//HOSPITALS
new GeoJsonLayer({
id: "hospitals",
data: hospitals,
// Styles
filled: true,
pointRadiusMinPixels: 20,
opacity: 1,
pointRadiusScale: 2000,
getRadius: energyItem => energyItem.properties.PUISSANCE * 3.14,
getFillColor: energyItem =>
getEnergyFillColor(energyItem.properties.REGIME),
// Interactive props
pickable: true,
autoHighlight: true,
onClick: _onClick
}),
//ROUTES
new GeoJsonLayer({
id: "roads",
data: roads,
pickable: true,
stroked: false,
filled: true,
extruded: true,
lineWidthScale: 20,
lineWidthMinPixels: 2,
getFillColor: [160, 160, 180, 200],
getLineColor: d => [255, 160, 20, 200],
// getLineColor: d => colorToRGBArray(d.properties.color),
getRadius: 100,
getLineWidth: 1,
getElevation: 30,
onHover: ({ object, x, y }) => {
// const tooltip = object.properties.name || object.properties.station;
/* Update tooltip
http://deck.gl/#/documentation/developer-guide/adding-interactivity?section=example-display-a-tooltip-for-hovered-object
*/
},
onClick: _onClick
})
];
return (
<>
<link
href="https://api.tiles.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css"
rel="stylesheet"
/>
<DeckGL
initialViewState={viewport}
viewState={viewport}
controller={true}
layers={layers}
onLoad={onLoad}
onViewportChange={nextViewport => setViewport(nextViewport)}
>
<StaticMap mapboxApiAccessToken={MAPBOX_TOKEN} mapStyle={mapStyle} />
</DeckGL>
</>
);
}
const mapStateToProps = ({
selectedLinks: { sectorSelected, provinceSelected, subSectorSelected },
mainProjects
}) => {
if (sectorSelected || provinceSelected || subSectorSelected) {
return {
mainProjects
};
} else {
return {
mainProjects: []
};
}
};
export default connect(mapStateToProps)(Map);
In the above code,i try to update my local state values by is setters,but useEffect doesn't seem to work.And it looks like it's only called once,at when the component renders for the first time.How can i solve this problem?
Thank you!!
Your useEffect has a set of dependencies that donˋt match those, which are actually used.
You are setting your local state with elements of mainProjects, so useEffect will only do something when mainProjects changes.
You donˋt seem to be doing anything with your useState-Variables, so you donˋt change state, so react doesnˋt rerender.
Update: it is really important to check, that the dependency-array (2nd argument to useEffect) and the used variables inside the function (1st argument) correspond, else bad things will happen ;-)
There is an eslint-rule for that: https://www.npmjs.com/package/eslint-plugin-react-hooks

Back button is not working on drilldown pie chart

Here, I am using highcharts library for creating drilldown pie chart, I have used click event for filtering and drillup for revert filtering but it is not working for me anybody check it for me..?
Here have shared some of my code :
Create Chart Function
createChart(chartData) {
this.chartData = chartData;
let currencySign = this.currencySign;
let options = {
chart: {
plotBackgroundColor: null,
plotBorderWidth: null,
plotShadow: false,
type: 'pie',
// backgroundColor: bgColor
},
title: {
text: '',
},
plotOptions: {
pie: {
series: {
borderWidth: 0,
dataLabels: {
enabled: true
}
},
cursor: 'pointer',
events: {}
}
},
series: [{
name: "Plant Types",
id: "plant_types",
data: chartData.series
}],
tooltip: {
formatter: function() {
let title = this.series.name;
if(title == 'Products') {
title += " ("+ this.point['rate_unit'] + ")";
}
let pointcolor = this.point.color;
let pointName = this.point.name;
let pointY = numeral(this.point.y).format('0,0');
let pointPercentage = numeral(this.point.percentage).format('0,0.00');
let pointTotal = numeral(this.point.total).format('0,0');
return '<span style="font-size:11px">'+title+'</span><br><span style="color:'+pointcolor+'">'+pointName+':</span> '+currencySign+'' + pointY +'<b>('+pointPercentage+'%)</b><br/>Total: '+currencySign+''+pointTotal;
}
},
drilldown: {
series: []
}
};
options['plotOptions']['pie']['events'] = this.chartClickHandler();
// set theme options if dark theme enabled.
if(this._localStorageService.getIsDarkTheme()) {
Highcharts['theme'] = this._chartThemeOptions.getSearchScreenDarkThemeOptions();
Highcharts.setOptions(Highcharts['theme']);
}
this.chart = new Chart(options);
}
Event Handler Method
chartClickHandler() {
let clickObj = {
click: (e) => {
let userOptions = e.point.series.userOptions;
let key = "";
if(userOptions['id'] == "plant_types") {
this.chart.ref.showLoading('Loading Technologies...');
this.loadTechnologyByPlantType(e.point);
key = "plant_plant_type";
} else if (userOptions['id'] == "technologies") {
this.chart.ref.showLoading('Loading Prime Movers...');
this.loadPrimeMoversByTechnologies(e.point);
key = "generator_technology";
} else if (userOptions['id'] == 'prime_movers') {
let nameArr = (e.point.name).split(' - ');
let primeMoverCode = nameArr[0];
key = "prime_mover";
}
let filterData = {
type: key,
selectedValues: e.point.name
};
this.onSelectFilter.emit(filterData);
},
drillUp: (e) => {
console.log("revert event");
}
};
return clickObj;
}
if drillup event is not using for revert then which event is used for revert any suggestion?

Resources