Help on resizing a brush

557 views
Skip to first unread message

Tim Dudgeon

unread,
Jul 15, 2016, 7:50:20 AM7/15/16
to d3-js
Hi All,
I'm working on a D3 scatterplot  that supports resizing, and need the currently brushed area to adapt to the new proportions. A bit like this: http://bl.ocks.org/timelyportfolio/5091037, but in 2D.
But that example regenerates the whole plot, rather than resizing and repositioning the elements, and doesn't remember the brushed region.
I've worked out how to resize all other elements of the plot (axes, points, legend) but am struggling with the brushing. I want this to remain intact after resizing, and reflect the correct region.
I've tried various things, all involving setting the new X and Y scale:

var xScale = ...;
var yScale = ...;
brush
.x(xScale)
.y(yScale);

but I can't get the brushed area (visual rectangle) to re-position.
From what I can gather the brush's extent will remain the same, and with the new scales it should know how to redraw in the right location. But I can't get it to do so.

All suggestions welcome.

Tim 

Drew Winget

unread,
Jul 15, 2016, 6:45:17 PM7/15/16
to d3...@googlegroups.com
jQueryUI's draggable/resizeable boxes make a number of extremely annoying and badly documented assumptions. They also don't play well with a lot of custom styling options. For example, I cannot even use the handles of the brush because jQuery is capturing my mouseup events.

However, in this case I believe you can solve the dragging problem by only letting the top bar be a "handle" in your jqueryUI setup. 

Also you should probably bind the redraw to the resize event rather than the stop event, since the interaction is quite janky. Using the stop event callback, the interface feels laggy and if you shrink the box the graph sticks outside of it in a very glitchy way. Using resize will have it redraw as the user drags, which is more natural. If there is any performance concern, simple throttle the callback. 

As for you actual question: the redraw function has [.3, .5] hard-coded as the brush extent, so that's what it will use. If you want to brush extent to persist, you need to store it somewhere outside of your redraw function and pass it in on redraw. See Mike's article on making charts reuseable/"editable" for a very well-written guide to accomplish this. 

--
You received this message because you are subscribed to the Google Groups "d3-js" group.
To unsubscribe from this group and stop receiving emails from it, send an email to d3-js+un...@googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Drew Winget

unread,
Jul 15, 2016, 6:46:48 PM7/15/16
to d3...@googlegroups.com
simply*
your*
to brush extent = the brush extent

Tim Dudgeon

unread,
Jul 17, 2016, 6:23:53 AM7/17/16
to d3-js
Not really sure what you're meaning there.
I create a cut down example that might help:
Drag out a region and then resize using the elements at the top. You'll see the axes and points adjusting to the new size, but not the brush.
(the brush bit is about 20 lines from the bottom of the file).

Tim

Drew Winget

unread,
Jul 19, 2016, 10:56:18 PM7/19/16
to d3...@googlegroups.com
Hi Tim, 

Thanks for reducing the test case. It seems I got distracted with your other issues, as jQueryUI is apt to cause PTSD-like flashbacks in many a web developer. In your first example the brush was seemed to be resizing correctly to me. The extent was hard-coded as [.3,.5], so it was always the correct shape for the extent and container dimensions given, but the extent was never saved or updated anywhere outside of the draw function, which was resetting it to [.3, .5] each time.

The second example has a different problem, or set of problems. Here, you've separated the draw and redraw functions. In some ways this makes the problem more complex. We'll address that further down, but the short answer to your resizing question is that in order to redraw the brush you must save and set the brush's extent property programmatically before calling "brush". See this updated bin for the quick and dirty solution to your brush redrawing question (comments added where I've made changes).

Brush Redraw Explanation (Updated Bin)
First let's take a look at the bin you provided. As opposed to your first example (the block), the brush is no longer being re-drawn (as you note in your comment at the end of the script). That is why it always remains the same size. Nothing has told it to redraw. To figure out how we might "re"-draw the brush, let's think about how it is drawn the first time, on initialisation. The following assumes we are reading the code from your example:
  1. We define brush as d3.svg.brush(). Like many things in d3, a d3.svg.brush() returns a function. A brush is implemented as a reusable chart. Here is the source for a generic re-useable chart that is described created in this link. Notice that the chart is defined by a function that returns a function. Nowhere in the chart is this function itself called. It is up to the consuming code to decide when and how to call this function. The outer function provides closure, so that we can keep private state hidden from the outside, and execute any configuration logic we need in order to return a properly configured chart function.
  2. We set the .x() and .y() properties of the brush object, by calling the setters. Each of them returns the whole brush. This is why the chaining syntax works.
  3. So, what is "brush" now? It's still just a function, but it has some properties defined on it. Its x and y properties are defined, but it has no extent, and it has not actually been placed in the DOM anywhere yet.
  4. Further down, we bind the "brushed" and "brushended" handlers to the "brush" and "brushend" events. The brush still hasn't been drawn, we have just defined two more properties. Notice that you can comment these event binding lines out and the brush element itself will behave the same; it just won't have any side effects on the plotted circles.
  5. Finally, we append a DOM element with the "brush" class and pass it into the brush function with .call(brush).
  6. We click and drag, and the brush appears. What happened? The brush component is doing some magic. It has stretched its container to fill its parent, and appended some other elements to help it respond to mouse events, which it is also binding on its own. The brush automatically sets its own extent when click and drag events are fired.
  7. So it looks like we have never drawn the brush at all. The brush always draws itself. What if we want to initialise the brush with a certain set of dimensions? Just as we set the x and y properties before, we can explicitly set the extent. Now, when we get to step 5, instead of the brush waiting for an event to draw itself, it detects that it has a renderable extent and renders itself. So we can get the brush to draw by giving it an extent and then rendering it. Changing its extent after it is rendered, without re-rendering it, will not work. The brush function must be called again.
  8. Now that we know how to render the brush with a given extent, we can easily see how to "re"-render it. In the regenerate function, pass in the previous extent and make sure it is set before re-rendering (see code and comments for more detail). But where is this extent going to go, and how or when does it get saved? In the edited example, I've saved the extent to the plotConfig object with other state that is meant to be passed between the chart's old and new renderings. Saving is done in the "brushed" callback, so everytime the brush updates, its new extent is available for the redraw/resize. The plotConfig is an awkward way of accomplishing this, however, so I hope you'll read on for a different approach to your chart which makes the details of resizing an intrinsic property of any re-rendering of your chart, with no need to manage prototypes or config objects.
Refactor Explanation (Bin)
However, as alluded to above, you might consider a different approach to defining your chart and managing the initialisation and updating. Along these lines, I have prepared another example to show how you can realise your scatterplot as a reusable chart component with an automatically-updating brush. See this bin. There were a number of changes I made to simplify the original approach. 
  1. I deleted the execute function. It was not necessary because I moved the script to the bottom of the html section (or "page"). HTML resources execute from the top to the bottom, with no guarantees on asynchronous returns. The explicit DOM will always be present before a script executed underneath it. As long as you're not waiting on other async resources, you can refer to them in scripts executed below your DOM declarations.
  2. I changed your resizing function to call the setter of the reusable scatterplot, and removed the explicit DOM update on the chart's container. This is now done in the reusable chart itself.
  3. I removed the plotConfig property that you were setting on the DOM node. This was the third place you were storing state information about the chart, the other two being the d3 chart variables themselves ("chart", "main", etc.), and the DOM itself (such as when you query the DOM for element size or expect the brush to update automatically when the DOM is resized. D3 and the re-useable chart pattern make it so you only ever store state in one place: inside the closure of the chart definition. The chart "data" (the data you are trying to represent) is considered a different kind of state that is always saved outside of your chart, and is handled automatically by d3 when information refreshes by the use of "joins". See Working with Joins and the General Update Pattern series. I would also recommend those resources to understand why d3 never requires you to explicitly remove elements on its behalf (for instance, you were using your plotConfig object to selectAll('*') and then .remove() them). For that you use selections, specifically the exit() selection (read Three Little Circles, which explains this concept in very thorough detail through easy-to-follow examples). Shared, mutable, and inconsistently-accessible state could be considered the primary source of frustration and bugs in most applications. Object orientation can encourage random state clumps to be hidden in many different places with no coherent way to access them all, and the DOM worsens the situation by providing a tempting scene-graph-like utility that is actually quite convenient at storing some application state. Patterns like CQRS, flux, and FRP and dataflow programming all aim to rectify the situation, with some degree of success. For a straightforward and "real-world" look at how these concepts matter and greatly help in developing front-end applications, see Nicholas Hery's Describing UI State with Data.
  4. In refactoring to the reusable chart pattern, I have moved the outermost element to the consuming code (bottom of JSBin JS file), so that it can be passed as the selection into which to render the chart. This could be considered a way of "pushing non-functional side-effects to the periphery", and decouples the chart's definition from the DOM, since the scatterplot's API not longer requires the containing element to have an id, or event know anything about the containing element.
  5. I added a function to re-render the chart with all the same display characteristics (scales, brushes, width/height), but different data. This shows the power of managing state in a very intentional way within the closure. State that describes the current shape of the UI is held constant while we updaet the rendered data. They do not effect each other in strange ways due to prototype and constructor confusion, public properties getting overwritten or being forgotten in an update, or any other such nonsense. 
Other Thoughts
Some other things you might consider: 
  • The use of Javascript's pseudo-classical inheritance syntax ("new" and "prototype") are part of what's muddying the waters. D3's code and API deploy a very functional style, and it can be very confusing to mix the two. In d3, the current appearance of the chart is perennially described by a pure function of some data, whether it is the first time the function is running or the 10,000th time the function is running. The "new"/OO/imperative style is also causing you to make incorrect assumptions about what d3's functions return. For instance, "d3.svg.brush" returns another function, and doesn't automatically call itself when its properties are updated. 
  • Along these lines, d3 provides a lot of magic to smooth over some places where the functional expression of the chart's appearance would not be so straightforward. Namely, d3 tracks what elements have been spewed into the global namespace that is the DOM, and it expects to be delegated complete authority to manage those for you as your data changes. This includes elements that exist before an update. See How Selections Work for more details.
  • If you just want a scatterplot or any other type of standard chart type, you are probably better off using a canned chart library like NVD3 or C3.js. Particularly if you are working on a deadline now and need to bang out a number of these standard chart types without investing some more time in tutorials/books, I would go that route. 
  • I noticed that you have since made another post about resizing other charts. To generalise the resize advice from all of this: resizing should be done by a function that just sets some properties on a reusable version of your chart and then re-renders it.

Tim Dudgeon

unread,
Jul 20, 2016, 4:38:05 AM7/20/16
to d3-js
Drew,
Thanks so much for that. Such a detailed and understandable answer.
Most appreciated!
Tim
Message has been deleted

john.d...@postmates.com

unread,
Jul 27, 2016, 7:26:00 PM7/27/16
to d3-js
Hi Drew,

The .x() and .y() properties of the brush object are no longer supported in v4. What is the solution in this case (for just these two properties)?

Many thanks,
John
Reply all
Reply to author
Forward
0 new messages