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>
Related
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)
}
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>
I don't understand why the color of legend's labels does not always correspond to the color in the map in my code:
This is my code (that is the modification of this code). The file provincias.json is available here:
<!DOCTYPE html>
<meta charset="utf-8">
<style>
.nombre{
stroke: #000;
stroke-width: 0.5px
}
.graticule {
fill: none;
stroke: #777;
stroke-width: .5px;
stroke-opacity: .5;
}
.legendLinear
{
font-family: "Lato";
fill:#c2b59b;
}
.legendTitle {
font-size: 1em;
}
#tooltip {
position: absolute;
top: 0;
left: 0;
z-index: 10;
margin: 0;
padding: 10px;
width: 200px;
height: 70px;
color: white;
font-family: sans-serif;
font-size: 1.0em;
font-weight: bold;
text-align: center;
background-color: rgba(0, 0, 0, 0.55);
opacity: 0;
pointer-events: none;
border-radius:5px;
transition: .2s;
}
</style>
<body>
<div id="container">
<div id="tooltip">
</div>
</div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.7.0/d3-legend.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-composite-projections/0.3.5/conicConformalSpain-proj.min.js"></script>
<script>
var width = 1000,
height = 900;
var projection = d3.geo.conicConformalSpain()
var graticule = d3.geo.graticule().step([2, 2]);
var path = d3.geo.path()
.projection(projection);
var svg = d3.select("#container").append("svg")
.attr("width", width)
.attr("height", height);
svg.append("path")
.datum(graticule)
.attr("class", "graticule")
.attr("d", path);
d3.json("provincias.json", function(error, provincias) {
d3.json("hdi.json", function(error, hdi) {
var land = topojson.feature(provincias, provincias.objects.provincias);
var color = d3.scale.linear()
.domain([0, 10, 1000, 10000, 100000, 300000])
.range(["#feebe2","#e5d1ff","#ba93ef", "#8D4CE5","#6100E5","#C94D8C"]); //#feebe2
svg.selectAll(".nombre")
.data(land.features)
.enter()
.append("path")
.attr("d", path)
.attr("class","nombre")
.style("fill",function(d){ return color(hdi[d.properties.nombre]) })
.on("mouseover", function(d){
//Show the tooltip
var x = d3.event.pageX;
var y = d3.event.pageY - 40;
d3.select("#tooltip")
.style("left", x + "px")
.style("top", y + "px")
.style("opacity", 1)
.text(d.properties.nombre + "," + hdi[d.properties.nombre]);
})
.on("mouseout", function(){
//Hide the tooltip
d3.select("#tooltip")
.style("opacity", 0);
});
svg
.append("path")
.style("fill","none")
.style("stroke","#000")
.attr("d", projection.getCompositionBorders());
d3.select("svg").append("g")
.attr("class", "legendLinear")
.attr("transform", "translate(100,500)");
var legendLinear = d3.legend.color()
.title("...")
.shapeHeight(20)
.shapeWidth(90)
.shapeRadius(10)
.cells([0, 10, 1000, 10000, 100000, 300000])
.orient("horizontal")
.labelFormat(d3.format(".00f"))
.labelAlign("start")
.scale(color);
svg.select(".legendLinear")
.call(legendLinear);
});
});
</script>
The content of hdi.json is the following:
{"Coruña, A":9, "Alicante":158, "Albacete":3,"Almería":0,"Asturias":13,"Álava":12,"Ávila":0,
"Badajoz":10,"Balears, Illes":331,"Barcelona":250000,"Burgos":5,
"Cantabria":12,"Castellón":316,"Ceuta":9,"Ciudad Real":9,"Cádiz":9,"Cuenca":4,
"Córdoba":11,"Cáceres":2,"Girona":21808,"Jaén":0,
"Granada":9,"Huelva":3,"Huesca":74,
"León":5,"Lleida":9672,"Lugo":3,
"Madrid":507,"Murcia":24,"Málaga":25,"Palencia":2,"Pontevedra":6,
"Navarra":23,"Salamanca":6,"Segovia":4,"Sevilla":16,"Soria":2,
"Santa Cruz de Tenerife":16,"Tarragona":22790,
"Teruel":23,"Toledo":4,"Valladolid":44,
"Valencia":423,"Vizcaya":19,"Zamora":0,"Zaragoza":56,"Guipúzcoa":21,
"Guadalajara":5,"Jaen":2,"Rioja, La": 12, "Palmas, Las": 10,"Ourense":2}
The particular problem is that Tarragona that has the value 22790 is colored in the same color as Lleida that has the value 9672. However, according to my code, 22790 (Tarragona) is smaller than 100000 and bigger than 10000, so it should be colored in #6100E5, but it's colored in #8D4CE5.
But, for example, 9672 (Lleida) is smaller than 10000, so it should be colored in #8D4CE5 (and it is colored in this color, so it's ok).
You shouldn't be using a linear scale, by definition it has a continuous range and will interpolate between the colors. What you are describing is a threshold scale. It has a discrete range mapped to subsets of domain values. Further, you must call it with a range that's N + 1 of the domain, so, this is what you should be after:
var color = d3.scale.threshold()
.domain([10, 1000, 10000, 100000, 30000])
.range(["#feebe2","#e5d1ff","#ba93ef", "#8D4CE5","#6100E5","#C94D8C"]);
Here's an example creating the legend using a threshold scale and d3-legend.
<!DOCTYPE html>
<html>
<head>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3-legend/1.7.0/d3-legend.min.js"></script>
</head>
<body>
<svg></svg>
<script>
var color = d3.scale.threshold()
.domain([10, 1000, 10000, 100000, 300000])
.range(["#feebe2", "#e5d1ff", "#ba93ef", "#8D4CE5", "#6100E5", "#C94D8C"]);
var svg = d3.select("svg");
svg.append("g")
.attr("class", "legendLog")
.attr("transform", "translate(20,20)");
var logLegend = d3.legend.color()
.labels([0, 10, 1000, 10000, 100000, 300000])
.scale(color);
svg.select(".legendLog")
.call(logLegend);
</script>
</body>
</html>
Actually it works as expected.
The exact colors of Lleida and Tarragona are different, the former being #8f4fe5, the latter #8741e5.
Because of the linear scales you use these colors are calculated as:
((9672-1000) * #8d4ce5 + (10000-9672) * #ba93ef)/(10000-1000) = #8f4fe5
((22790-10000) * #6100e5 + (100000-22790) * #8d4ce5)/(100000-10000) = #8741e5
Intuitively, your problem is that 9672 is much closer to 10000 as to 1000, and 22790 is also much closer to 10000 than to 100000, so even if one of them is below 10000, and the other one above it, they are still closer to it, than to the other ends of the ranges.
As Mark suggested in his answer, probably you do not want to use continuous linear scales.
I have a d3 layout with 10 nodes structured as follows...
<body>
<svg id="mainSvg" style="position: relative" width="1200" height = "1200">
<g>
<svg width = "800" height="800>
<g class="nodeGroupSVG" transform=translate(someXValue,someYValue) scale(someScaleValue)
<g class="node" transform=translate(someXValue,someYValue)>
<circle>
<text>
</g>
//9 more of these individual node groupings
</g>
</svg>
</g>
<g class="gToMoveTo">
//starts off empty
</g>
<svg>
</body>
I want to move the nodes to a new container, which can easily be achieved in jquery using...
$('.gToMoveTo').append($('.node'));
But when it comes to animating that transition to a new DOM position, I'm having troubles implementing the idea from the SO answer here: JQuery - animate moving DOM element to new parent?
Using that function has exactly the same effect for me as the simple jquery line (plus a minor delay in updating which I'm assuming is due to the animation function being called - even though there's no gradual transition to the new position).
All that's a long way of asking whether anyone knows of a good reason why the function I've listed would not be suited to animating the transition of SVG group elements? And if so, whether there's any ideas of a way to adapt it for use with SVG.
This is a common algorithm to do that:
Here's the code:
<html>
<head>
<script src="d3.v3.min.js"></script>
<script src="jquery-2.1.0.min.js"></script>
<style>
.svg_contariner {
background-color:#CCCCCC;
}
#origin {
width:200px;
height:200px;
margin:5px;
}
#target {
width:200px;
height:200px;
margin:5px;
}
circle {
fill:#e72;
}
text {
font-family:Tahoma, Geneva, sans-serif;
font-size:10px;
}
.clone {
margin:0;
padding:0;
width:20px;
height:20px;
position:absolute;
}
</style>
</head>
<body>
<div id="data"></div>
<script>
var data_text = "", data = [], r = 10;
/* --- add some random data --- */
for (i = 0; i < 10; i++) {
data.push( {
"x": Math.round(Math.random() * 180)+10,
"y": Math.round(Math.random() * 180)+10,
"text": i
});
data_text += "x:" + data[i].x + " y:" + data[i].y + " text:" + data[i].text + "<br>";
}
/* --- create 2 containers --- */
var svgContainerTar = d3.select("body").append("svg")
.attr("id","target")
.attr("class","svg_contariner");
var svgContainerOrg = d3.select("body").append("svg")
.attr("id","origin")
.attr("class","svg_contariner");
/* --- add g node to origin --- */
var ele = svgContainerOrg.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("class", "node")
.on("click", function (d) { /* --- bind onClick to every g --- */
d3.select(this).remove(); /* --- remove origin element --- */
moveCircle(d); /* --- create, animate ghost --- */
});
/* --- add circle to g --- */
var circles = ele.append("circle")
.attr("cx", function (d) { return d.x; })
.attr("cy", function (d) { return d.y; })
.attr("r", r+"px");
/* --- add text to g --- */
var labels = ele.append("text")
.attr("dx", function(d) {return d.x - 2})
.attr("dy", function(d) {return d.y + 3})
.text(function (d) {return d.text});
function moveCircle(d) {
ori_x = d.x;
ori_y = d.y;
ori_tex = d.text;
ori_pos = $("#origin").position();
tar_pos = $("#target").position();
/* --- create ghost using jQuery --- */
$("body").append("<div id='ghost' class='clone' style='left:"+(ori_x - r/2+ori_pos.left)+";top:"+(ori_y - r/2+ori_pos.top)+"';>"+
"<svg width='20px' height='20px'>"+
"<circle cx='"+r+"' cy='"+r+"' r='"+r+"px'></circle>"+
"<text x='"+(r - 2)+"' y='"+(r+3)+"'>"+ori_tex+"</text>"+
"</svg></div>");
/* --- animate ghost --- */
$("#ghost").animate({
"left" : tar_pos.left + ori_x,
"top" : tar_pos.top + ori_y
},100,"swing",
function () {
time = setTimeout(function () { /* --- when animation ends create target element --- */
var new_node = d3.select("#target")
.append ("g")
.attr("class", "node");
new_node.append("circle")
.attr("cx", ori_x+r/2+"px")
.attr("cy", ori_y+r/2+"px")
.attr("r", r+"px")
new_node.append("text")
.attr("dx", ori_x+r/2-2)
.attr("dy", ori_y+r/2+3)
.text(ori_tex);
$("#ghost").remove(); /* --- remove ghost --- */
},100)
});
}
</script>
</body></html>
Hope this help
I've got a piechart that's built with D3, the markup looks like:
<div class="display">
<svg height="300">
<g transform="translate(140,125)">
<g class="slice">
<path fill="#3182bd" d="M-103.92304845413268,-59.99999999999993A120,120 0 0,1 -1.2862432473281782e-13,-120L0,0Z"></path>
<text transform="translate(-30.000000000000075,-51.96152422706628)" text-anchor="middle">A</text>
</g>
<g class="slice">
<path fill="#6baed6" d="M7.347638122934264e-15,120A120,120 0 0,1 -103.92304845413268,-59.99999999999993L0,0Z"></path>
<text transform="translate(-51.961524227066306,30.00000000000002)" text-anchor="middle">B</text>
</g>
<g class="slice">
<path fill="#9ecae1" d="M7.347638122934264e-15,-120A120,120 0 1,1 7.347638122934264e-15,120L0,0Z"></path>
<text transform="translate(60,0)" text-anchor="middle">C</text>
</g>
</g>
</svg>
<div class="pieChartTooltip" style="position: absolute; z-index: 10; visibility: visible; top: 32px; left: 192px;">C: 3.00</div>
</div>
Ok nifty. Then I wanted to be able to resize the container that it's in (angular directive):
.directive("resizable", [ "debounce", (debounce) ->
restrict: "A"
link: (scope, element, attrs) ->
broadcastResize = (event, ui) ->
scope.$root.$broadcast "resize", ui.element.parents(".widget"), ui.size
debounceResize = debounce(broadcastResize, 800, false)
element.resizable
grid: 10
helper: "ui-resizable-helper"
stop: (event, ui) ->
debounceResize event, ui
])
All fine. Next I wanted to add an overlay for the pie chart that said "sample data" or "refreshed at 3:45" or other things like that. So I added this code to the object that builds the pie chart (coffee):
drawOverlay: (msg) =>
#logger.debug 'drawing overlay'
vis = d3.select(#selector + " svg ")
#logger.warn "no svg vis found" unless vis?
x = (#model.width / 2 ) - 75
y = (#model.height / 2 ) - 50
#jq(#selector + " svg foreignObject").remove()
text = vis.append("foreignObject")
.attr("class", "widgetOverlay")
.attr("x", x)
.attr("y", y)
.attr("width", 150)
.attr("height", 50)
.append("xhtml:body")
.html('<div>' + msg + '</div>')
Here's the problem: adding that overlay causes jquery ui's resize helper to get "stuck" on screen -- but only the first time it's drawn. Subsequent resizes work fine. I suspect it's because of the second body tag that gets inserted in the svg. As far as I know it is required -- taking it out caused the overlay to not be appended. If I put a return at the top of drawOverlay the resize directive works just fine. So it seems clear where the problem lies, I'm wondering if anyone can explain why this is happening -- and maybe offer a fix.
If adding a body tag confuses jquery.ui, then why not skip the body tag and append the div directly?
var msg = 'Hello World';
var x = 150, y = 150;
var svg = d3.select('body')
.append('svg')
.attr('width', 300)
.attr('height', 300)
.append("foreignObject")
.attr("class", "widgetOverlay")
.attr("x", x)
.attr("y", y)
.attr("width", 150)
.attr("height", 50)
.append('xhtml:div')
.text(msg);
Demo