Only title is not updated in react-chartjs-2 doughnut chart - reactjs

I am using react-chartjs-2 to implement a doughnut chart in my website.
Below is the code I am using to render the chart.
const Doughnut = (props) => {
const { title, labels, colors, projects, percentages, width, height } = props;
console.log(`title:${title}`); //this logs proper title
const plugins = [
{
beforeDraw: function (chart) {
console.log(`title:${title}`); //this logs previous title and thus renders previous title
var width = chart.width,
height = chart.height,
ctx = chart.ctx;
ctx.restore();
var fontSize = (height / 240).toFixed(2);
ctx.font = fontSize + "em sans-serif";
ctx.textBaseline = "top";
var text = `${title}`,
textX = Math.round((width - ctx.measureText(text).width) / 2),
textY = height / 2;
ctx.fillText(title, textX, textY);
ctx.save();
},
},
];
const data = {
datasets: [
{
data: percentages,
backgroundColor: colors,
},
],
};
return (
<DoughnutChart
data={data}
plugins={plugins}
width={width}
height={height}
/>
);
};
I am passing doughnut chart details from my parent component to child component and all props are passed correctly. When I log props.title outside beforeDraw function then it logs the proper value but when I log props.title inside beforeDraw function then it logs the previous value of title and thus it renders the previous value of title.
what am I doing wrong here?

Related

Trouble with drag functionality in an image slider (React, Next)

I'm trying to recreate a JS draggable slider in React/Next from this codepen but I just can't make it slide on mouse drag. Nothing happens on first click, but at the mouseUp event, the slider starts to slide and it won't stop until you reload the page.
Here is my codesandbox so far.
And here is the original from codepen:
const track = document.getElementById("image-track");
const handleOnDown = e => track.dataset.mouseDownAt = e.clientX;
const handleOnUp = () => {
track.dataset.mouseDownAt = "0";
track.dataset.prevPercentage = track.dataset.percentage;
}
const handleOnMove = e => {
if(track.dataset.mouseDownAt === "0") return;
const mouseDelta = parseFloat(track.dataset.mouseDownAt) - e.clientX,
maxDelta = window.innerWidth / 2;
const percentage = (mouseDelta / maxDelta) * -100,
nextPercentageUnconstrained = parseFloat(track.dataset.prevPercentage) + percentage,
nextPercentage = Math.max(Math.min(nextPercentageUnconstrained, 0), -100);
track.dataset.percentage = nextPercentage;
track.animate({
transform: `translate(${nextPercentage}%, -50%)`
}, { duration: 1200, fill: "forwards" });
for(const image of track.getElementsByClassName("image")) {
image.animate({
objectPosition: `${100 + nextPercentage}% center`
}, { duration: 1200, fill: "forwards" });
}
}
I just figured it out. I was using onClick to activate dragging functionality, which was wrong. The correct event is onMouseDown.

background image is not displaying in react-chartjs-2

I want use image background in react-chartjs-2 but its not working.
I tried in plane chartjs page and its working without any problem.
const image = new Image();
image.src = 'https://www.chartjs.org/img/chartjs-logo.svg';
const plugin = {
id: 'custom_canvas_background_image',
beforeDraw: (chart) => {
if (image.complete) {
const ctx = chart.ctx;
const {top, left, width, height} = chart.chartArea;
const x = left + width / 2 - image.width / 2;
const y = top + height / 2 - image.height / 2;
ctx.drawImage(image, x, y);
} else {
image.onload = () => chart.draw();
}
}
};
export default function Chart() {
return <Line options = {
options
}
data = {
data
}
plugins = {
plugin
}
/>;
}
plugins must be defined as an array as follows:
plugins = {[
plugin
]}
I couldn't find this information on the react-chartjs-2 page directly but taking a look at types.ts on GitHub, I found this...
/**
* The plugins array that is passed into the Chart.js chart
* #see https://www.chartjs.org/docs/latest/developers/plugins.html
* #default []
*/
plugins?: Plugin<TType>[];

How do I change the size of a React DnD Preview Image?

I am making a chess game, and I am using React Drag n Drop for the piece movement. I have a problem where the drag image is not scaling with the window size. The image stays at a static 60x60px (the same as the image I am serving from my static folder to the client).
Here is a gif of the problem so you can see:
Drag image not scaling with original image
Here is a code snippet of the "Piece" component:
import React, { useState, useEffect } from "react";
// React DnD Imports
import { useDrag, DragPreviewImage, DragLayer } from "react-dnd";
function Piece({ piece }) {
const { imageFile } = piece;
// drag and drop configuration
const [{ isDragging }, drag, preview] = useDrag({
item: { type: "piece", piece: piece },
collect: (monitor) => {
return { isDragging: !!monitor.isDragging() };
},
});
const opacityStyle = {
opacity: isDragging ? 0.5 : 1,
};
return (
<React.Fragment>
<DragPreviewImage connect={preview} src={imageFile} />
<img style={{ ...opacityStyle }} ref={drag} src={imageFile}></img>
</React.Fragment>
);
}
I've tried the following, but it doesn't work exactly right:
useEffect(() => {
const img = new Image();
img.src = imageFile;
const ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = "100%";
ctx.canvas.height = "100%";
img.onload = () => {
ctx.drawImage(img, 0, 0, ctx.canvas.width, ctx.canvas.height);
img.src = ctx.canvas.toDataURL();
preview(img);
};
}, []);
Drag Image is transparent
If you start dragging between UseEffect calls, you start dragging the whole tile behind the image
I really would love a way to easily tell my DragPreviewImage to scale with the original image... Thank you!
EDIT:
I have discovered the previewOptions property! I got my preview image to center on drag by doing this, but I can't get it to increase in size. Code below:
function Piece({ piece }) {
const { imageFile } = piece;
const previewOptions = {
offsetX: 27,
offsetY: 28,
};
// drag and drop configuration
const [{ isDragging }, drag, preview] = useDrag({
item: { type: "piece", piece: piece },
previewOptions,
collect: (monitor) => {
return { isDragging: !!monitor.isDragging() };
},
});
const img = new Image();
img.src = imageFile;
preview(img, previewOptions);
const callbackRef = useCallback(
(node) => {
drag(node);
preview(node, previewOptions);
},
[drag, preview],
);
const opacityStyle = {
opacity: isDragging ? 0.5 : 1,
};
return (
<React.Fragment>
<DragPreviewImage connect={preview} src={imageFile} />
<img style={{ ...opacityStyle }} ref={drag} src={imageFile} />
</React.Fragment>
);
}
I've looked through the documentation for react-dnd and what I found is that the only way to make the preview image the size you want is to edit the source image itself. In my case the images are 500x500 while the board tile is 75x75, so by shrinking the image in Photoshop / GIMP it got to the size I wanted. However if you want it to be responsive I have no idea. I tried to add style={{height: , width: }} to the DraggablePreview but that had no effect, tried with scale() also no effect.
But if the chess board you have is static (width and height) editing the images would be best.

Can't implement D3.js to React with useRef. What is the correct way?

I am trying to create a line chart with d3.js and implement it to React as a function component with hooks.
I tried to use useRef. Initialized them as null and then set them in the JSX as you can see in the code. When I want to use clientWidth which uses lineChart ref it says it is null.
I thought of creating an initializeDrawing function which will initialize empty assignments but then since component doesn't refresh at variable changes it doesn't work. I also thought of carrying those variables to useState but it doesn't seem like a good solution.
I want to learn from you what is the best solution to implement d3.js charts with function components.
I am trying to implement this example, but learning general principles of implementing d3.js to react function components with hooks is the main goal.
Here is the code:
...
...
...
const LineChart: FunctionComponent = (props) => {
let dataArrIndex = 0;
const lineChart = useRef((null as unknown) as HTMLDivElement);
const xAxis = useRef((null as unknown) as SVGGElement);
const yAxis = useRef((null as unknown) as SVGGElement);
const margin = { top: 40, right: 40, bottom: 40, left: 40 };
const aspectRatio = 9 / 16;
const chartWidth = lineChart.current.clientWidth - margin.left - margin.right;
const chartHeight =
lineChart.current.clientWidth * aspectRatio - margin.top - margin.bottom;
const yScale = d3.scaleLinear().range([chartHeight, 0]);
const xScale = d3.scaleBand().range([0, chartWidth]);
const update = () => {
yScale.domain([
0,
d3.max(
data,
(d): number => {
return d[dataArrIndex].balance;
},
) as number,
]);
xScale.domain(data[dataArrIndex].map((d) => d.month));
d3.select(yAxis.current).call(d3.axisLeft(yScale));
d3.select(xAxis.current).call(d3.axisBottom(xScale));
};
return (
<div className="line-chart" ref={lineChart}>
<svg width={chartWidth} height={chartHeight}>
<g transform={`translate(${margin.left},${margin.bottom})`} />
<g ref={yAxis} />
<g ref={xAxis} />
<path />
</svg>
</div>
);
};
export default LineChart;
The problem is that you trying to use ref for element before this element created in dom, so you have null instead of ref to htmlelement. To solve your issue you need to wrap all code that uses refs to useEffect hook.
Something like this:
useEffect(() => {
const chartWidth = lineChart.current.clientWidth - margin.left - margin.right;
const chartHeight =
lineChart.current.clientWidth * aspectRatio - margin.top - margin.bottom;
const yScale = d3.scaleLinear().range([chartHeight, 0]);
const xScale = d3.scaleBand().range([0, chartWidth]);
const update = () => {
yScale.domain([
0,
d3.max(
data,
(d): number => {
return d[dataArrIndex].balance;
},
) as number,
]);
xScale.domain(data[dataArrIndex].map((d) => d.month));
d3.select(yAxis.current).call(d3.axisLeft(yScale));
d3.select(xAxis.current).call(d3.axisBottom(xScale));
};
}
Use effect uses in functional component as the analog for componentDidMount/componentDidUpdate/componentWillUnmount
And if you need to call method 'update' every time when component receive updates you need to move it from useEffect to function body and call it in ueEffect:
const update = () => {
yScale.domain([
0,
d3.max(
data,
(d): number => {
return d[dataArrIndex].balance;
},
) as number,
]);
xScale.domain(data[dataArrIndex].map((d) => d.month));
d3.select(yAxis.current).call(d3.axisLeft(yScale));
d3.select(xAxis.current).call(d3.axisBottom(xScale));
};
useEffect(() => {
const chartWidth = lineChart.current.clientWidth - margin.left - margin.right;
const chartHeight =
lineChart.current.clientWidth * aspectRatio - margin.top - margin.bottom;
const yScale = d3.scaleLinear().range([chartHeight, 0]);
const xScale = d3.scaleBand().range([0, chartWidth]);
update();
}
You can use attributes like width height in several ways:
Set it to state using useState hook: const { width, setWidth } = useState(0) and update it in useEffect from ref
Check if ref !== null <svg width={chartRef && chartRef.current.offsetWidth} />

Deck.gl how to show popup onClick

Setup:
Basic react app using react-map-gl to show a map with a deck.gl ScatterplotLayer over the top to visualise the data
Goal:
1) To show points on a map as circles of a given radius and colour.
2) When a user clicks on a circle, a tooltip/popup should show with more data about it (included in the data provided) until the user clicks away (essentially the same as this graph but for click instead of hover, http://uber.github.io/deck.gl/#/documentation/layer-catalog/scatterplot-layer. FYI I looked at the code for this and the hover logic has been removed, I assume for simplicity).
Issue:
I have completed point 1 but I cannot get point 2 to work. The furthest I have gotten to prove the data is there is to log to the console.
To note:
I'm not married to react-tooltip - I don't mind taking it out entirely if there's a better way of doing this. I only need to keep mapbox and deck.gl.
Data: https://gist.github.com/NikkiChristofi/bf79ca37028b29b50cffb215360db999
deckgl-overlay.js
import React, {Component} from 'react';
import ReactTooltip from 'react-tooltip';
import DeckGL, {ScatterplotLayer} from 'deck.gl';
export default class DeckGLOverlay extends Component {
static get defaultViewport() {
return {
longitude: 0,
latitude: 0,
zoom: 2,
maxZoom: 16,
pitch: 0,
bearing: 0
};
}
# in this method I want to update the variable tooltipText with
# whatever object data has been clicked.
# The console log successfully logs the right data (i.e. the third
# element in the array), but the tooltip doesn't even show
onClickHandler = (info) => {
let dataToShow = info ? info.object[2] : "not found";
this.tooltipText = dataToShow;
console.log(dataToShow);
}
render() {
const {viewport, lowPerformerColor, highPerformerColor, data, radius, smallRadius, largeRadius} = this.props;
if (!data) {
return null;
}
const layer = new ScatterplotLayer({
id: 'scatter-plot',
data,
radiusScale: radius,
radiusMinPixels: 0.25,
getPosition: d => [d[1], d[0], 0],
getColor: d => d[2] > 50 ? lowPerformerColor : highPerformerColor,
getRadius: d => d[2] < 25 || d[2] > 75 ? smallRadius : largeRadius,
updateTriggers: {
getColor: [lowPerformerColor, highPerformerColor]
},
pickable: true,
onClick: info => this.onClickHandler(info),
opacity: 0.3
});
return (
<DeckGL {...viewport} layers={ [layer] } data-tip={this.tooltipText}>
<ReactTooltip />
</DeckGL>
);
}
}
app.js
import React, {Component} from 'react';
import {render} from 'react-dom';
import MapGL from 'react-map-gl';
import DeckGLOverlay from './deckgl-overlay.js';
import {json as requestJson} from 'd3-request';
const MAPBOX_TOKEN = process.env.MAPBOX_TOKEN; // eslint-disable-line
const lowPerformerColor = [204, 0, 0];
const highPerformerColor = [0, 255, 0];
const smallRadius = 500;
const largeRadius = 1000;
const DATA_URL = 'https://gist.github.com/NikkiChristofi/bf79ca37028b29b50cffb215360db999';
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
viewport: {
...DeckGLOverlay.defaultViewport,
width: 500,
height: 500
},
data: null
};
requestJson(DATA_URL, (error, response) => {
if (!error) {
console.log(response);
this.setState({data: response});
}
else{
console.log(error);
}
});
}
componentDidMount() {
window.addEventListener('resize', this._resize.bind(this));
this._resize();
}
_resize() {
this._onViewportChange({
width: window.innerWidth,
height: window.innerHeight
});
}
_onViewportChange(viewport) {
this.setState({
viewport: {...this.state.viewport, ...viewport}
});
}
render() {
const {viewport, data} = this.state;
return (
<MapGL
{...viewport}
onViewportChange={this._onViewportChange.bind(this)}
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle='mapbox://styles/mapbox/dark-v9'>
<DeckGLOverlay viewport={viewport}
data={data}
lowPerformerColor={lowPerformerColor}
highPerformerColor={highPerformerColor}
smallRadius={smallRadius}
largeRadius={largeRadius}
radius={300}
/>
</MapGL>
);
}
}
Figured out a way to do it.
Solution
I bubbled up the onClick event to the MapGL layer, and used the Popup element to display the data.
so in app.js:
1) import the Popup element from react-map-gl
import MapGL, { Popup } from 'react-map-gl';
2) Set coordinates state and "info" (to show in the popup)
constructor(props) {
super(props);
this.state = {
viewport: {
...DeckGLOverlay.defaultViewport,
width: 500,
height: 500
},
data: null,
coordinates: [-0.13235092163085938,51.518250335096376],
info: "Hello"
};
3) Create callback method that sets the state with the new data (info will just be an element from the data, can be anything you want to display in the popup though)
myCallback = (info) => {
console.log(info);
if(info){
this.setState({coordinates: info.lngLat, info: info.object[2]});
}
}
4) Render the popup and reference the callback method in the DeckGL layer
return (
<MapGL
{...viewport}
{...this.props}
onViewportChange={this._onViewportChange.bind(this)}
mapboxApiAccessToken={MAPBOX_TOKEN}
mapStyle='mapbox://styles/mapbox/dark-v9'>
<Popup
longitude={this.state.coordinates[0]}
latitude={this.state.coordinates[1]}>
<div style={style}>
<p>{this.state.info}</p>
</div>
</Popup>
<DeckGLOverlay viewport={viewport}
data={data}
lowPerformerColor={lowPerformerColor}
highPerformerColor={highPerformerColor}
smallRadius={smallRadius}
largeRadius={largeRadius}
radius={300}
callbackFromParent={this.myCallback}
/>
</MapGL>
);
and in deckgl-overlay.js:
1) Feed data information into the parent's (app.js) method
onClick: info => this.props.callbackFromParent(info),
(obviously delete the React-tooltip element and onClick event handler in deckoverlay.js to clean up)
For anyone reading this who wants to use a custom popover or one from a third party library like antd that doesn't support exact position as a prop I got around this problem by just creating a <div style={{ position: 'absolute', left: x, top: y}} /> to act as a child node for the popover to reference. X and Y are initially set to 0:
const [selectedPoint, setSelectedPoint] = useState({});
const [x, setX] = useState(0);
const [y, setY] = useState(0);
and then are set onClick in the GeoJsonLayer:
const onClick = ({ x, y, object }) => {
setSelectedPoint(object);
setX(x);
setY(y);
};
const layer = new GeoJsonLayer({
id: "geojson-layer",
data,
pickable: true,
stroked: false,
filled: true,
extruded: true,
lineWidthScale: 20,
lineWidthMinPixels: 2,
getFillColor: [0, 0, 0, 255],
getRadius: 50,
getLineWidth: 1,
getElevation: 30,
onClick
});
The downside to this approach is that the popover won't stay with the point if the map is zoomed/panned because X and Y are viewport coordinates vs lat and long.

Resources