I am using d3.js, and I'm working on a brushed area chart by modifying this example. In addition to the x-axis changing based on the brush, I'd like chart's y-axis to be redrawn, based on the y-values of the data that fall within the brush (similar to the behavior of a Google Finance chart).
I have gotten the functionality working, but only in a way that enables the brush to be drawn in both x- and y-space. I did this by first adding a y scale to the brush variable:
var brush = d3.svg.brush()
.x(x2)
.y(y2)
.on("brush", brush);
This makes brush.extent() return the following multi-dimensional array: [ [x0, y0], [x1, y1] ]. I then use this data in the brush() function to redefine the x- and y- domain for the focus chart:
function brush() {
var extent = brush.extent();
x.domain(brush.empty() ? x2.domain() : [ extent[0][0], extent[1][0] ]);
y.domain(brush.empty() ? y2.domain() : [ extent[0][1], extent[1][1] ]);
focus.select("path").attr("d", area);
focus.select(".x.axis").call(xAxis);
focus.select(".y.axis").call(yAxis);
}
This works, but by defining the y scale in the brush variable, the user can now drag 'boxes' in the focus chart, rather than only being able to drag west to east, as in the original chart.
Essentially, my question is: how do I get the range of values that fall within a brush's area, rather than the range of the brush's area itself? Is this even possible?
d3's brush documentation is here.
I came up with a solution.
I used the brush-filtered x.domain to filter down my original data set. This new filtered data set has only the values that fall within the brush:
// Use x.domain to filter the data, then find the max and min duration of this new set, then set y.domain to that
x.domain(brush.empty() ? x2.domain() : brush.extent());
var dataFiltered = data.filter(function(d, i) {
if ( (d.date >= x.domain()[0]) && (d.date <= x.domain()[1]) ) {
return d.duration;
}
})
y.domain([0, d3.max(dataFiltered.map(function(d) { return d.duration; }))]);
Finally, be sure to redraw the y-axis as well as the x-axis:
focus.select("path").attr("d", area);
focus.select(".x.axis").call(xAxis);
focus.select(".y.axis").call(yAxis);
A shorter possibility would be to use d3.scale.invert() on your brush.extent() like so:
var domainExtent = brush.extent().map(function(d){return scale.invert(d);});
var filteredData = data.filter(function(d){return ((d <= domainExtent[1]) && (d >= domainExtent[0]));});
However, by now d3 has gained d3.brushX(), which only allows brushing in East/West direction by design.
Related
I am trying to implement both zooming and brushing in my d3 (v4) chart.
I have got them both working separately, but my problem comes when I try to implement both features on the same chart.
My scenario is the following:
1. The user uses the brush to show a specific region of the chart.
2. They then zoom/pan, but this causes the view to jump back to the old location, because the stored zoom transform is not aware of the changes made by the brushing.
My understanding is that the current zoom transform (scale+translation) is stored inside the DOM element in an internal __zoom attribute. The zoom plugin automatically adjusts this whenever you interact with the element (e.g. by scrolling the mouse wheel).
I see that you can use d3.zoomTransform to get the current zoom transform for an element.
How can I reset/remove the stored zoom transform (e.g. after panning, so that any subsequent zooming carries on from where the brushing left off)?
Note: I don't want to have to change the zoom, but rather just update the stored zoom transform to treat that new scale as the "identity". This is important because I want to be able to smoothly transition from one scale to another when brushing etc.
The way I got around this in the end is:
in the zoom handler, use transform.rescaleX() to get a new transformed scale
Update the main scale's domain based on the transformed scale
Update the x-axis based on the scale
Reset the transform on the element to d3.zoomIdentity.
The key thing here is that after the scale has been updated, the stored transform on the DOM element is always put back to identity (i.e. scale=1, translate=0,0).
That means that we don't need to worry about brushing/zooming or any programatic changes to the scale on different elements won't conflict or have different values from each other. We effectively just keep applying very small scale factors to the element.
In terms of a code example, here are the relevant parts from my working chart:
// class contains:
// this.xScale - stored scale for x-axis
// this.xAxis - a d3 Axis
// this.xAxisElement - a d3 selection for the element on which the x-axis is drawn
// this.zoomX - a d3 ZoomBehavior
// this.chartElement - a d3 selection for the element on which the zooming is added
protected setupZooming(): void {
this.zoomX = d3.zoom().on('zoom', () => { this.onZoomX(); });
this.zoomXElement = this.xAxisElement
.append('rect')
.attr('fill', 'none')
.style('pointer-events', 'all')
.attr('width', this.width)
.attr('height', this.margin.bottom)
.call(this.zoomX);
}
onZoomX(): void {
const transform: d3.ZoomTransform = d3.event.transform;
if (transform.k === 1 && transform.x === 0 && transform.y === 0) {
return;
}
const transformedXScale = transform.rescaleX<any>(this.xScale);
const from = transformedXScale.domain()[0];
const to = transformedXScale.domain()[1];
this.zoomXTo(from, to, false);
this.chartElement.call(this.zoomX.transform, d3.zoomIdentity);
}
zoomXTo(x0: Date, x1: Date, animate: boolean): void {
const transitionSpeed = animate ? 750 : 0;
this.xScale.domain([x0, x1]);
this.xAxisElement.transition().duration(transitionSpeed).call(this.xAxis);
this.updateData(transitionSpeed);
}
updateData(transitionSpeed: number): void {
// ...
}
Apologies if this extract isn't easy to follow outside of the context of the rest of my code, but hopefully it is still helpful.
I am working on dc.js, i have draw pieChart
pieChart.width(300)
.height(200)
.transitionDuration(1500)
.dimension(startValue)
.group(startValueGroup, "StartValue")
.radius(100)
.minAngleForLabel(0.5)
.legend(dc.legend().x(230).y(0))
.title(function(d) {
return d.key + ": " + d.value;
})
.renderTitle(true)
.on("filtered", function (chart) {
dc.events.trigger(function () {
//console.log(total_payment);
});
});
Now, I want to set specific x axis value. Currently, pieChart taking center position of define width and height. That mean it's taking position of (150,100). I want to change this position to (100, 100).
How can i change position of x axis as per above code?
You can't set this directly in the dc options, but you can modify these values after the chart has been rendered. Assuming that "pie" is the ID of the DOM element that you rendered the pie chart into, you can do
d3.select("#pie > svg > g").attr("transform", "translate(100,100)");
I know it is an old post and that the answer works perfectly but when the chart is part of a bigger system (several charts) I found easier to add the above command directly to the chart by doing this:
pieChart.on('renderlet', function (chart) {
chart.select("svg > g").attr("transform", "translate("100,100)");
});
Edit:
Actually I just found out you can do:
.cx(100)
.cy(100)
which will set the centre of the pie chart to 100, 100 before render.
I've managed to make a d3.js line+area graph sync with focus/context brushing and pan/zoom, with a small example here:
http://jsfiddle.net/MtXvx/8/
I'm having trouble limiting the panning to stop at the original domain boundaries, while also working nicely with the brush. This is to prevent users from losing the graph in their view.
While I have tried manually detecting when panning has exceeded boundaries and then setting zoom.translate([0,0]), such as in these examples:
d3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median
Limiting domain when zooming or panning in D3.js
d3.js scatter plot - zoom/drag boundaries, zoom buttons, reset zoom, calculate median
...as I do here at line 183:
//If exceed original domain, limit panning by resetting translate
if (x.domain()[0] < x0.domain()[0]) {
zoom.translate([0, 0]);
}
The problem occurs when:
1) Create a brush region in the small context graph
2) Pan the big focus graph all the way towards the earliest date
3) Graph jumps when panning is almost at the boundary
Would appreciate any help to prevent the jumping from happening, or if there is any other way to limit the panning (and eventually the zooming out too) to the original domain boundaries.
Regarding limiting the zoom-out, setting:
var zoom = d3.behavior.zoom().x(x).scaleExtent([1,10]).on("zoom", zoomed);
...does not work nicely because the zoom-out would be limited to the brush region instead of the full extent of the graph data.
Much thanks!
I had similar problems combining D3 Brushing and Zoom & Pan, but figured it out eventually. I found the key to limit the panning is to reset the translate of the zoom behavior object. Specifically, here is my zoom callback function:
function zoomed() {
var t = d3.event.translate;
var s = d3.event.scale;
var size = width*s;
t[0] = Math.min(t[0], 0);
t[0] = Math.max(t[0], width-size);
zoom.translate(t);
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
var brushExtent = [x.invert(0), x.invert(width)];
context.select(".brush").call(brush.extent(brushExtent));
}
While not part of your question, also an important part to make the whole demo work right is to update the zoom translate and scale when brushing is done, so here is my brushed callback:
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".area").attr("d", area);
focus.select(".x.axis").call(xAxis);
var s = x.domain();
var s_orig = x2.domain();
var newS = (s_orig[1]-s_orig[0])/(s[1]-s[0]);
var t = (s[0]-s_orig[0])/(s_orig[1]-s_orig[0]);
var trans = width*newS*t;
zoom.scale(newS);
zoom.translate([-trans,0]);
}
Here is a complete example based one of D3 examples: http://bl.ocks.org/sbreslav/be9af0d809b49864b7d8
to limit the extent of the panning on the graph you could use clamp, although I couldn't see where or if you were using a scale in that fiddle (actually it didn't appear to be working). Here's a simple example in a fiddle
I have a problem that I haven't been able to solve for a number of weeks. I'm working on a modified version of this example: http://bl.ocks.org/mbostock/1667367 I've defined the brush initially so it has a brush extent between 0.5 and 0.8.
var brush = d3.svg.brush()
.x(x2)
.extent([0.5, 0.8])
.on("brush", brushed);
The brush selection shows up (on the context graph) in the correct location, but the initial view of the focus area is still set to the extent of the entire data set (and not to the clipping area of the brush). I've read that defining the brush doesn't automatically force a redraw of the area, but I can't seem to figure out how to make the view of the focus area automatically scale to the brush extents. Can someone please provide some input on this?
Update 1
I currently have a function called Brushed which does the following:
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select("path").attr("d", Light_area);
focus.select(".x.axis").call(xAxis);
Light_line_overlay.select("path").attr("d", Light_area);
rules.select(".y.grid").call(make_x_axis_light()
.tickSize(-height, 0, 0)
.tickFormat("")
);
var xx0=brush.x()(brush.extent()[0]);
var xx1=brush.x()(brush.extent()[1]);
brushfill.attr("x", xx0);
brushfill.attr("width", xx1-xx0);
}
It's slightly different from the example... because I've been modifying it to do different things from the base example. However, the first comment suggests that I should just call this brushed function after declaring the brush (see first post). However, calling this function doesn't do anything (or at least, it doesn't update the focus area to the extents of the brush). Do you have any suggestions?
I apologize for answering this two years late but I just ran in to the same situation and this was the only resource I found on the topic. I was able to figure it out, so hopefully this will help anybody else who stumbles upon it.
The code in the original question was almost all the way there, it just didn't have the right scaling on the extent initialization.
The data I'm using is an array of objects with a ts key (which is epoch milliseconds) that I use for my x values.
// These are needed for the brush construction to know how to scale
x2.domain(x.domain());
y2.domain(y.domain());
// Pick out the ~50% and ~80% marks from the data
var N = data.length;
var cx0 = new Date(data[Math.floor(N*0.50)].ts);
var cx1 = new Date(data[Math.floor(N*0.80)].ts);
// Construct with that extent, which will leave the
// context box started in the correct position.
var brush = d3.svg.brush()
.x(x2)
.extent([cx0, cx1])
.on("brush", brushed)
;
// This is just the original brushed example
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select(".area").attr("d", line);
focus.select(".x.axis").call(xAxis);
}
...
var focus = svg.append("g")
.attr("class", "focus")
.attr("transform",
"translate(" + margin.left + "," + margin.top + ")")
;
// Now that focus is defined we can manually update it
brushed();
I actually kept the call to brushed at the very end of the setup, just to keep things pretty, but the point here was just to illustrate that once focus is defined you can call brushed to do whatever updates you want there.
Ultimately it seems your main issue was getting the right type for the extent. Using [0.5, 0.8] worked on initialization, but if you check whenever brushed is called from actually sliding the focus around with the mouse, brush.extent() will be [Date(), Date()]. And that makes sense, since we're passing that extent to x.domain. So this sets up all the scaling before initializing the brush, so that the initialization extent can be a Date, then everything else is gravy.
You need to perform actions similar to the ones of the brushed function whenever your brush extent is changed programmatically. Resize the x.domain, refresh the view.
function brushed() {
x.domain(brush.empty() ? x2.domain() : brush.extent());
focus.select("path").attr("d", area);
focus.select(".x.axis").call(xAxis);
}
If that doesn't solve your problem, consider providing some code example.
I am a novice while working on d3.js.
I wanted to know how can we Animate some data (eg. Change colors) with respect to time.
eg. Let's say, in Monitoring app, I am projecting cluster data over US Map. Projection is done by drawing a circle and filling it by RED, GREEN or YELLOW color depending on it's status.
When we start monitoring, ideally all circles will be filled with "GREEN" color and then over time color can change to "YELLOW" or "RED" depending on how cluster is behaving.
So if I need to play these color changes over time in some time window, how can it be done ?
If you can point me to any of the similar examples , that will help too ?
Thanks
Take a look at http://mbostock.github.com/d3/tutorial/bar-2.html. Basically you'll need a redraw function that you'll call whenever you want to update your chart. (Note: there is nothing special about the name of this function, you can call it whatever you want.)
You can use setInterval to create a basic timer, this is the rate that your chart will be updated.
setInterval(function() {
redraw(); // call the function you created to update the chart
}, 1500);
Then you define redraw to update the chart data. This is a redraw function for a bar chart, but yours would be similar. You would just be adjusting the color based on the data instead of the y position and height.
function redraw() {
// Update…
chart.selectAll("rect")
.data(data)
.transition()
.duration(1000)
.attr("y", function(d) { return h - y(d.value) - .5; })
.attr("height", function(d) { return y(d.value); });
}
Note that this is a simplified version, I recommend reading the page that I linked above for a more complete example.