Skip to content

Commit a78db76

Browse files
committed
finalize multiselect functionality
- use new constraintrange info_array 1-2 dimension format - add multiselect attribute - some further cleanup of conversions - image test to cover most of multiselect logic & precision - interaction tests of ordinal and continuous multiselect - put constraint ranges back into existing mocks
1 parent a4f4a5f commit a78db76

17 files changed

+552
-67
lines changed

src/traces/parcoords/attributes.js

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -81,28 +81,17 @@ module.exports = {
8181
constraintrange: {
8282
valType: 'info_array',
8383
role: 'info',
84+
freeLength: true,
85+
dimensions: '1-2',
8486
items: [
85-
{
86-
valType: 'info_array',
87-
editType: 'calc',
88-
items: [
89-
{valType: 'number', editType: 'calc'},
90-
{valType: 'number', editType: 'calc'}
91-
]
92-
},
93-
{
94-
valType: 'info_array',
95-
editType: 'calc',
96-
items: [
97-
{valType: 'number', editType: 'calc'},
98-
{valType: 'number', editType: 'calc'}
99-
]
100-
}
87+
{valType: 'number', editType: 'calc'},
88+
{valType: 'number', editType: 'calc'}
10189
],
10290
editType: 'calc',
10391
description: [
10492
'The domain range to which the filter on the dimension is constrained. Must be an array',
105-
'of `[fromValue, toValue]` with finite numbers as elements.'
93+
'of `[fromValue, toValue]` with `fromValue <= toValue`, or if `multiselect` is not',
94+
'disabled, you may give an array of arrays, where each inner array is `[fromValue, toValue]`.'
10695
].join(' ')
10796
},
10897
multiselect: {

src/traces/parcoords/axisbrush.js

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var c = require('./constants');
1212
var d3 = require('d3');
1313
var keyFun = require('../../lib/gup').keyFun;
1414
var repeat = require('../../lib/gup').repeat;
15+
var sortAsc = require('../../lib').sorterAsc;
1516

1617
function addFilterBarDefs(defs) {
1718
var filterBarPattern = defs.selectAll('#' + c.id.filterBarPattern)
@@ -115,8 +116,7 @@ function setHighlight(d) {
115116
if(!filterActive(d.brush)) {
116117
return '0 ' + d.height;
117118
}
118-
var unitRanges = d.brush.filter.getConsolidated();
119-
var pixelRanges = unitRanges.map(function(pr) {return pr.map(d.unitScaleInOrder);});
119+
var pixelRanges = unitToPx(d.brush.filter.getConsolidated(), d.height);
120120
var dashArray = [0]; // we start with a 0 length selection as filter ranges are inclusive, not exclusive
121121
var p, sectionHeight, iNext;
122122
var currentGap = pixelRanges.length ? pixelRanges[0][0] : null;
@@ -138,6 +138,12 @@ function setHighlight(d) {
138138
return dashArray;
139139
}
140140

141+
function unitToPx(unitRanges, height) {
142+
return unitRanges.map(function(pr) {
143+
return pr.map(function(v) { return v * height; }).sort(sortAsc);
144+
});
145+
}
146+
141147
function differentInterval(int1) {
142148
// An interval is different if the extents don't match, which is a safe test only because the intervals
143149
// get consolidated anyway (ie. the identity of overlapping intervals won't be preserved; they get fused)
@@ -187,9 +193,9 @@ function renderHighlight(root, tweenCallback) {
187193
styleHighlight(barToStyle);
188194
}
189195

190-
function getInterval(b, unitScaleInOrder, y) {
196+
function getInterval(b, height, y) {
191197
var intervals = b.filter.getConsolidated();
192-
var pixIntervals = intervals.map(function(interval) {return interval.map(unitScaleInOrder);});
198+
var pixIntervals = unitToPx(intervals, height);
193199
var hoveredInterval = NaN;
194200
var previousInterval = NaN;
195201
var nextInterval = NaN;
@@ -243,8 +249,8 @@ function attachDragBehavior(selection) {
243249
if(d.parent.inBrushDrag) {
244250
return;
245251
}
246-
var y = d.unitScaleInOrder(d.unitScale.invert(d3.mouse(this)[1] + c.verticalPadding));
247-
var interval = getInterval(b, d.unitScaleInOrder, y);
252+
var y = d.height - d3.mouse(this)[1] - 2 * c.verticalPadding;
253+
var interval = getInterval(b, d.height, y);
248254
d3.select(document.body)
249255
.style('cursor', interval.n ? 'n-resize' : interval.s ? 's-resize' : !interval.m ? 'crosshair' : filterActive(b) ? 'ns-resize' : 'crosshair');
250256
})
@@ -258,10 +264,10 @@ function attachDragBehavior(selection) {
258264
.on('dragstart', function(d) {
259265
var e = d3.event;
260266
e.sourceEvent.stopPropagation();
261-
var y = d.unitScaleInOrder(d.unitScale.invert(d3.mouse(this)[1] + c.verticalPadding));
267+
var y = d.height - d3.mouse(this)[1] - 2 * c.verticalPadding;
262268
var unitLocation = d.unitScaleInOrder.invert(y);
263269
var b = d.brush;
264-
var intData = getInterval(b, d.unitScaleInOrder, y);
270+
var intData = getInterval(b, d.height, y);
265271
var unitRange = intData.interval;
266272
var pixelRange = unitRange.map(d.unitScaleInOrder);
267273
var s = b.svgBrush;
@@ -346,13 +352,16 @@ function attachDragBehavior(selection) {
346352
s.brushEndCallback(filter.get());
347353
return; // no need to fuse intervals or snap to ordinals, so we can bail early
348354
}
355+
349356
var mergeIntervals = function() {
350357
// Key piece of logic: once the button is released, possibly overlapping intervals will be fused:
351358
// Here it's done immediately on click release while on ordinal snap transition it's done at the end
352359
filter.set(filter.getConsolidated());
353360
};
361+
354362
if(d.ordinal) {
355-
var a = d.ordinalScale.range();
363+
var a = d.paddedUnitValues;
364+
if(a[a.length - 1] < a[0]) a = a.slice().sort(sortAsc);
356365
s.newExtent = [
357366
ordinalScaleSnapLo(a, s.newExtent[0], s.stayingIntervals),
358367
ordinalScaleSnapHi(a, s.newExtent[1], s.stayingIntervals)
@@ -366,11 +375,13 @@ function attachDragBehavior(selection) {
366375
} else {
367376
mergeIntervals(); // merging intervals immediately
368377
}
369-
s.brushEndCallback(filter.get());
378+
s.brushEndCallback(filter.getConsolidated());
370379
})
371380
);
372381
}
373382

383+
function startAsc(a, b) { return a[0] - b[0]; }
384+
374385
function renderAxisBrush(axisBrush) {
375386

376387
var background = axisBrush.selectAll('.background').data(repeat);
@@ -458,7 +469,7 @@ function axisBrushMoved(callback) {
458469
function dedupeRealRanges(intervals) {
459470
// Fuses elements of intervals if they overlap, yielding discontiguous intervals, results.length <= intervals.length
460471
// Currently uses closed intervals, ie. dedupeRealRanges([[400, 800], [300, 400]]) -> [300, 800]
461-
var queue = intervals.slice().sort(function(a, b) {return a[0] - b[0];}); // ordered by interval start
472+
var queue = intervals.slice();
462473
var result = [];
463474
var currentInterval;
464475
var current = queue.shift();
@@ -475,14 +486,20 @@ function dedupeRealRanges(intervals) {
475486
function makeFilter() {
476487
var filter = [];
477488
var consolidated;
489+
var bounds;
478490
return {
479491
set: function(a) {
480-
filter = a.slice().map(function(d) {return d.slice();});
481-
consolidated = dedupeRealRanges(a);
492+
filter = a
493+
.map(function(d) { return d.slice().sort(sortAsc); })
494+
.sort(startAsc);
495+
consolidated = dedupeRealRanges(filter);
496+
bounds = filter.reduce(function(p, n) {
497+
return [Math.min(p[0], n[0]), Math.max(p[1], n[1])];
498+
}, [Infinity, -Infinity]);
482499
},
483-
get: function() {return filter.slice();},
484-
getConsolidated: function() {return consolidated;}, // would be nice if slow to slice in two layers...
485-
getBounds: function() {return filter.reduce(function(p, n) {return [Math.min(p[0], n[0]), Math.max(p[1], n[1])];}, [Infinity, -Infinity]);}
500+
get: function() { return filter.slice(); },
501+
getConsolidated: function() { return consolidated; },
502+
getBounds: function() { return bounds; }
486503
};
487504
}
488505

@@ -501,9 +518,38 @@ function makeBrush(state, rangeSpecified, initialRange, brushStartCallback, brus
501518
};
502519
}
503520

521+
// for use by supplyDefaults, but it needed tons of pieces from here so
522+
// seemed to make more sense just to put the whole routine here
523+
function cleanRanges(ranges, dimension) {
524+
if(Array.isArray(ranges[0])) {
525+
ranges = ranges.map(function(ri) { return ri.sort(sortAsc); });
526+
527+
if(!dimension.multiselect) ranges = [ranges[0]];
528+
else ranges = dedupeRealRanges(ranges.sort(startAsc));
529+
}
530+
else ranges = [ranges.sort(sortAsc)];
531+
532+
// ordinal snapping
533+
if(dimension.tickvals) {
534+
var sortedTickVals = dimension.tickvals.slice().sort(sortAsc);
535+
ranges = ranges.map(function(ri) {
536+
var rSnapped = [
537+
ordinalScaleSnapLo(sortedTickVals, ri[0], []),
538+
ordinalScaleSnapHi(sortedTickVals, ri[1], [])
539+
];
540+
if(rSnapped[1] > rSnapped[0]) return rSnapped;
541+
})
542+
.filter(function(ri) { return ri; });
543+
544+
if(!ranges.length) return;
545+
}
546+
return ranges.length > 1 ? ranges : ranges[0];
547+
}
548+
504549
module.exports = {
505550
addFilterBarDefs: addFilterBarDefs,
506551
makeBrush: makeBrush,
507552
ensureAxisBrush: ensureAxisBrush,
508-
filterActive: filterActive
553+
filterActive: filterActive,
554+
cleanRanges: cleanRanges
509555
};

src/traces/parcoords/defaults.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var hasColorscale = require('../../components/colorscale/has_colorscale');
1414
var colorscaleDefaults = require('../../components/colorscale/defaults');
1515
var maxDimensionCount = require('./constants').maxDimensionCount;
1616
var handleDomainDefaults = require('../../plots/domain').defaults;
17+
var axisBrush = require('./axisbrush');
1718

1819
function handleLineDefaults(traceIn, traceOut, defaultColor, layout, coerce) {
1920
var lineColor = coerce('line.color', defaultColor);
@@ -71,7 +72,10 @@ function dimensionsDefaults(traceIn, traceOut) {
7172
coerce('range');
7273

7374
coerce('multiselect');
74-
coerce('constraintrange');
75+
var constraintRange = coerce('constraintrange');
76+
if(constraintRange) {
77+
dimensionOut.constraintrange = axisBrush.cleanRanges(constraintRange, dimensionOut);
78+
}
7579

7680
commonLength = Math.min(commonLength, values.length);
7781
}

src/traces/parcoords/lines.js

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,10 +173,6 @@ function makeAttributes(sampleCount, points) {
173173
return attributes;
174174
}
175175

176-
function valid(i, offset, panelCount) {
177-
return i + offset <= panelCount;
178-
}
179-
180176
module.exports = function(canvasGL, d) {
181177
var model = d.model,
182178
vm = d.viewModel,
@@ -380,23 +376,20 @@ module.exports = function(canvasGL, d) {
380376
for(d = 0; d < 16; d++) {
381377
var dimP = d + 16 * abcd;
382378
var lim;
383-
if(valid(d, 16 * abcd, panelCount)) {
384-
var dimi = initialDims[dimP === 0 ? 0 : 1 + ((dimP - 1) % (initialDims.length - 1))];
385-
lim = dimi.brush.filter.getBounds()[loHi];
379+
if(dimP < initialDims.length) {
380+
lim = initialDims[dimP].brush.filter.getBounds()[loHi];
386381
}
387382
else lim = loHi;
388383
lims[loHi][abcd][d] = lim + (2 * loHi - 1) * filterEpsilon;
389384
}
390385
}
391386
}
392387

393-
var maskExpansion = maskHeight / canvasHeight;
394-
395388
function expandedPixelRange(dim, bounds) {
396-
var originalPixelRange = bounds.map(dim.unitScaleInOrder);
389+
var maskHMinus = maskHeight - 1;
397390
return [
398-
Math.max(0, Math.floor(originalPixelRange[0] * maskExpansion)),
399-
Math.min(maskHeight - 1, Math.ceil(originalPixelRange[1] * maskExpansion))
391+
Math.max(0, Math.floor(bounds[0] * maskHMinus)),
392+
Math.min(maskHMinus, Math.ceil(bounds[1] * maskHMinus))
400393
];
401394
}
402395

@@ -408,7 +401,7 @@ module.exports = function(canvasGL, d) {
408401
var byteIndex = (dimIndex - bitIndex) / bitsPerByte;
409402
var bitMask = Math.pow(2, bitIndex);
410403
var dim = initialDims[dimIndex];
411-
var ranges = dim.brush.filter.get().sort(function(a, b) {return a[0] - b[0]; });
404+
var ranges = dim.brush.filter.get();
412405
if(ranges.length < 2) continue; // bail if the bounding box based filter is sufficient
413406

414407
var prevEnd = expandedPixelRange(dim, ranges[0])[1];

src/traces/parcoords/parcoords.js

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,12 @@ function toText(formatter, texts) {
6767
};
6868
}
6969

70-
function domainScale(height, padding, dimension) {
70+
function domainScale(height, padding, dimension, tickvals, ticktext) {
7171
var extent = dimensionExtent(dimension);
72-
var texts = dimension.ticktext;
73-
return dimension.tickvals ?
72+
return tickvals ?
7473
d3.scale.ordinal()
75-
.domain(dimension.tickvals.map(toText(d3.format(dimension.tickformat), texts)))
76-
.range(dimension.tickvals
74+
.domain(tickvals.map(toText(d3.format(dimension.tickformat), ticktext)))
75+
.range(tickvals
7776
.map(function(d) {return (d - extent[0]) / (extent[1] - extent[0]);})
7877
.map(function(d) {return (height - padding + d * (padding - (height - padding)));})) :
7978
d3.scale.linear()
@@ -207,6 +206,7 @@ function viewModel(state, callbacks, model) {
207206
var uScale = unitScale(height, c.verticalPadding);
208207
var specifiedConstraint = dimension.constraintrange;
209208
var filterRangeSpecified = specifiedConstraint && specifiedConstraint.length > 0;
209+
if(filterRangeSpecified && !Array.isArray(specifiedConstraint[0])) specifiedConstraint = [specifiedConstraint];
210210
var filterRange = filterRangeSpecified ? specifiedConstraint.map(function(d) {return d.map(domainToUnit).map(paddedUnitScale);}) : [[0, 1]];
211211
var brushMove = function() {
212212
var p = viewModel;
@@ -226,13 +226,45 @@ function viewModel(state, callbacks, model) {
226226
truncatedValues = truncatedValues.slice(0, dimension._length);
227227
}
228228

229+
var tickvals = dimension.tickvals;
230+
var ticktext;
231+
function makeTickItem(v, i) { return {val: v, text: ticktext[i]}; }
232+
function sortTickItem(a, b) { return a.val - b.val; }
233+
if(Array.isArray(tickvals) && tickvals.length) {
234+
ticktext = dimension.ticktext;
235+
236+
// ensure ticktext and tickvals have same length
237+
if(!Array.isArray(ticktext) || !ticktext.length) {
238+
ticktext = tickvals.map(d3.format(dimension.tickformat));
239+
}
240+
else if(ticktext.length > tickvals.length) {
241+
ticktext = ticktext.slice(0, tickvals.length);
242+
}
243+
else if(tickvals.length > ticktext.length) {
244+
tickvals = tickvals.slice(0, ticktext.length);
245+
}
246+
247+
// check if we need to sort tickvals/ticktext
248+
for(var j = 1; j < tickvals.length; j++) {
249+
if(tickvals[j] < tickvals[j - 1]) {
250+
var tickItems = tickvals.map(makeTickItem).sort(sortTickItem);
251+
for(var k = 0; k < tickvals.length; k++) {
252+
tickvals[k] = tickItems[k].val;
253+
ticktext[k] = tickItems[k].text;
254+
}
255+
break;
256+
}
257+
}
258+
}
259+
else tickvals = undefined;
260+
229261
return {
230262
key: key,
231263
label: dimension.label,
232264
tickFormat: dimension.tickformat,
233-
tickvals: dimension.tickvals,
234-
ticktext: dimension.ticktext,
235-
ordinal: !!dimension.tickvals,
265+
tickvals: tickvals,
266+
ticktext: ticktext,
267+
ordinal: !!tickvals,
236268
multiselect: dimension.multiselect,
237269
xIndex: i,
238270
crossfilterDimensionIndex: i,
@@ -246,7 +278,7 @@ function viewModel(state, callbacks, model) {
246278
// fixme remove the old unitScale
247279
unitScale: uScale,
248280
unitScaleInOrder: uScaleInOrder,
249-
domainScale: domainScale(height, c.verticalPadding, dimension),
281+
domainScale: domainScale(height, c.verticalPadding, dimension, tickvals, ticktext),
250282
ordinalScale: ordinalScale(dimension),
251283
domainToUnitScale: domainToUnit,
252284
parent: viewModel,
@@ -268,7 +300,9 @@ function viewModel(state, callbacks, model) {
268300
var invScale = domainToUnit.invert;
269301

270302
// update gd.data as if a Plotly.restyle were fired
271-
var newRanges = f.map(function(r) {return r.map(invertPaddedUnitScale).map(invScale);});
303+
var newRanges = f.map(function(r) {
304+
return r.map(invertPaddedUnitScale).map(invScale).sort(Lib.sorterAsc);
305+
}).sort(function(a, b) { return a[0] - b[0]; });
272306
callbacks.filterChanged(p.key, dimension._index, newRanges);
273307
}
274308
}

src/traces/parcoords/plot.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,22 @@ module.exports = function plot(gd, cdparcoords) {
4646
// without having to incur heavy UI blocking due to an actual `Plotly.restyle` call
4747

4848
var gdDimension = gdDimensionsOriginalOrder[i][originalDimensionIndex];
49-
gdDimension.constraintrange = newRanges.map(function(r) {return r.slice();});
49+
var newConstraints = newRanges.map(function(r) { return r.slice(); });
50+
if(!newConstraints.length) {
51+
delete gdDimension.constraintrange;
52+
newConstraints = null;
53+
}
54+
else {
55+
if(newConstraints.length === 1) newConstraints = newConstraints[0];
56+
gdDimension.constraintrange = newConstraints;
57+
// wrap in another array for restyle event data
58+
newConstraints = [newConstraints];
59+
}
5060

51-
gd.emit('plotly_restyle');
61+
var restyleData = {};
62+
var aStr = 'dimensions[' + originalDimensionIndex + '].constraintrange';
63+
restyleData[aStr] = newConstraints;
64+
gd.emit('plotly_restyle', [restyleData, [i]]);
5265
};
5366

5467
var hover = function(eventData) {
-2.35 KB
Loading
4 Bytes
Loading
-70 Bytes
Loading
56.6 KB
Loading

0 commit comments

Comments
 (0)