I am trying to replace a d3 legend drawing function which used manual creation and placement of the legend elements to one using .data().
I was able to get everything working, except one thing: with manually looping through the data array, I was able to ensure equal spacing between the legend elements by using a currentX tracker variable. With .data() however, I can't reference the other elements, and have to use fixed-width boxes, which the designer hates (and me too).
Given the code below, how do I make the new code (top row) behave exactly like the old code (bottom row)?
(Please do not suggest that I use a legend library, there is lots of event-handling and more stuff in the "real" code upon which this testcase was extracted)
var config = {
circleYCenter: 170,
circleXCenter: 150,
legendMarginTop: 52,
legendMarginInner: 18,
legendDotHeight: 8,
legendDotWidth: 16,
};
var colors = {
'lightyellow': '#FEE284',
'darkblue': '#2872A3',
'dirtyorange': '#E68406',
};
function drawLegend(container, map) {
var legendGroup = container.append("g").attr({
"transform": "translate(0,50)",
"class": "legendGroup"
});
//New code
var elGroup = legendGroup.selectAll("g").data(map).enter().append("g").attr({
transform: function(d, i) {
return "translate(" + (i * 100) + ",0)"
}
});
elGroup.append("text").text(function(d) {
return d.label;
}).attr({
x: config.legendDotWidth + config.legendMarginInner
});
elGroup.append("rect").attr({
x: 0,
width: config.legendDotWidth,
y: -5,
height: config.legendDotHeight,
"fill": function(d) {
return d.color;
}
});
//Old code
var currentX = 0;
map.forEach(function(el, key) {
var elGroup = legendGroup.append("g").attr("transform", "translate(" + currentX + ",100)");
elGroup.append("rect").attr({
x: 0,
width: config.legendDotWidth,
y: -5,
height: config.legendDotHeight,
"fill": el.color
});
elGroup.append("text").attr({
"x": (config.legendDotWidth + 10),
"y": 0,
"alignment-baseline": "middle"
}).text(el.label);
currentX += elGroup.node().getBBox().width + config.legendMarginInner;
});
}
drawLegend(d3.select("svg"), [{
label: "foo",
color: colors.dirtyorange
}, {
label: "Banana",
color: colors.darkblue
}, {
label: "baz",
color: colors.lightyellow
}]);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<svg width=600 height=300></svg>
First make the group:
var elGroup = legendGroup.selectAll("g").data(map).enter().append("g");
Append the text and rect DOM as you are doing:
elGroup.append("text").text(function(d) {
return d.label;
}).attr({
x: config.legendDotWidth + config.legendMarginInner
});
elGroup.append("rect").attr({
x: 0,
width: config.legendDotWidth,
y: -5,
height: config.legendDotHeight,
"fill": function(d) {
return d.color;
}
});
Now add the translate to the group using the bbox of the group.
var currentX = 0;
elGroup.attr({
transform: function(d, i) {
var ret = "translate(" + currentX + ",0)"
currentX += this.getBBox().width + config.legendMarginInner;
return ret;
}
});
working code here
Related
I'm using D3 v4 and can't get quadtree to work. It keeps returning undefined for the root. I don't think it likes the data I'm giving it.
const Nodes = [];
for (let i = 0; i < 10; i++) {
Nodes.push({
radius: 5,
x: Math.random() * 500,
y: Math.random() * 500,
velocityX: Math.random(),
velocityY: Math.random()
});
}
collide() {
var quadtree = d3.quadtree().extent([
[
0, 0
],
[1500, 1000]
]).addAll(Nodes);
console.log(quadtree);
}
Let's have a look at any given object in your Nodes array:
[{
radius: 5,
x: 301.25792388143293,
y: 35.626900264457696,
velocityX: 0.43542026096574715,
velocityY: 0.03662733324854717
}]
As you can see, the x and y coordinates are defined in properties with "x" and "y" as key.
However, this is the default function for the x coordinate in d3.quadtree:
function x(d) {
return d[0];
}
And for the y coordinate:
function y(d) {
return d[1];
}
As you can see, those functions won't work with your object structure.
Solution:
Set the x and y coordinates according to your object:
var quadtree = d3.quadtree()
.x(function(d) {
return d.x
})
.y(function(d) {
return d.y
})
Here is your code with that change, check the console:
const Nodes = [];
for (let i = 0; i < 10; i++) {
Nodes.push({
radius: 5,
x: Math.random() * 500,
y: Math.random() * 500,
velocityX: Math.random(),
velocityY: Math.random()
});
}
var quadtree = d3.quadtree().extent([
[
0, 0
],
[1500, 1000]
]).x(function(d) {
return d.x
})
.y(function(d) {
return d.y
})
.addAll(Nodes);
console.log(quadtree);
<script src="https://d3js.org/d3.v4.min.js"></script>
I am plotting some points using d3. I want to change the shape off all the points based on some condition. The join looks a bit like this:
var data=[{x:10,y:10}, {x:20, y:30}];
var shape = "rect";
...
var point = svg.selectAll(".point")
.data(data, function(d, idx) { return "row_" + idx + "_shape_" + shape;})
;
The d3 enter() and exit() selections do not seem to reflect any changes caused by "shape" changing.
Fiddle is here: http://jsfiddle.net/schmoo2k/jcpctbty/
You need to be aware that the key function is calculated on the selection with this as the SVG element and then on the data with the data array as this.
I think maybe this is what you are trying to do...
var data = [{
x: 10,
y: 10
}, {
x: 20,
y: 30
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500);
function update(data, shape) {
var point = svg.selectAll(".point")
.data(data, function(d, idx) {
var key = "row_" + idx + "_shape_" + (Array.isArray(this) ? "Data: " + shape :
d3.select(this).attr("shape"));
alert(key);
return key;
});
alert("enter selection size: " + point.enter().size());
point.enter().append(shape)
.attr("class", "point")
.style("fill", "red")
.attr("shape", shape);
switch (shape) {
case "rect":
point.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("width", 5)
.attr("height", 5);
break;
case "circle":
point.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", 5);
break;
}
point.exit().remove();
}
update(data, "rect");
setTimeout(function() {
update(data, "circle");
}, 5000);
text {
font: bold 48px monospace;
}
.enter {
fill: green;
}
.update {
fill: #333;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.js"></script>
Abstracted version
Just to tidy things up here is a more readable and idiomatic version (including fixing a problem with the text element)...
var data = [{
x: 10,
y: 10,
}, {
x: 20,
y: 30,
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500),
marker = Marker();
function update(data, shape) {
var point = svg.selectAll(".point")
.data(data, key("shape", shape)),
enter = point.enter().append("g")
.attr("class", "point")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")"
})
.attr("shape", shape);
enter.append(shape)
.style("fill", "red")
.attr(marker.width[shape], 5)
.attr(marker.height[shape], 5);
enter.append("text")
.attr({
"class": "title",
dx: 10,
"text-anchor": "start"
})
.text(shape);
point.exit().remove();
}
update(data, "rect");
setTimeout(function() {
update(data, "circle");
}, 2000);
function Marker() {
return {
width: {
rect: "width",
circle: "r"
},
height: {
rect: "height",
circle: "r"
},
shape: function(d) {
return d.shape
},
};
}
function key(attr, value) {
//join data and elements where value of attr is value
function _phase(that) {
return Array.isArray(that) ? "data" : "element";
}
function _Type(that) {
return {
data: value,
get element() {
return d3.select(that).attr(attr)
}
}
}
return function(d, i, j) {
var _value = _Type(this)
return i + "_" + _value[_phase(this)];
};
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
Generalised, data-driven approach
var data = [{
x: 10,
y: 10,
}, {
x: 20,
y: 30,
}];
var svg = d3.select("body").append("svg")
.attr("width", 500)
.attr("height", 500),
marker = Marker();
function update(data, shape) {
//data-driven approach
data.forEach(function(d, i) {
d.shape = shape[i]
});
var log = [],
point = svg.selectAll(".point")
.data(data, key({
shape: marker.shape,
transform: marker.transform
}, log)),
//UPDATE
update = point.classed("update", true),
updateSize = update.size();
update.selectAll("text").transition().duration(1000).style("fill", "#ccc");
update.selectAll(".shape").transition().duration(1000).style("fill", "#ccc")
//ENTER
var enter = point.enter().append("g")
.classed("point enter", true)
.attr("transform", marker.dock)
.attr("shape", marker.shape),
//UPDATE+ENTER
// ... not required on this occasion
updateAndEnter = point.classed("update-enter", true);
//EXIT
var exit = point.exit().classed("exit", true);
exit.selectAll("text").transition().duration(1000).style("fill", "red");
exit.selectAll(".shape").transition().duration(1000).style("fill", "red");
exit.transition().delay(1000.).duration(1000)
.attr("transform", marker.dock)
.remove();
//ADJUSTMENTS
enter.each(function(d) {
//append the specified shape for each data element
//wrap in each so that attr can be a function of the data
d3.select(this).append(marker.shape(d))
.style("fill", "green")
.classed("shape", true)
.attr(marker.width[marker.shape(d)], 5)
.attr(marker.height[marker.shape(d)], 5)
});
enter.append("text")
.attr({
"class": "title",
dx: 10,
"text-anchor": "start"
})
.text(marker.shape)
.style("fill", "green")
.style("opacity", 1);
enter.transition().delay(1000).duration(2000)
.attr("transform", marker.transform);
}
data = generateData(40, 10)
update(data, data.map(function(d, i) {
return ["rect", "circle"][Math.round(Math.random())]
}));
setInterval(function() {
update(data, data.map(function(d, i) {
return ["rect", "circle"][Math.round(Math.random())]
}));
}, 5000);
function generateData(n, p) {
var values = [];
for (var i = 0; i < n; i++) {
values.push({
x: (i + 1) * p,
y: (i + 1) * p
})
}
return values;
};
function Marker() {
return {
x: {
rect: "x",
circle: "cx"
},
y: {
rect: "y",
circle: "cy"
},
width: {
rect: "width",
circle: "r"
},
height: {
rect: "height",
circle: "r"
},
shape: function(d) {
return d.shape
},
transform: function(d) {
return "translate(" + f(d.x) + "," + f(d.y) + ")"
},
dock: function(d) {
return "translate(" + (d.x + 800) + "," + (d.y + 100) + ")"
}
};
function f(x) {
return d3.format(".0f")(x)
}
}
function key(attr, value, log) {
//join data and elements where value of attr is value
function _phase(that) {
return Array.isArray(that) ? "data" : "element";
}
function _Key(that) {
if (plural) {
return {
data: function(d, i, j) {
var a, key = "";
for (a in attr) {
key += (typeof attr[a] === "function" ? attr[a](d, i, j) : attr[a]);
}
return key;
},
element: function() {
var a, key = "";
for (a in attr) {
key += d3.select(that).attr(a);
}
return key;
}
}
} else {
return {
data: function(d, i, j) {
return typeof value === "function" ? value(d, i, j) : value;
},
element: function() {
return d3.select(that).attr(attr)
}
}
}
}
var plural = typeof attr === "object";
if (plural && arguments.length === 2) log = value;
return function(d, i, j) {
var key = _Key(this)[_phase(this)](d, i, j);
if (log) log.push(i + "_" + _phase(this) + "_" + key);
return key;
};
}
text {
font: bold 12px monospace;
fill: black;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
I started working with this d3.js Donut Chart: JSFiddleI am trying to change it into a Pie Chart without the circle in the middle. I am new to d3.js. I have tried several different ideas but have been unable to get this to remove the circle in the middle of the chart. Any and all help is appreciated
Here is my code:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
<style>
.label-text {
alignment-baseline : middle;
font-size: 12px;
font-family: arial,helvetica,"sans-serif";
fill: #393939;
}
.label-line {
stroke-width: 1;
stroke: #393939;
}
.label-circle {
fill: #393939;
}
</style>
<script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
<svg>
<g id="canvas">
<g id="art" />
<g id="labels" /></g>
</svg>
<script>
var data = [{
label: 'Star Wars',
instances: 207
}, {
label: 'Lost In Space',
instances: 3
}, {
label: 'the Boston Pops',
instances: 20
}, {
label: 'Indiana Jones',
instances: 150
}, {
label: 'Harry Potter',
instances: 75
}, {
label: 'Jaws',
instances: 5
}, {
label: 'Lincoln',
instances: 1
}];
svg = d3.select("svg");
canvas = d3.select("#canvas");
art = d3.select("#art");
labels = d3.select("#labels");
// Create the pie layout function.
// This function will add convenience
// data to our existing data, like
// the start angle and end angle
// for each data element.
jhw_pie = d3.layout.pie();
jhw_pie.sort(null);
jhw_pie.value(function (d) {
// Tells the layout function what
// property of our data object to
// use as the value.
return d.instances;
});
// Store our chart dimensions
cDim = {
height: 500,
width: 500,
innerRadius: 50,
outerRadius: 150,
labelRadius: 175
}
// Set the size of our SVG element
svg.attr({
height: cDim.height,
width: cDim.width
});
// This translate property moves the origin of the group's coordinate
// space to the center of the SVG element, saving us translating every
// coordinate individually.
canvas.attr("transform", "translate(" + (cDim.width / 2) + "," + (cDim.height / 2) + ")");
pied_data = jhw_pie(data);
// The pied_arc function we make here will calculate the path
// information for each wedge based on the data set. This is
// used in the "d" attribute.
pied_arc = d3.svg.arc()
.innerRadius(50)
.outerRadius(150);
// This is an ordinal scale that returns 10 predefined colors.
// It is part of d3 core.
pied_colors = d3.scale.ordinal()
.range(["#04B486", "#F2F2F2", "#F5F6CE", "#00BFFF","orange","purple","pink"]);
// Let's start drawing the arcs.
enteringArcs = art.selectAll(".wedge").data(pied_data)
.enter();
enteringArcs
.append("g")
.attr("class", "wedge")
.append("path")
.attr("d", pied_arc)
.style("fill", function (d, i) {
return pied_colors(i);
});
// Now we'll draw our label lines, etc.
enteringLabels = labels.selectAll(".label").data(pied_data).enter();
labelGroups = enteringLabels.append("g").attr("class", "label");
labelGroups.append("circle").attr({
x: 0,
y: 0,
r: 2,
fill: "#000",
transform: function (d, i) {
centroid = pied_arc.centroid(d);
return "translate(" + pied_arc.centroid(d) + ")";
},
'class': "label-circle"
});
// "When am I ever going to use this?" I said in
// 10th grade trig.
textLines = labelGroups.append("line").attr({
x1: function (d, i) {
return pied_arc.centroid(d)[0];
},
y1: function (d, i) {
return pied_arc.centroid(d)[1];
},
x2: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
x = Math.cos(midAngle) * cDim.labelRadius;
return x;
},
y2: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
y = Math.sin(midAngle) * cDim.labelRadius;
return y;
},
'class': "label-line"
});
textLabels = labelGroups.append("text").attr({
x: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
x = Math.cos(midAngle) * cDim.labelRadius;
sign = (x > 0) ? 1 : -1
labelX = x + (5 * sign)
return labelX;
},
y: function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
y = Math.sin(midAngle) * cDim.labelRadius;
return y;
},
'text-anchor': function (d, i) {
centroid = pied_arc.centroid(d);
midAngle = Math.atan2(centroid[1], centroid[0]);
x = Math.cos(midAngle) * cDim.labelRadius;
return (x > 0) ? "start" : "end";
},
'class': 'label-text'
}).text(function (d) {
return d.data.label
});
alpha = 0.5;
spacing = 12;
function relax() {
again = false;
textLabels.each(function (d, i) {
a = this;
da = d3.select(a);
y1 = da.attr("y");
textLabels.each(function (d, j) {
b = this;
// a & b are the same element and don't collide.
if (a == b) return;
db = d3.select(b);
// a & b are on opposite sides of the chart and
// don't collide
if (da.attr("text-anchor") != db.attr("text-anchor")) return;
// Now let's calculate the distance between
// these elements.
y2 = db.attr("y");
deltaY = y1 - y2;
// Our spacing is greater than our specified spacing,
// so they don't collide.
if (Math.abs(deltaY) > spacing) return;
// If the labels collide, we'll push each
// of the two labels up and down a little bit.
again = true;
sign = deltaY > 0 ? 1 : -1;
adjust = sign * alpha;
da.attr("y", +y1 + adjust);
db.attr("y", +y2 - adjust);
});
});
// Adjust our line leaders here
// so that they follow the labels.
if (again) {
labelElements = textLabels[0];
textLines.attr("y2", function (d, i) {
labelForLine = d3.select(labelElements[i]);
return labelForLine.attr("y");
});
setTimeout(relax, 20)
}
}
relax();
</script>
</body>
</html>
Thanks
See this updated fiddle.
The code contained the following lines, of which the innerRadious was changed to 0.
pied_arc = d3.svg.arc()
.innerRadius(00) // <- this
.outerRadius(150);
It's a bit misleading, as there's an innerRadius variable somewhere before that, but it's not used at this point. While you're at it, you might want to align all of that stuff.
I have this D3 code snippet that draws a table from data.
self.displayStateTable = function (stateTable) {
d3.select('#VisualizationSVG')
.selectAll('rect').data(stateTable).enter().append('rect')
.attr({
x: function (d, i) { return self.positionStateTableTile(d).x; },
y: function (d, i) { return self.positionStateTableTile(d).y; },
width: self.stateTableConfig.TileWidth,
height: self.stateTableConfig.TileHeight,
fill: function (d, i) { return self.positionStateTableTile(d).fill; }
});
};
The function positionStateTableTile which works out the x position, the y position, and the fill for each datum's tile is run three times for each datum when it only needs to be run once. How can I change this so that D3 only calls positionStateTableTile once for each datum instead of three times?
Typical - no sooner had posted the question than the answer seemed obvious!
I can run the function once for each datum first and store the results in an array and use that to set the attributes in D3.
self.displayStateTable = function (stateTable) {
var i = 0, stateTableTilePositions = [];
for (i = 0; i < stateTable.length; i++) {
stateTableTilePositions[i] = self.positionStateTableTile(stateTable[i]);
}
d3.select('#VisualizationSVG')
.selectAll('rect').data(stateTable).enter().append('rect')
.attr({
x: function (d, i) { return stateTableTilePositions[i].x; },
y: function (d, i) { return stateTableTilePositions[i].y; },
width: self.stateTableConfig.TileWidth,
height: self.stateTableConfig.TileHeight,
fill: function (d, i) { return stateTableTilePositions[i].fill; }
});
};
I'd like your assistance in building a kind of 'gauge', an 'arc' based circle that would populate a value from 0 to 100 dynamically (8 gauges).
I was able to get this working (based on Arc Clock gist https://gist.github.com/mbostock/1098617), but now I'm trying to force a transition on every update and on the start.
I'm trying to implement the following flow:
1. arch loaded - goes from 0 to 100 -> from 100 to initial value
2. arch updated - goes from previous value to 0 -> from 0 to new value
Can't seem to find the right way to implement this...
The values are currently being inserted at random (10 increments)
var w = 1500,
h = 300,
x = d3.scale.ordinal().domain(d3.range(8)).rangePoints([0, w], 2);
var fields = [
{ name: "A", value: 100, previous: 0, size: 100 },
{ name: "B", value: 100, previous: 0, size: 100 },
{ name: "C", value: 100, previous: 0, size: 100 },
{ name: "D", value: 100, previous: 0, size: 100 },
{ name: "E", value: 100, previous: 0, size: 100 },
{ name: "F", value: 100, previous: 0, size: 100 },
{ name: "G", value: 100, previous: 0, size: 100 },
{ name: "H", value: 100, previous: 0, size: 100 }
];
var arc = d3.svg.arc()
.innerRadius(40)
.outerRadius(60)
.startAngle(0)
.endAngle(function (d) { return (d.value / d.size) * 2 * Math.PI; });
var svg = d3.select("body").append("svg:svg")
.attr("width", w)
.attr("height", h)
.append("svg:g")
.attr("transform", "translate(0," + (h / 2) + ")");
var path = svg.selectAll("path")
.data(fields.filter(function (d) { return d.value; }), function (d) { return d.name; })
.enter().append("svg:path")
.attr("transform", function (d, i) { return "translate(" + x(i) + ",0)"; })
.transition()
.ease("liniar")
.duration(750)
.attrTween("d", arcTween);
setTimeout(function () { services() }, 750);
setInterval(function () { services(); }, 5000);
function services() {
for (var i = 0; i < fields.length; i++) {
fields[i].previous = fields[i].value;
fields[i].value = Math.floor((Math.random() * 100) + 1);
}
path = svg.selectAll("path").data(fields.filter(function (d) { return d.value; }), function (d) { return d.name; });
path.transition()
.ease("linear")
.duration(1600)
.attrTween("d", arcTweenReversed);
}
function arcTween(b) {
var i = d3.interpolate({ value: b.previous }, b);
return function (t) {
return arc(i(t));
};
}
Here is JSFiddle to see it live: http://jsfiddle.net/p5xWZ/2/
Thanks in advance!
Something like the following chain transition could complete the arc and then go back to the next value:
path.transition()
.ease("linear")
.duration(function(d, i) { return 1600 * ((d.size-d.value)/d.size); })
.delay(function(d, i) { return i * 10; })
.attrTween("d", completeArc)
.transition()
.ease("linear")
.duration(function(d, i) { return 1600 * (d.value/d.size); })
.attrTween("d", resetArc)
.style("fill", function (d) { if (d.value < 100) { return "green"; } else { return "red" } });
Where completing the arc goes to 100, and resetting the arc goes from 0 to the next value:
function completeArc(b) {
// clone the data for the purposes of interpolation
var newb = $.extend({}, b);
// Set to 100
newb.value = newb.size;
var i = d3.interpolate({value: newb.previous}, newb);
return function(t) {
return arc(i(t));
};
}
function resetArc(b) {
var i = d3.interpolate({value: 0}, b);
return function(t) {
return arc(i(t));
};
}
Fiddle here also with fill color added.