Change <svg:path/> Opacity on MouseOver - reactjs

I'm trying to create an effect where if a user mouses over a line chart the parts of the svg:path elements that are to the right of the mouse are faded out while the parts of the svg:path element to the left remain at full opacity.
I've tried a few options to no avail - see below.
My first try was to use a path with mask which does change the opacity, but the rest of the lines are hidden because they are not under the mask.
<defs>
<mask
id='mask-for-line'
maskUnits="userSpaceOnUse"
maskContentUnits="userSpaceOnUse"
>
<rect style={{opacity: .5, stroke: 'none', fill: 'white'}}
x={x}
y={y}
width={width}
height={height}
/>
</mask>
</defs>
<path mask='url(#mask-for-line)' ... />
My second try was to put an svg:rect over the faded-out section, but that doesn't work either.
<rect x={x} y={0} width={width} height={height}
style={{opacity: .1, stroke: 'none', fill: 'lightgray'}}/>

Thanks to the inspiration from michael-rovinsky I was able to solve the problem. Within the mask, I have one <rect/> at full opacity covering the left-side of the chart and a second <rect/> at 25% opacity covering the right-side of the chart.
<defs>
<mask
id='mask-for-line'
maskUnits="userSpaceOnUse"
maskContentUnits="userSpaceOnUse"
>
<rect style={{fillOpacity: .25, fill: 'white'}}
x={x}
y={y}
width={width - x}
height={height}
/>
<rect style={{fillOpacity: 1, fill: 'white'}}
width={x}
height={height}
/>
</mask>
</defs>

You can try linear gradient with variable stop offsets:
const svg = d3.select('svg');
const width = parseInt(svg.attr('width'));
const height = parseInt(svg.attr('height'));
console.log(width, height);
const colors = ['red', 'green', 'blue', 'orange', 'purple', 'brown'];
const defs = svg.append('defs');
colors.forEach(color => {
const grad = defs.append('linearGradient').attr('id', `${color}-opacity-mask`);
grad.append('stop').attr('offset', '0%').attr('stop-color', color).attr('stop-opacity', 1);
grad.append('stop').attr('stop-color', color).attr('stop-opacity', 1).classed('mid-stop', true);
grad.append('stop').attr('stop-color', color).attr('stop-opacity', 0.25).classed('mid-stop', true);
grad.append('stop').attr('offset', '100%').attr('stop-color', color).attr('stop-opacity', 0.25);
})
const step = 100;
const paths = colors.map(color => {
let path = '';
for (let index = 0; index <= width / step; index++)
if (!index)
path = `M 0,${Math.random() * height}`;
else
path += `L ${index * step},${Math.random() * height}`;
return {color, path};
});
paths.forEach(({path, color}) => svg.append('path').attr('d', path).style('stroke', `url(#${color}-opacity-mask)`).style('fill', 'none'));
const line = svg.append('line')
.attr('y1', 0)
.attr('y2', height)
.style('stroke', 'black')
.style('stroke-dasharray', '3 3')
.style('visibility', 'hidden');
svg.on('mousemove', e => {
const pct = Math.round(100 * e.layerX / width);
svg.selectAll('.mid-stop').attr('offset', `${pct}%`);
line.attr('x1', e.layerX).attr('x2', e.layerX).style('visibility', 'visible');
});
svg.on('mouseleave', e => line.style('visibility', 'hidden'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<svg width="500" height="200">
</svg>

Related

Best way to add conic gradient to the circle in SVG

I have a donut-like circle (progress circle) in React component and I want to add conic gradient to it. How to do that?
I know that in SVG we can not use conic gradients. I thought it can be done by using mask and usual block with added css with gradient but not sure how to do it correctly.
Now it looks like this:
React component:
import React from 'react';
import { Box, Text } from '#chakra-ui/react';
const GradientProgress = ({ modifier, score, size, strokeWidth }) => {
const DIAMETER = 51;
const WIDTH = DIAMETER + strokeWidth;
const RADIUS = DIAMETER / 2;
const CIRC = 2 * Math.PI * RADIUS;
const foregroundCirc = (CIRC * score) / 100;
const frontCirc = (CIRC * modifier) / 100;
return (
<Box
position='relative'
style={{ width: `${size}px`, height: `${size}px` }}
sx={{
circle: {
background:
'conic-gradient(from 270deg, #ff4800 10%, #dfd902 35%, #20dc68, #0092f4, #da54d8 72% 75%, #ff4800 95%)',
},
}}
>
<svg
className='donut'
transform='rotate(-90)'
viewBox={`0 0 ${WIDTH} ${WIDTH}`}
>
<circle
className='donut-ring'
cx={RADIUS + strokeWidth / 2}
cy={RADIUS + strokeWidth / 2}
fill='transparent'
pathLength={CIRC}
r={RADIUS}
stroke='#d2d3d4'
strokeWidth={strokeWidth}
/>
<circle
className='donut-segment'
cx={RADIUS + strokeWidth / 2}
cy={RADIUS + strokeWidth / 2}
fill='transparent'
opacity={0.5}
pathLength={CIRC}
r={RADIUS}
stroke='green'
strokeDasharray={`${frontCirc} ${CIRC - frontCirc}`}
strokeDashoffset={0}
strokeLinecap='round'
strokeWidth={strokeWidth}
/>
<circle
className='donut-segment'
cx={RADIUS + strokeWidth / 2}
cy={RADIUS + strokeWidth / 2}
fill='transparent'
pathLength={CIRC}
r={RADIUS}
stroke='red'
strokeDasharray={`${foregroundCirc} ${CIRC - foregroundCirc}`}
strokeDashoffset={0}
strokeLinecap='round'
strokeWidth={strokeWidth}
/>
</svg>
<Text>{modifier || score}</Text>
</Box>
);
};
export default GradientProgress;
You can achieve this by converting your circle to a mask and assign it to a foreignObject, this object contains a div with the conic-gradient style.
Here is an example how it works:
const control = document.getElementById('control');
const circle = document.getElementsByClassName('circle')[0];
const bg = document.getElementsByClassName('bg')[0];
control.addEventListener('input', function(event) {
circle.style.setProperty('--progress', event.target.valueAsNumber);
const deg = (event.target.valueAsNumber/100) * 360;
bg.style.setProperty('background', `conic-gradient(#00bcd4, #ffeb3b ${deg}deg)`);
});
.root {
width: 400px;
height: 400px;
text-align: center;
}
svg {
}
.circle {
stroke: white;
stroke-width: 3;
stroke-linecap: round;
stroke-dasharray: calc((2 * 3.14) * 45);
stroke-dashoffset: calc((2 * 3.14 * 45) * (1 - calc(var(--progress, 50) / 100)));
transform-origin: 50% 50%;
transform: rotate(-87deg);
}
.bg {
background: conic-gradient(#00bcd4, #ffeb3b 180deg);
width: 100%;
height: 100%;
}
<div class='wrap'>
<div class='root'>
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<mask id="mask">
<circle class='circle' cx="50" cy="50" r="45" stroke='white' stroke-width='3' fill='none' />
</mask>
</defs>
<foreignObject x="0" y="0" width="100" height="100" mask="url(#mask)">
<div class='bg'></div>
</foreignObject>
</svg>
<input id="control" type="range" value="60" />
</div>
</div>

Recharts Treemap Tooltip label

When creating a TreeMap with <Tooltip/> how do i get a label in the tooltip?
I'm only getting tooltips like : 5738
In the treemap itself the names are displayed properly.
I have the same behavior when i open example from the rechart docs in codesandbox and add a tooltip.
I played around with a custom tooltip as well but could not get it working.
I had to make a custom tooltip to get this to work.
This will put the name of the cell (the root name) in the tooltip, as well.
const CustomTooltip = ({ active, payload, label }) => {
if (active && payload && payload.length) {
return (
<div className="treemap-custom-tooltip">
<p>{`${payload[0].payload.root.name}`}</p>
<p>{`${payload[0].payload.name} : ${payload[0].value}`}</p>
</div>
);
}
return null;
};
<Treemap
width={400}
height={400}
aspectRatio={4 / 3}
data={formattedData}
dataKey="size"
stroke="#fff"
fill="#8884d8"
>
<Tooltip content={<CustomTooltip />}/>
</Treemap>
<Treemap
data={maxunit}
backgroundColor="rgb(137,141,141)"
dataKey="fundUnit"
nameKey="customerName"
content={<CustomizedContent />}
>
const CustomizedContent = (props) => {
const { depth, x, y, width, height, index, name } = props;
return (
<g>
<rect
x={x}
y={y}
width={width}
height={height}
style={{
fill:
depth < 2
? DEFAULT_COLORS[index % DEFAULT_COLORS.length]
: 'none',
stroke: '#fff',
strokeWidth: 2 / (depth + 1e-10),
strokeOpacity: 1 / (depth + 1e-10),
}}
/>
{depth === 1 ? (
<text
x={x + width / 2}
y={y + height / 2 + 7}
textAnchor="middle"
fill="#fff"
fontSize={14}
>
{name}
</text>
) : null}
{depth === 1 ? (
<text
x={x + 120}
y={y + 18}
fill="#fff"
fontSize={12}
fillOpacity={0.9}
>
{labels[index]}
</text>
) : null}
</g>
);
};
this code works for tree-map inside text

Firefox does not animate when <g> component attributes change

I'm creating some animated React components in SVG. When I run it in Chrome, the animation works, but when I run it in Firefox, it doesn't.
Here is an example:
const [x, setX] = useState(0)
useEffect(() => {
setTimeout(() => {
setX(100)
}, 3000)
}, [])
return (
<svg width={500} height={300}>
<g transform={`translate(${x}, ${0})`} style={{ transition: "3s all" }}>
<rect width={50} height={50} x={0} y={0} fill={'#f00'} />
</g>
</svg>
)
You can see that it works in Chrome, but not in Firefox: https://codesandbox.io/s/gracious-ives-ykmfp
If I remove the transform in g and change direct in x prop of the rect, that way will work in Firefox, but I don't want to do it that way.
Any help?
The transform attribute is not the same as the transform CSS property, even though it can be confusing (and apparently the two will be merged).
Instead of using the attribute, move the transform to the style property:
const [x, setX] = useState(0);
useEffect(() => {
setTimeout(() => {
setX(100);
}, 3000);
}, []);
return (
<svg width={500} height={300}>
<g style={{ transform: `translate(${x}px, 0px)`, transition: 'all 3s' }}>
<rect width={50} height={50} x={0} y={0} fill={'#f00'} />
</g>
</svg>
);
Demo: https://codesandbox.io/s/nifty-snowflake-664rk
Try to move the transform property to the style. It works for me this way in FF.:
<g style={{transform: `translateX(${x}px)`, transition: "3s all" }}>
<rect width={50} height={50} x={0} y={0} fill={"#f00"} />
</g>
https://codesandbox.io/s/musing-cdn-7eqg0

Position SVG path under image with dynamic height

I'm trying to position a custom path underneath images that are being loaded, the images will have the same width but the height will vary.
I'm using react-measure to get width, height of my image in svg to later on use that height to position the next element, but due to presumably aspect ratio, I get wrong values.
<svg viewBox="0 0 1000 2000">
<svg
x="150"
y="200"
>
<Measure
bounds
onResize={ (contentRef) => {
this.setState(prevState => ({ imgDimensions: contentRef.bounds }));
} }
>
{ ({ measureRef }) => (
<svg x="0">
<image className={ classes.img } ref={ measureRef } width="200" xlinkHref={ image } />
</svg>
) }
</Measure>
<rect x="5" y={ this.state.imgDimensions.height } height="2" width={ width } />
</svg>
</svg>
Example values:
imgDimensions:
bottom: 504.6000061035156
height: 176.39999389648438
left: 595
right: 835
top: 328.20001220703125
width: 240
and how it's positioned:
https://imgur.com/a/o4bbFG2
That black line should be exactly where the image ends.
But if I resize the website and refresh it - this is what I get: https://imgur.com/a/n9sxTic

Grouping and centering svg's with react

I am trying to create a progress indicator of sorts to show where in a process someone is inside a react application. The end result should look something like this:
To do this I am starting by creating a component for the circle with step number and label below it using svg's but my problem is that I can't figure out how to center the label text without it being clipped. Here is my code:
import React from 'react';
import colors from '../../stylesheets/colors';
const ProgressNode = (props) => {
const {
width = 4,
radius = 60,
color = colors.DISABLED,
text = '1',
textColor = colors.DISABLED,
fontSize = radius - 5,
label = 'Confirm Location',
} = props;
// subtract stroke width from radius to avoid clipping
const r = radius - (width * 2);
return (
<div style={{ margin: 100, border:'2px solid red'}}>
<svg
height={radius * 4}
width={radius * 4}
>
<svg
height={radius * 2}
width={radius * 2}
cx={radius}
cy={radius}
>
<circle
fill="transparent"
strokeWidth={width}
r={r}
cx={radius}
cy={radius}
stroke={color}
/>
<text
x="50%"
y="50%"
textAnchor="middle"
stroke={textColor}
strokeWidth={width - 1}
fontSize={fontSize}
fill={textColor}
// dy=".3em"
>
<tspan x="50%" dy=".3em">{text}</tspan>
<tspan x="50%" dy="1.8em">{label}</tspan>
</text>
</svg>
</svg>
</div>
);
};
export default ProgressNode;
The problem is that the label text is being clipped
Does anyone know how I can center the label text under the circle and if you have any ideas how to arrange the nodes and lines that would be a bonus :)

Resources