SVG overlapping shapes with Recharts - javascript

I am using Recharts to create a simple pie chart.
My issue likely stems from the fact the entire thing is SVG-based.
I would like to have a selected pie slice change color (both fill and stroke).
So I do the following:
import { useState } from 'react';
import { Sector, Pie, PieChart, ResponsiveContainer } from 'recharts';
export function SinglePie({ data }) {
const [selectedCellLabel, setHighlightedCell] = useState(undefined);
const onMouseEnter = (event) => setHighlightedCell(event.payload.payload.label);
const onMouseLeave = () => setHighlightedCell(undefined);
function renderActiveShape(props) {
return <Sector {...props} fill="#CFBCF7" stroke="#7C53E4" />;
}
return (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={data}
activeIndex={data.findIndex((datum) => datum.label === selectedCellLabel)}
activeShape={renderActiveShape}
dataKey={ChartDataKeys.VALUE}
nameKey={ChartDataKeys.LABEL}
fill="#CEF1EE"
stroke="#64E0D5"
onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave}
/>
</PieChart>
</ResponsiveContainer>
);
}
Then render with:
<div style={{ width: 250, height: 250 }}>
<SinglePie data={[
{
label: 'First Slice',
value: 4,
},
{
label: 'Second Slice',
value: 6,
},
{
label: 'Third Slice',
value: 2,
},
]} />
</div>
And unfortunately, on hovering the First Slice, what I see is this:
However, hovering the Third Slice looks ok:
You can see the difference between stroke width in the two cases. this is because the SVG slices overlap each other.
I know that SVG rendering is based on order, and adding a z prop won't help. But what will?
I would like to be able to see all slices with their strokes, as is required by my UI designer:

You can use .raise() function of d3 library.
First, you need to add <script src="https://d3js.org/d3.v7.min.js"></script> tag to your index.html.
And then you can move the selected slice to last in your onMouseEnter function like this:
const onMouseEnter = (event) => {
setHighlightedCell(event.payload.payload.label);
const selectedIndex = data.findIndex(
(datum) => datum.label === event.payload.payload.label
);
if (selectedIndex > -1) {
const gListItem = d3.select(
`.recharts-pie-sector:has(path[name="${event.payload.payload.label}"])`
);
gListItem.raise();
}
};
You can take a look at this stackblitz for a live working example of this solution.

Related

ToolTip does not disappear on scroll

I have a button on the site and a ToolTip to it, which describes the action of the button.
But there is one bug that I can not solve (and I'm already starting to doubt if there is a solution to this problem).
Description of the problem: when the user hovers over the icon, a tooltip appears - everything works fine here. But if at this moment the table is scrolling, then the tooltip flies out of bounds. It's hard to describe, take a look
Pay attention to how the tooltip (if the cursor is hovered over) flies up or down when scrolling.
Tell me how to solve this problem?
<div>
<Tooltip
title="Delete"
arrow
componentsProps={{
tooltip: {
sx: {
bgcolor: '#a3a3a3',
'& .MuiTooltip-arrow': {
color: '#a3a3a3',
},
},
},
}}
PopperProps={{
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
}}>
<DeleteForeverIcon/>
</Tooltip>
</div>
Instruction: hover over any cell from the first column, wait for the tooltip to appear. Then scroll the wheel up or down and see how the tooltip goes outside the table
P.s. Please note that this question has already been answered. And in principle this solution is working. But I had a lot of problems when adding this solution to my real code. Probably a simple solution for me here would be to simply cancel the scrolling when you hover over the button. Tell me how this can be done (but keep in mind that position: fixed is not suitable in this case)
My approach is different, where each tooltip maintains its own state. It is using IntersectionObserver to determine if the ToolTip component is viewable. When the component is no longer viewable, it will hide the Popper (the tooltip popup) by setting the CSS to display: 'none' via the sx prop on PopperProps.
Codesandbox Example: Here
Here is the modified file FileDownloadButton.jsx:
import React from "react";
import FileDownloadIcon from "#mui/icons-material/FileDownload";
import { ButtonGroup, Tooltip } from "#mui/material";
export default function FileDownloadButton() {
const tipRef = React.useRef(null);
const [inView, setInView] = React.useState(false);
const cb = (entries) => {
const [entry] = entries;
entry.isIntersecting ? setInView(true) : setInView(false);
};
React.useEffect(() => {
const options = {
root: null,
rootMargin: "0px"
};
const ref = tipRef.current;
const observer = new IntersectionObserver(cb, options);
if (ref) observer.observe(ref);
return () => {
if (ref) observer.unobserve(ref);
};
}, [tipRef]);
return (
<ButtonGroup>
<div>
<Tooltip
ref={tipRef}
title="Download record "
arrow
componentsProps={{
tooltip: {
sx: {
bgcolor: "#a3a3a3",
"& .MuiTooltip-arrow": {
color: "#a3a3a3"
}
}
}
}}
PopperProps={{
sx: { display: inView ? "block" : "none" },
modifiers: [
{
name: "offset",
options: {
offset: [0, -8]
}
}
]
}}
>
<FileDownloadIcon />
</Tooltip>
</div>
</ButtonGroup>
);
}
Changes for reference
Change 1
export default function FileDownloadButton() {
const tipRef = React.useRef(null);
const [inView, setInView] = React.useState(false);
const cb = (entries) => {
const [entry] = entries;
entry.isIntersecting ? setInView(true) : setInView(false);
};
React.useEffect(() => {
const options = {
root: null,
rootMargin: "0px"
};
const ref = tipRef.current;
const observer = new IntersectionObserver(cb, options);
if (ref) observer.observe(ref);
return () => {
if (ref) observer.unobserve(ref);
};
}, [tipRef]);
Change 2
PopperProps={{
sx: { display: inView ? "block" : "none" },
Update 1
Original poster wants toggle
Codesandbox example
import React, { useState } from "react";
import FileDownloadIcon from "#mui/icons-material/FileDownload";
import { ButtonGroup, IconButton, Tooltip } from "#mui/material";
import VisibilityOffIcon from "#mui/icons-material/VisibilityOff";
import VisibilityIcon from "#mui/icons-material/Visibility";
export default function FileDownloadButton() {
const [click, setClick] = useState(true);
const tipRef = React.useRef(null);
const [inView, setInView] = React.useState(false);
const cb = (entries) => {
const [entry] = entries;
entry.isIntersecting ? setInView(true) : setInView(false);
};
React.useEffect(() => {
const options = {
root: null,
rootMargin: "0px"
};
const ref = tipRef.current;
const observer = new IntersectionObserver(cb, options);
if (ref) observer.observe(ref);
return () => {
if (ref) observer.unobserve(ref);
};
}, [tipRef]);
return (
<ButtonGroup>
<div>
<Tooltip
ref={tipRef}
title={click ? "Show item" : "Hide Item"}
arrow
componentsProps={{
tooltip: {
sx: {
bgcolor: "#a3a3a3",
"& .MuiTooltip-arrow": {
color: "#a3a3a3"
}
}
}
}}
PopperProps={{
sx: { display: inView ? "block" : "none" },
modifiers: [
{
name: "offset",
options: {
offset: [0, -8]
}
}
]
}}
>
<IconButton onClick={() => setClick(!click)}>
{click ? <VisibilityOffIcon /> : <VisibilityIcon />}
</IconButton>
</Tooltip>
</div>
</ButtonGroup>
);
}
I think this is browser specific issue. When I checked the given url( https://codesandbox.io/s/silly-grass-1lb3qw) in firefox browser it was working fine(but not in the chrome). Later figured that out hover while scrolling on element will work differently in the chrome compare to other browsers since latest versions.
I made following changes to make it work in chrome. Basically whenever we hover any item then the material tooltip is being added to the document. So what I did was I have attached an scroll event and if there is any material tooltip element is present I just simply removed it.
DeviceTable.jsx
export default function DevicesTable() {
const tableRef = useRef();
function removeElementsByClass(className){
const elements = document.getElementsByClassName(className);
while(elements.length > 0){
elements[0].remove();
}
}
useEffect(() => {
if (tableRef.current) {
tableRef.current.addEventListener("scroll", (e) => {
// CLASS NAME OF THE TOOLTIP ATTACHED TO THE DOM. THERE ARE MULTIPLE CLASSES BUT I FOUND FOLLOWING CLASSNAME TO BE UNIQUE. PLEASE CROSS CHECK FROM YOUR END AS WELL.
//YOU CAN CHECK THIS BY PASSING open={true} attribute on <Tooltip> AND INSPECT DOM
removeElementsByClass("css-yk351k-MuiTooltip-tooltip")
});
}
return () => {
if(tableRef.current) {
tableRef.current.removeEventListener("scroll", ()=>{});
}
}
}, []);
return (
<TableContainer className="TableContainerGridStyle">
<Table className="TableStyle">
<DevicesTableHeader />
// CHANGED LINE
<TableBody ref={tableRef} className="TableBodyStyle">
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
<DevicesTableCell />
</TableBody>
</Table>
</TableContainer>
);
}
Apart from the above I think you can use another alternatives like followCursor, setting the position relative attribute to the table cell(TableCellStyle) or body. But these don't solve the problem fully.
As you are passing Table component as props children to the StateLabel component so in order to display/render we need to update StateLabel component to use props.children
export default function StateLabel({children}) {
return <div>{children}</div>;
}
Div hover not working when scrolling in chrome

useEffect hook isn't triggered inside ReactDOMServer.renderToString()

I'm using leaflet and react-leaflet libraries to create a map inside a React Js app as well as Material UI library for the core UI components.
I'm creating a custom cluster and marker components (using React component, not using image/icon file) for the cluster and marker inside the map. I'm using react-leaflet-markercluster for the custom cluster feature and the pie chart from Apache Echarts library for the custom cluster component.
Problem
The problem I'm facing is the useEffect hook inside my CustomCluster component is never triggered.
Steps to produce
Run the playground here: https://codesandbox.io/s/stackoverflow-custom-cluster-react-leaflet-s2wwsh
This is the initial state
Press the zoom out button (top left corner)
We can see that the 3 markers become a single cluster. The console prints the cluster value from the CustomCluster component but there is no "update chart" message. It means that the useEffect hook is not triggered.
Press again the zoom out button
We can see that all 4 markers become a single cluster. The console prints the updated cluster value from the CustomCluster component but again there is no "update chart" message. It means that the useEffect hook is not triggered again.
Code
App.jsx
const customClusterIcon = (cluster, dummyLocationList) => {
return L.divIcon({
// className: 'marker-cluster-custom',
// html: `<span>${cluster.getChildCount()}</span>`,
// iconSize: L.point(40, 40, true),
className: "custom-icon",
html: ReactDOMServer.renderToString(
<ThemeProvider theme={customTheme}>
<StyledEngineProvider injectFirst>
<CustomCluster cluster={cluster} locationList={dummyLocationList} />
</StyledEngineProvider>
</ThemeProvider>
)
});
};
<MarkerClusterGroup
showCoverageOnHover={false}
spiderfyDistanceMultiplier={2}
iconCreateFunction={(cluster) =>
customClusterIcon(cluster, dummyLocationList)
}
>
{dummyLocationList.map((item, index) => (
<Marker
key={index}
position={[item.latitude, item.longitude]}
icon={L.divIcon({
className: "custom-icon",
html: ReactDOMServer.renderToString(
<ThemeProvider theme={customTheme}>
<StyledEngineProvider injectFirst>
<CustomMarker
movingStatus={item.status}
label={item.deviceName}
/>
</StyledEngineProvider>
</ThemeProvider>
)
})}
/>
))}
</MarkerClusterGroup>
CustomCluster.jsx
const CustomCluster = (props) => {
const { cluster, locationList } = props;
const classes = useStyles();
const chartRef = useRef();
let clusterLocationList = [];
cluster.getAllChildMarkers().forEach((itemCluster) => {
locationList.forEach((itemLocation) => {
if (
itemCluster._latlng.lat === itemLocation.latitude &&
itemCluster._latlng.lng === itemLocation.longitude
)
clusterLocationList.push(itemLocation);
});
});
const chartOption = {
series: [
{
name: "Access From",
type: "pie",
radius: ["40%", "70%"],
avoidLabelOverlap: false,
label: {
show: true,
position: "inner"
},
labelLine: {
show: false
},
data: [
{ value: 1048, name: "Search Engine" },
{ value: 735, name: "Direct" },
{ value: 580, name: "Email" },
{ value: 484, name: "Union Ads" },
{ value: 300, name: "Video Ads" }
]
}
]
};
useEffect(() => {
console.log("update chart");
let chart;
if (chartRef.current !== null) chart = init(chartRef.current);
const resizeChart = () => {
chart?.resize();
};
window.addEventListener("resize", resizeChart);
if (chartRef.current !== null) {
const chart = getInstanceByDom(chartRef.current);
chart.setOption(chartOption);
}
return () => {
chart?.dispose();
window.removeEventListener("resize", resizeChart);
};
}, [cluster]);
console.log(cluster);
return (
<>
{/* AVATAR */}
<Avatar>{cluster.getChildCount()}</Avatar>
{/* PIE CHART */}
<Box className={classes.chartContainer}>
<Box ref={chartRef} className={classes.chart} />
</Box>
</>
);
};
export default CustomCluster;
Question
Based on some articles on the internet, the useEffect hook is not triggered on React server-side render (SSR) for example here https://codewithhugo.com/react-useeffect-ssr/.
So what's the solution for this case?
The goal is to create a custom cluster feature using a pie chart.
Here is the sample http://bl.ocks.org/gisminister/10001728 but it uses Vanilla Js, not React Js.

How to get onClick Event for a Label in ChartJS and React?

I have a Radar chart with labels, I want to have a click event on the Label of the Radar chart but the element always returns null. I have looked at other Stack over flow questions notedly
1 and this 2. one talks about doing it in vanilla JS approach and other one just is not working for me , Can some one point me to what am i doing wrong ?
End goal -> I want to get the label which is clicked and add a strike through toggle to that label so that i can toggle the data point on and off in the radar chart.
My Implementation
class Chart extends Component {
constructor(props) {
super(props);
this.state = {
chartData: props.chartData
};
}
render() {
return (
<div className="chart">
<Radar
data={this.state.chartData}
options={{
title: {
display: true,
text: "Testing",
fontSize: 25
},
legend: {
display: true,
position: "right"
},
onClick: function(evt, element) {
// onClickNot working element null
console.log(evt, element);
if (element.length > 0) {
console.log(element, element[0]._datasetInde);
// you can also get dataset of your selected element
console.log(data.datasets[element[0]._datasetIndex]);
}
}
}}
/>
</div>
);
}
}
**Link to the sample implementation **
Note: This answer implementation doesn't implement strikethrough. Strikethrough could be implemented by putting unicode character \u0366 between each character of the label string. Here's an example how do this with Javascript. The reason I'm not showcasing this here, is because it didn't really look that great when I tested it on codesandbox.
In a newer version of chart.js radial scale point label positions were exposed. In the example below I'm using chart.js version 3.2.0 and react-chartjs-2 version 3.0.3.
We can use the bounding box of each label and the clicked position to determine if we've clicked on a label.
I've used a ref on the chart to get access to the label data.
I've chosen to set the data value corresponding to a label to 0. I do this, because if you were to remove an element corresponding to a label, the label would disappear along with it. My choice probably makes more sense if you see it in action in the demo below.
const swapPreviousCurrent = (data) => {
const temp = data.currentValue;
data.currentValue = data.previousValue;
data.previousValue = temp;
};
class Chart extends Component {
constructor(props) {
super(props);
this.state = {
chartData: props.chartData
};
this.radarRef = {};
this.labelsStrikeThrough = props.chartData.datasets.map((dataset) => {
return dataset.data.map((d, dataIndex) => {
return {
data: {
previousValue: 0,
currentValue: d
},
label: {
previousValue: `${props.chartData.labels[dataIndex]} (x)`,
currentValue: props.chartData.labels[dataIndex]
}
};
});
});
}
render() {
return (
<div className="chart">
<Radar
ref={(radarRef) => (this.radarRef = radarRef)}
data={this.state.chartData}
options={{
title: {
display: true,
text: "Testing",
fontSize: 25
},
legend: {
display: true,
position: "right"
}
}}
getElementAtEvent={(element, event) => {
const clickX = event.nativeEvent.offsetX;
const clickY = event.nativeEvent.offsetY;
const scale = this.radarRef.scales.r;
const pointLabelItems = scale._pointLabelItems;
pointLabelItems.forEach((pointLabelItem, index) => {
if (
clickX >= pointLabelItem.left &&
clickX <= pointLabelItem.right &&
clickY >= pointLabelItem.top &&
clickY <= pointLabelItem.bottom
) {
// We've clicked inside the bounding box, swap labels and label data for each dataset
this.radarRef.data.datasets.forEach((dataset, datasetIndex) => {
swapPreviousCurrent(
this.labelsStrikeThrough[datasetIndex][index].data
);
swapPreviousCurrent(
this.labelsStrikeThrough[datasetIndex][index].label
);
this.radarRef.data.datasets[datasetIndex].data[
index
] = this.labelsStrikeThrough[datasetIndex][
index
].data.previousValue;
this.radarRef.data.labels[index] = this.labelsStrikeThrough[
datasetIndex
][index].label.previousValue;
});
// labels and data have been changed, update the graph
this.radarRef.update();
}
});
}}
/>
</div>
);
}
}
So I use the ref on the chart to get acces to the label positions and I use the event of getElementAtEvent to get the clicked x and y positions using event.nativeEvent.offsetX and event.nativeEvent.offsetY.
When we've clicked on the label I've chosen to update the value of the ref and swap the label data value between 0 and its actual value. I swap the label itself between itself and itself concatenated with '(x)'.
sandbox example
The reason I'm not using state here is because I don't want to rerender the chart when the label data updates.
You could run a function that modifies your dataset:
You would create the function where you have your data set
chartClick(index) {
console.log(index);
//this.setState({}) Modify your datasets properties
}
Pass the function as props
<Chart chartClick={this.chartClick} chartData={this.state.chartData} />
Execute the function when clicked
onClick: (e, element) => {
if (element.length) {
this.props.chartClick(element[0]._datasetIndex);
}
}

LightningChart JS crashes with t.toFixed is not a function

I am using a LightningChart JS by Arction to plot a bar graph and it keeps crashing after adding the rectangle figures with an error message: t.toFixed is not a function
The series being used is a rectangle series and I'd like to use only one rectangle series because I need them all under one group.
Below is my code
// Core react imports
import React, { useEffect, useCallback } from 'react'
// React bootstrap imports
import { Col } from "react-bootstrap"
// Chart imports
import {
lightningChart,
SolidFill,
SolidLine,
emptyTick,
emptyLine,
FontSettings,
ColorHEX,
} from "#arction/lcjs"
import axios from "axios"
export default function Histogram() {
const createChart = useCallback(
() => {
const chart = lightningChart().ChartXY({ containerId: "myplot" });
chart
.setTitle("RR Histogram")
.setTitleFillStyle((solidFill) => solidFill.setColor(ColorHEX("#000")))
.setTitleMarginTop(0)
.setTitleMarginBottom(0)
.setChartBackgroundFillStyle((solidFill) =>
solidFill.setColor(ColorHEX("#FFF"))
)
.setBackgroundFillStyle((solidFill) =>
solidFill.setColor(ColorHEX("#FFF"))
)
.setZoomingRectangleStrokeStyle(
new SolidLine({
fillStyle: new SolidFill({ color: ColorHEX("#000") }),
})
)
.setTitleFont(new FontSettings({ size: 20 }));
// Configure X-axis of chart to be progressive and have nice interval.
chart
.getDefaultAxisX();
// .setTickStyle(emptyTick)
// .setNibStyle(emptyLine)
// .setTitleFont(new FontSettings({ size: 12 }))
// .setStrokeStyle(emptyLine);
chart
.getDefaultAxisY();
// .setTickStyle(emptyTick)
// .setNibStyle(emptyLine)
// .setStrokeStyle(emptyLine);
let rectSeries = chart
.addRectangleSeries()
.setDefaultStyle(figure => figure.setFillStyle(new SolidFill({
color: ColorHEX("#000")
})));
let rr_hist = {};
axios
.get("Api url here")
.then((res) => {
console.log(res)
rr_hist = res.data;
})
.catch((err) => console.log(err));
setTimeout(() => {
for (let point in rr_hist) {
let insert_Point = {
height: rr_hist[point],
y: 0,
x: point,
width: 1
}
let bar = rectSeries.add(insert_Point);
bar.setDimensions(insert_Point);
bar.setFillStyle(new SolidFill({ color: ColorHEX("#000") }));
bar.setStrokeStyle(new SolidLine({
fillStyle: new SolidFill({ color: ColorHEX("#000") }),
}))
}
console.log(rr_hist)
}, 2000)
},
[],
)
useEffect(() => {
createChart()
}, [createChart])
return (
<Col xs={12} style={{ height: "100%", width: "100%" }}>
<div id="myplot" style={{ height: "100%", width: "100%" }}></div>
</Col>
)
}
Also could you please let me know how to improve the styling?
Most likely reason for the crash is that your height or x field for the new rectangle figure is not a number. LightningChart JS doesn't do type conversions for input values.
So when adding new rectangles to rectangle series make sure to do the type conversion from string to number yourself.
let insert_Point = {
height: Number(rr_hist[point]),
y: 0,
x: Number(point),
width: 1
}
let bar = rectSeries.add(insert_Point);
Instead of using Number for the conversion you could use parseFloat or parseInt depending on the type of data you use. See https://stackoverflow.com/a/13676265/6198227 that answer for more detailed differences between Number and parseFloat.
For styling, it looks like you would benefit from using a light colored theme. When creating the chart with ChartXY you can specify theme option.
const chart = lightningChart().ChartXY({
theme: Themes.light
})
You can see the available themes in our docs Themes

Accessing a slice of a react-chartjs Pie chart

I'm trying to create a static (non-clickable) Pie chart using react-chartjs-2.
However I want one of the slices to "pop out", or appear bigger than the others:
Hence, I'm trying to access one of the slices in the pie chart and modify its outerRadius property.
I've encountered multiple similar questions both in Stack Overflow and in Github, which helped me come up with this:
import { Pie } from 'react-chartjs-2';
<Pie
data={data}
options={options}
getElementsAtEvent={(elems) => {
// Modify the size of the clicked slice
elems[0]['_model'].outerRadius = 100;
}}
/>
However I didn't find anything about getting a slice to pop out without the user clicking it.
After looking under Pie component's hood, I ended up finding the answer.You can find it inside componentDidMount():
import React, { Component } from 'react';
import { Pie } from 'react-chartjs-2';
class PieChart extends Component {
componentDidMount() {
const change = {
sliceIndex: 0,
newOuterRadius: 100
}
const meta = this.pie.props.data.datasets[0]._meta;
meta[Object.keys(meta)[0]]
.data[change.sliceIndex]
._model
.outerRadius = change.newOuterRadius;
}
render() {
const data = {
type: 'pie',
datasets: [ { data: [10, 20, 30] } ],
labels: ['a', 'b', 'c'],
};
const options = {};
return <Pie
ref={(self) => this.pie = self}
data={data}
options={options}
/>
}
}
export default PieChart;
I have one more solution with version 4.3.1.
Try to add offset property into datasets.
import { Pie } from 'react-chartjs-2';
<Pie
data={
datasets: [
{
data: [1, 1],
borderColor: ['black', 'transparent'],
offset: [10, 0],
},
],
}
/>
It will render Pie with 2 segments. For the first segment, you will have black border and 10px offset

Categories

Resources