I want to create a kind of infographic where I can represent percentages intuitively using a kind of fill logic.
Example
For the sake of simplicity let's just assume intervals of 25%. For the task of 75% of households, there would be four houses in total and 3 of them would be filled in. The remaining house would remain fill:'none'.
I had something in mind like:
It would be in SVG form.
The only way I can think of to achieve this is pre-draw the houses as a collective image and link the file like:
var fileMap = { 50:'fifty.svg', 75:'seventy-five.svg'};
But this doesn't seem to be very modular, and it doesn't utilize d3 hardly.
Question: Is it possible/feasible to create a simple 25% interval conditional fill using d3 compatible logic? What would my .data() call expect? It has to be an array, maybe a binary:
var data = [1,1,1,0] //75%;
Maybe there's a better way altogether, but that's the best I have got.
"I want to create a kind of infographic where I can represent percentages intuitively using a kind of fill logic"... The technical name for this is pictogram.
For creating a pictogram you don't need anything special, you can use a common enter selection. So, given your data...
var data = [1,1,1,0]
... we will create one house for each array element...
var house = svg.selectAll(null)
.data(data)
.enter()
.append("path")
... and fill them according to the datum:
.style("fill", function(d){
return d ? "blue" : "white"
})
Here is a basic demo:
var d = "m787.67 1599.58l148.83 157.74 124.02-131.45v630.95h396.87 198.44 396.87v-630.95l124.02 131.45 148.83-157.74-768.94-814.97-768.94 814.97m1066.6-709.82v78.868l198.44 210.32v-289.18h-198.44z";
var svg = d3.select("svg");
var data = [1, 1, 1, 0];
var house = svg.selectAll(null)
.data(data)
.enter()
.append("path")
.attr("d", d)
.attr("transform", function(_, i) {
return "translate(" + (i * 70) + ",100) matrix(.04 0 0 .03-4.159-50.852)"
})
.style("stroke", "black")
.style("stroke-width", "50px")
.style("fill", function(d) {
return d ? "blue" : "white"
})
<script src="https://d3js.org/d3.v5.min.js"></script>
<svg></svg>
Related
I need to add a line with multiple values along the x axis of a multiBarChart. I cannot use another nvd3 control like multiChart or linePlusBarChart due to existing functionality breaking if i switch.
The following code and fiddle show what i have so far.
var limits = [[60,166990904656],[300,154990904656],[500,142990904656]];
var lineFunction = d3.svg.line()
.x(function(d) { return d[0] })
.y(function(d) {
d = d[1] / 1000000000;
return d
}).interpolate('step-after');
//The line SVG Path we draw
d3.select("#chart svg")
.append("path")
.attr("d", lineFunction(limits))
.attr("stroke", "red")
.attr("stroke-width", 1)
.attr("fill", "none");
https://jsfiddle.net/s2vemzht/11/
I am facing 3 issues: First is the x axis placement of the line. At the moment i have hardcoded this value into the limits array because i am unsure how to position it dynamically based on where the next bar starts.
The second issue is the limit value in relation to the other values in the data array. It does not seem accurately positioned.
The third issue is with the line not drawing over the 3rd bar even though there are 3 values in the limits array. I tried changing the interpolate property but it's still an issue.
I am a beginner with D3 so apologies for all the questions :-)
I managed to figure it out by drawing lines over the bars at particular points and positioning based on the y and x axis scales
var yValueScale = chart.yAxis.scale(),
yValue = 0,
xValueScale = chart.xAxis.scale(),
g = d3.select("#chart svg .nvd3");
for(var i=0;i<limits.length;i++){
g.append("line")
.attr("class","limit-line")
.attr("x1", xValueScale(d3.time.format('%b-%d')(new Date(limits[i][0]))))
.attr("y1", yValueScale(limits[i][1]/ 1000000000))
.attr("x2", xValueScale(d3.time.format('%b-%d')(new Date(limits[i][0]))) + parseInt(d3.select("#chart svg .nv-bar").attr("width")))
.attr("y2", yValueScale(limits[i][1]/ 1000000000))
;
}
https://jsfiddle.net/s2vemzht/25/
In version d3 v3, a common workflow for me was to create several svg group elements and then append a number of other child elements to each g. as an example, below I've created 3 group elements and appended a circle to each group. I then use the selection.each method to update the radius of each circle:
var data = [2, 4, 8]
var g = d3.select('svg').selectAll('.g').data(data)
g.each(function(datum) {
var thisG = d3.select(this)
var circle = thisG.selectAll('.circle')
circle.transition().attr('r', datum * 2)
})
var enterG = g.enter().append('g').attr('class', 'g')
enterG.append('circle')
.attr('class', 'circle')
.attr('r', function(d) { return d })
g.exit().remove()
What is the proper way to do this in d3 v4? I am very confused on how best to do this. Here's an example of what i'm trying:
var data = [2, 4, 8]
var g = d3.select('svg').selectAll('.g').data(data)
g.enter()
// do stuff to the entering group
.append('g')
.attr('class', 'g')
// do stuff to the entering AND updating group
.merge(g)
// why do i need to reselect all groups here to append additional elements?
// is it because selections are now immutable?
var g = d3.select('svg').selectAll('g')
g.append('circle')
.attr('class', 'circle')
.attr('r', function(d) { return d })
// for each of the enter and updated groups, adjust the radius of the child circles
g.each(function(datum) {
var thisG = d3.select(this)
var circle = thisG.selectAll('.circle')
circle.transition().attr('r', datum * 2)
})
g.exit().remove()
Thanks in advance for any help you can provide. I've used d3 v3 for a long time and feel pretty comfortable with it. However, I am having a very hard time understanding some of the different behaviors in v4.
I think your code could be modified as follow (untested, so unsure):
var data = [2, 4, 8]
var g = d3.select('svg').selectAll('.g').data(data);
// do stuff to the entering group
var enterSelection = g.enter();
var enterG = enterSelection.append('g')
.attr('class', 'g');
//Append circles only to new elements
enterG.append('circle')
.attr('class', 'circle')
.attr('r', function(d) { return d })
// for each of the enter and updated groups, adjust the radius of the child circles
enterG.merge(g)
.select('.circle')
.transition()
.attr('r',function(d){return d*2});
g.exit().remove()
When using the first .selectAll, only existing elements are selected. Then, by entering, you are creating new elements, that generate a new selection. When you need to update all, you simply merge the new and existing elements in a single selection.
From that selection, I simply selected all .circle (single select - one element per g), and then update the radius thanks to the binding API that prevents me from making a .each call. I am unsure as how these two compares, I simply always did it this way.
Finally, here is a bl.ocks demonstrating the pattern.
I've see the Example of D3.js-Voronoi Tessellation.But I want to put some text in each of polygons instead of a circle,Here is my js code:
var width = 600, height = 400;
var vertices = d3.range(20).map(function(d){
return [Math.random() * width, Math.random() * height]
});
var voronoi = d3.geom.voronoi();
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
path = svg.append("g").selectAll("path");
svg.selectAll("info")
.data(vertices.slice(1))
.enter().append("text")
.attr("transform", function(d) {
return "translate(" + d + ")";
})
.text("someText")
.attr("shape-rendering","crispEdges")
.style("text-anchor","middle");
redraw();
function redraw(){
path = path
.data(voronoi(vertices), polygon);
path.exit().remove();
path.enter().append("path")
.attr("class", function(d, i) {return "q" + (i % 9) + "-9";})
.attr("d", polygon);
path.order();
}
function polygon(d){
return "M" + d.join("L") + "Z";
}
I have a JSFiddle for that basic example here:
my voronoi code
now, I want each of the polygons' text in the center of the polygon, and don't cross with the polygon's border. If the polygon have not enough space to contain the all text, just contain the first part of it!
Let me know if there is anything I can do to solve this issue, thank you!
PS:I'm so sorry to my English, yes, it's so poor! :)
Have a look at this example http://bl.ocks.org/mbostock/6909318 , you probably want to place the text at the polygon centroid and not the seed (point) used to determine the voronoi tessellation.
That should fix the majority of your layout issues.
Automatically scaling the text to fit is a little bit harder, if you are willing to scale and rotate the text you can use a technique similar to the following to determine the length of the line at that point:
https://mathoverflow.net/questions/116418/find-longest-segment-through-centroid-of-2d-convex-polygon
Then you need to determine the angle of the line. I have a plugin that should help with that:
http://bl.ocks.org/stephen101/7640188/3ffe0c5dbb040f785b91687640a893bae07e36c3
Lastly you need to scale and rotate the text to fit. To determine the width of the text use getBBox() on the text element:
var text = svg.append("svg:text")
.attr("x", 480)
.attr("y", 250)
.attr("dy", ".35em")
.attr("text-anchor", "middle")
.style("font", "300 128px Helvetica Neue")
.text("Hello, getBBox!");
var bbox = text.node().getBBox();
Then you use the angle you calculated earlier to scale and rotate your text:
text.attr("transform", "rotate(40) scale(7)")
I would love to give a complete example but this is quite a bit of work to get it right.
There are other options to achieve the same effect but none of them are simple (ie you could anneal the layout similar to the way d3 does the Sankey layout)
http://jsfiddle.net/ytvka/4/
I know this one has been asked before but I've not been able to use those examples to figure out what I'm doing wrong.
I want to create a simple 6 point, 3 series chart with data that looks like:
data = [
{"key":"D78","date":"2013-09-23T17:26:21.258Z","value":1.25},
{"key":"D78","date":"2013-09-23T17:28:21.258Z","value":2.25},
{"key":"R71","date":"2013-09-23T17:26:21.258Z","value":2.45},
{"key":"R71","date":"2013-09-23T17:28:21.258Z","value":2.85},
{"key":"X44","date":"2013-09-23T17:26:21.258Z","value":3.87},
{"key":"X44","date":"2013-09-23T17:28:21.258Z","value":3.87}
]
Nothing exciting there. What I'd ideally like to do is make a 3-series line chart out of this data.
svg = d3.select(selector).append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom).append("g")
x = d3.time.scale().range([ 0, width ])
y = d3.scale.linear().range([ height, 0 ])
format = d3.time.format("%Y-%m-%dT%H:%M:%S.%LZ")
valueLine = d3.svg.line().interpolate("basis")
.x((d) ->
console.log format.parse(d.date)
x(format.parse(d.date))
)
.y((d) ->
console.log d.value
y d.value
)
svg.append("path").attr("class", "line")
.attr "d", valueLine(u.where(data, key: "X44"))
Which generates SVG: <path class="line" d="M137998238125800,-287L137998250125800,-287"></path>
This code just pulls out one of the series using lodash. Problem is: nothing on the screen. It runs through and grabs the value but there's no lines. I'm finding that existing examples are either complex and not well explained (http://bl.ocks.org/mbostock/3884955) or missing key parts (http://www.d3noob.org/2013/01/adding-more-than-one-line-to-graph-in.html).
What's wrong with my code?
How can I add in the other series (R71, D78)?
Is there a good tutorial of this out there that has complete code and walks through all the steps?
Your first point is at (137998238125800,-287) pixel coordinate, which is far away from the visible screen area. You don't use the selectAll/enter pattern which is at the core of D3. So you should start with this fundamental tutorial, then probably the code example you mention will make more sense:
var city = svg.selectAll(".city")
.data(cities)
.enter().append("g")
.attr("class", "city");
city.append("path")
.attr("class", "line")
.attr("d", function(d) { return line(d.values); })
.style("stroke", function(d) { return color(d.name); });
I am using D3 to plot a rectangle for each object in an array, the height of the rectangle being dependant on the 'Size' property of the object. These rectangles are stacked on top of each other. I currently set the y position by summing the 'Size' of each subsequent rect that gets plotted - but this seems wrong - and I was wondering if there was a better way to do this, such as accessing the 'y' attribute of the previous item (and how?) or another way...
This is what the essence of my code looks like. There is a link to the fiddle below.
var cumY = 0;
var blocks1 = sampleSVG.selectAll("rect")
.data(fpp)
.enter()
.append("rect")
.sort(SortBySize)
.style("stroke", "gray")
.style("opacity", blockOpacity)
.style("fill", function (d) {return d.Colour})
.attr("width", 80)
.attr("height", function (d) {return d.Size})
.attr("x", 5)
.attr("y", function (d, i) {
var thisY = cumY;
cumY += d.Size;
// perhaps I could just return something like d.Size + previousItem.GetAttribute("y") ???
return thisY;
});
http://jsfiddle.net/ninjaPixel/bvER3/
This is tricky do! You're right that keeping track of the cumulative height 'seems wrong' - it works now but it isn't very idiomatic d3 and will get pretty messy once you start trying to do something more complicated.
I would try using d3's built in stack-layout which was created solve this problem. You might want to start working off of this example and posting an updated fiddle if you get stuck. Good luck!