D3 - resizable chart with brush - d3.js

I've created a timeseries chart with brush selection based on MB's example however I am trying to make my chart resizable.
I need the brush selection to persist between resize's so that the "focus" chart is showing the same data range before and after a resize event.
I believe I need to save the selection (as t1, t2) and then re-apply it after all elements have been resized but I'm not sure of the best way to achieve this.
I've read the documentation and can't find any suggestion regarding programatically getting/setting the selection from t values.
I gather a call to brush.move will be necessary:
context.select("g.brush")
.call(brush)
.call(brush.move, x.range());
but i'm not sure how to modify this to regain the previous selection.
In-case it helps, I've created a fiddle which is working, except for the brush selection which resets on every resize.
https://jsfiddle.net/vdqg4rsm/12/

Firstly, on resize - record the ratio of change in width:
var widthBefore = width;
width = svgWidth - margin.left - margin.right;
and then, determine the current brush selection
var brushSelection = null;
var brushNode = context.select("g.brush").node();
if (brushNode)
brushSelection = d3.brushSelection(brushNode);
modify the selection according to the ratio of change
if (brushSelection) {
brushSelection = brushSelection.map(function(x) {
return x * (width / widthBefore);
});
}
if there isn't a selection, fallback to something valid
else
brushSelection = x.range();
then apply this to the brush
context.select("g.brush")
.call(brush)
.call(brush.move, brushSelection);

Related

Label auto resizing for the bar charts dc.js

I have a bar chart with in dc.js. The x axis is dimension and y is the measure ranging from 1 - 10k. I want to show all the bars and their labels not overlapping each other. Chart looks fine when there are few bars when the number of bars starts to increase they look not okay. I am looking to auto resize the labels for the bar-chart.
Sample Chart
I tried this method of renderlet to change the fontsize automatically
stackedBarChart.on('renderlet', function(chart) {
chart.selectAll('text.barLabel')
.attr('transform', function(d) {
//How do i get/set the width of the label here ?
});
I tried the below and I am able to do dynamic label sizing. I used pretransistion and inside that fetched all bar lables and altered their font-size. The font sizing is altered according to Gordon's Idea, Getting each bar's size and and assigning the font size dynamically. For testing I have used .2 as a factor to reduce the font size.
.on('pretransition', function(chart) {
var chart_width = chart.width();
chart.selectAll("text.barLabel").attr('font-size', function(data) {
let total_bars = chart.group().all().length;
let sing_bar_width = Math.floor(chart_width / total_bars);
return (sing_bar_width * .2) + "px";
});
});

Rendering in the background of a dc.js chart with renderlet

I use dc.js for showing the results of multiple classification algorithms. More specifically, I want to show a precision recall chart (each point corresponds to a result of a classification system).
I already used a dc.js scatter chart for this which works fine.
Additionally I would like to have a d3 contour in the background of the chart which shows the F-measure.
This is already implemented. The only issue is that the contour part is in the foreground and not in the background of the chart.
Please have a look at the jsfiddle for a full example.
Two questions are still open for me because I'm not a dc.js or d3 expert:
Is there a way to put the contour in the background or the symbols(cycles) of the scatter chart in the foreground (I already tried it with the help of this stackoverflow question but with no success)
I used the 'g.brush' selector to get the area of the inner chart. This works fine as long as the brushing is turned on. Is the selector a good way to go or are there better alternatives (which may also work if brushing is switched off).
In my example I put the contour part in the upper left corner to see if it works but I also provide the code (currently uncommented) to increase the width and height of the contour to the correct size.
chart
.on('renderlet', function (chart) {
var innerChart = chart.select('g.brush');
var width = 300, height=300;
//getting the correct width, height
//var innerChartBoundingRect = innerChart.node().getBoundingClientRect();
//var width = innerChartBoundingRect.width, height=innerChartBoundingRect.height;
[contours, color] = generateFmeasureContours(width,height, 1);
innerChart
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
var symbols = chart.chartBodyG().selectAll('path.symbol');
symbols.moveToFront();
});
jsfiddle
Putting something in the background is a general purpose SVG skill.
SVG renders everything in the order it is declared, from back to front, so the key is to put your content syntactically before everything else in the chart.
I recommend encapsulating it in an svg <g> element, and to get the order right you can use d3-selection's insert method and the :first-child CSS selector instead of append:
.on('pretransition', function (chart) {
// add contour layer to back (beginning of svg) only if it doesn't exist
var contourLayer = chart.g().selectAll('g.contour-layer').data([0]);
contourLayer = contourLayer
.enter().insert('g', ':first-child')
.attr('class', 'contour-layer')
.attr('transform', 'translate(' + [chart.margins().left,chart.margins().top].join(',') + ')')
.merge(contourLayer);
A few more points on this implementation:
use dc's pretransition event because it happens immediately after rendering and redrawing (whereas renderlet waits for transitions to complete)
the pattern .data([0]).enter() adds the element only if it doesn't exist. (It binds a 1-element array; it doesn't matter what that element is.) This matters because the event handler will get called on every redraw and we don't want to keep adding layers.
we give our layer the distinct class name contour-layer so that we can identify it, and so the add-once pattern works
contourLayer = contourLayer.enter().insert(...)...merge(contourLayer) is another common D3 pattern to insert stuff and merge it back into the selection so that we treat insertion and modification the same later on. This would probably be simpler with the newer selection.join method but tbh I haven't tried that yet.
(I think there may also have been some improvements in ordering that might be easier than insert, but again, I'm going with what I know works.)
finally, we fetch the upper-left offset from the margin mixin
Next, we can retrieve the width and height of the actual chart body using
(sigh, undocumented) methods from dc.marginMixin:
var width = chart.effectiveWidth(), height = chart.effectiveHeight();
And we don't need to move dots to front or any of that; the rest of your code is as before except we use this new layer instead of drawing to the brushing layer:
contourLayer
.selectAll("path")
.data(contours)
.enter()
.append("path")
.attr("d", d3.geoPath())
.attr("fill", d => color(d.value));
Fork of your fiddle.
Again, if you'd like to collaborate on getting a contour example into dc.js, that would be awesome!

How to reset/remove zoom transform stored in an element in d3 v4?

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.

D3 Redraw Brush

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.

How do you set the width of the legend of a Microsoft .Net chart control?

The legend currently auto-sizes according to the max width of the labels it contains, which is throwing my layout. I need to set a width or min-width. Here is my legend creation method:
public Legend CreateLegend()
{
var legend = new Legend();
legend.Enabled = true;
legend.Font = new Font("Arial", 11F);
legend.ForeColor = Color.FromArgb(102, 102, 102);
return legend;
}
It seems like anything I do which accesses the Position object for the legend (defines X an Y coords, width and height) makes the legend disappear completely.
So legend.Position.Width = 5 would make it disappear...
Okay, turns out that if you do not define all four properties of the Position object, it sets them to zero...so height was zero...so it disappeared. Awesome.

Resources