I am using d3 to render a simplified gantt chart, with panning and zooming using d3.behavior.zoom.
The x scale is a time scale (slightly modified to center calendar days in columns, etc) and works beatifully, but I'm having problems deciding how to zoom/pan the y scale, whose domain is a list of tasks which will often be too many to fit in the chart area, so the need for panning/zooming.
Is there a way to tell the default ordinal scale to react to zoom/pan events, or should I write a custom scale? And if I need to write a custom scale, would it be better to base it on d3.scale.ordinal (having it store the whole list of tasks, and use only the visible subset as its domain), or on d3.scale.linear (and then implement something similar to the ordinal scale for rangebands etc?).
Or is there something I'm missing (entirely probable as it's my first project using d3)?
Expanding slightly from my previous answer as I have been contacted privately to explain how I did this.
First, a screenshot of the app in question, whos emain job is aggregating and displaying planning data gathered from various sources (PowerPoint files, corporate databases, etc.).
The relevant bit is the right vertical axis with the colored dots, where each dot represents a project's effort and involved organization. The gray area on the axis is a d3.js brush, and can be panned/resized to change the chart/table data in real time.
// the axis is a regular ordinal axis, with a brush
y2 = d3.scale.ordinal(),
brush = d3.svg.brush().y(y2).on('brush', brushReact),
// [...]
// brush event handler
function brushReact() {
if (tasksSlice == null)
return;
var yrb = y2.rangeBand(),
extent = brush.extent(),
s0 = Math.round(extent[0]/yrb),
s1 = Math.round(extent[1]/yrb);
if (s0 == tasksSlice[0] && s1 == tasksSlice[1])
return;
tasksSlice = [s0, s1];
inner.refresh();
}
What happens inside the handler is pretty simple:
- get the brush extent
- transpose it to indexes in my data array
- slice my data array and set the result as the data to display
- refresh the chart and table
I hope this clears it up.
Turns out it's not that hard, I had to:
- write a custom scale, mostly identical to d3.scale.ordinal, except that it stores the full range of domain values and implements a slice([min, max]) method that sets the range of visible domain values
- track the y translation delta in the zoom event callback, and add it to a variable that stores total y translation
- check if the total translation amount is >= than the y delta between two range values, if it is check that we are not already on one of the (visible or non-visible) domain bounds (0 or length), and if we are not increment or decrement the slice indexes by 1, reset the total translation variable, then redraw the axis