I'm trying to modify the focus+ context example to study the blackbody spectrum with my students.
http://bl.ocks.org/1667367
The problem is that for me the only way to color segment is to plot bar colored charts.
I'd like to do it with the area generator, like this I can have all the benefits of the interpolation when I zoom on.
I can't access to each area element to fill it in the rgb color I want.
Is there any way to acces to the individual area elements and fill it with a function?
Here's my files with an orange filled area(focus) that I'd like to color as I do with the bar chart(context).
http://bl.ocks.org/4345931
In advance thanks.
I think the right approach here is to use a linearGradient element (specs). You can see my implementation here: http://jsfiddle.net/nrabinowitz/JZACC/
The main point here is that instead of drawing each color as a separate element, you define a gradient of spectrum colors and apply the same gradient to both the area and the context bar:
// set up gradient elements
var gradient = defs.append('linearGradient')
.attr('id', 'spectrumGradient');
// set up gradient scale
var gx = d3.scale.linear().range([0, 1]);
Then, when the data is loaded, you create stop elements for each color:
// update gradient scale
gx.domain(x.domain());
// create gradient
gradient.selectAll('stop')
// remove duplicates
.data(data)
.enter().append('stop')
.attr('offset', function(d) { return gx(d.wl); })
.attr('stop-color', toRGB);
And apply it using url(#id) notation:
focus.append("path")
.datum(data)
.attr("clip-path", "url(#clip)")
.attr("d", area)
.attr('fill', 'url(#spectrumGradient)');
Related
I am trying to draw a vertical line marker in my graph in D3. It is modeled off of this example: https://bl.ocks.org/mbostock/34f08d5e11952a80609169b7917d4172
My issue is that after I draw my line, it doesn't move as I zoom/scroll the graph. An example is shown below:
Currently, I have it calculated as a d3.area().
this.pastDateArea = d3.area()
.x(function(d) { return this.x(this.props.pastDate.toDate()) }.bind(this))
.y0(0)
.y1(function(d) { return this.height }.bind(this))
It is appended as
var pastDateData = [{x:this.props.pastDate.toDate(), y:150}]
this.focus.append("path")
.datum(pastDateData)
.attr("class", "area")
.attr("d", this.pastDateArea)
and zoomed/brushed using
//zoom
var t = d3.event.transform;
this.x.domain(t.rescaleX(this.x2).domain());
//brush
this.svg.select(".zoom").call(this.zoom.transform, d3.zoomIdentity
.scale(this.width / (s[1] - s[0]))
.translate(-s[0], 0));
I know there are similar questions to this one (namely, Draw a vertical line representing the current date in d3 gantt chart) but none of them include the zooming/panning features I have in my graph.
Please let me know if you need more information and thanks!
The issue is that you are not updating the vertical bar with each zoom event. Using the code of the example you show, several things are done when the chart is zoomed, including as you note:
x.domain(t.rescaleX(x2).domain()); // update x scale
focus.select(".area").attr("d", area); // redraw chart area
While you do give the new area the class of area, d3.select will only pick the first matching element. So, on zoom, only one .area element is updated (the first encountered, generally the first appended). But, replacing this with d3.selectAll(".area") will not generate the intended results as the area function referenced (.attr("d",area) ) is only used for the first area (that of the graph, not of the vertical bar).
A solution is to select each area (the chart and the bar) independently and update the area with their respective area generators. To do so, append the vertical bar with a unique class name, or an id and use that to select it later. Then when updating the graph on zoom or brush you can use:
x.domain(s.map(x2.invert, x2)); // update x scale
focus.select(".area").attr("d", area); // redraw chart area
focus.select(".bar").attr("d", pastDateArea);// redraw vertical bar
Remember that this needs to be done for both zoom and brush. Also, in the given example, a clip path is assigned in the css for .area, so you need to keep that in mind as well.
Here's a modified example.
I'm having an issue with my graph. When I zoom, the line goes over the edge of the canvas area and over the x/y axis. I tried adding a clippath but that doesn't seem to work. If I inspect the DOM in the debugger I can see the clippath rectangle is position exactly where it needs to be.
//The canvasGroup is the area between the axis
var clipGroup = canvasGroup.append("clipPath")
.attr("id", "canvasGroup-clipPath");
var clipRect = clipGroup.append("rect")
.attr("width", width)
.attr("height", height);
//Function that does the zooming
function doZoom()
{
paths.forEach(function(path)
{
path.attr("transform", d3.event.transform);
});
gX.call(xAxis.scale(d3.event.transform.rescaleX(xScale)));
gY.call(yAxis.scale(d3.event.transform.rescaleY(yScale)));
}
var zoom = d3.zoom()
.scaleExtent([1, 5])
.translateExtent([[0, 0], [width, height]])
.on("zoom", doZoom);
//Register the event listener
canvasGroup.call(zoom);
//now loop over the data sets and plot each one
//For brevity, I'm skipping the loop and only showing the call to create the line
var path = canvasGroup.append("path")
.attr("clip-path", "url(#canvasGroup-clipPath)")
.attr("fill", "steelblue")
.attr("class", "line")
.attr("id", lineId + "-line")
.style("stroke", lineColor)
.style("stroke-width", 1)
.style("fill", "none");
paths.push(path);
path.attr("d", function(d) { return plotline(i)});
Here is how it looks.
Before zoom:
After zoom:
The problem is caused by setting the clipping path on the path representing your data. When applying the clipping path the browser needs to decide which coordinate system should be used for the contents of <clipPath> element. This is controlled by the clipPathUnits attribute which defaults to userSpaceOnUse:
userSpaceOnUse
The contents of the <clippath> element represent values in the current user coordinate system in place at the time when the <clippath> element is referenced (i.e., the user coordinate system for the element referencing the <clippath> element via the clip-path attribute).
When transforming the path in your doZoom() function, you are actually establishing a new coordinate system for the path. And, apart from drawing the path itself, this coordinate system will be used to compute the position and dimension of the clipping path. Thus, by transforming the path according to the zoom behavior, you are applying the same transformation to the clipping path whereby moving it away from the desired position.
Although it is tempting to set the clipPathUnits attribute to its other valid value objectBoundingBox, this is most likely not what you want for this case as it further complicates matters. When setting this value the positions and lengths of the <clipPath>'s contents need to be specified as fractions of the bounding box!
Knowing all this, there is a much easier solution to it! You just need to apply the clip-path to an element which will not be transformed during zooming ,e.g. a parent group. Given the incomplete code you provided, this might very well work by setting the clipping path to canvasGroup:
// Set the clipping path on the parent group.
canvasGroup.attr("clip-path", "url(#canvasGroup-clipPath)")
// Append the path to the group as before.
var path = canvasGroup.append("path")
.attr("fill", "steelblue")
.attr("class", "line")
// ...
I want to make a donut chart with text labels as per this example:
However, I dont want the lines to enter into the inner area of the arc. I just want them to touch the circumference of the arc without entering it.
Does D3.js provide any ready made functions to find out the center of the arc circumference or is there any other way using SVG to modify the lines.
Probably the easiest way will be to rearrange the order of the elements. Because SVGs elements will be drawn in document order this will have the effect of elements being rendered on top of other elements which they succeed. The example uses three groups to arrange the slices, the text labels and the lines.
svg.append("g").attr("class", "slices"); // 1.
svg.append("g").attr("class", "labels"); // 2.
svg.append("g").attr("class", "lines"); // 3.
By putting the <g> for the slices to the end this will cover part of the lines giving the look like they ended on the outer arc of the slices.
svg.append("g").attr("class", "labels"); // 2.
svg.append("g").attr("class", "lines"); // 3.
svg.append("g").attr("class", "slices"); // 1.
There is no need for further adjustements. Have a look at this working example.
I wanted to customize the colors in zoomable tree map.
I am using
color = d3.scale.category20c();
.attr("fill", function(d) {return color(d.name);})
here the name(d.name) attribute can be red, green, blue or any color and I want to have color according to given name. as of my current code color is not coming according to name.
Thanks in advance
Since your dataset provides the names of the colors, you shouldn't need to use a scale at all. As long as the color names used in your dataset are valid CSS color names, you can simply set the fill attribute to the name directly:
.attr('fill', function(d){ return d.name; })
Here is jsfiddle of a Sankey diagram:
I am trying to modify colors of the links so that the color of each link is actually gradient from its source node color to its target node color. (it is assumed that opacity will remain 0.2 or 0.5 depending whether a mouse hovers or not over the link; so links will remain a little "paler" than nodes)
I took a look at this nice and instructive example, which draws this gradient filled loop:
However, I simply couldn't integrate that solution to mine, it looks too complex for the given task.
Also, note that links in original Sankey diagram move while node is being dragged, and must display gradient even in those transitory states. A slight problem is also transparency of links and nodes, and order of drawing. I would appreciate ideas, hints.
#VividD: Just saw your comment, but I was about done anyway. Feel free to ignore this until you've figured it out on the own, but I wanted to make sure I knew how to do it, too. Plus, it's a really common question, so good to have for reference.
How to get a gradient positioned along a line
With the caveat for anyone reading this later, that it will only work because the paths are almost straight lines, so a linear gradient will look half-decent -- setting a path stroke to a gradient does not make the gradient curve with the path!
In initialization, create a <defs> (definitions) element in the SVG and save the selection to a variable:
var defs = svg.append("defs");
Define a function that will create a unique id for your gradient from a link data object. It's also a good idea to give a name to the function for determining node colour:
function getGradID(d){return "linkGrad-" + d.source.name + d.target.name;}
function nodeColor(d) { return d.color = color(d.name.replace(/ .*/, ""));}
Create a selection of <linearGradient> objects within <defs> and join it to your link data, then set the stop offsets and line coordinates according to the source and target data objects.
For your example, it probably will look fine if you just make all the gradients horizontal. Since that's conveniently the default I thought all we would have to do is tell the gradient to fit to the size of the path it is painting:
var grads = defs.selectAll("linearGradient")
.data(graph.links, getLinkID);
grads.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "objectBoundingBox"); //stretch to fit
grads.html("") //erase any existing <stop> elements on update
.append("stop")
.attr("offset", "0%")
.attr("stop-color", function(d){
return nodeColor( (d.source.x <= d.target.x)? d.source: d.target)
});
grads.append("stop")
.attr("offset", "100%")
.attr("stop-color", function(d){
return nodeColor( (d.source.x > d.target.x)? d.source: d.target)
});
Unfortunately, when the path is a completely straight line, its bounding box doesn't exist (no matter how wide the stroke width), and the net result is the gradient doesn't get painted.
So I had to switch to the more general pattern, in which the gradient is positioned and angled along the line between source and target:
grads.enter().append("linearGradient")
.attr("id", getGradID)
.attr("gradientUnits", "userSpaceOnUse");
grads.attr("x1", function(d){return d.source.x;})
.attr("y1", function(d){return d.source.y;})
.attr("x2", function(d){return d.target.x;})
.attr("y2", function(d){return d.target.y;});
/* and the stops set as before */
Of course, now that the gradient is defined based on the coordinate system instead of based on the length of the path, you have to update those coordinates whenever a node moves, so I had to wrap those positioning statements in a function that I could call in the dragmove() function.
Finally, when creating your link paths, set their fill to be a CSS url() function referencing the corresponding unique gradient id derived from the data (using the pre-defined utility function):
link.style("stroke", function(d){
return "url(#" + getGradID(d) + ")";
})
And Voila!