generate clipPaths for multiple elements in d3.js - javascript

I am trying to create partially filled circles like the ones in the final NYT political convention visualization: http://www.nytimes.com/interactive/2012/09/06/us/politics/convention-word-counts.html
The two clearest code examples I've found for clipPaths in d3 ( https://gist.github.com/1067636 and http://bl.ocks.org/3422480) create individual div elements with unique ids for each clip-path and then apply these paths to single elements.
I can not figure out how to go from these examples to a visualization with a unique circular clipPath for each element in a set of elements based on data values so that I can create my effect.
Here is what I have so far:
Given data with the following structure:
data = [
{value: 500, pctFull: 0.20, name: "20%"},
{value: 250, pctFull: 0.75, name: "75%"},
{value: 700, pctFull: 0.50, name: "50%"},
]
1) Create a force diagram with a circle for each object in the dataset. The area of the circle is derived from the objects value.
2) Calculate k (and h) from a proportion (pctFull) for each datapoint using the algorithm from the mbostock example http://bl.ocks.org/3422480
3) Use k to generate a rect for each datapoint that covers the appropriate area of the circle.
I think the illustration would be done if I could limit the visibility of each rect to its respective circle but this is where I am stuck. I've tried a bunch of things, none of which have worked.
Here's the jsfilddle: http://jsfiddle.net/G8YxU/2/

See a working fiddle here: http://jsfiddle.net/nrabinowitz/79yry/
// blue circle
node.append("circle")
.attr("r", function(d, i) {return rVals[i];})
.style("fill", "#80dabe")
.style("stroke", "#1a4876");
// clip path for the brown circle
node.append("clipPath")
// make an id unique to this node
.attr('id', function(d) { return "clip" + d.index })
// use the rectangle to specify the clip path itself
.append('rect')
.attr("x", function(d, i){ return rVals[i] * (-1);})
.attr("width", function(d, i){ return rVals[i] * 2;})
.attr("y", function(d, i) {return rVals[i] - (2 * rVals[i] * kVals[i]);})
.attr("height", function(d, i) {return 2 * rVals[i] * kVals[i];});
// brown circle
node.append("circle")
// clip with the node-specific clip path
.attr("clip-path", function(d) { return "url(#clip" + d.index + ")"})
.attr("r", function(d, i) {return rVals[i];})
.style("fill", "#dabe80")
.style("stroke", "#1a4876");
It looks like the only way to specify a clip path for an element is to use the url(IRI) notation in the clip-path attribute, which means that you'll need a unique id for each clip path based on the node data. I've used the form clip<node index> for the id - so each node gets its own clip path, and other sub-elements of the node can refer to it.
It seemed easiest, following Mike's example, to make two circles of different colors and use the rectangle itself for the clip path, rather than making a circle-based clip path. But you could do it either way.

Related

Redrawing a key on top of a D3 projection after transition

I'm working with a D3 map projection similar to Mike Bostock's Choropleth seen here.
The issue I'm having is that I've added a transition; and when I transition the projection, the map key (seen in the top right corner) is being covered by the background color of the map.
I know I probably just need to redraw the g layer after the transition, but I'm not able to get that working as expected.
I'm originally drawing the key on the map with the following code:
var g = svg.append("g")
.attr("class", "key")
.attr("transform", "translate(0,40)");
g.selectAll("rect")
.data(color.range().map(function(d) {
d = color.invertExtent(d);
if (d[0] == null) d[0] = x.domain()[0];
if (d[1] == null) d[1] = x.domain()[1];
return d;
}))
.enter().append("rect")
.attr("height", 8)
.attr("x", function(d, i) { return 350 + (i * 30)})
.attr("width", 30)
.attr("fill", function(d) { console.log(d[1]); return color(d[1]); });
g.append("text")
.attr("class", "caption")
.attr("x", x.range()[0])
.attr("y", -6)
.attr("fill", "#000")
.attr("text-anchor", "start")
.attr("font-weight", "bold")
.text("Number of Licensed Establishments");
g.call(d3.axisBottom(x)
.tickSize(13)
.tickValues(color.domain()))
.select(".domain")
.remove();
Then I'm transitioning the projection with this code (which also works fine).
path = d3.geoPath(projection);
svg.selectAll("path").transition().duration(2000).attr("d", path);
But the key gets covered. I've tried redrawing it like this:
g.selectAll("g").attr("transform", "translate(0,40)");
It doesn't do anything though. What step am I missing to correctly redraw that g layer on top?
Transitioning a path shouldn't change where it appears in the DOM. Transitioning element attributes with d3 modifies that element in place in the DOM. The following example should demonstrate this (path is appended first and should be behind the text, the path then transitions its d attribute through two d3 symbol paths remaining behind the text):
var svg = d3.select('body').append('svg').attr('width',400).attr('height',200);
var cross = "M-21.213203435596427,-7.0710678118654755L-7.0710678118654755,-7.0710678118654755L-7.0710678118654755,-21.213203435596427L7.0710678118654755,-21.213203435596427L7.0710678118654755,-7.0710678118654755L21.213203435596427,-7.0710678118654755L21.213203435596427,7.0710678118654755L7.0710678118654755,7.0710678118654755L7.0710678118654755,21.213203435596427L-7.0710678118654755,21.213203435596427L-7.0710678118654755,7.0710678118654755L-21.213203435596427,7.0710678118654755Z";
var star = "M0,-29.846492114305246L6.700954981042517,-9.223073285798176L28.38570081386192,-9.223073285798177L10.8423729164097,3.5229005144437298L17.543327897452222,24.146319342950797L1.7763568394002505e-15,11.400345542708891L-17.543327897452215,24.1463193429508L-10.842372916409698,3.522900514443731L-28.38570081386192,-9.22307328579817L-6.7009549810425195,-9.223073285798176Z";
var wye = "M8.533600336205877,4.926876451265144L8.533600336205877,21.9940771236769L-8.533600336205877,21.9940771236769L-8.533600336205877,4.9268764512651435L-23.31422969000131,-3.6067238849407337L-14.78062935379543,-18.387353238736164L0,-9.853752902530289L14.78062935379543,-18.387353238736164L23.31422969000131,-3.6067238849407337Z"
var symbol = svg.append('path')
.attr('transform','translate(100,100)')
.attr('d', cross )
.attr("fill","orange");
var text = svg.append('text')
.attr('x', 100)
.attr('y', 105)
.style('text-anchor','middle')
.text('THIS IS SOME TEXT')
symbol.transition()
.delay(2000)
.attr('d', star )
.duration(2000)
.transition()
.attr('d', wye )
.duration(2000);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
Given your example, it is likely that the key is initially rendered behind the features of the map - only there is no overlap between the two. Each appears as intended. When transitioning, with say a zoom, the features overlap and the key is hidden. As noted in the comments, try g.raise() or d3.select(".key").raise() to move the key to the bottom of the parent container, effectively lifting it above other svg elements (as elements are rendered in the order they appear in the DOM, as close as we get to a z-index in svg). You should only need to apply .raise() once - as the transition won't change the ordering, or alternatively, ensure that the key is appended to the svg last.

d3.js pie chart legends slice toggle

I'm working on a d3.js pie chart application. I am trying to develop the functionality that when you click on the legend rectangles, it toggles the slice on/off as well as the fill inside the legend rectangle.
http://jsfiddle.net/Qh9X5/3136/
legend
Rects
.enter()
.append("rect")
.attr("x", w - 65)
.attr("y", function(d, i){ return i * 20;})
.attr("width", 10)
.attr("height", 10)
.style("fill", function(d, i) {
return methods.color(i);
})
.style("stroke", function(d, i) {
return methods.color(i);
})
.on('click', function(d, i){
onLegendClick(d, i);
})
Here's one way to solve your problem:
One change required in your code is to use key functions, so that d3 matches the filtered data to the corresponding DOM node. Labels seem to be a proper key in your dataset.
Simply use:
.data(this.piedata, function(d) { return d.data.label});
instead of
.data(this.piedata);
Then, in your OnLegendClick function, you want to select all the legend's rect and all the svg arcs matching with the clicked element.
Workflow is :
select the DOM elements
match with the selected data
apply changes
Here's how to do it:
function onLegendClick(dt){
d3.selectAll('rect').data([dt], function(d) { return d.data.label}).style("opacity", function(d) {return Math.abs(1-d3.select(this).style("opacity"))})
d3.selectAll('.pie').data([dt], function(d) { return d.data.label}).style("opacity", function(d) {return Math.abs(1-d3.select(this).style("opacity"))})
}
I let you adjust the "toggle" feature. You might also want to change the texts in addition to the arcs, for this use another selection.
Updated jsfiddle: http://jsfiddle.net/Qh9X5/3138/

D3 choropleth map with legend

I have a choropleth map of the united states showing total population. I would like to add a legend to the map showing the quantile range values.I’ve seen other similar questions about this topic but can’t seem to get it to work for my specific case. I know I need to include the color range or color domain but just not sure if this is the correct way. As of right now just one feature shows up in the legend, could it be that all the legend features are stacked on top of each other. How can I know for sure and how can I fix this.
//Define default colorbrewer scheme
var colorSchemeSelect = "Greens";
var colorScheme = colorbrewer[colorSchemeSelect];
//define default number of quantiles
var quantiles = 5;
//Define quantile scale to sort data values into buckets of color
var color = d3.scale.quantile()
.range(colorScheme[quantiles]);
d3.csv(data, function (data) {
color.domain([
d3.min(data, function (d) {
return d.value;
}),
d3.max(data, function (d
return d.value
})
]);
//legend
var legend = svg.selectAll('rect')
.data(color.domain().reverse())
.enter()
.append('rect')
.attr("x", width - 780)
.attr("y", function(d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("fill", color);
The legend code that you're using would work perfectly well if you had an ordinal scale, where the domain is made up of discrete values that correlate to the range of colours on a one-to-one basis. But you're using a quantile scale, and so need a different approach.
For a d3 quantile scale, the domain is the list of all possible input values, and the range is a list of discrete output values. The domain list is sorted in ascending order and then divided into equal-sized groups, which are assigned to each output value from the range. The number of groups is determined by the number of output values.
With that in mind, in order to get one legend entry for each colour, you're going to need to use your colour scale's range, not the domain, as the data for your legend. Then you can use the quantileScale.invertExtent() method to find the minimum and maximum input values that are getting drawn with that colour.
Sample code, making each legend entry a <g> containing both the coloured rectangle and a text label showing the corresponding values.
var legend = svg.selectAll('g.legendEntry')
.data(color.range().reverse())
.enter()
.append('g').attr('class', 'legendEntry');
legend
.append('rect')
.attr("x", width - 780)
.attr("y", function(d, i) {
return i * 20;
})
.attr("width", 10)
.attr("height", 10)
.style("stroke", "black")
.style("stroke-width", 1)
.style("fill", function(d){return d;});
//the data objects are the fill colors
legend
.append('text')
.attr("x", width - 765) //leave 5 pixel space after the <rect>
.attr("y", function(d, i) {
return i * 20;
})
.attr("dy", "0.8em") //place text one line *below* the x,y point
.text(function(d,i) {
var extent = color.invertExtent(d);
//extent will be a two-element array, format it however you want:
var format = d3.format("0.2f");
return format(+extent[0]) + " - " + format(+extent[1]);
});

d3 - Rotate text elements in an array

I am trying to use selectAll("text") in d3 to to add an array of string values (called 'data') to my graph. I want each individual data point to be rotated at the point it is placed at, defined here as (i * (width/ data.length) + 8, 170). However, it is currently rotating the entire array set as one long string, with the first element at the (x, y) point I set. How can I appropriately apply the translate rotation to rotate each element individually?
new_svg.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d, i) {
return i * (width / data.length) + 8;
})
.attr("y", function(d) {
return 170;
})
.attr("dx", -barWidth/2)
.attr("text-anchor", "middle")
.attr("style", "font-size: 12; font-family: Garamond, sans-serif")
.text(function(d) { return d;})
.attr("transform", function(d) {
return "rotate(45)"
});
There was more than one text element in the array, which I was able to verify. It turns out that the problem comes from setting the x and y elements separate from the translate transform. The solution offered here worked for me: d3 x axis labels outputted as long string

D3 - how to deal with JSON data structures?

I'm new to D3, and spent already a few hours to find out anything about dealing with structured data, but without positive result.
I want to create a bar chart using data structure below.
Bars are drawn (horizontally), but only for user "jim".
var data = [{"user":"jim","scores":[40,20,30,24,18,40]},
{"user":"ray","scores":[24,20,30,41,12,34]}];
var chart = d3.select("div#charts").append("svg")
.data(data)
.attr("class","chart")
.attr("width",800)
.attr("height",350);
chart.selectAll("rect")
.data(function(d){return d3.values(d.scores);})
.enter().append("rect")
.attr("y", function(d,i){return i * 20;})
.attr("width",function(d){return d;})
.attr("height", 20);
Could anyone point what I did wrong?
When you join data to a selection via selection.data, the number of elements in your data array should match the number of elements in the selection. Your data array has two elements (for Jim and Ray), but the selection you are binding it to only has one SVG element. Are you trying to create multiple SVG elements, or put the score rects for both Jim and Ray in the same SVG element?
If you want to bind both data elements to the singular SVG element, you can wrap the data in another array:
var chart = d3.select("#charts").append("svg")
.data([data])
.attr("class", "chart")
…
Alternatively, use selection.datum, which binds data directly without computing a join:
var chart = d3.select("#charts").append("svg")
.datum(data)
.attr("class", "chart")
…
If you want to create multiple SVG elements for each person, then you'll need a data-join:
var chart = d3.select("#charts").selectAll("svg")
.data(data)
.enter().append("svg")
.attr("class", "chart")
…
A second problem is that you shouldn't use d3.values with an array; that function is for extracting the values of an object. Assuming you wanted one SVG element per person (so, two in this example), then the data for the rect is simply that person's associated scores:
var rect = chart.selectAll("rect")
.data(function(d) { return d.scores; })
.enter().append("rect")
…
If you haven't already, I recommend reading these tutorials:
Thinking with Joins
Nested Selections
This may clarify the nested aspect, in addition to mbostock's fine answer.
Your data has 2 degrees of nesting. You have an array of 2 objects, each has an array of ints. If you want your final image to reflect these differences, you need to do a join for each.
Here's one solution: Each user is represented by a group g element, with each score represented by a rect. You can do this a couple of ways: Either use datum on the svg, then an identity function on each g, or you can directly join the data on the g. Using data on the g is more typical, but here are both ways:
Using datum on the svg:
var chart = d3.select('body').append('svg')
.datum(data) // <---- datum
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(function(d){ return d; }) // <----- identity function
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Using data on the group (g) element:
var chart = d3.select('body').append('svg')
.attr('width',800)
.attr('height',350)
.selectAll('g')
.data(data) // <--- attach directly to the g
.enter().append('g')
.attr('class', function(d) { return d.user; })
.attr('transform', function(d, i) { return 'translate(0, ' + i * 140 + ')'; })
.selectAll('rect')
.data(function(d) { return d.scores; })
.enter().append('rect')
.attr('y', function(d, i) { return i * 20; })
.attr('width', function(d) { return d; })
.attr('height', 20);
Again, you don't have to create these g elements, but by doing so I can now represent the user scores differently (they have different y from the transform) and I can also give them different styles, like this:
.jim {
fill: red;
}
.ray {
fill: blue;
}

Categories

Resources