d3 custom bar chart bars using svg paths instead of rects - svg

I'm trying to build a d3 bar chart, but due to a sneaky designer, I need rounded corners on the top-left and -right of each bar. I think I'm getting somewhere, but I could use a bit of help to push it over the line.
Still very much on the uphill curve learning about d3 and svg, so I hope I haven't missed anything obvious.
I have successfully made this work using the rects (commented out section), but there's a flaw, probably in how I'm drawing the paths. The anonymous functions I'm passing as arguments into the topRoundedRect function (e.g. function(datum, index) { return x(index); }) are not being evaluated before being appended to the d attribute of the path, and I don't understand why. So I end up getting an error like this:
Error: Problem parsing d="Mfunction (datum, index) { return x(index); }3,function (datum) { return height - y(datum); }h34a3,3 0 0 1 3,3vNaNh-40vNaNa3,3 0 0 1 3,-3z"
Any advice you can offer would be fantastic. Code below.
var data = [60.45,60.45,89.54,120.34,106.45,127.43];
var barWidth = 40;
var width = (barWidth + 10) * data.length;
var height = 500;
var x = d3.scale.linear().domain([0, data.length]).range([0, width]);
var y = d3.scale.linear().domain([0, d3.max(data, function(datum) { return datum; })]).
rangeRound([0, height]);
// add the canvas to the DOM
var barDemo = d3.select("body").
append("svg").
attr("width", width).
attr("height", height);
function topRoundedRect(x, y, width, height, radius) {
return "M" + (x + radius) + "," + y
+ "h" + (width - (radius * 2))
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + radius
+ "v" + (height - radius)
+ "h" + (0-width)
+ "v" + (0-(height-radius))
+ "a" + radius + "," + radius + " 0 0 1 " + radius + "," + -radius
+ "z";
}
/*
barDemo.selectAll("rect").
data(data).
enter().
append("svg:rect").
attr("x", function(datum, index) { return x(index); }).
attr("y", function(datum) { return height - y(datum); }).
attr("height", function(datum) { return y(datum); }).
attr("width", barWidth).
attr("fill", "url(#barGradient)"); */
barDemo.selectAll("path").
data(data).
enter().append("path").
attr("d", topRoundedRect(
function(datum, index) { return x(index); },
function(datum) { return height - y(datum); },
barWidth,
function(datum) { return y(datum); },
3))
.style("fill","#ffcc00")
.style("stroke","none");

The argument for your attribute should be a function that takes the datum and index as parameters. Try:
attr("d", function(datum, index) {
return topRoundedRect( x(index),
height - y(datum),
barWidth,
y(datum),
3);
})

Related

Find the position to have the div overlapping with svg arrow

Do i need for that to Find distance between Two parallel lines. i couldnt understand the formula.
the line and the div are in the same width and are parallel.
how can i find the new position for the div to overlap the svg arrow.
-I have the start point and end point of the arrow.
i couldn't understand the Formula of distance between Two parallel lines.
examples in javascript with numeric example will be great thanks.
div {
position: absolute;
transform: rotate(324deg);
display: block;
left: 372.457px;
width: 311.086px;
top: 179.269px;
}
and the path is
<path _ngcontent-vxm-c52="" set-element-to-model="" marker-end="url(#arrowDefId)" class="main-path" ng-reflect-model="[object Object]" d=" M 625.4601140022278 192.3263964653015 C 625.4601140022278 192.3263964653015 372.45663499832153 373.333354473114 372.45663499832153 373.333354473114"></path>
// calculate the arritbute d of path
// this.element is the div.
//this.fromPos and this.toPos is the points of the arrow
private connectionCenter() {
const elementRect = this.element ? this.element.getBoundingClientRect() : null;
this.labelDeg = this.calculateAngle();
// set label with same width as arrow width.
this.labelWidth = distanceBetweenTwoPoints(this.fromPos, this.toPos);
this.labelX = (this.fromPos.x) - Math.abs(this.fromPos.x - this.toPos.x);
this.labelY = ((this.fromPos.y + this.toPos.y) /2 - (elementRect ? elementRect.height / 2 : 0));
console.log('connectionCenter', elementRect, this.labelX, this.labelY);
}
calculateAngle() {
let deg = Math.atan2(this.fromPos.y - this.toPos.y, this.fromPos.x - this.toPos.x) * (180 / Math.PI);
this.angle = deg;
return this.angle;
}
export function createCurvature(start_pos_x: number, start_pos_y: any, end_pos_x: number, end_pos_y: any, curvature_value = 0.5, type = 'openclose') {
const line_x = start_pos_x;
const line_y = start_pos_y;
const x = end_pos_x;
const y = end_pos_y;
const curvature = curvature_value;
let hx1 = null;
let hx2 = null;
if(start_pos_x >= end_pos_x) {
hx1 = line_x + Math.abs(x - line_x) * curvature;
hx2 = x - Math.abs(x - line_x) * (curvature*-1);
} else {
hx1 = line_x + Math.abs(x - line_x) * curvature;
hx2 = x - Math.abs(x - line_x) * curvature;
}
return ' M '+ line_x +' '+ line_y +' C '+ hx1 +' '+ line_y +' '+ hx2 +' ' + y +' ' + x +' ' + y;
}
On 0 Degree:
On 360 Degree:
On 270 Degree:
On 90 Degree:
i cant understand the logic. seems like its an issue with the degree.
only on degree 0 it works. maybe someone can find the pattern :/

How can I understand the following SVG code?

I have code from a web site, and it looks like it should be simple, but too simple for SVG. How can I determine if this is truly SVG, and what it does? I am especially interested in what looks like nested & and dots[.], then split, map.
Snippet:
// the shape of the dragon, converted from a SVG image
'! ((&(&*$($,&.)/-.0,4%3"7$;(#/EAA<?:<9;;88573729/7,6(8&;'.split("").map(function(a,i) {
shape[i] = a.charCodeAt(0) - 32;
});
Full code:
//7 Dragons
//Rauri
// full source for entry into js1k dragons: http://js1k.com/2014-dragons/demo/1837
// thanks to simon for grunt help and sean for inspiration help
// js1k shim
var a = document.getElementsByTagName('canvas')[0];
var b = document.body;
var d = function(e){ return function(){ e.parentNode.removeChild(e); }; }(a);
// unprefix some popular vendor prefixed things (but stick to their original name)
var AudioContext =
window.AudioContext ||
window.webkitAudioContext;
var requestAnimationFrame =
window.requestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function(f){ setTimeout(f, 1000/30); };
// stretch canvas to screen size (once, wont onresize!)
a.style.width = (a.width = innerWidth - 0) + 'px';
a.style.height = (a.height = innerHeight - 0) + 'px';
var c = a.getContext('2d');
// end shim
var sw = a.width,
sh = a.height,
M = Math,
Mc = M.cos,
Ms = M.sin,
ran = M.random,
pfloat = 0,
pi = M.PI,
dragons = [],
shape = [],
loop = function() {
a.width = sw; // clear screen
for ( j = 0; j < 7; j++) {
if ( !dragons[j] ) dragons[j] = dragon(j); // create dragons initially
dragons[j]();
}
pfloat++;
requestAnimationFrame(loop);
},
dragon = function(index) {
var scale = 0.1 + index * index / 49,
gx = ran() * sw / scale,
gy = sh / scale,
lim = 300, // this gets inlined, no good!
speed = 3 + ran() * 5,
direction = pi, //0, //ran() * pi * 2, //ran(0,TAU),
direction1 = direction,
spine = [];
return function() {
// check if dragon flies off screen
if (gx < -lim || gx > sw / scale + lim || gy < -lim || gy > sh / scale + lim) {
// flip them around
var dx = sw / scale / 2 - gx,
dy = sh / scale / 2 - gy;
direction = direction1 = M.atan(dx/dy) + (dy < 0 ? pi : 0);
} else {
direction1 += ran() * .1 - .05;
direction -= (direction - direction1) * .1;
}
// move the dragon forwards
gx += Ms(direction) * speed;
gy += Mc(direction) * speed;
// calculate a spine - a chain of points
// the first point in the array follows a floating position: gx,gy
// the rest of the chain of points following each other in turn
for (i=0; i < 70; i++) {
if (i) {
if (!pfloat) spine[i] = {x: gx, y: gy}
var p = spine[i - 1],
dx = spine[i].x - p.x,
dy = spine[i].y - p.y,
d = M.sqrt(dx * dx + dy * dy),
perpendicular = M.atan(dy/dx) + pi / 2 + (dx < 0 ? pi : 0);
// make each point chase the previous, but never get too close
if (d > 4) {
var mod = .5;
} else if (d > 2){
mod = (d - 2) / 4;
} else {
mod = 0;
}
spine[i].x -= dx * mod;
spine[i].y -= dy * mod;
// perpendicular is used to map the coordinates on to the spine
spine[i].px = Mc(perpendicular);
spine[i].py = Ms(perpendicular);
if (i == 20) { // average point in the middle of the wings so the wings remain symmetrical
var wingPerpendicular = perpendicular;
}
} else {
// i is 0 - first point in spine
spine[i] = {x: gx, y: gy, px: 0, py: 0};
}
}
// map the dragon to the spine
// the x co-ordinates of each point of the dragon shape are honoured
// the y co-ordinates of each point of the dragon are mapped to the spine
c.moveTo(spine[0].x,spine[0].y)
for (i=0; i < 154; i+=2) { // shape.length * 2 - it's symmetrical, so draw up one side and back down the other
if (i < 77 ) { // shape.length
// draw the one half from nose to tail
var index = i; // even index is x, odd (index + 1) is y of each coordinate
var L = 1;
} else {
// draw the other half from tail back to nose
index = 152 - i;
L = -1;
}
var x = shape[index];
var spineNode = spine[shape[index+1]]; // get the equivalent spine position from the dragon shape
if (index >= 56) { // draw tail
var wobbleIndex = 56 - index; // table wobbles more towards the end
var wobble = Ms(wobbleIndex / 3 + pfloat * 0.1) * wobbleIndex * L;
x = 20 - index / 4 + wobble;
// override the node for the correct tail position
spineNode = spine[ index * 2 - 83 ];
} else if (index > 13) { // draw "flappy wings"
// 4 is hinge point
x = 4 + (x-4) * (Ms(( -x / 2 + pfloat) / 25 * speed / 4) + 2) * 2; // feed x into sin to make wings "bend"
// override the perpindicular lines for the wings
spineNode.px = Mc(wingPerpendicular);
spineNode.py = Ms(wingPerpendicular);
}
c.lineTo(
(spineNode.x + x * L * spineNode.px) * scale,
(spineNode.y + x * L * spineNode.py) * scale
);
}
c.fill();
}
}
// the shape of the dragon, converted from a SVG image
'! ((&(&*$($,&.)/-.0,4%3"7$;(#/EAA<?:<9;;88573729/7,6(8&;'.split("").map(function(a,i) {
shape[i] = a.charCodeAt(0) - 32;
});
loop();
While the context this is used in is <canvas>, the origin may well be a SVG <polyline>.
In a first step, the letters are mapped to numbers. A bit of obscuration, but nothing too serious: get the number representing the letter and write it to an array.
const shape = [];
'! ((&(&*$($,&.)/-.0,4%3"7$;(#/EAA<?:<9;;88573729/7,6(8&;'.split("").map(function(a,i) {
shape[i] = a.charCodeAt(0) - 32;
});
results in an array
[1,0,8,8,6,8,6,10,4,8,4,12,6,14,9,15,13,14,16,12,20,5,19,2,23,4,27,8,32,15,37,33,33,28,31,26,28,25,27,27,24,24,21,23,19,23,18,25,15,23,12,22,8,24,6,27]
Now just write this array to a points attribute of a polyline, joining the numbers with a space character:
const outline = document.querySelector('#outline');
const shape = [];
'! ((&(&*$($,&.)/-.0,4%3"7$;(#/EAA<?:<9;;88573729/7,6(8&;'.split("").map(function(a,i) {
shape[i] = a.charCodeAt(0) - 32;
});
outline.setAttribute('points', shape.join(' '))
#outline {
stroke: black;
stroke-width: 0.5;
fill:none;
}
<svg viewBox="0 0 77 77" width="300" height="300">
<polyline id="outline" />
</svg>
and you get the basic outline of (half) a dragon. The rest is repetition and transformation to make things a bit more complex.

Wrapping long text in d3.js

I want to wrap long text elements to a width. The example here is taken from Bostock's wrap function, but seems to have 2 problems: firstly the result of wrap has not inherited the element's x value (texts are shifted left); secondly it's wrapping on the same line, and lineHeight argument has no effect.
Grateful for suggestions. http://jsfiddle.net/geotheory/bk87ja3g/
var svg = d3.select("body").append("svg")
.attr("width", 300)
.attr("height", 300)
.style("background-color", '#ddd');
dat = ["Ukip has peaked, but no one wants to admit it - Nigel Farage now resembles every other politician",
"Ashley Judd isn't alone: most women who talk about sport on Twitter face abuse",
"I'm on list to be a Mars One astronaut - but I won't see the red planet"];
svg.selectAll("text").data(dat).enter().append("text")
.attr('x', 25)
.attr('y', function(d, i){ return 30 + i * 90; })
.text(function(d){ return d; })
.call(wrap, 250);
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 1,
lineHeight = 1.2, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
Bostock's original function assumes that the text element has an initial dy set. It also drops any x attribute on the text. Finally, you changed the wrap function to start at lineNumber = 1, that needs to be 0.
Refactoring a bit:
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0, //<-- 0!
lineHeight = 1.2, // ems
x = text.attr("x"), //<-- include the x!
y = text.attr("y"),
dy = text.attr("dy") ? text.attr("dy") : 0; //<-- null check
tspan = text.text(null).append("tspan").attr("x", x).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", x).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
Updated fiddle.
The problem is this line:
dy = parseFloat(text.attr("dy"))
In the example you've linked to, dy is set on the text elements, but not in your case. So you're getting NaN there which in turn causes the dy for the tspan to be NaN. Fix by assigning 0 to dy if NaN:
dy = parseFloat(text.attr("dy")) || 0
Complete demo here.

D3 force layout with CSS positioning

I'm placing circles on a map corresponding to GPS coordinates. Each circle is contained within an svg container which is placed on the page using CSS top and left properties. In my implementation, these containers often sit atop one another.
I am trying to implement collision detection and/or add a slight negative charge to these containers so that overlaps cause containers to distance themselves from one another.
Thus far, my tests with force layouts have either resulted in no change, or resulted in an error ('cannot set property index of null' or 'cannot set property x of null'). It's apparent that I'm doing something wrong but I have been unable to identify a path to resolution from the articles I've read online.
Any ideas on how I can stop the containers from sitting atop one another?
var self = this;
var data = [{lat: 127, lon: 36, name: 'a', radius: 9},{lat:127, lon: 36, name: 'b', radius: 9}];
// Position SVG containers correctly
var latLngToPx = function(d) {
var temp = new google.maps.LatLng(d.lat, d.lon);
temp = self.map.projection.fromLatLngToDivPixel(temp);
d.x = temp.x;
d.y = temp.y;
return d3.select(this)
.style('left', d.x + 'px')
.style('top', d.y + 'px');
};
var collide = function(node) {
var r = node.radius + 16,
nx1 = node.x - r,
nx2 = node.x + r,
ny1 = node.y - r,
ny2 = node.y + r;
return function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = Math.sqrt(x * x + y * y),
r = node.radius + quad.point.radius;
if (l < r) {
l = (l - r) / l * 0.5;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
};
}
var svgBind = d3.select(settings[type].layer).selectAll('svg')
.data(data, function(d){ return d.name; })
.each(latLngToPx);
var svg = svgBind.enter().append('svg')
.each(latLngToPx)
// svg[0] contains the svg elements
var nodes = svg[0];
var force = d3.layout.force()
.nodes(nodes)
.charge(-100)
.start();
force.on('tick', function(){
var q = d3.geom.quadtree(nodes),
i = 0,
n = nodes.length;
while (++i < n) {
q.visit(collide(nodes[i]));
}
svg
.style('left', function(d){ return (d.x - lm.config.offset) + 'px';})
.style('top', function(d){ return (d.y - lm.config.offset) + 'px';});
});
var circ = svg.append('circle')
.attr('r', settings[type].r)
.attr('cx',10)
.attr('cy',10)
You shouldn't need to do the collision detection yourself -- the force layout should take care of that for you. Here are the basic steps you need to take.
To each data element that represents a circle, add x and y members that contain their current (screen) coordinates. This is what the force layout will operate on.
Pass the array of these elements to the force layout as nodes. There's no need to set links to start with, although you might want to do so later to control the placement of nodes with respect to each other.
Start the force layout.
For each tick, redraw the elements at the appropriate position.
Tweak the parameters of the force layout to your liking.
You are doing most of this already, I'm just mentioning it again to clarify. The code would look something like this.
function latLngToPx(d) {
var temp = new google.maps.LatLng(d.lat, d.lon);
temp = self.map.projection.fromLatLngToDivPixel(temp);
d.x = temp.x;
d.y = temp.y;
};
data.forEach(function(d) { latLngToPx(d); });
var nodes = d3.select("body").selectAll("svg").data(data).enter().append("svg");
var force = d3.layout.force().nodes(data);
force.on("tick", function() {
nodes.style('left', function(d){ return (d.x - lm.config.offset) + 'px';})
.style('top', function(d){ return (d.y - lm.config.offset) + 'px';});
});

How do I adjust my SVG transform based on the viewport?

I'm working with the d3 library and have had success working with the chloropleth example, as well as getting a click action to zoom in to a particular state (see this question for details). In particular, here is the code I'm using for my click to zoom event on a state:
// Since height is smaller than width,
var baseWidth = 564;
var baseHeight = 400;
d3.selectAll('#states path')
.on('click', function(d) {
// getBBox() is a native SVG element method
var bbox = this.getBBox(),
centroid = [bbox.x + bbox.width/2, bbox.y + bbox.height/2],
// since height is smaller than width, I scale based off of it.
zoomScaleFactor = baseHeight / bbox.height,
zoomX = -centroid[0],
zoomY = -centroid[1];
// set a transform on the parent group element
d3.select('#states')
.attr("transform", "scale(" + scaleFactor + ")" +
"translate(" + zoomX + "," + zoomY + ")");
});
However, when I click to view on the state, my transform is not in the center of my viewport, but off to the top left, and it might not have the proper scale to it as well. If I make minor adjustments manually to the scaleFactor or zoomX/zoomY parameters, I lose the item altogether. I'm familiar with the concept that doing a scale and transform together can have significantly different results, so I'm not sure how to adjust.
The only other thing I can think of is that the original chloropleth image is set for a 960 x 500 image. To accomodate for this. I create an albersUSA projection and use my d3.geo.path with this projection and continue to add my paths accordingly.
Is my transform being affected by the projection? How would I accomodate for it if it was?
The scale transform needs to be handled like a rotate transform (without the optional cx,cy parameters), that is, the object you want to transform must first be moved to the origin.
d3.select('#states')
.attr("transform",
"translate(" + (-zoomX) + "," + (-zoomY) + ")" +
"scale(" + scaleFactor + ")" +
"translate(" + zoomX + "," + zoomY + ")");
For futher reference,
I found this article where you should find how to use the matrix transformation to achieve zoom and pan effects very simple.
Excerption:
<script type="text/ecmascript">
<![CDATA[
var transMatrix = [1,0,0,1,0,0];
function init(evt)
{
if ( window.svgDocument == null )
{
svgDoc = evt.target.ownerDocument;
}
mapMatrix = svgDoc.getElementById("map-matrix");
width = evt.target.getAttributeNS(null, "width");
height = evt.target.getAttributeNS(null, "height");
}
]]>
</script>
function pan(dx, dy)
{
transMatrix[4] += dx;
transMatrix[5] += dy;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}
function zoom(scale)
{
for (var i=0; i<transMatrix.length; i++)
{
transMatrix[i] *= scale;
}
transMatrix[4] += (1-scale)*width/2;
transMatrix[5] += (1-scale)*height/2;
newMatrix = "matrix(" + transMatrix.join(' ') + ")";
mapMatrix.setAttributeNS(null, "transform", newMatrix);
}

Resources