Grouping and centering svg's with react - reactjs

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 :)

Related

make SVG document responsive with the viewbox based on aspect ratio

I have created this codesandbox to illustrate the problem.
I have this ResponsiveSVG component:
export function ResponsiveSVG({
height,
width,
children,
origin = { x: 0, y: 0 },
preserveAspectRatio = "xMidYMid meet",
innerRef,
className,
...props
}: ResponsiveSVGProps): JSX.Element {
const aspect = height === 0 ? 1 : width / height;
const adjustedHeight = Math.ceil(width / aspect);
return (
<div
data-testid="cutting-svg-container"
style={{
position: 'relative',
overflow: 'visible',
height: '1px',
}}
>
<svg
style={{ overflow: 'visible' }}
className={className}
preserveAspectRatio={preserveAspectRatio}
width={width}
height={adjustedHeight}
viewBox={`${origin.x} ${origin.y} ${width} ${height}`}
ref={innerRef}
{...props}
>
{children}
</svg>
</div>
);
}
I would like my ResponsiveSVG component to know how to fit its content to the full width of the container while keeping the aspect ratio the same.
In the example I have a circle as a child of the svg document:
<ResponsiveSVG width={width} height={height}>
<circle cx={width / 2} cy={height / 2} r={radius} />
</ResponsiveSVG>
On desktop, it looks like this:
But in mobile view, it looks like this:
My calculations make the viewBox width and height the same as the actual width and height I am passing in, so no change happens.
The viewBox coordinates are exactly the same as the viewport.
Passing the width and height of the containing element to the viewBox is actually the wrong direction. viewBox defines the canvas on which the svg content elements are drawn: if you draw a circle with r="50", the viewBox width value must be at least 100, otherwise, it will never fit. If the center is at cx=50, the viewBox x value must not be greater than 0, otherwise the left side is cut off.
Responsiveness in SVG is achieved because there needs not to be any relation between the viewBox values and the width and height of the <svg> element. The canvas the child elements are drawn on is always fitted to the parent element dimensions.
In short, if all you want to achieve is to fit a circle in a <svg> element, choose a arbitrary radius r, set cx=cy=r and viewBox="0 0 2r 2r", and it will work. There is no need to know anything explicit about the parent size.
If you want your <svg> element to be the width of the container, set it so: width: 100%. The default height is auto, so no need to write that or preserveAspectRatio. It will size itself such that the canvas defined by viewBox fits itself.
<div
style="
position: relative;
overflow: visible;
height: 1px;
"
>
<svg
style="
width: 100%;
overflow: visible;
"
viewBox="0 0 100 100"
>
<circle cx="50" cy="50" r="50" />
</svg>
</div>

React crop image to specific cropped dimensions

I am trying to crop an image in react by using the react-image-crop library, and I got the cropping functionality working.
import React, { useCallback, useRef, useState } from "react";
import ReactCrop, { Crop } from "react-image-crop";
import "react-image-crop/dist/ReactCrop.css";
export const ImageCropper = () => {
const [upImg, setUpImg] = useState<string>(
"https://www.vintagemovieposters.co.uk/wp-content/uploads/2020/04/IMG_5274-scaled.jpeg"
);
const imgRef = useRef<HTMLImageElement | null>(null);
const [crop, setCrop] = useState<Partial<Crop>>({
unit: "%",
aspect: 0.68,
height: 100
});
const onLoad: (image: HTMLImageElement) => boolean | void = useCallback(
(img) => {
imgRef.current = img;
const aspect = 0.68;
const width =
img.width / aspect < img.height * aspect
? 100
: ((img.height * aspect) / img.width) * 100;
const height =
img.width / aspect > img.height * aspect
? 100
: (img.width / aspect / img.height) * 100;
const y = (100 - height) / 2;
const x = (100 - width) / 2;
setCrop({
unit: "%",
width,
height,
x,
y,
aspect
});
},
[]
);
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<ReactCrop
src={upImg}
onImageLoaded={onLoad}
crop={crop}
onChange={(crop, percentageCrop) => {
setCrop(percentageCrop);
}}
keepSelection={true}
/>
<div
style={{
width: imgRef.current?.width! * (crop.width! / 100),
height: imgRef.current?.height! * (crop.height! / 100),
overflow: "hidden"
}}
>
<img
alt="cropped_image"
src={upImg}
style={{
width: imgRef.current?.width!,
height: imgRef.current?.height!,
transform: `translate(-${
(crop.x! / 100) * imgRef.current?.width!
}px, -${(crop.y! / 100) * imgRef.current?.height!}px )`
}}
/>
</div>
</div>
);
};
However, what I am trying to achieve is:
keep the original image after cropping
put the image in a preview div with specific dimensions (235px x 346px)
transform the image to fit within that preview div with the same defined crop
make sure the preview div matches with the highlighted crop
what I tried is the code above, but the issue with it is that the width + height change dynamically.
I tried to use fixed width and height values, but the cropping is off.
I also tried using the scale property in transform, but it was off too:
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<ReactCrop
src={upImg}
onImageLoaded={onLoad}
crop={crop}
onChange={(crop, percentageCrop) => {
console.log("percent", percentageCrop);
setCrop(percentageCrop);
}}
keepSelection={true}
/>
<div
style={{
width: 235,
height: 346,
overflow: "hidden"
}}
>
<img
alt="cropped_image"
src={upImg}
style={{
width: imgRef.current?.width!,
height: imgRef.current?.height!,
transform: `translate(-${
(crop.x! / 100) * imgRef.current?.width!
}px, -${(crop.y! / 100) * imgRef.current?.height!}px) scale(${
crop.width/100
}, ${crop.height/100})`
}}
/>
</div>
</div>
);
I need to figure out how to constrain them to (235px x 346px), and "zoom" the image to match the crop from react-image-crop.
How can I do that?
Example in code sandbox

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

Change <svg:path/> Opacity on MouseOver

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>

SVG Image viewbox resizing

I have this code that you can run below in the snippet. My issue is that in my production environment I have 2 SVGs, one on top of the other, each containing all sorts of content. When the user clicks on either side of the header, as seen in the snippet, the SVG that is on top should grow or shrink to either show more or less of the SVG underneath (I know this isn't perhaps performant with regards to the SVG potentially being drawn beneath - but my question likely involves this, as the answer probably has something to do with a lack of understanding of SVG viewbox on my part).
The issue is that because I want the images contents to remain the same size as the SVG on top grows or shrinks I decided to use Greensock in order to animate the grow/shrink of both the width AND the viewbox. This though causes very nasty glitching - in the snippet, run the code and click the Black box (pay attention especially to the bottom left corner of the image, but also to the jumps in the leftmost circle)!
Am I mistaken in changing both? Is there a way to only alter the width? Would this still cause the glitch as seen below?
Appreciate your help!
class Header extends React.Component {
constructor(props) {
super(props);
this.leftPanel = null;
this.leftPanelTween = null;
this.rightPanel = null;
this.rightPanelTween = null;
}
slideLeftHandler = () => {
this.leftPanelTween = TweenMax.to(this.leftPanel, 2, {width: '100%', attr: {viewBox: "0 0 500 150"}});
};
slideRightHandler = () => {
this.rightPanelTween = TweenMax.to(this.leftPanel, 2, {width: 0, attr: {viewBox: "0 0 0 150"}});
};
render() {
return (
<div class="Header">
<svg width="100%" viewBox="0 0 500 150" ref={el => this.rightPanel = el} onClick={this.slideRightHandler}>
<rect id = "middle" width="100%" height="100%" fill="black">
</rect>
<circle cx="400" cy="75" r="25" fill="red">
</circle>
<circle cx="100" cy="75" r="25" fill="red">
</circle>
</svg>
<svg width="50%" viewBox="0 0 250 150" ref={el => this.leftPanel = el} onClick={this.slideLeftHandler}>
<rect id = "middle" width="100%" height="100%" fill="red">
</rect>
<circle cx="100" cy="75" r="25" fill="black">
</circle>
<circle cx="400" cy="75" r="25" fill="black">
</circle>
</svg>
</div>
);
}
}
ReactDOM.render(
<Header />,
document.getElementById('app')
);
.Header {
margin: 0;
cursor: pointer;
}
.Header svg {
position: absolute;
}
.Header .leftPanel {
top: 0;
left: 0;
}
.Header .rightPanel {
top: 0;
right: 0;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/1.20.2/TweenMax.min.js"></script>
<div id="app"></div>
Animating the viewBox is not performant. I recommend making the SVG as large as it needs to be at the end all of the time and just animate the width of the rectangle within the SVG that was clicked.

Resources