Data
MP,Party,Constituency,Status
"Abbott, Diane",Labour,Hackney North and Stoke Newington,Remain
"Abrahams, Debbie",Labour,Oldham East and Saddleworth,Remain
"Adams, Nigel",Conservative,Selby and Ainsty,Leave
I have created a svg group for each 'Party' populated with many circles to represent each 'MP' belonging to that Party.
The problem I have is that some of the parties are so large that they run right off the screen. Ideally I would like to set the width at about 10 circles before they return to the next 'line'.
I have found examples for setting the width of SVG text but not SVG shapes. Is it possible to create multiple lines of SVG shapes using D3?
Plunker
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height)
var chart = svg
.append('g')
.attr('transform', 'translate(50,100)')
var party = chart.selectAll(".party")
.data(mps)
.enter()
.append('g')
.attr('transform', function(d, i) {
return "translate(0," + (i * (height / mps.length) + ")")
})
party
.append('text')
.text(function(d) {
return d.key
})
party.selectAll('.members')
.data(function(d) {
return d.values
})
.enter()
.append('circle')
.attr('class', 'chart')
.attr('r', 5)
.attr('cy', '10')
.style('fill', function(d) {
return '#' + d.Color
})
.attr("transform", function(d, i) {
return "translate(" + i * 13 + ",20)"
});
Something like this perhaps? You'll likely have to adjust the values to suit.
.attr('transform', function(d, i) {
return "translate(" + ((i / 10) * 20)) + "," + ((i % 10) * (height / mps.length)) + ")")
Related
I have created a d3 visualization that takes an json data, create a rect for each data point, and then displays the text in the rect. However, drag
works only for the 1st rect.
I am wondering how to acts a natural drag action for each rect.
my codepen project: https://codepen.io/moriakijp/project/editor/ZRnVwr
here is the code:
drawNumbers = layout => {
const width = innerWidth;
const height = width * 0.5;
const margin = {
top: height * 0.05,
bottom: height * 0.05,
left: width * 0.05,
right: width * 0.05
};
d3.json(layout).then(data => {
const colsize = data[data.length-1].col;
const rowsize = data[data.length-1].row;
const blocksize = colsize < rowsize ?
(width - margin.left - margin.right) / colsize:
(height - margin.left - margin.right) / rowsize;
function dragstarted(d) {
}
function dragged(d) {
d3
.select(this)
.select("rect")
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
d3
.select(this)
.select("text")
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
}
const dragended = (d) => {
}
const drag = d3
.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
const svg = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter()
.append("g")
.call(drag)
svg
.selectAll("g")
.data(data)
.enter()
.append("rect")
.attr("id", "block")
.attr("class", "block")
.attr("x", (d, i) => blocksize * (i % colsize)) // relative to 'svg'
.attr("y", (d, i) => blocksize * (data[i].row - 1)) // relative to 'svg'
.attr("width", blocksize)
.attr("height", blocksize)
.attr("fill", "#d00a")
.style("opacity", 0.5)
.attr("stroke", "#000")
.attr("stroke-width", "2")
svg
.selectAll("g")
.data(data)
.enter()
.append("text")
.attr("id", "text")
.attr("class", "text")
.text(d => `${d.char}`)
.attr("x", (d, i) => blocksize * (i % colsize))
.attr("y", (d, i) => blocksize * (data[i].row - 1))
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#333")
.attr("dx", blocksize / 2)
.attr("dy", blocksize / 2)
.style("font-size", blocksize / 2 );
});
};
drawNumbers('number.json');
You aren't quite using the enter pattern correctly if you want to take "data, create a rect for each data point, and then displays the text in the rect."
Let's break down what you have:
const svg = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter()
.append("g")
.call(drag)
Here you select the element with id heatmap append an svg, and then enter a g for each item in your data array. As such, svg is a selection of three g elements, and you call the drag on these g elements.
Next you take this selection of three g elements and select child g elements. As there are no child g elements (this is an empty selection), entering and appending (rects), creates three child rectangles for each g in the selection svg:
svg
.selectAll("g")
.data(data)
.enter()
.append("rect")
....
You do the same thing with the text. Now we have 9 rectangles and 9 texts, three each in each of the parent g elements (held the selection svg). Each of those parent g elements has a drag function that positions the first rectangle in it:
d3
.select(this)
.select("rect") // select first matching element
.attr("x", (d.x = d3.event.x))
.attr("y", (d.y = d3.event.y));
As each g has three rectangles, only the first one will be moved.
One solution would be to not do an enter cycle for each g in svg: your data is not nested, we already have a g for each item in the data array. So we just need to append a single text element and a single rect element to each g:
svg.append("rect").attr("x", function(d) {...
The data bound originally to the g is also bound to this child element, no need to rebind data. Though, I'd rename svg to something else so that it is more reflective of its role and contents though.
Overall this might look something like:
const g = d3
.select("#heatmap")
.append("svg")
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${margin.left}, ${margin.top})`)
.selectAll("g")
.data(data)
.enter() // create a g for each item in the data array
.append("g")
.call(drag)
// add a rect to each g
g.append("rect")
.attr("id", "block")
.attr("class", "block")
.attr("x", (d, i) => blocksize * (i % colsize)) // relative to 'svg'
.attr("y", (d, i) => blocksize * (data[i].row - 1)) // relative to 'svg'
.attr("width", blocksize)
.attr("height", blocksize)
.attr("fill", "#d00a")
.style("opacity", 0.5)
.attr("stroke", "#000")
.attr("stroke-width", "2")
// add text to each g
g.append("text")
.attr("id", "text")
.attr("class", "text")
.text(d => `${d.char}`)
.attr("x", (d, i) => blocksize * (i % colsize))
.attr("y", (d, i) => blocksize * (data[i].row - 1))
.attr("text-anchor", "middle")
.attr("dominant-baseline", "middle")
.attr("fill", "#333")
.attr("dx", blocksize / 2)
.attr("dy", blocksize / 2)
.style("font-size", blocksize / 2 );
Here's a running example with the above modification.
I'm building a d3 pie chart with labels on the centroids just like so many examples. I've checked my code against many of those examples but can't figure out where or how my centroids are being calculated. The labels appear to be arranged around the origin of the svg and not the center of the chart like I'd expect. I feel like there's a grouping issue since the text is added but it's not grouped with the arc. Unfortunately, I have no idea how to fix it.
var arcs = svg.datum(domain)
.selectAll('path')
.data(pie);
arcs.enter()
.append('path')
.attr('fill', function(d, i){
return color[i];
})
.attr('d', arc)
.each(function(d) {
this._current = d;
}) // store the initial angles
.attr('transform', 'translate(' + outerRadius + ", " + outerRadius + ')');
arcs.enter()
.append("text")
.attr("transform", function(d) {
console.log(arc.centroid(d));
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function(d,i) {
return data[i].playerName+": "+data[i].playerScore;
});
Here's a fiddle with the complete code.
You need to also move the text over to adjust for the radius of the chart:
arcs.enter()
.append("text")
.attr("transform", function(d) {
console.log(arc.centroid(d));
return "translate(" + arc.centroid(d) + ")translate(" + outerRadius + ", " + outerRadius + ")";
})
.attr("text-anchor", "middle")
.text(function(d,i) {
return data[i].playerName+": "+data[i].playerScore;
});
This will shift everything over to match the locations of the arcs' centroids.
I'm trying to get drag functionality to work on D3, and have copied the code directly from the developer's example.
However it seems the origin (what is being clicked) is not being passed correctly into the variable d, which leads to the error: 'Cannot read property 'x' of undefined'
The relevant code:
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
d.x += d3.event.dx
d.y += d3.event.dy
d3.select(this).attr("transform", function(d,i){
return "translate(" + [ d.x,d.y ] + ")"
})
});
var svg = d3.select("body").append("svg")
.attr("width", 1000)
.attr("height", 300);
var group = svg.append("svg:g")
.attr("transform", "translate(10, 10)")
.attr("id", "group");
var rect1 = group.append("svg:rect")
.attr("rx", 6)
.attr("ry", 6)
.attr("x", 5/2)
.attr("y", 5/2)
.attr("id", "rect")
.attr("width", 250)
.attr("height", 125)
.style("fill", 'white')
.style("stroke", d3.scale.category20c())
.style('stroke-width', 5)
.call(drag);
Usually, in D3 you create elements out of some sort of datasets. In your case you have just one (perhaps, one day you'll want more than that). Here's how you can do it:
var data = [{x: 2.5, y: 2.5}], // here's a dataset that has one item in it
rects = group.selectAll('rect').data(data) // do a data join on 'rect' nodes
.enter().append('rect') // for all new items append new nodes with the following attributes:
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
... // other attributes here to modify
.call(drag);
As for the 'drag' event handler:
var drag = d3.behavior.drag()
.on('drag', function (d) {
d.x += d3.event.dx;
d.y += d3.event.dy;
d3.select(this)
.attr('transform', 'translate(' + d.x + ',' + d.y + ')');
});
Oleg's got it, I just wanted to mention one other thing you might do in your case.
Since you only have a single rect, you can bind data directly to it with .datum() and not bother with computing a join or having an enter selection:
var rect1 = svg.append('rect')
.datum([{x: 2.5, y: 2.5}])
.attr('x', function (d) { return d.x; })
.attr('y', function (d) { return d.y; })
//... other attributes here
.call(drag);
I am trying to create the image above using d3
http://jsfiddle.net/Spwizard/LBzx7/1/
var dataset = {
hddrives: [20301672, 9408258, 2147483, 21474836, 35622,32210000],
};
var width = 460,
height = 300,
radius = Math.min(width, height) / 2;
var color = d3.scale.ordinal()
.range(["#2DA7E2"]);
var pie = d3.layout.pie()
.sort(null);
var arc = d3.svg.arc()
.innerRadius(radius - 100)
.outerRadius(radius - 70);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
var path = svg.selectAll("path")
.data(pie(dataset.hddrives))
.enter().append("path")
.attr("class", "arc")
.attr("fill", function(d, i) { return color(i); })
.attr("d", arc);
svg.append("text")
.attr("dy", ".35em")
.style("text-anchor", "middle")
.attr("class", "inside")
.text(function(d) { return '56%'; });
svg.append("text")
.attr("dy", "2em")
.style("text-anchor", "middle")
.attr("class", "data")
.text(function(d) { return 'some text'; });
Im struggling to see how to deal with the background color of the inner circle and dealing with the space left for storage
Thanks
To get a "background", you can add another circle with the respective fill colour. To deal with the free space, you can selectively set the opacity of one of the segments to 0. In your example, I've done that for the last slice:
.style("opacity", function(d, i) { return i == dataset.hddrives.length - 1 ? 0 : 1; })
Complete example (provided by OP) here.
Just append text:
svg.append("text")
.attr("text-anchor", "middle")
.attr('font-size', '20px')
.attr('y', '5')
.text(dataset.hddrives + "%");
I'm looking for a way of limiting the column width in a chart, I'm sure this ought to be relatively easy but I cant find a way of doing it.
I'm populating a chart from some dynamic data, where the number of columns can vary quite dramatically - between 1 and 20.
e.g: sample of csv
Location,Col1
"Your house",20
Location,Col1,Col2,Col3,Col4,Col5
"My House",12,5,23,1,5
This is working fine, and the col widths are dynamic, however when there is only one column in the data, I end up with one bar of width 756 (the whole chart), and I dont like the way this looks.
What I'd like to do is only ever have a maximum column of width 100px irrespective of the number of columns of data.
Below is my script for the chart
Many thanks,
<script>
var margin = {
top : 40,
right : 80,
bottom : 80,
left : 40
},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.scale.linear().range([ 0, width ]);
var y = d3.scale.linear().range([ height, 0 ]);
var x0 = d3.scale.ordinal()
.rangeRoundBands([0, width], .05);
var x1 = d3.scale.ordinal();
var y = d3.scale.linear()
.range([height, 0]);
var chart = d3.select("body").append("svg")
.attr("class","chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
var legendChart = d3.select("body").append("svg")
.attr("class","chart")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.csv("/sampledata.csv.txt", function(error, data) {
// Use the first row of csv for header names
var reasonNames = d3.keys(data[0]).filter(function(key) {
return key !== "Location";
});
//console.log(reasonNames);
data.forEach(function(d) {
d.reasons = reasonNames.map(function(name) {
return {
name : name,
value : +d[name]
};
});
//console.log(d.reasons);
});
x0.domain(data.map(function(d) {return d.Location; }));
x1.domain(reasonNames).rangeRoundBands([0, x0.rangeBand()]);
console.log(x0.rangeBand());
y.domain([0, d3.max(data, function(d) { return d3.max(d.reasons, function(d) { return d.value; }); })]);
var maxVal = d3.max(data, function(d) { return d3.max(d.reasons, function(d) { return d.value; }); });
//console.log(maxVal);
var xAxis = d3.svg.axis()
.scale(x0)
.orient("bottom");
var yAxis = d3.svg.axis()
.scale(y)
.orient("left")
//.tickFormat(d3.format(".2s"));
chart.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis);
chart.append("g")
.attr("class", "y axis")
.call(yAxis);
var location = chart.selectAll(".name")
.data(data)
.enter().append("g")
.attr("class", "g")
.attr("transform", function(d) { return "translate(" + x0(d.Location) + ",0)"; });
location.selectAll("rect")
.data(function(d) { return d.reasons; })
.enter().append("rect")
.attr("width", x1.rangeBand()-2)
.attr("x", function(d) { return x1(d.name); })
.attr("y", function(d) { return y(d.value); })
.attr("height", function(d) { return height - y(d.value); })
.style("fill", function(d,i) { return "#"+3+"9"+i; /*color(d.name);*/ });
chart.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d) { return x1(d.name)+ x.rangeBand() / 2; })
.attr("y", function(d) { return y(d.value); })
.attr("dx", -3) // padding-right
.attr("dy", ".35em") // vertical-align: middle
.attr("text-anchor", "end") // text-align: right
.text("String");
var legend = legendChart.selectAll(".legend")
.data(reasonNames.slice().reverse())
.enter()
.append("g")
.attr("class", "legend")
.attr("transform", function(d, i) { return "translate(0," + i * 20 + ")";
});
legend.append("rect")
//.attr("x", width - 18)
.attr("x", 18)
.attr("width", 18)
.attr("height", 18)
.style("fill", function(d, i) {/*console.log(i);*/return "#" + 3 + "9" + i;
});
legend.append("text")
//.attr("x", width - 24)
.attr("x", 48)
.attr("y", 9).attr("dy",".35em")
//.style("text-anchor", "end")
//.text(function(d,i) { return String.fromCharCode((65+i))+i; });
.text(function(d) { return d; });
});
</script>
The easiest way to achieve this is by changing the line
.attr("width", x1.rangeBand()-2)
to
.attr("width", Math.min(x1.rangeBand()-2, 100))
You might also want to adjust the starting position and/or padding.
Code for adjusting starting position if anyone is stuck on it:
.attr("x", function(d, i) { return x1(d.seriesName) + (x1.rangeBand() - 100)/2 ;})
P.S. : referring answer from Lars.
Setting an absolute maximum width for the columns doesn't allow proper rendering for different screen resolutions, div sizes, etc.
In my case, I just wanted the columns not to look so large when the number of columns itself is small
I found it easier and more straight-forward to play with the scale definition, by changing the maximum width (where all columns will fit), their inner and outer paddings.
var w = 600
// var w = parseInt(d3.select(parentID).style('width'), 10) // retrieve the div width dynamically
var inner_padding = 0.1
var outer_padding = 0.8
var xScale = d3.scale.ordinal().rangeRoundBands([0, w], inner_padding, outer_padding)
When rendering the plot, I just ran a switch/if-else statement, which assigns different padding values. The lower the number of columns to plot, the greater the outer_padding (and eventually inner-padding) values I use.
This way, I keep the plots responsive.
I am able to change the width of the bar using the above answer. But unfortunately, my X Axis labels are not aligned when there is a single bar in the chart and it uses the max width set.
var tradeGroup = svg.selectAll("g.trade")
.data(trades)
.enter()
.append("svg:g")
.attr("class", "trade")
.style("fill", function (d, i) {
return self.color(self.color.domain()[i]);
})
.style("stroke", function (d, i) {
return d3.rgb(self.color(self.color.domain()[i])).darker();
});
var aWidth = Math.min.apply(null, [x.rangeBand(), 100]);
// Add a rect for each date.
var rect = tradeGroup.selectAll("rect")
.data(Object)
.enter()
.append("svg:rect")
.attr("x", function (d) {
return x(d.x);
})
.attr("y", function (d) { return y( (d.y || 0) + (d.y0 || 0)); })
.attr("height", function (d) { return y(d.y0 || 0) - y((d.y || 0) + (d.y0 || 0)); })
.attr("width", Math.min.apply(null, [x.rangeBand(), 100]));
For completeness the full answer would look like this:
svg.selectAll(".bar")
.data(data)
.enter().append("rect")
.attr("class", "bar")
.attr("x", (d) -> x1(d.name) + (x1.rangeBand() - d3.min([x1.rangeBand(), 100]))/2)
.attr("width", d3.min([x1.rangeBand(), 100]))
.attr("y", (d) -> y(d.grade) )
.attr("height", (d)-> height - y(d.value) )
(coffeescript syntax)
Note this include the full answer, the 'width' and the 'x' settings. Also 'x' settings is accounting for a when 100 width is not the min value.
Thought I'd share that I came up with a slightly different answer to this. I didn't want to hard code in a maximum bar width because 1) it wasn't responsive to different screen sizes and 2) it also required playing with the x-coordinate attribute or accepting some irregular spacing.
Instead, I just set a minimum number of bars, based on the point where the bars became too wide (in my case, I found that less than 12 bars made my chart look weird). I then adjusted the scaleBand's range attribute, proportionately, if there were less than that number of bars. So, for example, if the minimum was set to 12 and there were only 5 items in the data, rather than rendering each of them at 1/5th of the full width, I scaled the range down to 5/12ths of the original width.
Basically, something like this:
// x is a scaleBand() that was previously defined, and this would run on update
var minBarSlots = 12;
if (data.length < minBarSlots) {
x.range([0, width*(data.length/minBarSlots)])
}
else {
x.range([0, width])
}`