Following is my code, I hope make tooltip follow mousemove like echart(render select svg) tooltip
but the result transition become stuck, how to make transition smooth
var it: SVGTextElement
var c: SVGGElement
ReactDOM.createRoot(document.querySelector("#root")).render(
<svg>
<g ref={e => c = e}
onMouseMove={(ev: any) => {
var r = c.getBoundingClientRect()
var x = ev.clientX - r.x
var y = ev.clientY - r.y
it.setAttribute("transform", `translate(${x} ${y})`)
}}>
<rect x={0} y={0} width={300} height={300} fill={"transparent"} stroke={"black"}></rect>
<text ref={e => it = e} style={{transition: "all 0.5s ease"}}>tooltip</text>
</g>
</svg>)
The mouse move handler should be attached to the SVG, not the tooltip. As it is now, if you move the mouse quickly enough to escape the tooltip, the event will no longer fire. And the tooltip will stop and appear to get stuck.
I find transition "ease" has a little stuck, use "linear" instead will make move smooth, following is my correct code
var it: SVGTextElement
var c: SVGGElement
ReactDOM.createRoot(document.querySelector("#root")).render(
<svg width={1000} height={500}>
<g ref={e => c = e}
onMouseMove={(ev: any) => {
var r = c.getBoundingClientRect()
var x = ev.clientX - r.x
var y = ev.clientY - r.y
it.setAttribute("transform", `translate(${x} ${y})`)
}}>
<rect x={0} y={0} width={1000} height={500} fill={"transparent"} stroke={"black"}></rect>
<text ref={e => it = e} style={{transition: "all 0.5s linear"}}>tooltip</text>
</g>
</svg>
)
Related
I used the Pie component from Recharts js and the problem is that I get labels overlapping for labels with the same value.
here is some of my code:
<PieChart>
<Pie dataKey="value"
data={data}
fill="#536A6D"
label nameKey="name"
>
<LabelList dataKey="name" position="insideStart" />
<Pie>
</PieChart>
Is it possible to arrange the labels so that they do not collide with each other?
Thank you in advance!
Yes, you will have to conditionally render the labelline and label attribute. In my case only the zero values overlap so when the value is zero I do not render the value. Other examples online will help with the custom label but there is nothing over the little label line left over, I had this problem and had to dig through the source code to come up with the custom code /:
<Pie
data={dataZ}
cx={150 + wid - pad / 2}
cy={150}
innerRadius={70 + scaler}
outerRadius={100 + scaler}
fill="#8884d8"
paddingAngle={1}
dataKey="value"
label={RenderLabel2}
labelLine={RenderCustomizedLabelLine}
>
let RenderCustomizedLabelLine = function (props: any) {
return (props.value != 0 ? <path stroke={props.stroke} d={`M${props.points[0].x},${props.points[0].y}L${props.points[1].x},${props.points[1].y}`} className="customized-label-line" /> : <polyline stroke={props.stroke} fill="none" />)
}
let RenderLabel2 = function (props: any) {
const RADIAN = Math.PI / 180;
const radius = 25 + props.innerRadius + (props.outerRadius - props.innerRadius);
const x = props.cx + radius * Math.cos(-props.midAngle * RADIAN);
const y = props.cy + radius * Math.sin(-props.midAngle * RADIAN);
return (props.value != 0 ? <text
className="recharts-text recharts-pie-label-text"
x={x}
y={y}
fontSize='16'
fontFamily='sans-serif'
dominantBaseline="central"
cy={props.cy}
cx={props.cx}
fill="#666"
textAnchor={props.x > props.cx ? 'start' : 'end'}
>{Number.isInteger(props.value) ? Number(props.value) : Number(props.value).toFixed(1)}%</text> : <g>
<text x={500} y={y} fill="#transparent" rotate="90"></text>
</g>)
}
I want to move an animated gif along an svg path on scroll, I've been trying to adapt Text moving along an SVG <textPath> but it's not working. I'd like to know what the best solution is.
<svg id="text-container" viewBox="0 0 1000 194" xmlns="http://www.w3.org/2000/svg">
<path id="text-curve" d="M0 100s269.931 86.612 520 0c250.069-86.612 480 0 480 0" fill="none"/>
<text y="40" font-size="2.1em">
<textPath id="text-path" href="#text-curve">
<img src="../imagesIndex/originals/dragon.gif" height="194px"/>
</textPath>
</text>
</svg>
I can get Text moving along the SVG curve but not the image. I've tried expanding the SVG viewbox, shrinking the image with the defined height above, I've tried changing the SVG <textPath to <path it didn't work. I'm getting nowhere.
The image appears, but it won't move along the SVG's path.
Here's the Javascript
<script>
console.clear();
var textPath = document.querySelector('#text-path');
var textContainer = document.querySelector('#text-container');
var path = document.querySelector( textPath.getAttribute('href') );
var pathLength = path.getTotalLength();
console.log(pathLength);
function updateTextPathOffset(offset){
textPath.setAttribute('startOffset', offset);
}
updateTextPathOffset(pathLength);
function onScroll(){
requestAnimationFrame(function(){
var rect = textContainer.getBoundingClientRect();
var scrollPercent = rect.y / window.innerHeight;
console.log(scrollPercent);
updateTextPathOffset( scrollPercent * 2 * pathLength );
});
}
window.addEventListener('scroll',onScroll);
</script>
Apologies if this question is a duplicate. I do have a Greensock GSAP, ShockinglyGreen subscription, all libraries available, but I'm yet to dig into it.
Here's some sample code to position an SVG <image> element at a position along a path determined by the page scroll.
var path = document.querySelector('#text-curve');
var cat = document.querySelector('#cat');
var catWidth = 40;
var catHeight = 40;
function updateImagePosition(offset) {
let pt = path.getPointAtLength(offset * path.getTotalLength());
cat.setAttribute("x", pt.x - catWidth/2);
cat.setAttribute("y", pt.y - catHeight/2);
}
// From: https://stackoverflow.com/questions/2387136/cross-browser-method-to-determine-vertical-scroll-percentage-in-javascript
function getScrollFraction() {
var h = document.documentElement,
b = document.body,
st = 'scrollTop',
sh = 'scrollHeight';
return (h[st]||b[st]) / ((h[sh]||b[sh]) - h.clientHeight);
}
function onScroll() {
updateImagePosition( getScrollFraction() );
}
updateImagePosition(0);
window.addEventListener('scroll', onScroll);
body {
min-height: 1000px;
}
svg {
display: block;
position: sticky;
top: 20px;
}
<svg id="text-container" viewBox="0 0 1000 194">
<path id="text-curve" d="M0 100s269.931 86.612 520 0c250.069-86.612 480 0 480 0" fill="none" stroke="gold"/>
<image id="cat" x="0" y="100" xlink:href="https://placekitten.com/40/40"/>
</svg>
I want the dot to follow mouse cursor, e.g. on click.
Code seems simple, but with every click the dot runs shorter distance and doesn't reach the target.
The question is why?
The code is here:
https://jsfiddle.net/thiefunny/ny0chx3q/3/
HTML
<circle r="10" cx="300" cy="300" />
JavaScript
const circle = document.querySelector("circle")
window.addEventListener("click", mouse => {
const animation = _ => {
let getCx = Number(circle.getAttribute('cx'))
let getCy = Number(circle.getAttribute('cy'))
circle.setAttribute("cx", `${getCx + (mouse.clientX - getCx)/10}`);
circle.setAttribute("cy", `${getCy + (mouse.clientY - getCy)/10}`);
requestAnimationFrame(animation)
}
requestAnimationFrame(animation)
});
EDIT: for this task I need requestAnimationFrame(), not CSS, because this is just the simplest example, but I want to add much more complexity later to the movement, including multiple dots, random parametres etc., like I did here: https://walanus.pl
I spent lots of time experimenting, but the only conclusion I have is that after click event I should somehow cancel the current animation and start new one to make a fresh start for next animation.
you don't need requestAnimationFrame for this:
const circle = document.querySelector("circle")
window.addEventListener("click", e => {
let targetCircleX = e.clientX;
let targetCircleY = e.clientY;
let getCx = Number(circle.getAttribute('cx'))
let getCy = Number(circle.getAttribute('cy'))
let cx = targetCircleX - getCx;
let cy = targetCircleY - getCy;
circle.style.transform = `translate3d(${cx}px, ${cy}px, 0)`;
});
circle {
transition-duration: .2s;
}
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500">
<circle r="10" cx="100" cy="100" />
</svg>
EDIT: CSS animations are an easy yet powerful method to animate things in web, but manual control over the animation, done properly, always require more work, i.e. performant loops, proper timings, etc. (by the way, the mentioned site doesn't bother with these). So, for fullness of answer, a variant with requestAnimationFrame is below
const circle = document.querySelector("circle");
const fps = 60;
const delay = 1000 / fps;
let rafId;
window.addEventListener("click", e => {
cancelAnimationFrame(rafId);
let [time, cx, cy, xf, yf] = [0];
let r = +circle.getAttribute('r');
let [X, x] = [e.clientX - r, +circle.getAttribute('cx')];
let [Y, y] = [e.clientY - r, +circle.getAttribute('cy')];
const decel = 10;
const anim = now => {
const delta = now - time;
if (delta > delay) {
time = now - (delta % delay);
[x, y] = [x + (X - x) / decel, y + (Y - y) / decel];
[xf, yf] = [x.toFixed(1), y.toFixed(1)];
if (cx === xf && cy === yf)
return;
circle.setAttribute("cx", cx = xf);
circle.setAttribute("cy", cy = yf);
}
rafId = requestAnimationFrame(anim);
}
anim(time);
});
<svg version="1.1" baseProfile="full" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="500" height="500" style="background: black">
<circle r="10" cx="100" cy="100" fill="red"/>
</svg>
The question is why
Well, you seem to know why: you never stop your animation loop, so at every frame it will try to go to the mouse.clientN position of when that animation loop started. Then at the next click, a second animation loop will start, running in parallel of the first one, and both will fight against each other to go toward their own mouse.clientN position.
To avoid this situation, as you have identified, you can simply stop the previous loop by using cancelAnimationFrame. All it takes is a variable accessible both to the animation scope and to the click handler.
However, keeping your animation loop going on is just killing trees. So make your code check if it has reached the target position before calling again requestAnimationFrame from inside animation.
const circle = document.querySelector("circle")
{
let anim_id; // to be able to cancel the animation loop
window.addEventListener("click", mouse => {
const animation = _ => {
const getCx = Number(circle.getAttribute('cx'))
const getCy = Number(circle.getAttribute('cy'))
const setCx = getCx + (mouse.clientX - getCx)/10;
const setCy = getCy + (mouse.clientY - getCy)/10;
circle.setAttribute("cx", setCx);
circle.setAttribute("cy", setCy);
// only if we didn't reach the target
if(
Math.floor( setCx ) !== mouse.x &&
Math.floor( setCy ) !== mouse.y
) {
// continue this loop
anim_id = requestAnimationFrame(animation);
}
}
// clear any previous animation loop
cancelAnimationFrame( anim_id );
anim_id = requestAnimationFrame(animation)
});
}
svg { border: 1px solid }
<svg viewBox="0 0 500 500" width="500" height="500">
<circle r="10" cx="100" cy="100" />
</svg>
Also, beware that your animation will run twice faster on devices with a 120Hz monitor than the ones with a 60Hz monitor, and even faster on a 240Hz. To avoid that, use a delta time.
I'm building a circular progress bar as a react component with SVG elements.
Desired output:
Instead, I get it like this
My question is, how to rotate the score/text in the yellow circle to match the first image?
React component
const Gauge = ({ width, height, thick, progress, score }) => {
let angle = (progress / 100 * 360);
let progressValue = angle / 2;
let ticks = [];
for (let i = 1; i <= 180; i++) {
let tickClass = (progressValue > i) ? styles.tickActive : styles.tick;
ticks.push(<use key={i} className={tickClass} href="#tick" transform={`rotate(${i * 2} 60 60)`}> </use>)
}
ticks.push(<use key={0} className={styles.tickActive } href="#tick" transform={`rotate(0 60 60)`}> </use>);
return (
<div className={ styles.svgContainer }>
<svg className={styles.progress} width={`${width}`} height={`${height}`} viewBox="-20 -20 160 160">
<defs>
<line id="tick" x1="-5" y1="60" x2={`${thick}`} y2="60"> </line>
</defs>
<g id="ticks">
{ticks}
</g>
<g transform={`rotate(${progressValue * 2} 60 60)`}>
<line className={styles.scoreCircle} x1="0" y1="60" x2="0" y2="60"> </line>
<text className={styles.score2} fontSize={8} x={-10} y={63}>
{score}K
</text>
</g>
</svg >
</div>
);
};
In the code below, I am rotating a selection box (parent) with two children in SVG.
It works fine, however, when the parent (selection box) is removed, the children go back to their original (pre-rotation) co-ordinates.
How can I apply the updated co-ordinates on the children once the parent is removed. I specifically need to maintain the new position of the children in X,Y co-oridinates, i.e. the rotate should be converted to translate, r.g. transform = translate (X , Y) . I only need New x,y for children so that i can 'translate' them to new location.
here is the fiddle link http://jsfiddle.net/rehankhalid/QK8L8/6/
HTML CODE:-
<button data-action="rotate" onclick="rotateMainRect()">Rotate +5 Angle</button>
<button data-action="rotate" onclick="removeRectRotation()">Remove Selection</button>
<br/>
<svg id="mainSvg" xmlns="http://www.w3.org/2000/svg" version="1.1" width="600" height="500">
<g id="selectedRect">
<rect id="rectangle" x="135" y="135" width="110" height="35" stroke="red" stroke-width="2" fill="grey" opacity="0.4" />
<g id="button_1" transform="translate(0,0)">
<circle cx="150" cy="150" r="5" stroke="grey" stroke-width="1" fill="none" />
</g>
<g id="button_2" transform="translate(0,0)">
<circle cx="230" cy="150" r="5" stroke="grey" stroke-width="1" fill="none" />
</g>
</g>
</svg>
JAVASCRIPT CODE
var angle_incr = 5;
var angle = 0;
function rotateMainRect() {
var selectedRect = document.getElementById('selectedRect');
var rectangle = document.getElementById('rectangle');
var x = rectangle.getAttribute('x');
if (x != 0) {
var centxy = calculateCenterXY(selectedRect);
angle += angle_incr;
selectedRect.setAttributeNS(null, 'transform', 'rotate(' + angle + ',' + centxy.x + ',' + centxy.y + ')');
} else {
rectangle.setAttribute('x', '135');
rectangle.setAttribute('y', '135');
rectangle.setAttribute('width', '110');
rectangle.setAttribute('height', '35');
}
}
function calculateCenterXY(node) {
var x = node.getBBox().x + (node.getBBox().width / 2);
var y = node.getBBox().y + (node.getBBox().height / 2);
var xy_co = {
x: x,
y: y
};
return xy_co;
}
function removeRectRotation() {
var selectedRect = document.getElementById('selectedRect');
selectedRect.setAttributeNS(null, 'transform', '');
var rectangle = document.getElementById('rectangle');
rectangle.setAttribute('x', '0');
rectangle.setAttribute('y', '0');
rectangle.setAttribute('width', '0');
rectangle.setAttribute('height', '0');
angle = 0;
}
- What i Want:-
First Rotate the selection rectangle to some angle, and then press 'Remove selection'. After Removing the selection, Childrens must be placed at the New postion. (which now, move back to the original position)
If you are asking if you can read the absolute positions of the two transformed circles directly using JS, then the answer is no.
You will need to calculate their positions yourself using a bit of trigonometry.