Path centroid calculation - javascript
I'm trying to display an icon in the center of each path element but it doesn't look right
My code simply calculated the center point based on the width & height of the path
const center = {
x: (bbox.x - svg_box.x) + bbox.width / 2,
y: (bbox.y - svg_box.y) + bbox.height / 2,
}
JSFiddle
Can this be improved using a centroid function? Or using d3?
I could not figure out how to find the centroid of an existing path using d3.
Thank you
D3 has two centroid methods: arc.centroid and path.centroid (from d3-geo), and none will work with path elements like you have here.
However, we can use path.centroid for getting the centroids of those paths, but it's quite hacky: you have to create a geoJSON object based on your actual path just to pass that object to path.centroid. Therefore, you'd be better creating your own.
That said, let's see how that approach works. We can iterate over each path, getting its length and setting a dummy geoJSON object:
const pathLength = n[i].getTotalLength();
let index = 0;
const geoJSONObject = {
"type": "Polygon",
"coordinates": [
[]
]
};
Then, we move along the path and populate the geoJSON object (here 400/1237 is just a quick way to calculate the viewport values, you can use a proper matrix if you want)...
while (index < pathLength) {
const point = n[i].getPointAtLength(index);
geoJSONObject.coordinates[0].push([point.x * (400 / 1237), point.y * (400 / 1232)]);
index += precision;
};
...and finally we pass that object to path.centroid:
const centroid = path.centroid(geoJSONObject);
Here's the snippet with that solution:
const controls = d3.select(".controls"),
path = d3.geoPath()
.projection(d3.geoIdentity()),
precision = 100;
d3.selectAll("path").each((_, i, n) => {
const pathLength = n[i].getTotalLength();
let index = 0;
const geoJSONObject = {
"type": "Polygon",
"coordinates": [
[]
]
};
while (index < pathLength) {
const point = n[i].getPointAtLength(index);
geoJSONObject.coordinates[0].push([point.x * (400 / 1237), point.y * (400 / 1232)]);
index += precision;
};
const centroid = path.centroid(geoJSONObject);
controls.append("div")
.style("left", centroid[0] + "px")
.style("top", centroid[1] + "px");
})
.container {
position: relative;
display: inline-flex;
}
path {
outline: 1px solid #0F0;
}
.controls {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.controls>div {
position: absolute;
width: 5px;
height: 5px;
background-color: red;
}
<script src="https://d3js.org/d3.v7.min.js"></script>
<div class="container">
<div class="controls"></div>
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.4.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="400" x="0px" y="0px" viewBox="0 0 1237 1232" style="enable-background:new 0 0 1237 1232;" xml:space="preserve">
<style type="text/css">
.st1 {
fill: none;
stroke: #000000;
stroke-miterlimit: 10;
}
</style>
<g>
<path class="st1" d="M1036.3,1040.8C893.1,896.6,750.5,753,607.8,609.4c0.1-0.4,0.3-0.8,0.4-1.2c3.4,0.4,6.9,0.8,10.3,1.4
c37.9,6.1,75.9,12.3,113.8,18.5c40.4,6.5,80.8,13.1,121.2,19.6c40.2,6.5,80.5,13,120.7,19.5c39.9,6.5,79.8,12.9,119.7,19.4
c36.5,5.9,72.9,11.8,109.4,17.8c1.8,0.3,3.9,1.3,4.8,2.6c0.5,0.7-1,3.2-2.1,4.5c-35.1,42-67.4,86.1-92.8,134.8
c-20.4,39.2-36.2,80.4-51.4,121.9c-8.4,23-16.1,46.2-24.1,69.3C1037.4,1038.2,1037,1039.1,1036.3,1040.8z" />
<path class="st1" d="M604.1,609.4c0.9,5.3,1.9,10.6,2.7,15.9c3.6,23.1,7.2,46.2,10.7,69.3c3.1,20.5,6.2,41,9.4,61.4
c3.5,22.4,7.1,44.9,10.5,67.3c3.2,20.5,6.3,41,9.4,61.4c3.2,20.5,6.4,40.9,9.6,61.4c2.8,18.2,5.6,36.4,8.4,54.6
c3.5,22.6,7,45.2,10.5,67.8c3.2,20.5,6.3,40.9,9.5,61.4c3.1,20.3,6.3,40.6,9.4,60.9c0.7,4.7,1.8,9.5,2.3,14.2
c0.2,1.8,0.2,4.5-0.9,5.5c-0.9,0.8-3.7,0.2-5.3-0.4c-43.3-17.2-87-33.3-131.7-46.7c-31.9-9.5-64.4-14.5-97.8-15.8
c-45-1.8-89.9,0.3-135.9,2.9c92.9-180.7,185.4-360.9,278-541.1C603.4,609.4,603.8,609.4,604.1,609.4z" />
<path class="st1" d="M511.8,1.5c31.1,200.5,62,400.4,93,600.2c-0.3,0.2-0.6,0.4-0.9,0.6c-2.2-2.1-4.5-4.2-6.7-6.3
c-26.4-26.6-52.8-53.3-79.2-79.9c-22.7-22.8-45.4-45.6-68.1-68.4c-49.7-50-99.3-100-149-150c-36.8-37-73.5-74-110.3-111
c-3.8-3.8-7.5-7.5-11.1-11.5c-1.3-1.5-2.1-3.5-3.1-5.2c1.7-0.8,3.4-1.9,5.2-2.3c21.6-4.8,43.3-9,64.7-14.4
c44.9-11.3,89.2-24.6,130.9-44.9c39-19,72.8-45.5,103.9-75.5C491.4,22.9,501.1,12.4,511.8,1.5z" />
<path class="st1" d="M600.9,606.3c-10.5,5.3-21,10.6-31.5,16c-69.1,34.9-138.3,69.8-207.4,104.7c-62.6,31.6-125.2,63.2-187.7,94.9
c-36.4,18.4-72.8,36.9-109.2,55.3c-0.9,0.5-1.7,1.1-2.7,1.3c-1.4,0.4-2.8,0.5-4.2,0.7c-0.2-1.6-0.9-3.2-0.6-4.6
c1.6-9.2,3.8-18.2,4.9-27.5c2.3-20.2,4.6-40.3,5.7-60.6c1.8-30.3,0.1-60.6-4-90.7c-5.7-41.4-15.8-81.6-31.6-120.3
C23.7,554,13,533.3,3.2,512.2c-0.5-1-1-2-2.1-4.3c200.5,32.5,400.1,64.8,599.6,97C600.7,605.4,600.8,605.9,600.9,606.3z" />
<path class="st1" d="M1150.2,329.6c-180,90.5-359.5,180.8-540.4,271.8c1.2-2.8,1.6-4.3,2.3-5.7c30.1-58.7,60.3-117.3,90.4-176
c22.5-43.9,45-87.7,67.5-131.6c37.8-73.6,75.6-147.3,113.5-220.8c0.8-1.6,2.5-2.7,3.8-4.1c1.2,1.5,2.8,2.7,3.7,4.4
c24.3,46.6,51.6,91.3,84.4,132.6c33.1,41.6,74.4,73.3,119.8,99.9c16.9,9.9,34.6,18.6,51.9,27.8
C1148.1,328.4,1148.9,328.9,1150.2,329.6z" />
</g>
</svg>
</div>
Thank you #Gerardo, you gave me the idea to write this code using polylabel which may have a smaller footprint than d3
import polylabel from "#mapbox/polylabel"
function centroid (path: SVGPathElement, resolution = 100): [number, number] {
const poly = []
const step = path.getTotalLength() / resolution
for (let i = 0; i < resolution; i++) {
const point = path.getPointAtLength(i * step)
poly.push([point.x, point.y])
}
return polylabel([poly], 1.0, false)
}
Related
SVGGeometryElement.isPointInFill() does not work with Chromium but works with Firefox
I'm trying to work on a way to detect if a path of an svg file that is within a canvas is clicked on. My code works in Firefox however it does not work on Chromium browsers. I have surrounded the code with try and catch and I receive no errors on Firefox and everything works however on Chromium browsers I receive the error: TypeError: Failed to execute 'isPointInFill' on 'SVGGeometryElement': parameter 1 is not of type 'SVGPoint'. at getIdOfElementAtPoint (main.js:39:27) at HTMLCanvasElement.<anonymous> (main.js:70:9) However on the next piece of code no error is thrown. This is my code: function getIdOfElementAtPoint(event) { var paths = svg.getElementsByTagName("path"); //loop through all the path element in svg for (var i = 0; i < paths.length; i++) { var path = paths[i]; // Check if point (x, y) is inside the fill of the path let inFill = false; try { inFill = path.isPointInFill(new DOMPoint(event.clientX, event.clientY)) } catch(error) { console.log(error) try { const point = svg.createSVGPoint(); // Get the coordinates of the click point.x = event.clientX; point.y = event.clientY; inFill = path.isPointInFill(point) } catch(error) { console.log(error) } } if (inFill) { console.log("The point is inside the element with id: " + path.getAttribute("id")); return path.getAttribute("id"); } } console.log("The point is outside of any element."); }
As commented by #Kaiido: document.elementsFromPoint() or document.elementFromPoint() are probably the better options to get SVG elements at certain cursor positions. However you address several issues: browser support for DOMPoint() used for SVG methods (firefox vs. chromium vs. webkit) translation between HTML DOM coordinates (e.g. via mouse inputs) to SVG user units retrieving all underlying elements at certain coordinates Example snippet /** * static example: is #circleInside within #circle2 */ let isInfill = false; let p1 = { x: circleInside.cx.baseVal.value, y: circleInside.cy.baseVal.value }; let inFill1 = isPointInFill1(circle2, p1); if (inFill1) { circleInside.setAttribute("fill", "green"); } function isPointInFill1(el, p) { let log = []; let point; try { point = new DOMPoint(p.x, p.y); el.isPointInFill(point); log.push("DOMPoint"); } catch { let svg = el.nearestViewportElement; point = svg.createSVGPoint(); [point.x, point.y] = [p.x, p.y]; log.push("SVGPoint"); } console.log(log.join(" ")); let inFill = el.isPointInFill(point); return inFill; } document.addEventListener("mousemove", (e) => { let cursorPos = { x: e.clientX, y: e.clientY }; // move svg cursor let svgCursor = screenToSVG(svg, cursorPos); cursor.setAttribute("cx", svgCursor.x); cursor.setAttribute("cy", svgCursor.y); /** * pretty nuts: * we're just testing the reversal of svg units to HTML DOM units * just use the initial: e.clientX, e.clientY */ // move html cursor let domCursorPos = SVGToScreen(svg, svgCursor); cursorDOM.style.left = domCursorPos.x + "px"; cursorDOM.style.top = domCursorPos.y + "px"; // highlight let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y); let log = ['elementsFromPoint: ']; elsInPoint.forEach((el) => { if (el instanceof SVGGeometryElement && el!==cursor) { log.push(el.id); } }); result.textContent = log.join(" | "); }); /** * helper function to translate between * svg and HTML DOM coordinates: * based on #Paul LeBeau's anser to * "How to convert svg element coordinates to screen coordinates?" * https://stackoverflow.com/questions/48343436/how-to-convert-svg-element-coordinates-to-screen-coordinates/48354404#48354404 */ function screenToSVG(svg, p) { let pSvg = svg.createSVGPoint(); pSvg.x = p.x; pSvg.y = p.y; return pSvg.matrixTransform(svg.getScreenCTM().inverse()); } function SVGToScreen(svg, pSvg) { let p = svg.createSVGPoint(); p.x = pSvg.x; p.y = pSvg.y; return p.matrixTransform(svg.getScreenCTM()); } body{ margin: 0em } svg{ width:25%; border:1px solid #ccc } .highlight{ opacity:0.5 } .cursorDOM{ display:block; position:absolute; top:0; left:0; margin-left:-0.5em; margin-top:-0.5em; font-size:2em; width:1em; height:1em; outline: 1px solid green; border-radius:50%; pointer-events:none; } <p id="result"></p> <p id="resultNext"></p> <div id="cursorDOM" class="cursorDOM"></div> <svg id="svg" viewBox="0 0 100 100"> <rect id="rectBg" x="0" y="0" width="100%" height="100%" fill="#eee" /> <circle id="circle0" cx="75" cy="50" r="66" fill="#ccc" /> <circle id="circle1" cx="75" cy="50" r="50" fill="#999" /> <circle id="circle2" cx="75" cy="50" r="33" /> <circle id="circleInside" cx="95" cy="50" r="5" fill="red" /> <circle id="cursor" cx="0" cy="0" r="2" fill="red" /> </svg> 1. DOMPoint() or createSVGPoint() for pointInFill()? You're right: DOMPoint() is currently (as of 2023) not supported by chromium (blink) for some svg related methods. createSVGPoint() works well in chromium as well as in firefox – although it's classified as deprecated. Quite likely chromium will catch up to firefox. But isPointInFill() or isPointInStroke() are used for checking point intersection for single elements. 2. Translate coordinates Depending on your layout, you probably need to convert coordinates. See #Paul LeBeau's answer: "How to convert svg element coordinates to screen coordinates?" 3. Get all underlying elements document.elementsFromPoint() is probably the best way to go. However, this method will also return HTML DOM elements. So you might need some filtering for svg geometry elements like so: let elsInPoint = document.elementsFromPoint(cursorPos.x, cursorPos.y); elsInPoint.forEach((el) => { if (el instanceof SVGGeometryElement) { console.log(el) } });
How did they do this with SVG's, a static image, hotspots and javascript
About half way down on the following site there is an image of a house with SVG animations and hotspots. https://enphase.com/en-us/homeowners I see all the individual elements but I don't understand how the they put it all together. The elements are positioned using percentages to 5 decimal places. I'm assuming they used some software to create the SVGs put more importantly, the layout. Any idea what that software is? There is now way they hand coded the layout and calculated the positioning.
You can have the same functionality using SVG stroke-dasharray and stroke-dashoffset attributes mainipulation with javascript animation timers, I usually use D3.js to do this type of animation/SVG manipulation, but you can also do it purely in javascript, here is a block example by Noah Veltman on bl.ocks.org: <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <style> path { fill: none; stroke: #d3008c; stroke-width: 2px; } #arrowhead { fill: #d3008c; stroke: none; } </style> </head> <body> <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500"> <path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9 c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2 c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8 c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29 c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4 c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2 c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/> <defs> <path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" /> </defs> </svg> <script src="https://d3js.org/d3.v4.min.js"></script> <script> var path = document.querySelector("path"), totalLength = path.getTotalLength(), group = totalLength / 20, start; var arrowheads = d3.select("svg").selectAll("use") .data(d3.range(20).map(function(d){ return d * group + 50; })) .enter() .append("use") .attr("xlink:href", "#arrowhead"); path.style.strokeDasharray = "50," + (group - 50); requestAnimationFrame(update); function update(t) { if (!start) { start = t; } var offset = -group * ((t - start) % 900) / 900; path.style.strokeDashoffset = offset; arrowheads.attr("transform",function(d){ var l = d - offset; if (l < 0) { l = totalLength + l; } else if (l > totalLength) { l -= totalLength; } var p = pointAtLength(l); return "translate(" + p + ") rotate( " + angleAtLength(l) + ")"; }); requestAnimationFrame(update); } function pointAtLength(l) { var xy = path.getPointAtLength(l); return [xy.x, xy.y]; } // Approximate tangent function angleAtLength(l) { var a = pointAtLength(Math.max(l - 0.01,0)), // this could be slightly negative b = pointAtLength(l + 0.01); // browsers cap at total length return Math.atan2(b[1] - a[1], b[0] - a[0]) * 180 / Math.PI; } </script> And this is how the <path> looks like without animations: path { fill: none; stroke: #d3008c; stroke-width: 2px; } #arrowhead { fill: #d3008c; stroke: none; } <svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="960" height="500"> <path d="M636.5,315c-0.4-18.7,1.9-27.9-5.3-35.9 c-22.7-25-107.3-2.8-118.3,35.9c-7,24.4,20.6,37.2,16,71c-4,29.6-30.8,60.7-56.5,61.1c-30.8,0.4-32.9-43.8-81.7-70.2 c-50.9-27.6-110.1-12.9-125.2-9.2c-66.1,16.4-82.2,56.9-109.2,47.3c-38-13.6-55.9-112.1-19.8-143.5c39-34,121.2,27.7,148.1-3.8 c18-21.1,3.1-74.3-25.2-105.3c-31.1-34.1-70.1-32.4-105.3-76.3c-8.2-10.2-16.9-23.8-15.3-39.7c1.2-11.4,7.5-23.3,15.3-29 c33.8-25,101.6,62.6,193.1,59.5c40.1-1.3,38.7-18.5,99.2-38.9c126.2-42.6,242.4-4.9,297.7,13c54.7,17.7,105.4,35,129.8,82.4 c13,25.3,22.9,67.7,4.6,87c-11.6,12.3-25.1,5.1-46.6,20.6c-2.8,2-28.9,21.4-32.1,49.6c-3.1,27.4,18.7,35,29,70.2 c8.8,30.1,8.5,77.8-18.3,99.2c-32.3,25.8-87,0.6-100-5.3c-69.6-32-67.2-88.4-73.3-109.2z"/> <defs> <path id="arrowhead" d="M7,0 L-7,-5 L-7,5 Z" /> </defs> </svg>
How to change the icon of a pictogram?
I was trying to change the icon from the pictogram of this example: http://bl.ocks.org/alansmithy/d832fc03f6e6a91e99f4 The part of the code that render the icon is that: svgDoc.append("defs") .append("g") .attr("id","iconCustom") .append("path") .attr("d","M3.5,2H2.7C3,1.8,3.3,1.5,3.3,1.1c0-0.6-0.4-1-1-1c-0.6,0-1,0.4-1,1c0,0.4,0.2,0.7,0.6,0.9H1.1C0.7,2,0.4,2.3,0.4,2.6v1.9c0,0.3,0.3,0.6,0.6,0.6h0.2c0,0,0,0.1,0,0.1v1.9c0,0.3,0.2,0.6,0.3,0.6h1.3c0.2,0,0.3-0.3,0.3-0.6V5.3c0,0,0-0.1,0-0.1h0.2c0.3,0,0.6-0.3,0.6-0.6V2.6C4.1,2.3,3.8,2,3.5,2z"); I downloaded this SVG from flat icon: <svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 459.568 459.568" style="enable-background:new 0 0 459.568 459.568;" xml:space="preserve"> <g> <path d="M246.817,335.072v22.294c6.493-1.547,9.753-5.069,9.753-10.583C256.57,340.489,252.547,337.797,246.817,335.072z"/> <path d="M214.638,278.05c0,5.329,2.549,8.266,7.179,10.798v-21.562C217.032,269.302,214.638,272.891,214.638,278.05z"/> <path d="M338.492,240.601c-23.908-18.506-42.082-42.997-52.978-70.59c9.327-1.696,16.404-9.844,16.404-19.66 c0-11.046-8.954-20-20-20h-6.649c-0.002-0.014-0.003-0.029-0.005-0.043c10.651-0.447,19.154-9.197,19.154-19.957 c0-10.213-7.659-18.621-17.544-19.834c2.814-11.83,7.72-23.151,14.61-33.38l19.381-28.771c3.759-5.58,4.132-12.777,0.972-18.716 C308.676,3.711,302.497,0,295.77,0H163.799c-6.727,0-12.906,3.711-16.066,9.65c-3.16,5.939-2.786,13.137,0.972,18.716 l19.381,28.771c6.89,10.229,11.796,21.549,14.61,33.38c-9.885,1.213-17.544,9.622-17.544,19.834 c0,10.76,8.504,19.511,19.154,19.957c-0.002,0.014-0.003,0.028-0.005,0.043h-6.649c-11.046,0-20,8.954-20,20 c0,9.819,7.081,17.968,16.411,19.661c-10.89,27.592-29.06,52.068-52.986,70.589c-33.52,25.947-54.626,63.707-54.626,105.745 c0,35.448,15.012,67.851,39.818,92.695c13.122,13.143,30.933,20.528,49.505,20.528h148.031c18.577,0,36.393-7.39,49.516-20.539 c24.794-24.844,39.797-57.243,39.797-92.684C393.118,304.307,372.012,266.547,338.492,240.601z M246.817,389.698v14.246 c0,6.904-5.596,12.5-12.5,12.5s-12.5-5.596-12.5-12.5v-14.77c-11.288-1.477-22.959-4.491-33.823-9.161 c-4.434-1.906-7.882-5.559-9.535-10.093c-1.653-4.534-1.372-9.558,0.799-13.869l0.02-0.039c3.95-7.846,13.421-11.105,21.367-7.362 c6.454,3.04,14.213,5.987,21.173,7.653V326.74c-25.719-7.501-43.706-16.848-43.706-43.07c0-23.728,14.366-43.537,43.706-48.711 v-9.781c0-6.904,5.596-12.5,12.5-12.5s12.5,5.596,12.5,12.5v9.526c9.152,1.259,18.407,4.132,27.216,7.889 c4.251,1.813,7.546,5.325,9.091,9.681c1.545,4.356,1.207,9.169-0.956,13.254l-0.023,0.044c-3.89,7.348-12.801,10.386-20.377,6.964 c-4.67-2.11-10.007-4.136-14.951-5.411v29.722l0.026,0.007c28.259,7.766,47.767,17.032,47.767,46.47 C294.611,372.013,275.029,387.041,246.817,389.698z"/> </g> </svg> But when i change the Path in original code, nothing appears. The code modified is: svgDoc.append("defs") .append("g") .attr("id","iconCustom") .append("path") .attr("d","M338.492,240.601c-23.908-18.506-42.082-42.997-52.978-70.59c9.327-1.696,16.404-9.844,16.404-19.66c0-11.046-8.954-20-20-20h-6.649c-0.002-0.014-0.003-0.029-0.005-0.043c10.651-0.447,19.154-9.197,19.154-19.957c0-10.213-7.659-18.621-17.544-19.834c2.814-11.83,7.72-23.151,14.61-33.38l19.381-28.771c3.759-5.58,4.132-12.777,0.972-18.716C308.676,3.711,302.497,0,295.77,0H163.799c-6.727,0-12.906,3.711-16.066,9.65c-3.16,5.939-2.786,13.137,0.972,18.716l19.381,28.771c6.89,10.229,11.796,21.549,14.61,33.38c-9.885,1.213-17.544,9.622-17.544,19.834c0,10.76,8.504,19.511,19.154,19.957c-0.002,0.014-0.003,0.028-0.005,0.043h-6.649c-11.046,0-20,8.954-20,20c0,9.819,7.081,17.968,16.411,19.661c-10.89,27.592-29.06,52.068-52.986,70.589c-33.52,25.947-54.626,63.707-54.626,105.745c0,35.448,15.012,67.851,39.818,92.695c13.122,13.143,30.933,20.528,49.505,20.528h148.031c18.577,0,36.393-7.39,49.516-20.539c24.794-24.844,39.797-57.243,39.797-92.684C393.118,304.307,372.012,266.547,338.492,240.601zM246.817,389.698v14.246c0,6.904-5.596,12.5-12.5,12.5s-12.5-5.596-12.5-12.5v-14.77c-11.288-1.477-22.959-4.491-33.823-9.161c-4.434-1.906-7.882-5.559-9.535-10.093c-1.653-4.534-1.372-9.558,0.799-13.869l0.02-0.039c3.95-7.846,13.421-11.105,21.367-7.362c6.454,3.04,14.213,5.987,21.173,7.653V326.74c-25.719-7.501-43.706-16.848-43.706-43.07c0-23.728,14.366-43.537,43.706-48.711v-9.781c0-6.904,5.596-12.5,12.5-12.5s12.5,5.596,12.5,12.5v9.526c9.152,1.259,18.407,4.132,27.216,7.889c4.251,1.813,7.546,5.325,9.091,9.681c1.545,4.356,1.207,9.169-0.956,13.254l-0.023,0.044c-3.89,7.348-12.801,10.386-20.377,6.964c-4.67-2.11-10.007-4.136-14.951-5.411v29.722l0.026,0.007c28.259,7.766,47.767,17.032,47.767,46.47C294.611,372.013,275.029,387.041,246.817,389.698z") .attr("d","M246.817,335.072v22.294c6.493-1.547,9.753-5.069,9.753-10.583C256.57,340.489,252.547,337.797,246.817,335.072z") .attr("d","M214.638,278.05c0,5.329,2.549,8.266,7.179,10.798v-21.562C217.032,269.302,214.638,272.891,214.638,278.05z"); And it doesn't show anything. What's going on? How can i change the icon?
That moneybag is not a single path, like the pictogram in the original code you linked. Instead of that, the moneybag is a complex SVG, viewBox included... therefore, you cannot simply replace the d attribute like that. That being said, an easy approach is converting that whole SVG into a string... const moneyBagHtml = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3 etc... ... that you append using html(): svgDoc.append("defs") .append("g") .attr("id","iconCustom") .html(moneyBagHtml) Of course, you have to set a width and a height to scale down that SVG. Here is the result: <!doctype html> <html> <head> <!--demonstration of using the svg 'use' element to create a pictogram--> <script src="https://d3js.org/d3.v3.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script> <link rel="stylesheet" href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/themes/smoothness/jquery-ui.css" /> <script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.11.1/jquery-ui.min.js"></script> <script src="https://cdn.jsdelivr.net/jquery.ui.touch-punch/0.2.3/jquery.ui.touch-punch.min.js"></script> <style type="text/css"> #sliderDiv { margin: 10px; margin-top: 30px; height: 15px; width: 300px; } svg { overflow: none; padding: 10px; float: left; width: 400px; height: 400px; } text { fill: #bb6d82; text-anchor: left; font-size: 12px; font-family: sans-serif, Helvetica, Arial; font-weight: bold; } .iconPlain { fill: #a7a59b; } .iconSelected { fill: #bb6d82; } rect { fill: #fff1e0; } </style> </head> <body> <script> const moneyBagHtml = '<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 459.568 459.568" style="enable-background:new 0 0 459.568 459.568;" xml:space="preserve" width="7px" height="7px"><g><path d="M246.817,335.072v22.294c6.493-1.547,9.753-5.069,9.753-10.583C256.57,340.489,252.547,337.797,246.817,335.072z"/><path d="M214.638,278.05c0,5.329,2.549,8.266,7.179,10.798v-21.562C217.032,269.302,214.638,272.891,214.638,278.05z"/><path d="M338.492,240.601c-23.908-18.506-42.082-42.997-52.978-70.59c9.327-1.696,16.404-9.844,16.404-19.66c0-11.046-8.954-20-20-20h-6.649c-0.002-0.014-0.003-0.029-0.005-0.043c10.651-0.447,19.154-9.197,19.154-19.957c0-10.213-7.659-18.621-17.544-19.834c2.814-11.83,7.72-23.151,14.61-33.38l19.381-28.771c3.759-5.58,4.132-12.777,0.972-18.716C308.676,3.711,302.497,0,295.77,0H163.799c-6.727,0-12.906,3.711-16.066,9.65c-3.16,5.939-2.786,13.137,0.972,18.716l19.381,28.771c6.89,10.229,11.796,21.549,14.61,33.38c-9.885,1.213-17.544,9.622-17.544,19.834c0,10.76,8.504,19.511,19.154,19.957c-0.002,0.014-0.003,0.028-0.005,0.043h-6.649c-11.046,0-20,8.954-20,20c0,9.819,7.081,17.968,16.411,19.661c-10.89,27.592-29.06,52.068-52.986,70.589c-33.52,25.947-54.626,63.707-54.626,105.745c0,35.448,15.012,67.851,39.818,92.695c13.122,13.143,30.933,20.528,49.505,20.528h148.031c18.577,0,36.393-7.39,49.516-20.539c24.794-24.844,39.797-57.243,39.797-92.684C393.118,304.307,372.012,266.547,338.492,240.601z M246.817,389.698v14.246c0,6.904-5.596,12.5-12.5,12.5s-12.5-5.596-12.5-12.5v-14.77c-11.288-1.477-22.959-4.491-33.823-9.161c-4.434-1.906-7.882-5.559-9.535-10.093c-1.653-4.534-1.372-9.558,0.799-13.869l0.02-0.039c3.95-7.846,13.421-11.105,21.367-7.362c6.454,3.04,14.213,5.987,21.173,7.653V326.74c-25.719-7.501-43.706-16.848-43.706-43.07c0-23.728,14.366-43.537,43.706-48.711v-9.781c0-6.904,5.596-12.5,12.5-12.5s12.5,5.596,12.5,12.5v9.526c9.152,1.259,18.407,4.132,27.216,7.889c4.251,1.813,7.546,5.325,9.091,9.681c1.545,4.356,1.207,9.169-0.956,13.254l-0.023,0.044c-3.89,7.348-12.801,10.386-20.377,6.964c-4.67-2.11-10.007-4.136-14.951-5.411v29.722l0.026,0.007c28.259,7.766,47.767,17.032,47.767,46.47C294.611,372.013,275.029,387.041,246.817,389.698z"/></g></svg>'; //placeholder div for jquery slider d3.select("body").append("div").attr("id", "sliderDiv"); //create svg element var svgDoc = d3.select("body").append("svg").attr("viewBox", "0 0 100 100"); //define an icon store it in svg <defs> elements as a reusable component - this geometry can be generated from Inkscape, Illustrator or similar svgDoc.append("defs") .append("g") .attr("id", "iconCustom") .html(moneyBagHtml) //background rectangle svgDoc.append("rect").attr("width", 100).attr("height", 100); //specify the number of columns and rows for pictogram layout var numCols = 10; var numRows = 10; //padding for the grid var xPadding = 10; var yPadding = 15; //horizontal and vertical spacing between the icons var hBuffer = 8; var wBuffer = 8; //generate a d3 range for the total number of required elements var myIndex = d3.range(numCols * numRows); //text element to display number of icons highlighted svgDoc.append("text") .attr("id", "txtValue") .attr("x", xPadding) .attr("y", yPadding) .attr("dy", -3) .text("0"); //create group element and create an svg <use> element for each icon svgDoc.append("g") .attr("id", "pictoLayer") .selectAll("use") .data(myIndex) .enter() .append("use") .attr("xlink:href", "#iconCustom") .attr("id", function(d) { return "icon" + d; }) .attr("x", function(d) { var remainder = d % numCols; //calculates the x position (column number) using modulus return xPadding + (remainder * wBuffer); //apply the buffer and return value }) .attr("y", function(d) { var whole = Math.floor(d / numCols) //calculates the y position (row number) return yPadding + (whole * hBuffer); //apply the buffer and return the value }) .classed("iconPlain", true); //create a jquery slider to control the pictogram $("#sliderDiv").slider({ orientation: "horizontal", min: 0, max: numCols * numRows, value: 0, slide: function(event, ui) { d3.select("#txtValue").text(ui.value); d3.selectAll("use").attr("class", function(d, i) { if (d < ui.value) { return "iconSelected"; } else { return "iconPlain"; } }); } }); </script> </body> </html>
Place div on svg track
I'm trying to place a 25px * 25px div on a certain percentage of an svg track. This is the way i'm doing it now: getCheckpointPercent(); //Get the readers from the Json file, and add the percentage to the array. function getCheckpointPercent(){ $.getJSON("readers.json", function (data) { var total = 0; var semitotal = 0; var currentdistance = 0; $.each(data, function (index, value) { total = total + value['distanceToNext']; }); console.log(total); $.each(data, function (index, value) { value['percent'] = semitotal / total * 100; semitotal = semitotal + value['distanceToNext']; }); console.log(data); placeCheckpoints(data); }); } //Place the checkpoints on the track, on the spot the corresponds to their percentage function placeCheckpoints(readers){ $.each(readers, function (index, value) { var punt = getPointOnTrack(value['percent']); $('#cps').append('<div class="checkpoint" id="'+index+'"></div>'); $( "#"+index ).css('left',punt.x); $( "#"+index ).css('top',punt.y); $( "#"+index ).css('position','absolute'); }); } //Get coords of point on track using percentage function getPointOnTrack(prcnt){ var track = $( '#track' ); var trackLength = document.getElementById( 'track' ).getTotalLength(); var part = trackLength * prcnt / 100; pt = document.getElementById( 'track' ).getPointAtLength(part); return pt; } With the following result: https://imgur.com/a/bW0KuCN As you can see it's off by some pixels. How can i place them correctly? EDIT: SVG track: <svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 1783 903" id="svg"> <path d="M1697.17,63.491c-105.906,-119.003 -609.921,-29.945 -876.794,34.426c-164.703,39.726 -224.547,269.311 -335.753,272.609c-214.672,6.366 -258.259,-379.064 -345.329,-337.073c-178.323,85.998 -184.301,834.002 13.654,836.966c177.382,2.655 251.631,-254.971 409.655,-235.198c181.21,22.674 152.502,168.163 391.991,209.317c228.308,39.232 223.472,-183.574 312.715,-193.699c73.817,-8.375 276.248,275.455 417.573,244.156c130.744,-28.956 112.095,-279.189 12.288,-326.222c-157.212,-74.083 -693.907,-55.006 -724.395,-117.798c-54.001,-111.215 464.9,-139.592 415.502,-226.446c-53.998,-94.941 428.86,-26.236 308.893,-161.038Z" stroke="#000000" stroke-width="2" fill="none" id="track"/> </svg>
I'm not very sure that this is what you want: the div #label's position is the position of the mouse over the div #SVG The text content of the #label is the value of the x and y attributes of the point on the path. Please read the comments in the code. const SVG_NS = "http://www.w3.org/2000/svg"; // the total length of the path let l = test.getTotalLength(); // the array of the points on the path let points = []; for (let i = 0; i < l; i += l / 4) { // test is the id for the path let point = test.getPointAtLength(i); points.push(point); } // for every point I draw a circle let circles = []; for (let i = 0; i < points.length; i++) { let o = { cx: points[i].x, cy: points[i].y, r: 30, fill: "blue" }; // create a new circle and save the circle in the circles array circles.push(drawCircle(o, svg)); } // a function to draw a circle function drawCircle(o, parent) { let circle = document.createElementNS(SVG_NS, "circle"); for (var name in o) { if (o.hasOwnProperty(name)) { circle.setAttributeNS(null, name, o[name]); } } parent.appendChild(circle); return circle; } // for each circle there is an event listener that calculate the position of the div #label circles.forEach(c => { c.addEventListener("mouseenter", evt => { let x = parseInt(c.getAttribute("cx")); let y = parseInt(c.getAttribute("cy")); label.innerHTML = `x: ${x} <br> y: ${y}`; let m = oMousePos(svg, evt); label.style.cssText = `top:${m.y}px; left:${m.x}px; opacity:1`; }); }); //when the mouse is not over the circle, the label's opacity is 0 divSVG.addEventListener("mouseover", () => { label.style.opacity = 0; }); // a function that gets the position of the mouse over an HTML element function oMousePos(elmt, evt) { var ClientRect = elmt.getBoundingClientRect(); return { //objeto x: Math.round(evt.clientX - ClientRect.left), y: Math.round(evt.clientY - ClientRect.top) }; } path { stroke: black; fill:none; } #label { position: absolute; top: 0; left: 0; width: 50px; height: 50px; opacity: 0; background:white; pointer-events:none; transition: all 0.5s; } .svg { position: relative; } <div class="svg" id="divSVG"> <svg id="svg" xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" viewBox="0 0 1783 903" id="svg"> <path id="test" d="M1697.17,63.491c-105.906,-119.003 -609.921,-29.945 -876.794,34.426c-164.703,39.726 -224.547,269.311 -335.753,272.609c-214.672,6.366 -258.259,-379.064 -345.329,-337.073c-178.323,85.998 -184.301,834.002 13.654,836.966c177.382,2.655 251.631,-254.971 409.655,-235.198c181.21,22.674 152.502,168.163 391.991,209.317c228.308,39.232 223.472,-183.574 312.715,-193.699c73.817,-8.375 276.248,275.455 417.573,244.156c130.744,-28.956 112.095,-279.189 12.288,-326.222c-157.212,-74.083 -693.907,-55.006 -724.395,-117.798c-54.001,-111.215 464.9,-139.592 415.502,-226.446c-53.998,-94.941 428.86,-26.236 308.893,-161.038Z" stroke="#000000" stroke-width="2" fill="none" id="track"/> </svg> <div id="label" ></div> </div>
spawn & drag of SVG elements - approach
I am on my learning curve for Javascript/SVG combo (animating and making interactive SVGs). I wanted to create a code snippet where menu elements ("inventory") can be dragged over to the main screen ("canvas") while the originating element would remain in its place (as if one would move a copy of it off the original element). Here I crafted the code snippet as best as I could: http://codepen.io/cmer41k/pen/f2b5eea274cdde29b0b2dc8a2424a645 So I sort of managed to do something but its buggy: I could deal with 1 copy and making it draggable, but then I don't know how to deal with IDs for all those spawning elements, which causes dragging issues I fail to understand how to make it work indefinitely (so that it can spawn any amount of circles draggable to canvas) Draggable elements in canvas often overlap and I fail to attach the listeners the way they don't overlap so that the listener on the element I am dragging would propagate "through" whatever other elements there;( Question is basically - can someone suggest logic that I should put into this snippet so that it was not as cumbersome. I am pretty sure I am missing something here;( (e.g. it should not be that hard is it not?) HTML: <body> <svg id="svg" height="800" width="480" viewbox="0 0 480 800" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <rect id="canvasBackground" width="480" height="480" x="0" y="0"/> <rect id="inventoryBackground" width="480" height="100" x="0" y="480"/> <g id="inventory"> <path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" /> <path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" /> <path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" /> <path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" /> </g> <g id="canvas"> </g> </svg> </body> Javascript: // define meta objects var drag = null; // this stores all "curves"-circles var curves = {}; var canvas = {}; var inventory = {}; window.onload = function() { // creates the curve-circles in the object and at their initial x,y coords curves.curve1 = document.getElementById("curve1"); curves.curve1.x = 0; curves.curve1.y = 0; curves.curve2 = document.getElementById("curve2"); curves.curve2.x = 0; curves.curve2.y = 0; curves.curve3 = document.getElementById("curve3"); curves.curve3.x = 0; curves.curve3.y = 0; curves.curve4 = document.getElementById("curve4"); curves.curve4.x = 0; curves.curve4.y = 0; canvas = document.getElementById("canvas"); inventory = document.getElementById("inventory"); // attach events listeners AttachListeners(); } function AttachListeners() { var ttt = document.getElementsByClassName('inventory'), i; for (i = 0; i < ttt.length; i++) { document.getElementsByClassName("inventory")[i].onmousedown=Drag; document.getElementsByClassName("inventory")[i].onmousemove=Drag; document.getElementsByClassName("inventory")[i].onmouseup=Drag; } } // Drag function that needs to be modified;// function Drag(e) { e.stopPropagation(); var t = e.target, id = t.id, et = e.type; m = MousePos(e); if (!drag && (et == "mousedown")) { if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable? copy = t.cloneNode(true); copy.onmousedown=copy.onmousemove=onmouseup=Drag; inventory.insertBefore(copy, inventory.firstChild); drag = t; dPoint = m; } if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around drag = t; dPoint = m; } } // drag the spawned/copied draggable element now if (drag && (et == "mousemove")) { curves[id].x += m.x - dPoint.x; curves[id].y += m.y - dPoint.y; dPoint = m; curves[id].setAttribute("transform", "translate(" +curves[id].x+","+curves[id].y+")"); } // stop drag if (drag && (et == "mouseup")) { t.className.baseVal="draggable"; drag = null; } } // adjust mouse position to the matrix of SVG & screen size function MousePos(event) { var p = svg.createSVGPoint(); p.x = event.clientX; p.y = event.clientY; var matrix = svg.getScreenCTM(); p = p.matrixTransform(matrix.inverse()); return { x: p.x, y: p.y } }
You were close. You had a couple of bugs. Eg. copy.onmousedown=copy.onmousemove=onmouseup=Drag; should have been: copy.onmousedown=copy.onmousemove=copy.onmouseup=Drag; And drag = t should have been drag = copy (?) Also you were appending the clones to the inventory section, when I think you intended to append them to the "canvas" section. But there were also also some less-obvious mistakes that were contributing to the unreliableness. For example, if you attach the mousemove and mouseup events to the inventory and clone shapes, then you will won't get the events if you drag too fast. The mouse will get outside the shape, and the events won't be passed to the shapes. The fix is to move those event handlers to the root SVG. Another change I made was to store the x and y positions in the DOM for the clone as _x and _y. This makes it easier than trying to keep them in a separate array. Anyway, here's my modified version of your example which works a lot more reliably. // define meta objects var drag = null; var canvas = {}; var inventory = {}; window.onload = function() { canvas = document.getElementById("canvas"); inventory = document.getElementById("inventory"); // attach events listeners AttachListeners(); } function AttachListeners() { var ttt = document.getElementsByClassName('inventory'), i; for (i = 0; i < ttt.length; i++) { document.getElementsByClassName("inventory")[i].onmousedown=Drag; } document.getElementById("svg").onmousemove=Drag; document.getElementById("svg").onmouseup=Drag; } // Drag function that needs to be modified;// function Drag(e) { var t = e.target, id = t.id, et = e.type; m = MousePos(e); if (!drag && (et == "mousedown")) { if (t.className.baseVal=="inventory") { //if its inventory class item, this should get cloned into draggable? copy = t.cloneNode(true); copy.onmousedown = Drag; copy.removeAttribute("id"); copy._x = 0; copy._y = 0; canvas.appendChild(copy); drag = copy; dPoint = m; } else if (t.className.baseVal=="draggable") { //if its just draggable class - it can be dragged around drag = t; dPoint = m; } } // drag the spawned/copied draggable element now if (drag && (et == "mousemove")) { drag._x += m.x - dPoint.x; drag._y += m.y - dPoint.y; dPoint = m; drag.setAttribute("transform", "translate(" +drag._x+","+drag._y+")"); } // stop drag if (drag && (et == "mouseup")) { drag.className.baseVal="draggable"; drag = null; } } // adjust mouse position to the matrix of SVG & screen size function MousePos(event) { var p = svg.createSVGPoint(); p.x = event.clientX; p.y = event.clientY; var matrix = svg.getScreenCTM(); p = p.matrixTransform(matrix.inverse()); return { x: p.x, y: p.y } } /* SVG styles */ path { stroke-width: 4; stroke: #000; stroke-linecap: round; } path.fill { fill: #3ff; } html, body { margin: 0; padding: 0; border: 0; overflow:hidden; background-color: #fff; } body { -ms-touch-action: none; } #canvasBackground { fill: lightgrey; } #inventoryBackground { fill: grey; } .inventory { fill: red; } .draggable { fill: green; } svg { position: fixed; top:0%; left:0%; width:100%; height:100%; } <svg id="svg" height="800" width="480" viewbox="0 0 480 800" preserveAspectRatio="xMinYMin meet" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <rect id="canvasBackground" width="480" height="480" x="0" y="0"/> <rect id="inventoryBackground" width="480" height="100" x="0" y="480"/> <g id="inventory"> <path id="curve4" class="inventory" d="M60,530 A35,35 0 1,1 60,531" /> <path id="curve3" class="inventory" d="M150,530 A35,35 0 1,1 150,531" /> <path id="curve2" class="inventory" d="M240,530 A35,35 0 1,1 240,531" /> <path id="curve1" class="inventory" d="M330,530 A35,35 0 1,1 330,531" /> </g> <g id="canvas"> </g> </svg>