diff --git a/src/components/drawing/index.js b/src/components/drawing/index.js index b949d8d8393..a95828cd11d 100644 --- a/src/components/drawing/index.js +++ b/src/components/drawing/index.js @@ -22,6 +22,7 @@ var svgTextUtils = require('../../lib/svg_text_utils'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var alignment = require('../../constants/alignment'); var LINE_SPACING = alignment.LINE_SPACING; +var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; var subTypes = require('../../traces/scatter/subtypes'); var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); @@ -247,9 +248,12 @@ drawing.symbolNumber = function(v) { return Math.floor(Math.max(v, 0)); }; +function makePointPath(symbolNumber, r) { + var base = symbolNumber % 100; + return drawing.symbolFuncs[base](r) + (symbolNumber >= 200 ? DOTPATH : ''); +} + function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerLine, gd) { - // only scatter & box plots get marker path and opacity - // bars, histograms don't if(Registry.traceIs(trace, 'symbols')) { var sizeFn = makeBubbleSizeFn(trace); @@ -257,8 +261,9 @@ function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerL var r; // handle multi-trace graph edit case - if(d.ms === 'various' || marker.size === 'various') r = 3; - else { + if(d.ms === 'various' || marker.size === 'various') { + r = 3; + } else { r = subTypes.isBubble(trace) ? sizeFn(d.ms) : (marker.size || 6) / 2; } @@ -267,21 +272,20 @@ function singlePointStyle(d, sel, trace, markerScale, lineScale, marker, markerL d.mrc = r; // turn the symbol into a sanitized number - var x = drawing.symbolNumber(d.mx || marker.symbol) || 0, - xBase = x % 100; + var x = drawing.symbolNumber(d.mx || marker.symbol) || 0; // save if this marker is open // because that impacts how to handle colors d.om = x % 200 >= 100; - return drawing.symbolFuncs[xBase](r) + - (x >= 200 ? DOTPATH : ''); - }) - .style('opacity', function(d) { - return (d.mo + 1 || marker.opacity + 1) - 1; + return makePointPath(x, r); }); } + sel.style('opacity', function(d) { + return (d.mo + 1 || marker.opacity + 1) - 1; + }); + var perPointGradient = false; // 'so' is suspected outliers, for box plots @@ -409,7 +413,6 @@ drawing.singlePointStyle = function(d, sel, trace, markerScale, lineScale, gd) { var marker = trace.marker; singlePointStyle(d, sel, trace, markerScale, lineScale, marker, marker.line, gd); - }; drawing.pointStyle = function(s, trace, gd) { @@ -426,6 +429,84 @@ drawing.pointStyle = function(s, trace, gd) { }); }; +drawing.selectedPointStyle = function(s, trace) { + if(!s.size() || !trace.selectedpoints) return; + + var selectedAttrs = trace.selected || {}; + var unselectedAttrs = trace.unselected || {}; + + var marker = trace.marker || {}; + var selectedMarker = selectedAttrs.marker || {}; + var unselectedMarker = unselectedAttrs.marker || {}; + + var mo = marker.opacity; + var smo = selectedMarker.opacity; + var usmo = unselectedMarker.opacity; + var smoIsDefined = smo !== undefined; + var usmoIsDefined = usmo !== undefined; + + s.each(function(d) { + var pt = d3.select(this); + var dmo = d.mo; + var dmoIsDefined = dmo !== undefined; + var mo2; + + if(dmoIsDefined || smoIsDefined || usmoIsDefined) { + if(d.selected) { + if(smoIsDefined) mo2 = smo; + } else { + if(usmoIsDefined) mo2 = usmo; + else mo2 = DESELECTDIM * (dmoIsDefined ? dmo : mo); + } + } + + if(mo2 !== undefined) pt.style('opacity', mo2); + }); + + var smc = selectedMarker.color; + var usmc = unselectedMarker.color; + + if(smc || usmc) { + s.each(function(d) { + var pt = d3.select(this); + var mc2; + + if(d.selected) { + if(smc) mc2 = smc; + } else { + if(usmc) mc2 = usmc; + } + + if(mc2) Color.fill(pt, mc2); + }); + } + + var sms = selectedMarker.size; + var usms = unselectedMarker.size; + var smsIsDefined = sms !== undefined; + var usmsIsDefined = usms !== undefined; + + if(Registry.traceIs(trace, 'symbols') && (smsIsDefined || usmsIsDefined)) { + s.each(function(d) { + var pt = d3.select(this); + var mrc = d.mrc; + var mx = d.mx || marker.symbol || 0; + var mrc2; + + if(d.selected) { + mrc2 = (smsIsDefined) ? sms / 2 : mrc; + } else { + mrc2 = (usmsIsDefined) ? usms / 2 : mrc; + } + + pt.attr('d', makePointPath(drawing.symbolNumber(mx), mrc2)); + + // save for selectedTextStyle + d.mrc2 = mrc2; + }); + } +}; + drawing.tryColorscale = function(marker, prefix) { var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker, scl = cont.colorscale, @@ -439,8 +520,39 @@ drawing.tryColorscale = function(marker, prefix) { else return Lib.identity; }; -// draw text at points var TEXTOFFSETSIGN = {start: 1, end: -1, middle: 0, bottom: 1, top: -1}; + +function textPointPosition(s, textPosition, fontSize, markerRadius) { + var group = d3.select(s.node().parentNode); + + var v = textPosition.indexOf('top') !== -1 ? + 'top' : + textPosition.indexOf('bottom') !== -1 ? 'bottom' : 'middle'; + var h = textPosition.indexOf('left') !== -1 ? + 'end' : + textPosition.indexOf('right') !== -1 ? 'start' : 'middle'; + + // if markers are shown, offset a little more than + // the nominal marker size + // ie 2/1.6 * nominal, bcs some markers are a bit bigger + var r = markerRadius ? markerRadius / 0.8 + 1 : 0; + + var numLines = (svgTextUtils.lineCount(s) - 1) * LINE_SPACING + 1; + var dx = TEXTOFFSETSIGN[h] * r; + var dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r + + (TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2; + + // fix the overall text group position + s.attr('text-anchor', h); + group.attr('transform', 'translate(' + dx + ',' + dy + ')'); +} + +function extracTextFontSize(d, trace) { + var fontSize = d.ts || trace.textfont.size; + return (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0; +} + +// draw text at points drawing.textPointStyle = function(s, trace, gd) { s.each(function(d) { var p = d3.select(this); @@ -451,35 +563,43 @@ drawing.textPointStyle = function(s, trace, gd) { return; } - var pos = d.tp || trace.textposition, - v = pos.indexOf('top') !== -1 ? 'top' : - pos.indexOf('bottom') !== -1 ? 'bottom' : 'middle', - h = pos.indexOf('left') !== -1 ? 'end' : - pos.indexOf('right') !== -1 ? 'start' : 'middle', - fontSize = d.ts || trace.textfont.size, - // if markers are shown, offset a little more than - // the nominal marker size - // ie 2/1.6 * nominal, bcs some markers are a bit bigger - r = d.mrc ? (d.mrc / 0.8 + 1) : 0; - - fontSize = (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0; + var pos = d.tp || trace.textposition; + var fontSize = extracTextFontSize(d, trace); p.call(drawing.font, d.tf || trace.textfont.family, fontSize, d.tc || trace.textfont.color) - .attr('text-anchor', h) .text(text) - .call(svgTextUtils.convertToTspans, gd); + .call(svgTextUtils.convertToTspans, gd) + .call(textPointPosition, pos, fontSize, d.mrc); + }); +}; - var pgroup = d3.select(this.parentNode); - var numLines = (svgTextUtils.lineCount(p) - 1) * LINE_SPACING + 1; - var dx = TEXTOFFSETSIGN[h] * r; - var dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r + - (TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2; +drawing.selectedTextStyle = function(s, trace) { + if(!s.size() || !trace.selectedpoints) return; + + var selectedAttrs = trace.selected || {}; + var unselectedAttrs = trace.unselected || {}; + + s.each(function(d) { + var tx = d3.select(this); + var tc = d.tc || trace.textfont.color; + var tp = d.tp || trace.textposition; + var fontSize = extracTextFontSize(d, trace); + var stc = (selectedAttrs.textfont || {}).color; + var utc = (unselectedAttrs.textfont || {}).color; + var tc2; + + if(d.selected) { + if(stc) tc2 = stc; + } else { + if(utc) tc2 = utc; + else if(!stc) tc2 = Color.addOpacity(tc, DESELECTDIM); + } - // fix the overall text group position - pgroup.attr('transform', 'translate(' + dx + ',' + dy + ')'); + if(tc2) Color.fill(tx, tc2); + textPointPosition(tx, tp, fontSize, d.mrc2 || d.mrc); }); }; diff --git a/src/components/fx/helpers.js b/src/components/fx/helpers.js index fd005f52824..4474c709613 100644 --- a/src/components/fx/helpers.js +++ b/src/components/fx/helpers.js @@ -85,6 +85,63 @@ exports.quadrature = function quadrature(dx, dy) { }; }; +/** Fill event data point object for hover and selection. + * Invokes _module.eventData if present. + * + * N.B. note that point 'index' corresponds to input data array index + * whereas 'number' is its post-transform version. + * + * If the hovered/selected pt corresponds to an multiple input points + * (e.g. for histogram and transformed traces), 'pointNumbers` and 'pointIndices' + * are include in the event data. + * + * @param {object} pt + * @param {object} trace + * @param {object} cd + * @return {object} + */ +exports.makeEventData = function makeEventData(pt, trace, cd) { + // hover uses 'index', select uses 'pointNumber' + var pointNumber = 'index' in pt ? pt.index : pt.pointNumber; + + var out = { + data: trace._input, + fullData: trace, + curveNumber: trace.index, + pointNumber: pointNumber + }; + + if(trace._indexToPoints) { + var pointIndices = trace._indexToPoints[pointNumber]; + + if(pointIndices.length === 1) { + out.pointIndex = pointIndices[0]; + } else { + out.pointIndices = pointIndices; + } + } else { + out.pointIndex = pointNumber; + } + + if(trace._module.eventData) { + out = trace._module.eventData(out, pt, trace, cd, pointNumber); + } else { + if('xVal' in pt) out.x = pt.xVal; + else if('x' in pt) out.x = pt.x; + + if('yVal' in pt) out.y = pt.yVal; + else if('y' in pt) out.y = pt.y; + + if(pt.xa) out.xaxis = pt.xa; + if(pt.ya) out.yaxis = pt.ya; + if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal; + } + + exports.appendArrayPointValue(out, trace, pointNumber); + + return out; +}; + /** Appends values inside array attributes corresponding to given point number * * @param {object} pointData : point data object (gets mutated here) diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 37d80fbc126..3df5fd605ae 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -417,26 +417,7 @@ function _hover(gd, evt, subplot, noHoverEvent) { // other people and send it to the event for(itemnum = 0; itemnum < hoverData.length; itemnum++) { var pt = hoverData[itemnum]; - - var out = { - data: pt.trace._input, - fullData: pt.trace, - curveNumber: pt.trace.index, - pointNumber: pt.index - }; - - if(pt.trace._module.eventData) out = pt.trace._module.eventData(out, pt); - else { - out.x = pt.xVal; - out.y = pt.yVal; - out.xaxis = pt.xa; - out.yaxis = pt.ya; - - if(pt.zLabelVal !== undefined) out.z = pt.zLabelVal; - } - - helpers.appendArrayPointValue(out, pt.trace, pt.index); - newhoverdata.push(out); + newhoverdata.push(helpers.makeEventData(pt, pt.trace, pt.cd)); } gd._hoverdata = newhoverdata; diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 5810311ed99..f6de4e51ae6 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -17,6 +17,7 @@ var getColorscale = require('../components/colorscale/get_scale'); var colorscaleNames = Object.keys(require('../components/colorscale/scales')); var nestedProperty = require('./nested_property'); var counterRegex = require('./regex').counter; +var DESELECTDIM = require('../constants/interactions').DESELECTDIM; exports.valObjectMeta = { data_array: { @@ -376,6 +377,38 @@ exports.coerceHoverinfo = function(traceIn, traceOut, layoutOut) { return exports.coerce(traceIn, traceOut, attrs, 'hoverinfo', dflt); }; +/** Coerce shortcut for [un]selected.marker.opacity, + * which has special default logic, to ensure that it corresponds to the + * default selection behavior while allowing to be overtaken by any other + * [un]selected attribute. + * + * N.B. This must be called *after* coercing all the other [un]selected attrs, + * to give the intended result. + * + * @param {object} traceOut : fullData item + * @param {function} coerce : lib.coerce wrapper with implied first three arguments + */ +exports.coerceSelectionMarkerOpacity = function(traceOut, coerce) { + if(!traceOut.marker) return; + + var mo = traceOut.marker.opacity; + var smoDflt; + var usmoDflt; + + // Don't give [un]selected.marker.opacity a default value if + // marker.opacity is an array: handle this during style step. + // + // Only give [un]selected.marker.opacity a default value if you don't + // set any other [un]selected attributes. + if(!Array.isArray(mo) && !traceOut.selected && !traceOut.unselected) { + smoDflt = mo; + usmoDflt = DESELECTDIM * mo; + } + + coerce('selected.marker.opacity', smoDflt); + coerce('unselected.marker.opacity', usmoDflt); +}; + exports.validate = function(value, opts) { var valObjectDef = exports.valObjectMeta[opts.valType]; diff --git a/src/lib/index.js b/src/lib/index.js index b34099227d6..2912bfdb642 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -34,6 +34,7 @@ lib.coerce = coerceModule.coerce; lib.coerce2 = coerceModule.coerce2; lib.coerceFont = coerceModule.coerceFont; lib.coerceHoverinfo = coerceModule.coerceHoverinfo; +lib.coerceSelectionMarkerOpacity = coerceModule.coerceSelectionMarkerOpacity; lib.validate = coerceModule.validate; var datesModule = require('./dates'); @@ -465,6 +466,57 @@ lib.extractOption = function(calcPt, trace, calcKey, traceKey) { if(!Array.isArray(traceVal)) return traceVal; }; +/** Tag selected calcdata items + * + * N.B. note that point 'index' corresponds to input data array index + * whereas 'number' is its post-transform version. + * + * @param {array} calcTrace + * @param {object} trace + * - selectedpoints {array} + * - _indexToPoints {object} + * @param {ptNumber2cdIndex} ptNumber2cdIndex (optional) + * optional map object for trace types that do not have 1-to-1 point number to + * calcdata item index correspondence (e.g. histogram) + */ +lib.tagSelected = function(calcTrace, trace, ptNumber2cdIndex) { + var selectedpoints = trace.selectedpoints; + var indexToPoints = trace._indexToPoints; + var ptIndex2ptNumber; + + // make pt index-to-number map object, which takes care of transformed traces + if(indexToPoints) { + ptIndex2ptNumber = {}; + for(var k in indexToPoints) { + var pts = indexToPoints[k]; + for(var j = 0; j < pts.length; j++) { + ptIndex2ptNumber[pts[j]] = k; + } + } + } + + function isPtIndexValid(v) { + return isNumeric(v) && v >= 0 && v % 1 === 0; + } + + function isCdIndexValid(v) { + return v !== undefined && v < calcTrace.length; + } + + for(var i = 0; i < selectedpoints.length; i++) { + var ptIndex = selectedpoints[i]; + + if(isPtIndexValid(ptIndex)) { + var ptNumber = ptIndex2ptNumber ? ptIndex2ptNumber[ptIndex] : ptIndex; + var cdIndex = ptNumber2cdIndex ? ptNumber2cdIndex[ptNumber] : ptNumber; + + if(isCdIndexValid(cdIndex)) { + calcTrace[cdIndex].selected = 1; + } + } + } +}; + /** Returns target as set by 'target' transform attribute * * @param {object} trace : full trace object diff --git a/src/plots/attributes.js b/src/plots/attributes.js index 40c36be371c..051e45fd5df 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -94,6 +94,25 @@ module.exports = { 'DOM elements' ].join(' ') }, + + // N.B. these cannot be 'data_array' as they do not have the same length as + // other data arrays and arrayOk attributes in general + // + // Maybe add another valType: + // https://github.com/plotly/plotly.js/issues/1894 + selectedpoints: { + valType: 'any', + role: 'info', + editType: 'calc', + description: [ + 'Array containing integer indices of selected points.', + 'Has an effect only for traces that support selections.', + 'Note that an empty array means an empty selection where the `unselected`', + 'are turned on for all points, whereas, any other non-array values means no', + 'selection all where the `selected` and `unselected` styles have no effect.' + ].join(' ') + }, + hoverinfo: { valType: 'flaglist', role: 'info', diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 0f1a1f88d3e..be7226e5590 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -13,7 +13,7 @@ var polybool = require('polybooljs'); var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); var color = require('../../components/color'); -var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue; +var makeEventData = require('../../components/fx/helpers').makeEventData; var axes = require('./axes'); var constants = require('./constants'); @@ -26,7 +26,9 @@ var MINSELECT = constants.MINSELECT; function getAxId(ax) { return ax._id; } module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { - var zoomLayer = dragOptions.gd._fullLayout._zoomlayer, + var gd = dragOptions.gd, + fullLayout = gd._fullLayout, + zoomLayer = fullLayout._zoomlayer, dragBBox = dragOptions.element.getBoundingClientRect(), plotinfo = dragOptions.plotinfo, xs = plotinfo.xaxis._offset, @@ -82,8 +84,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { // find the traces to search for selection points var searchTraces = []; - var gd = dragOptions.gd; - var throttleID = gd._fullLayout._uid + constants.SELECTID; + var throttleID = fullLayout._uid + constants.SELECTID; var selection = []; var i, cd, trace, searchInfo, eventData; @@ -99,6 +100,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { ) { searchTraces.push({ selectPoints: trace._module.selectPoints, + style: trace._module.style, cd: cd, xaxis: dragOptions.xaxes[0], yaxis: dragOptions.yaxes[0] @@ -110,6 +112,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { searchTraces.push({ selectPoints: trace._module.selectPoints, + style: trace._module.style, cd: cd, xaxis: axes.getFromId(gd, trace.xaxis), yaxis: axes.getFromId(gd, trace.yaxis) @@ -237,9 +240,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { traceSelection = searchInfo.selectPoints(searchInfo, testPoly); traceSelections.push(traceSelection); - var thisSelection = fillSelectionItem( - traceSelection, searchInfo - ); + var thisSelection = fillSelectionItem(traceSelection, searchInfo); if(selection.length) { for(var j = 0; j < thisSelection.length; j++) { selection.push(thisSelection[j]); @@ -249,6 +250,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { } eventData = {points: selection}; + updateSelectedState(gd, searchTraces, eventData); fillRangeItems(eventData, currentPolygon, filterPoly); dragOptions.gd.emit('plotly_selecting', eventData); } @@ -269,6 +271,7 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { searchInfo.selectPoints(searchInfo, false); } + updateSelectedState(gd, searchTraces); gd.emit('plotly_deselect', null); } else { @@ -288,6 +291,46 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { }; }; +function updateSelectedState(gd, searchTraces, eventData) { + var i, searchInfo, trace; + + if(eventData) { + var pts = eventData.points || []; + + for(i = 0; i < searchTraces.length; i++) { + trace = searchTraces[i].cd[0].trace; + trace.selectedpoints = []; + trace._input.selectedpoints = []; + } + + for(i = 0; i < pts.length; i++) { + var pt = pts[i]; + var data = pt.data; + var fullData = pt.fullData; + + if(pt.pointIndices) { + data.selectedpoints = data.selectedpoints.concat(pt.pointIndices); + fullData.selectedpoints = fullData.selectedpoints.concat(pt.pointIndices); + } else { + data.selectedpoints.push(pt.pointIndex); + fullData.selectedpoints.push(pt.pointIndex); + } + } + } + else { + for(i = 0; i < searchTraces.length; i++) { + trace = searchTraces[i].cd[0].trace; + delete trace.selectedpoints; + delete trace._input.selectedpoints; + } + } + + for(i = 0; i < searchTraces.length; i++) { + searchInfo = searchTraces[i]; + if(searchInfo.style) searchInfo.style(gd, searchInfo.cd); + } +} + function mergePolygons(list, poly, subtract) { var res; @@ -316,15 +359,11 @@ function mergePolygons(list, poly, subtract) { function fillSelectionItem(selection, searchInfo) { if(Array.isArray(selection)) { + var cd = searchInfo.cd; var trace = searchInfo.cd[0].trace; for(var i = 0; i < selection.length; i++) { - var sel = selection[i]; - - sel.curveNumber = trace.index; - sel.data = trace._input; - sel.fullData = trace; - appendArrayPointValue(sel, trace, sel.pointNumber); + selection[i] = makeEventData(selection[i], trace, cd); } } diff --git a/src/plots/plots.js b/src/plots/plots.js index 7e0973196f6..cb7264ee242 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1022,6 +1022,10 @@ plots.supplyTraceDefaults = function(traceIn, traceOutIndex, layout, traceInInde traceOut.visible = !!traceOut.visible; } + if(_module && _module.selectPoints && traceOut.type !== 'scattergl') { + coerce('selectedpoints'); + } + plots.supplyTransformDefaults(traceIn, traceOut, layout); } @@ -2236,7 +2240,21 @@ plots.doCalcdata = function(gd, traces) { if(trace.visible === true) { _module = trace._module; - if(_module && _module.calc) cd = _module.calc(gd, trace); + + // keep ref of index-to-points map object of the *last* enabled transform, + // this index-to-points map object is required to determine the calcdata indices + // that correspond to input indices (e.g. from 'selectedpoints') + var transforms = trace.transforms || []; + for(j = transforms.length - 1; j >= 0; j--) { + if(transforms[j].enabled) { + trace._indexToPoints = transforms[j]._indexToPoints; + break; + } + } + + if(_module && _module.calc) { + cd = _module.calc(gd, trace); + } } // Make sure there is a first point. diff --git a/src/traces/bar/arrays_to_calcdata.js b/src/traces/bar/arrays_to_calcdata.js index 675364e9920..f209e0b4cca 100644 --- a/src/traces/bar/arrays_to_calcdata.js +++ b/src/traces/bar/arrays_to_calcdata.js @@ -14,6 +14,8 @@ var mergeArray = require('../../lib').mergeArray; // arrayOk attributes, merge them into calcdata array module.exports = function arraysToCalcdata(cd, trace) { + for(var i = 0; i < cd.length; i++) cd[i].i = i; + mergeArray(trace.text, cd, 'tx'); mergeArray(trace.hovertext, cd, 'htx'); diff --git a/src/traces/bar/attributes.js b/src/traces/bar/attributes.js index fe07126e117..a0552de269e 100644 --- a/src/traces/bar/attributes.js +++ b/src/traces/bar/attributes.js @@ -38,10 +38,19 @@ var marker = extendFlat({ editType: 'calc' }, colorAttributes('marker'), { showscale: scatterMarkerAttrs.showscale, - colorbar: colorbarAttrs + colorbar: colorbarAttrs, + opacity: { + valType: 'number', + arrayOk: true, + dflt: 1, + min: 0, + max: 1, + role: 'style', + editType: 'style', + description: 'Sets the opacity of the bars.' + } }); - module.exports = { x: scatterAttrs.x, x0: scatterAttrs.x0, @@ -150,6 +159,25 @@ module.exports = { marker: marker, + selected: { + marker: { + opacity: scatterAttrs.selected.marker.opacity, + color: scatterAttrs.selected.marker.color, + editType: 'style' + }, + textfont: scatterAttrs.selected.textfont, + editType: 'style' + }, + unselected: { + marker: { + opacity: scatterAttrs.unselected.marker.opacity, + color: scatterAttrs.unselected.marker.color, + editType: 'style' + }, + textfont: scatterAttrs.unselected.textfont, + editType: 'style' + }, + r: scatterAttrs.r, t: scatterAttrs.t, diff --git a/src/traces/bar/calc.js b/src/traces/bar/calc.js index 40389a6d514..632ce0adec6 100644 --- a/src/traces/bar/calc.js +++ b/src/traces/bar/calc.js @@ -14,9 +14,8 @@ var isNumeric = require('fast-isnumeric'); var Axes = require('../../plots/cartesian/axes'); var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleCalc = require('../../components/colorscale/calc'); - var arraysToCalcdata = require('./arrays_to_calcdata'); - +var calcSelection = require('../scatter/calc_selection'); module.exports = function calc(gd, trace) { // depending on bar direction, set position and size axes @@ -92,6 +91,7 @@ module.exports = function calc(gd, trace) { } arraysToCalcdata(cd, trace); + calcSelection(cd, trace); return cd; }; diff --git a/src/traces/bar/defaults.js b/src/traces/bar/defaults.js index 84e26f5a0f2..0f85aa5f7dd 100644 --- a/src/traces/bar/defaults.js +++ b/src/traces/bar/defaults.js @@ -17,7 +17,6 @@ var handleStyleDefaults = require('../bar/style_defaults'); var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); var attributes = require('./attributes'); - module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); @@ -44,11 +43,14 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout var hasBoth = Array.isArray(textPosition) || textPosition === 'auto', hasInside = hasBoth || textPosition === 'inside', hasOutside = hasBoth || textPosition === 'outside'; + if(hasInside || hasOutside) { var textFont = coerceFont(coerce, 'textfont', layout.font); if(hasInside) coerceFont(coerce, 'insidetextfont', textFont); if(hasOutside) coerceFont(coerce, 'outsidetextfont', textFont); coerce('constraintext'); + coerce('selected.textfont.color'); + coerce('unselected.textfont.color'); } handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout); @@ -56,4 +58,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // override defaultColor for error bars with defaultLine errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'x', inherit: 'y'}); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/bar/plot.js b/src/traces/bar/plot.js index 2e37ef7292a..8b7912916b6 100644 --- a/src/traces/bar/plot.js +++ b/src/traces/bar/plot.js @@ -42,10 +42,14 @@ module.exports = function plot(gd, plotinfo, cdbar) { bartraces.enter().append('g') .attr('class', 'trace bars'); + bartraces.each(function(d) { + d[0].node3 = d3.select(this); + }); + bartraces.append('g') .attr('class', 'points') .each(function(d) { - var sel = d[0].node3 = d3.select(this); + var sel = d3.select(this); var t = d[0].t; var trace = d[0].trace; var poffset = t.poffset; @@ -146,11 +150,13 @@ module.exports = function plot(gd, plotinfo, cdbar) { }; function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { + var textPosition; + function appendTextNode(bar, text, textFont) { var textSelection = bar.append('text') .text(text) .attr({ - 'class': 'bartext', + 'class': 'bartext bartext-' + textPosition, transform: '', 'text-anchor': 'middle', // prohibit tex interpretation until we can handle @@ -170,7 +176,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { var text = getText(trace, i); if(!text) return; - var textPosition = getTextPosition(trace, i); + textPosition = getTextPosition(trace, i); if(textPosition === 'none') return; var textFont = getTextFont(trace, i, gd._fullLayout.font), @@ -201,6 +207,7 @@ function appendBarText(gd, bar, calcTrace, i, x0, x1, y0, y1) { if(textPosition === 'auto') { if(isOutmostBar) { // draw text using insideTextFont and check if it fits inside bar + textPosition = 'inside'; textSelection = appendTextNode(bar, text, insideTextFont); textBB = Drawing.bBox(textSelection.node()), diff --git a/src/traces/bar/select.js b/src/traces/bar/select.js index 76b1b786b7f..16f40dbaec3 100644 --- a/src/traces/bar/select.js +++ b/src/traces/bar/select.js @@ -8,20 +8,17 @@ 'use strict'; -var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; - module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd; var xa = searchInfo.xaxis; var ya = searchInfo.yaxis; var selection = []; - var node3 = cd[0].node3; var i; if(polygon === false) { // clear selection for(i = 0; i < cd.length; i++) { - cd[i].dim = 0; + cd[i].selected = 0; } } else { for(i = 0; i < cd.length; i++) { @@ -33,19 +30,12 @@ module.exports = function selectPoints(searchInfo, polygon) { x: xa.c2d(di.x), y: ya.c2d(di.y) }); - di.dim = 0; + di.selected = 1; } else { - di.dim = 1; + di.selected = 0; } } } - node3.selectAll('.point').style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); - node3.selectAll('text').style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); - return selection; }; diff --git a/src/traces/bar/style.js b/src/traces/bar/style.js index 633a89dbc7d..cecdd00239d 100644 --- a/src/traces/bar/style.js +++ b/src/traces/bar/style.js @@ -10,16 +10,13 @@ 'use strict'; var d3 = require('d3'); - -var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var ErrorBars = require('../../components/errorbars'); - -module.exports = function style(gd) { - var s = d3.select(gd).selectAll('g.trace.bars'), - barcount = s.size(), - fullLayout = gd._fullLayout; +module.exports = function style(gd, cd) { + var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.trace.bars'); + var barcount = s.size(); + var fullLayout = gd._fullLayout; // trace styling s.style('opacity', function(d) { return d[0].trace.opacity; }) @@ -36,38 +33,36 @@ module.exports = function style(gd) { } }); - // then style the individual bars s.selectAll('g.points').each(function(d) { - var trace = d[0].trace, - marker = trace.marker, - markerLine = marker.line, - markerScale = Drawing.tryColorscale(marker, ''), - lineScale = Drawing.tryColorscale(marker, 'line'); + var sel = d3.select(this); + var pts = sel.selectAll('path'); + var txs = sel.selectAll('text'); + var trace = d[0].trace; - d3.select(this).selectAll('path').each(function(d) { - // allow all marker and marker line colors to be scaled - // by given max and min to colorscales - var fillColor, - lineColor, - lineWidth = (d.mlw + 1 || markerLine.width + 1) - 1, - p = d3.select(this); + Drawing.pointStyle(pts, trace, gd); + Drawing.selectedPointStyle(pts, trace); - if('mc' in d) fillColor = d.mcc = markerScale(d.mc); - else if(Array.isArray(marker.color)) fillColor = Color.defaultLine; - else fillColor = marker.color; + txs.each(function(d) { + var tx = d3.select(this); + var textFont; - p.style('stroke-width', lineWidth + 'px') - .call(Color.fill, fillColor); - if(lineWidth) { - if('mlc' in d) lineColor = d.mlcc = lineScale(d.mlc); - // weird case: array wasn't long enough to apply to every point - else if(Array.isArray(markerLine.color)) lineColor = Color.defaultLine; - else lineColor = markerLine.color; + if(tx.classed('bartext-inside')) { + textFont = trace.insidetextfont; + } else if(tx.classed('bartext-outside')) { + textFont = trace.outsidetextfont; + } + if(!textFont) textFont = trace.textfont; - p.call(Color.stroke, lineColor); + function cast(k) { + var cont = textFont[k]; + return Array.isArray(cont) ? cont[d.i] : cont; } + + Drawing.font(tx, cast('family'), cast('size'), cast('color')); }); + + Drawing.selectedTextStyle(txs, trace); }); - s.call(ErrorBars.style); + ErrorBars.style(s); }; diff --git a/src/traces/bar/style_defaults.js b/src/traces/bar/style_defaults.js index 3ccd7494554..6b3681c9b84 100644 --- a/src/traces/bar/style_defaults.js +++ b/src/traces/bar/style_defaults.js @@ -13,7 +13,6 @@ var Color = require('../../components/color'); var hasColorscale = require('../../components/colorscale/has_colorscale'); var colorscaleDefaults = require('../../components/colorscale/defaults'); - module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, defaultColor, layout) { coerce('marker.color', defaultColor); @@ -32,4 +31,7 @@ module.exports = function handleStyleDefaults(traceIn, traceOut, coerce, default } coerce('marker.line.width'); + coerce('marker.opacity'); + coerce('selected.marker.color'); + coerce('unselected.marker.color'); }; diff --git a/src/traces/box/attributes.js b/src/traces/box/attributes.js index baa4e056210..2cf4c0b2f78 100644 --- a/src/traces/box/attributes.js +++ b/src/traces/box/attributes.js @@ -214,6 +214,16 @@ module.exports = { editType: 'plot' }, fillcolor: scatterAttrs.fillcolor, + + selected: { + marker: scatterAttrs.selected.marker, + editType: 'style' + }, + unselected: { + marker: scatterAttrs.unselected.marker, + editType: 'style' + }, + hoveron: { valType: 'flaglist', flags: ['boxes', 'points'], diff --git a/src/traces/box/calc.js b/src/traces/box/calc.js index 2f79e0c368c..f085eb9306b 100644 --- a/src/traces/box/calc.js +++ b/src/traces/box/calc.js @@ -114,6 +114,7 @@ module.exports = function calc(gd, trace) { } } + calcSelection(cd, trace); Axes.expand(valAxis, val, {padded: true}); if(cd.length > 0) { @@ -195,6 +196,21 @@ function arraysToCalcdata(pt, trace, i) { } } +function calcSelection(cd, trace) { + if(Array.isArray(trace.selectedpoints)) { + for(var i = 0; i < cd.length; i++) { + var pts = cd[i].pts || []; + var ptNumber2cdIndex = {}; + + for(var j = 0; j < pts.length; j++) { + ptNumber2cdIndex[pts[j].i] = j; + } + + Lib.tagSelected(pts, trace, ptNumber2cdIndex); + } + } +} + function sortByVal(a, b) { return a.v - b.v; } function extractVal(o) { return o.v; } diff --git a/src/traces/box/defaults.js b/src/traces/box/defaults.js index 9ea8e7964af..68d530bc12a 100644 --- a/src/traces/box/defaults.js +++ b/src/traces/box/defaults.js @@ -82,12 +82,19 @@ function handlePointsDefaults(traceIn, traceOut, coerce, opts) { coerce('marker.line.outlierwidth'); } + coerce('selected.marker.color'); + coerce('unselected.marker.color'); + coerce('selected.marker.size'); + coerce('unselected.marker.size'); + coerce('text'); } else { delete traceOut.marker; } coerce('hoveron'); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); } module.exports = { diff --git a/src/traces/box/select.js b/src/traces/box/select.js index 755246dd20a..f6b8fc76e81 100644 --- a/src/traces/box/select.js +++ b/src/traces/box/select.js @@ -8,24 +8,18 @@ 'use strict'; -var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; - module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd; var xa = searchInfo.xaxis; var ya = searchInfo.yaxis; - var trace = cd[0].trace; - var node3 = cd[0].node3; var selection = []; var i, j; - if(trace.visible !== true) return []; - if(polygon === false) { for(i = 0; i < cd.length; i++) { for(j = 0; j < (cd[i].pts || []).length; j++) { // clear selection - cd[i].pts[j].dim = 0; + cd[i].pts[j].selected = 0; } } } else { @@ -41,17 +35,13 @@ module.exports = function selectPoints(searchInfo, polygon) { x: xa.c2d(pt.x), y: ya.c2d(pt.y) }); - pt.dim = 0; + pt.selected = 1; } else { - pt.dim = 1; + pt.selected = 0; } } } } - node3.selectAll('.point').style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); - return selection; }; diff --git a/src/traces/box/style.js b/src/traces/box/style.js index 7dd3ae4a4c7..fcb75d7e6ac 100644 --- a/src/traces/box/style.js +++ b/src/traces/box/style.js @@ -9,29 +9,33 @@ 'use strict'; var d3 = require('d3'); - var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); +module.exports = function style(gd, cd) { + var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.trace.boxes'); + + s.style('opacity', function(d) { return d[0].trace.opacity; }); + + s.each(function(d) { + var el = d3.select(this); + var trace = d[0].trace; + var lineWidth = trace.line.width; + + el.selectAll('path.box') + .style('stroke-width', lineWidth + 'px') + .call(Color.stroke, trace.line.color) + .call(Color.fill, trace.fillcolor); + + el.selectAll('path.mean') + .style({ + 'stroke-width': lineWidth, + 'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px' + }) + .call(Color.stroke, trace.line.color); -module.exports = function style(gd) { - var s = d3.select(gd).selectAll('g.trace.boxes'); - - s.style('opacity', function(d) { return d[0].trace.opacity; }) - .each(function(d) { - var trace = d[0].trace, - lineWidth = trace.line.width; - d3.select(this).selectAll('path.box') - .style('stroke-width', lineWidth + 'px') - .call(Color.stroke, trace.line.color) - .call(Color.fill, trace.fillcolor); - d3.select(this).selectAll('path.mean') - .style({ - 'stroke-width': lineWidth, - 'stroke-dasharray': (2 * lineWidth) + 'px,' + lineWidth + 'px' - }) - .call(Color.stroke, trace.line.color); - d3.select(this).selectAll('g.points path') - .call(Drawing.pointStyle, trace, gd); - }); + var pts = el.selectAll('path.point'); + Drawing.pointStyle(pts, trace, gd); + Drawing.selectedPointStyle(pts, trace); + }); }; diff --git a/src/traces/choropleth/attributes.js b/src/traces/choropleth/attributes.js index 6c9ffb93cbf..65957f6e81b 100644 --- a/src/traces/choropleth/attributes.js +++ b/src/traces/choropleth/attributes.js @@ -8,7 +8,7 @@ 'use strict'; -var ScatterGeoAttrs = require('../scattergeo/attributes'); +var scatterGeoAttrs = require('../scattergeo/attributes'); var colorscaleAttrs = require('../../components/colorscale/attributes'); var colorbarAttrs = require('../../components/colorbar/attributes'); var plotAttrs = require('../../plots/attributes'); @@ -17,7 +17,7 @@ var extend = require('../../lib/extend'); var extendFlat = extend.extendFlat; var extendDeepAll = extend.extendDeepAll; -var ScatterGeoMarkerLineAttrs = ScatterGeoAttrs.marker.line; +var scatterGeoMarkerLineAttrs = scatterGeoAttrs.marker.line; module.exports = extendFlat({ locations: { @@ -28,23 +28,49 @@ module.exports = extendFlat({ 'See `locationmode` for more info.' ].join(' ') }, - locationmode: ScatterGeoAttrs.locationmode, + locationmode: scatterGeoAttrs.locationmode, z: { valType: 'data_array', editType: 'calc', description: 'Sets the color values.' }, - text: extendFlat({}, ScatterGeoAttrs.text, { + text: extendFlat({}, scatterGeoAttrs.text, { description: 'Sets the text elements associated with each location.' }), marker: { line: { - color: ScatterGeoMarkerLineAttrs.color, - width: extendFlat({}, ScatterGeoMarkerLineAttrs.width, {dflt: 1}), + color: scatterGeoMarkerLineAttrs.color, + width: extendFlat({}, scatterGeoMarkerLineAttrs.width, {dflt: 1}), editType: 'calc' }, + opacity: { + valType: 'number', + arrayOk: true, + min: 0, + max: 1, + dflt: 1, + role: 'style', + editType: 'style', + description: 'Sets the opacity of the locations.' + }, editType: 'calc' }, + + selected: { + marker: { + opacity: scatterGeoAttrs.selected.marker.opacity, + editType: 'plot' + }, + editType: 'plot' + }, + unselected: { + marker: { + opacity: scatterGeoAttrs.unselected.marker.opacity, + editType: 'plot' + }, + editType: 'plot' + }, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { editType: 'calc', flags: ['location', 'z', 'text', 'name'] diff --git a/src/traces/choropleth/calc.js b/src/traces/choropleth/calc.js index f4e479a98f7..960e85ec038 100644 --- a/src/traces/choropleth/calc.js +++ b/src/traces/choropleth/calc.js @@ -14,6 +14,7 @@ var BADNUM = require('../../constants/numerical').BADNUM; var colorscaleCalc = require('../../components/colorscale/calc'); var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); +var calcSelection = require('../scatter/calc_selection'); module.exports = function calc(gd, trace) { var len = trace.locations.length; @@ -30,6 +31,7 @@ module.exports = function calc(gd, trace) { arraysToCalcdata(calcTrace, trace); colorscaleCalc(trace, trace.z, '', 'z'); + calcSelection(calcTrace, trace); return calcTrace; }; diff --git a/src/traces/choropleth/defaults.js b/src/traces/choropleth/defaults.js index 9b279725c69..a280158df8b 100644 --- a/src/traces/choropleth/defaults.js +++ b/src/traces/choropleth/defaults.js @@ -10,11 +10,9 @@ 'use strict'; var Lib = require('../../lib'); - var colorscaleDefaults = require('../../components/colorscale/defaults'); var attributes = require('./attributes'); - module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); @@ -44,8 +42,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('marker.line.color'); coerce('marker.line.width'); + coerce('marker.opacity'); colorscaleDefaults( traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} ); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/choropleth/index.js b/src/traces/choropleth/index.js index a5940e41cd6..3c946176017 100644 --- a/src/traces/choropleth/index.js +++ b/src/traces/choropleth/index.js @@ -16,6 +16,7 @@ Choropleth.supplyDefaults = require('./defaults'); Choropleth.colorbar = require('../heatmap/colorbar'); Choropleth.calc = require('./calc'); Choropleth.plot = require('./plot'); +Choropleth.style = require('./style'); Choropleth.hoverPoints = require('./hover'); Choropleth.eventData = require('./event_data'); Choropleth.selectPoints = require('./select'); diff --git a/src/traces/choropleth/plot.js b/src/traces/choropleth/plot.js index 1d2c30d4234..982aef72f3e 100644 --- a/src/traces/choropleth/plot.js +++ b/src/traces/choropleth/plot.js @@ -6,19 +6,16 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var d3 = require('d3'); var Lib = require('../../lib'); -var Color = require('../../components/color'); -var Drawing = require('../../components/drawing'); -var Colorscale = require('../../components/colorscale'); var polygon = require('../../lib/polygon'); var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures; var locationToFeature = require('../../lib/geo_location_utils').locationToFeature; +var style = require('./style'); module.exports = function plot(geo, calcData) { for(var i = 0; i < calcData.length; i++) { @@ -46,9 +43,10 @@ module.exports = function plot(geo, calcData) { .classed('choroplethlocation', true); paths.exit().remove(); - }); - style(geo); + // call style here within topojson request callback + style(geo.graphDiv, calcTrace); + }); }; function calcGeoJSON(calcTrace, topojson) { @@ -176,28 +174,3 @@ function feature2polygons(feature) { return polygons; } - -function style(geo) { - var gTraces = geo.layers.backplot.selectAll('.trace.choropleth'); - - gTraces.each(function(calcTrace) { - var trace = calcTrace[0].trace; - var marker = trace.marker || {}; - var markerLine = marker.line || {}; - - var sclFunc = Colorscale.makeColorScaleFunc( - Colorscale.extractScale( - trace.colorscale, - trace.zmin, - trace.zmax - ) - ); - - d3.select(this).selectAll('.choroplethlocation').each(function(d) { - d3.select(this) - .attr('fill', sclFunc(d.z)) - .call(Color.stroke, d.mlc || markerLine.color) - .call(Drawing.dashLine, '', d.mlw || markerLine.width || 0); - }); - }); -} diff --git a/src/traces/choropleth/select.js b/src/traces/choropleth/select.js index 209b1ba35a7..18d4a5e5ee5 100644 --- a/src/traces/choropleth/select.js +++ b/src/traces/choropleth/select.js @@ -8,20 +8,17 @@ 'use strict'; -var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; - module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd; var xa = searchInfo.xaxis; var ya = searchInfo.yaxis; var selection = []; - var node3 = cd[0].node3; var i, di, ct, x, y; if(polygon === false) { for(i = 0; i < cd.length; i++) { - cd[i].dim = 0; + cd[i].selected = 0; } } else { for(i = 0; i < cd.length; i++) { @@ -39,16 +36,12 @@ module.exports = function selectPoints(searchInfo, polygon) { lon: ct[0], lat: ct[1] }); - di.dim = 0; + di.selected = 1; } else { - di.dim = 1; + di.selected = 0; } } } - node3.selectAll('path').style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); - return selection; }; diff --git a/src/traces/choropleth/style.js b/src/traces/choropleth/style.js new file mode 100644 index 00000000000..a3519cccdeb --- /dev/null +++ b/src/traces/choropleth/style.js @@ -0,0 +1,44 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var d3 = require('d3'); +var Color = require('../../components/color'); +var Drawing = require('../../components/drawing'); +var Colorscale = require('../../components/colorscale'); + +module.exports = function style(gd, calcTrace) { + if(calcTrace) styleTrace(gd, calcTrace); +}; + +function styleTrace(gd, calcTrace) { + var trace = calcTrace[0].trace; + var s = calcTrace[0].node3; + var locs = s.selectAll('.choroplethlocation'); + var marker = trace.marker || {}; + var markerLine = marker.line || {}; + + var sclFunc = Colorscale.makeColorScaleFunc( + Colorscale.extractScale( + trace.colorscale, + trace.zmin, + trace.zmax + ) + ); + + locs.each(function(d) { + d3.select(this) + .attr('fill', sclFunc(d.z)) + .call(Color.stroke, d.mlc || markerLine.color) + .call(Drawing.dashLine, '', d.mlw || markerLine.width || 0) + .style('opacity', marker.opacity); + }); + + Drawing.selectedPointStyle(locs, trace); +} diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js index 2abb11323e7..acd3453144f 100644 --- a/src/traces/histogram/attributes.js +++ b/src/traces/histogram/attributes.js @@ -191,6 +191,9 @@ module.exports = { marker: barAttrs.marker, + selected: barAttrs.selected, + unselected: barAttrs.unselected, + error_y: barAttrs.error_y, error_x: barAttrs.error_x, diff --git a/src/traces/histogram/calc.js b/src/traces/histogram/calc.js index ebf22aaf7f2..c53587148da 100644 --- a/src/traces/histogram/calc.js +++ b/src/traces/histogram/calc.js @@ -22,7 +22,6 @@ var cleanBins = require('./clean_bins'); var oneMonth = require('../../constants/numerical').ONEAVGMONTH; var getBinSpanLabelRound = require('./bin_label_vals'); - module.exports = function calc(gd, trace) { // ignore as much processing as possible (and including in autorange) if bar is not visible if(trace.visible !== true) return; @@ -113,11 +112,13 @@ module.exports = function calc(gd, trace) { }; } + // bin the data + // and make histogram-specific pt-number-to-cd-index map object var nMax = size.length; var uniqueValsPerBin = true; var leftGap = Infinity; var rightGap = Infinity; - // bin the data + var ptNumber2cdIndex = {}; for(i = 0; i < pos0.length; i++) { var posi = pos0[i]; n = Lib.findBin(posi, bins); @@ -127,6 +128,7 @@ module.exports = function calc(gd, trace) { uniqueValsPerBin = false; } inputPoints[n].push(i); + ptNumber2cdIndex[i] = n; leftGap = Math.min(leftGap, posi - binEdges[n]); rightGap = Math.min(rightGap, binEdges[n + 1] - posi); @@ -197,6 +199,10 @@ module.exports = function calc(gd, trace) { arraysToCalcdata(cd, trace); + if(Array.isArray(trace.selectedpoints)) { + Lib.tagSelected(cd, trace, ptNumber2cdIndex); + } + return cd; }; diff --git a/src/traces/histogram/defaults.js b/src/traces/histogram/defaults.js index 534bddf3f8f..0476ba7877e 100644 --- a/src/traces/histogram/defaults.js +++ b/src/traces/histogram/defaults.js @@ -18,7 +18,6 @@ var handleStyleDefaults = require('../bar/style_defaults'); var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); var attributes = require('./attributes'); - module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); @@ -57,4 +56,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout // override defaultColor for error bars with defaultLine errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, Color.defaultLine, {axis: 'x', inherit: 'y'}); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/histogram/event_data.js b/src/traces/histogram/event_data.js index 7c39678087d..c863695ac63 100644 --- a/src/traces/histogram/event_data.js +++ b/src/traces/histogram/event_data.js @@ -6,23 +6,38 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; - -module.exports = function eventData(out, pt) { +module.exports = function eventData(out, pt, trace, cd, pointNumber) { // standard cartesian event data - out.x = pt.xVal; - out.y = pt.yVal; - out.xaxis = pt.xa; - out.yaxis = pt.ya; - - // specific to histogram - // CDFs do not have pts (yet?) - if(pt.pts) { - out.pointNumbers = pt.pts; + out.x = 'xVal' in pt ? pt.xVal : pt.x; + out.y = 'yVal' in pt ? pt.yVal : pt.y; + + if(pt.xa) out.xaxis = pt.xa; + if(pt.ya) out.yaxis = pt.ya; + + // specific to histogram - CDFs do not have pts (yet?) + if(!(trace.cumulative || {}).enabled) { + var pts = Array.isArray(pointNumber) ? + cd[0].pts[pointNumber[0]][pointNumber[1]] : + cd[pointNumber].pts; + + out.pointNumbers = pts; out.binNumber = out.pointNumber; delete out.pointNumber; + delete out.pointIndex; + + var pointIndices; + if(trace._indexToPoints) { + pointIndices = []; + for(var i = 0; i < pts.length; i++) { + pointIndices = pointIndices.concat(trace._indexToPoints[pts[i]]); + } + } else { + pointIndices = pts; + } + + out.pointIndices = pointIndices; } return out; diff --git a/src/traces/histogram/hover.js b/src/traces/histogram/hover.js index 5bfe0317840..6fe0b2222b3 100644 --- a/src/traces/histogram/hover.js +++ b/src/traces/histogram/hover.js @@ -25,7 +25,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { var posLetter = trace.orientation === 'h' ? 'y' : 'x'; pointData[posLetter + 'Label'] = hoverLabelText(pointData[posLetter + 'a'], di.p0, di.p1); - pointData.pts = di.pts; } return pts; diff --git a/src/traces/histogram2d/hover.js b/src/traces/histogram2d/hover.js index ccce7d3d712..73bad554923 100644 --- a/src/traces/histogram2d/hover.js +++ b/src/traces/histogram2d/hover.js @@ -27,7 +27,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay pointData.xLabel = hoverLabelText(pointData.xa, xRange[0], xRange[1]); pointData.yLabel = hoverLabelText(pointData.ya, yRange[0], yRange[1]); - pointData.pts = cd0.pts[ny][nx]; return pts; }; diff --git a/src/traces/scatter/attributes.js b/src/traces/scatter/attributes.js index 4b719db4a9a..a7eb216d80b 100644 --- a/src/traces/scatter/attributes.js +++ b/src/traces/scatter/attributes.js @@ -377,6 +377,79 @@ module.exports = { }, colorAttributes('marker') ), + selected: { + marker: { + opacity: { + valType: 'number', + min: 0, + max: 1, + role: 'style', + editType: 'style', + description: 'Sets the marker opacity of selected points.' + }, + color: { + valType: 'color', + role: 'style', + editType: 'style', + description: 'Sets the marker color of selected points.' + }, + size: { + valType: 'number', + min: 0, + role: 'style', + editType: 'style', + description: 'Sets the marker size of selected points.' + }, + editType: 'style' + }, + textfont: { + color: { + valType: 'color', + role: 'style', + editType: 'style', + description: 'Sets the text font color of selected points.' + }, + editType: 'style' + }, + editType: 'style' + }, + unselected: { + marker: { + opacity: { + valType: 'number', + min: 0, + max: 1, + role: 'style', + editType: 'style', + description: 'Sets the marker opacity of unselected points, applied only when a selection exists.' + }, + color: { + valType: 'color', + role: 'style', + editType: 'style', + description: 'Sets the marker color of unselected points, applied only when a selection exists.' + }, + size: { + valType: 'number', + min: 0, + role: 'style', + editType: 'style', + description: 'Sets the marker size of unselected points, applied only when a selection exists.' + }, + editType: 'style' + }, + textfont: { + color: { + valType: 'color', + role: 'style', + editType: 'style', + description: 'Sets the text font color of unselected points, applied only when a selection exists.' + }, + editType: 'style' + }, + editType: 'style' + }, + textposition: { valType: 'enumerated', values: [ diff --git a/src/traces/scatter/calc.js b/src/traces/scatter/calc.js index e5c64209128..b50551169c4 100644 --- a/src/traces/scatter/calc.js +++ b/src/traces/scatter/calc.js @@ -17,7 +17,7 @@ var BADNUM = require('../../constants/numerical').BADNUM; var subTypes = require('./subtypes'); var calcColorscale = require('./colorscale_calc'); var arraysToCalcdata = require('./arrays_to_calcdata'); - +var calcSelection = require('./calc_selection'); module.exports = function calc(gd, trace) { var xa = Axes.getFromId(gd, trace.xaxis || 'x'), @@ -123,6 +123,7 @@ module.exports = function calc(gd, trace) { } arraysToCalcdata(cd, trace); + calcSelection(cd, trace); gd.firstscatter = false; return cd; diff --git a/src/traces/scatter/calc_selection.js b/src/traces/scatter/calc_selection.js new file mode 100644 index 00000000000..8076e21fa54 --- /dev/null +++ b/src/traces/scatter/calc_selection.js @@ -0,0 +1,17 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var Lib = require('../../lib'); + +module.exports = function calcSelection(cd, trace) { + if(Array.isArray(trace.selectedpoints)) { + Lib.tagSelected(cd, trace); + } +}; diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index 9699469615f..e33b614a878 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -22,7 +22,6 @@ var handleTextDefaults = require('./text_defaults'); var handleFillColorDefaults = require('./fillcolor_defaults'); var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); - module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); @@ -77,4 +76,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); coerce('cliponaxis'); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index 6817e4c8162..e886ebd2716 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -26,7 +26,7 @@ Scatter.calc = require('./calc'); Scatter.arraysToCalcdata = require('./arrays_to_calcdata'); Scatter.plot = require('./plot'); Scatter.colorbar = require('./colorbar'); -Scatter.style = require('./style'); +Scatter.style = require('./style').style; Scatter.hoverPoints = require('./hover'); Scatter.selectPoints = require('./select'); Scatter.animatable = true; diff --git a/src/traces/scatter/marker_defaults.js b/src/traces/scatter/marker_defaults.js index caf679bd35f..b5a58867e76 100644 --- a/src/traces/scatter/marker_defaults.js +++ b/src/traces/scatter/marker_defaults.js @@ -19,6 +19,7 @@ var subTypes = require('./subtypes'); * opts: object of flags to control features not all marker users support * noLine: caller does not support marker lines * gradient: caller supports gradients + * noSelect: caller does not support selected/unselected attribute containers */ module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout, coerce, opts) { var isBubble = subTypes.isBubble(traceIn), @@ -39,6 +40,13 @@ module.exports = function markerDefaults(traceIn, traceOut, defaultColor, layout colorscaleDefaults(traceIn, traceOut, layout, coerce, {prefix: 'marker.', cLetter: 'c'}); } + if(!opts.noSelect) { + coerce('selected.marker.color'); + coerce('unselected.marker.color'); + coerce('selected.marker.size'); + coerce('unselected.marker.size'); + } + if(!opts.noLine) { // if there's a line with a different color than the marker, use // that line color as the default marker line color diff --git a/src/traces/scatter/select.js b/src/traces/scatter/select.js index 7dd2b1cd1c4..099977a409f 100644 --- a/src/traces/scatter/select.js +++ b/src/traces/scatter/select.js @@ -10,7 +10,6 @@ 'use strict'; var subtypes = require('./subtypes'); -var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd, @@ -18,20 +17,18 @@ module.exports = function selectPoints(searchInfo, polygon) { ya = searchInfo.yaxis, selection = [], trace = cd[0].trace, - marker = trace.marker, i, di, x, y; - // TODO: include lines? that would require per-segment line properties var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace)); if(hasOnlyLines) return []; - var opacity = Array.isArray(marker.opacity) ? 1 : marker.opacity; - if(polygon === false) { // clear selection - for(i = 0; i < cd.length; i++) cd[i].dim = 0; + for(i = 0; i < cd.length; i++) { + cd[i].selected = 0; + } } else { for(i = 0; i < cd.length; i++) { @@ -45,23 +42,12 @@ module.exports = function selectPoints(searchInfo, polygon) { x: xa.c2d(di.x), y: ya.c2d(di.y) }); - di.dim = 0; + di.selected = 1; + } else { + di.selected = 0; } - else di.dim = 1; } } - // do the dimming here, as well as returning the selection - // The logic here duplicates Drawing.pointStyle, but I don't want - // d.dim in pointStyle in case something goes wrong with selection. - cd[0].node3.selectAll('path.point') - .style('opacity', function(d) { - return ((d.mo + 1 || opacity + 1) - 1) * (d.dim ? DESELECTDIM : 1); - }); - cd[0].node3.selectAll('text') - .style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); - return selection; }; diff --git a/src/traces/scatter/style.js b/src/traces/scatter/style.js index b4475f53d6c..bd27c38e189 100644 --- a/src/traces/scatter/style.js +++ b/src/traces/scatter/style.js @@ -10,29 +10,21 @@ 'use strict'; var d3 = require('d3'); - var Drawing = require('../../components/drawing'); var ErrorBars = require('../../components/errorbars'); - -module.exports = function style(gd) { - var s = d3.select(gd).selectAll('g.trace.scatter'); +function style(gd, cd) { + var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.trace.scatter'); s.style('opacity', function(d) { return d[0].trace.opacity; }); - s.selectAll('g.points') - .each(function(d) { - var el = d3.select(this); - var pts = el.selectAll('path.point'); - var trace = d.trace || d[0].trace; - - pts.call(Drawing.pointStyle, trace, gd); - - el.selectAll('text') - .call(Drawing.textPointStyle, trace, gd); - }); + s.selectAll('g.points').each(function(d) { + var sel = d3.select(this); + var trace = d.trace || d[0].trace; + stylePoints(sel, trace, gd); + }); s.selectAll('g.trace path.js-line') .call(Drawing.lineGroupStyle); @@ -41,4 +33,19 @@ module.exports = function style(gd) { .call(Drawing.fillGroupStyle); s.call(ErrorBars.style); +} + +function stylePoints(sel, trace, gd) { + var pts = sel.selectAll('path.point'); + var txs = sel.selectAll('text'); + + Drawing.pointStyle(pts, trace, gd); + Drawing.textPointStyle(txs, trace, gd); + Drawing.selectedPointStyle(pts, trace); + Drawing.selectedTextStyle(txs, trace); +} + +module.exports = { + style: style, + stylePoints: stylePoints }; diff --git a/src/traces/scatter/text_defaults.js b/src/traces/scatter/text_defaults.js index 2860d127825..18af9ef07ba 100644 --- a/src/traces/scatter/text_defaults.js +++ b/src/traces/scatter/text_defaults.js @@ -11,9 +11,18 @@ var Lib = require('../../lib'); +/* + * opts: object of flags to control features not all text users support + * noSelect: caller does not support selected/unselected attribute containers + */ +module.exports = function(traceIn, traceOut, layout, coerce, opts) { + opts = opts || {}; -// common to 'scatter', 'scatter3d' and 'scattergeo' -module.exports = function(traceIn, traceOut, layout, coerce) { coerce('textposition'); Lib.coerceFont(coerce, 'textfont', layout.font); + + if(!opts.noSelect) { + coerce('selected.textfont.color'); + coerce('unselected.textfont.color'); + } }; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js index 89e0985709f..f33ded47f93 100644 --- a/src/traces/scatter3d/defaults.js +++ b/src/traces/scatter3d/defaults.js @@ -43,11 +43,11 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noSelect: true}); } if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); + handleTextDefaults(traceIn, traceOut, layout, coerce, {noSelect: true}); } var lineColor = (traceOut.line || {}).color, diff --git a/src/traces/scattercarpet/attributes.js b/src/traces/scattercarpet/attributes.js index 391a5b9a727..886fa2aa955 100644 --- a/src/traces/scattercarpet/attributes.js +++ b/src/traces/scattercarpet/attributes.js @@ -110,6 +110,10 @@ module.exports = { textfont: scatterAttrs.textfont, textposition: scatterAttrs.textposition, + + selected: scatterAttrs.selected, + unselected: scatterAttrs.unselected, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['a', 'b', 'text', 'name'] }), diff --git a/src/traces/scattercarpet/calc.js b/src/traces/scattercarpet/calc.js index 25888b59b02..58f57fe1361 100644 --- a/src/traces/scattercarpet/calc.js +++ b/src/traces/scattercarpet/calc.js @@ -16,6 +16,7 @@ var Axes = require('../../plots/cartesian/axes'); var subTypes = require('../scatter/subtypes'); var calcColorscale = require('../scatter/colorscale_calc'); var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); +var calcSelection = require('../scatter/calc_selection'); var lookupCarpet = require('../carpet/lookup_carpetid'); module.exports = function calc(gd, trace) { @@ -67,8 +68,8 @@ module.exports = function calc(gd, trace) { } calcColorscale(trace); - arraysToCalcdata(cd, trace); + calcSelection(cd, trace); return cd; }; diff --git a/src/traces/scattercarpet/defaults.js b/src/traces/scattercarpet/defaults.js index 5bf92d1a02f..32715a00a4e 100644 --- a/src/traces/scattercarpet/defaults.js +++ b/src/traces/scattercarpet/defaults.js @@ -21,7 +21,6 @@ var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); var attributes = require('./attributes'); - module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); @@ -84,4 +83,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout dfltHoverOn.push('fills'); } coerce('hoveron', dfltHoverOn.join('+') || 'points'); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/scattercarpet/event_data.js b/src/traces/scattercarpet/event_data.js new file mode 100644 index 00000000000..829dcdaef20 --- /dev/null +++ b/src/traces/scattercarpet/event_data.js @@ -0,0 +1,18 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = function eventData(out, pt, trace, cd, pointNumber) { + var cdi = cd[pointNumber]; + + out.a = cdi.a; + out.b = cdi.b; + + return out; +}; diff --git a/src/traces/scattercarpet/index.js b/src/traces/scattercarpet/index.js index a5d84296fd1..c7633f1f4f5 100644 --- a/src/traces/scattercarpet/index.js +++ b/src/traces/scattercarpet/index.js @@ -17,7 +17,8 @@ ScatterCarpet.calc = require('./calc'); ScatterCarpet.plot = require('./plot'); ScatterCarpet.style = require('./style'); ScatterCarpet.hoverPoints = require('./hover'); -ScatterCarpet.selectPoints = require('./select'); +ScatterCarpet.selectPoints = require('../scatter/select'); +ScatterCarpet.eventData = require('./event_data'); ScatterCarpet.moduleType = 'trace'; ScatterCarpet.name = 'scattercarpet'; diff --git a/src/traces/scattercarpet/select.js b/src/traces/scattercarpet/select.js deleted file mode 100644 index ff40c61a271..00000000000 --- a/src/traces/scattercarpet/select.js +++ /dev/null @@ -1,32 +0,0 @@ -/** -* Copyright 2012-2017, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var scatterSelect = require('../scatter/select'); - - -module.exports = function selectPoints(searchInfo, polygon) { - var selection = scatterSelect(searchInfo, polygon); - if(!selection) return; - - var cd = searchInfo.cd, - pt, cdi, i; - - for(i = 0; i < selection.length; i++) { - pt = selection[i]; - cdi = cd[pt.pointNumber]; - pt.a = cdi.a; - pt.b = cdi.b; - delete pt.x; - delete pt.y; - } - - return selection; -}; diff --git a/src/traces/scattercarpet/style.js b/src/traces/scattercarpet/style.js index 8ead87cc97e..b6241334f05 100644 --- a/src/traces/scattercarpet/style.js +++ b/src/traces/scattercarpet/style.js @@ -9,19 +9,19 @@ 'use strict'; -var scatterStyle = require('../scatter/style'); - - -module.exports = function style(gd) { - var modules = gd._fullLayout._modules; +var scatterStyle = require('../scatter/style').style; +module.exports = function style(gd, cd) { // we're just going to call scatter style... if we already // called it, don't need to redo. // Later though we may want differences, or we may make style // more specific in its scope, then we can remove this. - for(var i = 0; i < modules.length; i++) { - if(modules[i].name === 'scatter') return; + if(!cd) { + var modules = gd._fullLayout._modules; + for(var i = 0; i < modules.length; i++) { + if(modules[i].name === 'scatter') return; + } } - scatterStyle(gd); + scatterStyle(gd, cd); }; diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index a6075bdd81f..95d7e9672b6 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -118,6 +118,9 @@ module.exports = overrideAll({ }, fillcolor: scatterAttrs.fillcolor, + selected: scatterAttrs.selected, + unselected: scatterAttrs.unselected, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['lon', 'lat', 'location', 'text', 'name'] }) diff --git a/src/traces/scattergeo/calc.js b/src/traces/scattergeo/calc.js index 8f2fcdee80f..668e4e24aed 100644 --- a/src/traces/scattergeo/calc.js +++ b/src/traces/scattergeo/calc.js @@ -14,6 +14,7 @@ var BADNUM = require('../../constants/numerical').BADNUM; var calcMarkerColorscale = require('../scatter/colorscale_calc'); var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); +var calcSelection = require('../scatter/calc_selection'); module.exports = function calc(gd, trace) { var hasLocationData = Array.isArray(trace.locations); @@ -37,6 +38,7 @@ module.exports = function calc(gd, trace) { arraysToCalcdata(calcTrace, trace); calcMarkerColorscale(trace); + calcSelection(calcTrace, trace); return calcTrace; }; diff --git a/src/traces/scattergeo/index.js b/src/traces/scattergeo/index.js index 4e8c989ee33..3f71a2eeda9 100644 --- a/src/traces/scattergeo/index.js +++ b/src/traces/scattergeo/index.js @@ -16,6 +16,7 @@ ScatterGeo.supplyDefaults = require('./defaults'); ScatterGeo.colorbar = require('../scatter/colorbar'); ScatterGeo.calc = require('./calc'); ScatterGeo.plot = require('./plot'); +ScatterGeo.style = require('./style'); ScatterGeo.hoverPoints = require('./hover'); ScatterGeo.eventData = require('./event_data'); ScatterGeo.selectPoints = require('./select'); diff --git a/src/traces/scattergeo/plot.js b/src/traces/scattergeo/plot.js index 42965cee10c..cb736f8f163 100644 --- a/src/traces/scattergeo/plot.js +++ b/src/traces/scattergeo/plot.js @@ -11,15 +11,13 @@ var d3 = require('d3'); -var Drawing = require('../../components/drawing'); -var Color = require('../../components/color'); - var Lib = require('../../lib'); var BADNUM = require('../../constants/numerical').BADNUM; var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures; var locationToFeature = require('../../lib/geo_location_utils').locationToFeature; var geoJsonUtils = require('../../lib/geojson_utils'); var subTypes = require('../scatter/subtypes'); +var style = require('./style'); module.exports = function plot(geo, calcData) { for(var i = 0; i < calcData.length; i++) { @@ -79,10 +77,10 @@ module.exports = function plot(geo, calcData) { .append('text') .each(function(calcPt) { removeBADNUM(calcPt, this); }); } - }); - // call style here within topojson request callback - style(geo); + // call style here within topojson request callback + style(geo.graphDiv, calcTrace); + }); }; function calcGeoJSON(calcTrace, topojson) { @@ -100,37 +98,3 @@ function calcGeoJSON(calcTrace, topojson) { calcPt.lonlat = feature ? feature.properties.ct : [BADNUM, BADNUM]; } } - -function style(geo) { - var gTraces = geo.layers.frontplot.selectAll('.trace.scattergeo'); - - gTraces.style('opacity', function(calcTrace) { - return calcTrace[0].trace.opacity; - }); - - gTraces.each(function(calcTrace) { - var trace = calcTrace[0].trace; - var group = d3.select(this); - - group.selectAll('path.point') - .call(Drawing.pointStyle, trace, geo.graphDiv); - group.selectAll('text') - .call(Drawing.textPointStyle, trace, geo.graphDiv); - }); - - // this part is incompatible with Drawing.lineGroupStyle - gTraces.selectAll('path.js-line') - .style('fill', 'none') - .each(function(d) { - var path = d3.select(this); - var trace = d.trace; - var line = trace.line || {}; - - path.call(Color.stroke, line.color) - .call(Drawing.dashLine, line.dash || '', line.width || 0); - - if(trace.fill !== 'none') { - path.call(Color.fill, trace.fillcolor); - } - }); -} diff --git a/src/traces/scattergeo/select.js b/src/traces/scattergeo/select.js index aeacd646423..31601b142f9 100644 --- a/src/traces/scattergeo/select.js +++ b/src/traces/scattergeo/select.js @@ -9,7 +9,6 @@ 'use strict'; var subtypes = require('../scatter/subtypes'); -var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; module.exports = function selectPoints(searchInfo, polygon) { var cd = searchInfo.cd; @@ -17,19 +16,15 @@ module.exports = function selectPoints(searchInfo, polygon) { var ya = searchInfo.yaxis; var selection = []; var trace = cd[0].trace; - var node3 = cd[0].node3; var di, lonlat, x, y, i; var hasOnlyLines = (!subtypes.hasMarkers(trace) && !subtypes.hasText(trace)); if(hasOnlyLines) return []; - var marker = trace.marker; - var opacity = Array.isArray(marker.opacity) ? 1 : marker.opacity; - if(polygon === false) { for(i = 0; i < cd.length; i++) { - cd[i].dim = 0; + cd[i].selected = 0; } } else { for(i = 0; i < cd.length; i++) { @@ -44,20 +39,12 @@ module.exports = function selectPoints(searchInfo, polygon) { lon: lonlat[0], lat: lonlat[1] }); - di.dim = 0; + di.selected = 1; } else { - di.dim = 1; + di.selected = 0; } } } - node3.selectAll('path.point').style('opacity', function(d) { - return ((d.mo + 1 || opacity + 1) - 1) * (d.dim ? DESELECTDIM : 1); - }); - - node3.selectAll('text').style('opacity', function(d) { - return d.dim ? DESELECTDIM : 1; - }); - return selection; }; diff --git a/src/traces/scattergeo/style.js b/src/traces/scattergeo/style.js new file mode 100644 index 00000000000..e4194bea969 --- /dev/null +++ b/src/traces/scattergeo/style.js @@ -0,0 +1,44 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var d3 = require('d3'); +var Drawing = require('../../components/drawing'); +var Color = require('../../components/color'); + +var stylePoints = require('../scatter/style').stylePoints; + +module.exports = function style(gd, calcTrace) { + if(calcTrace) styleTrace(gd, calcTrace); +}; + +function styleTrace(gd, calcTrace) { + var trace = calcTrace[0].trace; + var s = calcTrace[0].node3; + + s.style('opacity', calcTrace[0].trace.opacity); + + stylePoints(s, trace, gd); + + // this part is incompatible with Drawing.lineGroupStyle + s.selectAll('path.js-line') + .style('fill', 'none') + .each(function(d) { + var path = d3.select(this); + var trace = d.trace; + var line = trace.line || {}; + + path.call(Color.stroke, line.color) + .call(Drawing.dashLine, line.dash || '', line.width || 0); + + if(trace.fill !== 'none') { + path.call(Color.fill, trace.fillcolor); + } + }); +} diff --git a/src/traces/scattergl/defaults.js b/src/traces/scattergl/defaults.js index 442363ae113..b2bbdf5ca98 100644 --- a/src/traces/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -42,7 +42,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noSelect: true}); } coerce('fill'); diff --git a/src/traces/scattermapbox/attributes.js b/src/traces/scattermapbox/attributes.js index a907e34905d..3a27f18924a 100644 --- a/src/traces/scattermapbox/attributes.js +++ b/src/traces/scattermapbox/attributes.js @@ -107,6 +107,17 @@ module.exports = overrideAll({ textfont: mapboxAttrs.layers.symbol.textfont, textposition: mapboxAttrs.layers.symbol.textposition, + selected: { + marker: { + opacity: scatterAttrs.selected.marker.opacity + } + }, + unselected: { + marker: { + opacity: scatterAttrs.unselected.marker.opacity + } + }, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['lon', 'lat', 'text', 'name'] }) diff --git a/src/traces/scattermapbox/convert.js b/src/traces/scattermapbox/convert.js index 7b88ee647d6..790d97eb962 100644 --- a/src/traces/scattermapbox/convert.js +++ b/src/traces/scattermapbox/convert.js @@ -185,8 +185,15 @@ function makeCircleGeoJSON(calcTrace, hash) { sizeFn = makeBubbleSizeFn(trace); } - function combineOpacities(d, mo) { - return trace.opacity * mo * (d.dim ? DESELECTDIM : 1); + var combineOpacities; + if(trace.selectedpoints) { + combineOpacities = function(d, mo) { + return trace.opacity * mo * (d.selected ? 1 : DESELECTDIM); + }; + } else { + combineOpacities = function(d, mo) { + return trace.opacity * mo; + }; } var opacityFn; @@ -195,7 +202,7 @@ function makeCircleGeoJSON(calcTrace, hash) { var mo = isNumeric(d.mo) ? +Lib.constrain(d.mo, 0, 1) : 0; return combineOpacities(d, mo); }; - } else if(trace._hasDimmedPts) { + } else if(trace.selectedpoints) { opacityFn = function(d) { return combineOpacities(d, marker.opacity); }; @@ -343,7 +350,7 @@ function calcCircleOpacity(trace, hash) { var marker = trace.marker; var out; - if(Array.isArray(marker.opacity) || trace._hasDimmedPts) { + if(Array.isArray(marker.opacity) || trace.selectedpoints) { var vals = Object.keys(hash[OPACITY_PROP]); var stops = []; diff --git a/src/traces/scattermapbox/defaults.js b/src/traces/scattermapbox/defaults.js index 4ff10674e2e..9f0547ad1d2 100644 --- a/src/traces/scattermapbox/defaults.js +++ b/src/traces/scattermapbox/defaults.js @@ -16,10 +16,8 @@ var handleMarkerDefaults = require('../scatter/marker_defaults'); var handleLineDefaults = require('../scatter/line_defaults'); var handleTextDefaults = require('../scatter/text_defaults'); var handleFillColorDefaults = require('../scatter/fillcolor_defaults'); - var attributes = require('./attributes'); - module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); @@ -41,7 +39,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasMarkers(traceOut)) { - handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noLine: true}); + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce, {noLine: true, noSelect: true}); // array marker.size and marker.color are only supported with circles @@ -56,13 +54,15 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout } if(subTypes.hasText(traceOut)) { - handleTextDefaults(traceIn, traceOut, layout, coerce); + handleTextDefaults(traceIn, traceOut, layout, coerce, {noSelect: true}); } coerce('fill'); if(traceOut.fill !== 'none') { handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); } + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; function handleLonLatDefaults(traceIn, traceOut, coerce) { diff --git a/src/traces/scattermapbox/index.js b/src/traces/scattermapbox/index.js index 251432a82ff..aff2dc9bf5f 100644 --- a/src/traces/scattermapbox/index.js +++ b/src/traces/scattermapbox/index.js @@ -20,6 +20,13 @@ ScatterMapbox.hoverPoints = require('./hover'); ScatterMapbox.eventData = require('./event_data'); ScatterMapbox.selectPoints = require('./select'); +ScatterMapbox.style = function(_, cd) { + if(cd) { + var trace = cd[0].trace; + trace._glTrace.update(cd); + } +}; + ScatterMapbox.moduleType = 'trace'; ScatterMapbox.name = 'scattermapbox'; ScatterMapbox.basePlotModule = require('../../plots/mapbox'); diff --git a/src/traces/scattermapbox/select.js b/src/traces/scattermapbox/select.js index 35eaf523e9a..6e2dc7f25a4 100644 --- a/src/traces/scattermapbox/select.js +++ b/src/traces/scattermapbox/select.js @@ -20,15 +20,11 @@ module.exports = function selectPoints(searchInfo, polygon) { var di, lonlat, x, y, i; - // flag used in ./convert.js - // to not insert data-driven 'circle-opacity' when we don't need to - trace._hasDimmedPts = false; - if(!subtypes.hasMarkers(trace)) return []; if(polygon === false) { for(i = 0; i < cd.length; i++) { - cd[i].dim = 0; + cd[i].selected = 0; } } else { for(i = 0; i < cd.length; i++) { @@ -38,20 +34,17 @@ module.exports = function selectPoints(searchInfo, polygon) { y = ya.c2p(lonlat); if(polygon.contains([x, y])) { - trace._hasDimmedPts = true; selection.push({ pointNumber: i, lon: lonlat[0], lat: lonlat[1] }); - di.dim = 0; + di.selected = 1; } else { - di.dim = 1; + di.selected = 0; } } } - trace._glTrace.update(cd); - return selection; }; diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js index 5af29cb9f89..5f5c333fbff 100644 --- a/src/traces/scatterternary/attributes.js +++ b/src/traces/scatterternary/attributes.js @@ -139,6 +139,10 @@ module.exports = { textfont: scatterAttrs.textfont, textposition: scatterAttrs.textposition, + + selected: scatterAttrs.selected, + unselected: scatterAttrs.unselected, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['a', 'b', 'c', 'text', 'name'] }), diff --git a/src/traces/scatterternary/calc.js b/src/traces/scatterternary/calc.js index c0a5d958212..bc0cec5ea9a 100644 --- a/src/traces/scatterternary/calc.js +++ b/src/traces/scatterternary/calc.js @@ -16,11 +16,11 @@ var Axes = require('../../plots/cartesian/axes'); var subTypes = require('../scatter/subtypes'); var calcColorscale = require('../scatter/colorscale_calc'); var arraysToCalcdata = require('../scatter/arrays_to_calcdata'); +var calcSelection = require('../scatter/calc_selection'); var dataArrays = ['a', 'b', 'c']; var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']}; - module.exports = function calc(gd, trace) { var ternary = gd._fullLayout[trace.subplot], displaySum = ternary.sum, @@ -90,6 +90,7 @@ module.exports = function calc(gd, trace) { calcColorscale(trace); arraysToCalcdata(cd, trace); + calcSelection(cd, trace); return cd; }; diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js index 9a1ca4f4fcd..6bc413f6976 100644 --- a/src/traces/scatterternary/defaults.js +++ b/src/traces/scatterternary/defaults.js @@ -101,4 +101,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('hoveron', dfltHoverOn.join('+') || 'points'); coerce('cliponaxis'); + + Lib.coerceSelectionMarkerOpacity(traceOut, coerce); }; diff --git a/src/traces/scatterternary/event_data.js b/src/traces/scatterternary/event_data.js new file mode 100644 index 00000000000..ffec801c0cb --- /dev/null +++ b/src/traces/scatterternary/event_data.js @@ -0,0 +1,30 @@ +/** +* Copyright 2012-2017, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +module.exports = function eventData(out, pt, trace, cd, pointNumber) { + if(pt.xa) out.xaxis = pt.xa; + if(pt.ya) out.yaxis = pt.ya; + + if(cd[pointNumber]) { + var cdi = cd[pointNumber]; + + // N.B. These are the normalized coordinates. + out.a = cdi.a; + out.b = cdi.b; + out.c = cdi.c; + } else { + // for fill-hover only + out.a = pt.a; + out.b = pt.b; + out.c = pt.c; + } + + return out; +}; diff --git a/src/traces/scatterternary/index.js b/src/traces/scatterternary/index.js index abbdc7f06b1..781cf0c10fe 100644 --- a/src/traces/scatterternary/index.js +++ b/src/traces/scatterternary/index.js @@ -17,7 +17,8 @@ ScatterTernary.calc = require('./calc'); ScatterTernary.plot = require('./plot'); ScatterTernary.style = require('./style'); ScatterTernary.hoverPoints = require('./hover'); -ScatterTernary.selectPoints = require('./select'); +ScatterTernary.selectPoints = require('../scatter/select'); +ScatterTernary.eventData = require('./event_data'); ScatterTernary.moduleType = 'trace'; ScatterTernary.name = 'scatterternary'; diff --git a/src/traces/scatterternary/select.js b/src/traces/scatterternary/select.js deleted file mode 100644 index 5682b0e1669..00000000000 --- a/src/traces/scatterternary/select.js +++ /dev/null @@ -1,33 +0,0 @@ -/** -* Copyright 2012-2017, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var scatterSelect = require('../scatter/select'); - - -module.exports = function selectPoints(searchInfo, polygon) { - var selection = scatterSelect(searchInfo, polygon); - if(!selection) return; - - var cd = searchInfo.cd, - pt, cdi, i; - - for(i = 0; i < selection.length; i++) { - pt = selection[i]; - cdi = cd[pt.pointNumber]; - pt.a = cdi.a; - pt.b = cdi.b; - pt.c = cdi.c; - delete pt.x; - delete pt.y; - } - - return selection; -}; diff --git a/src/traces/scatterternary/style.js b/src/traces/scatterternary/style.js index 8ead87cc97e..b6241334f05 100644 --- a/src/traces/scatterternary/style.js +++ b/src/traces/scatterternary/style.js @@ -9,19 +9,19 @@ 'use strict'; -var scatterStyle = require('../scatter/style'); - - -module.exports = function style(gd) { - var modules = gd._fullLayout._modules; +var scatterStyle = require('../scatter/style').style; +module.exports = function style(gd, cd) { // we're just going to call scatter style... if we already // called it, don't need to redo. // Later though we may want differences, or we may make style // more specific in its scope, then we can remove this. - for(var i = 0; i < modules.length; i++) { - if(modules[i].name === 'scatter') return; + if(!cd) { + var modules = gd._fullLayout._modules; + for(var i = 0; i < modules.length; i++) { + if(modules[i].name === 'scatter') return; + } } - scatterStyle(gd); + scatterStyle(gd, cd); }; diff --git a/src/traces/violin/attributes.js b/src/traces/violin/attributes.js index 6f7540cf7a2..b01ce5a3a45 100644 --- a/src/traces/violin/attributes.js +++ b/src/traces/violin/attributes.js @@ -229,6 +229,9 @@ module.exports = { ].join(' ') }, + selected: boxAttrs.selected, + unselected: boxAttrs.unselected, + hoveron: { valType: 'flaglist', flags: ['violins', 'points', 'kde'], diff --git a/src/traces/violin/style.js b/src/traces/violin/style.js index a3f0c2d8db0..65336f3a065 100644 --- a/src/traces/violin/style.js +++ b/src/traces/violin/style.js @@ -9,39 +9,39 @@ 'use strict'; var d3 = require('d3'); -var Drawing = require('../../components/drawing'); var Color = require('../../components/color'); - -module.exports = function style(gd) { - var traces = d3.select(gd).selectAll('g.trace.violins'); - - traces.style('opacity', function(d) { return d[0].trace.opacity; }) - .each(function(d) { - var trace = d[0].trace; - var sel = d3.select(this); - var box = trace.box || {}; - var boxLine = box.line || {}; - var meanline = trace.meanline || {}; - var meanLineWidth = meanline.width; - - sel.selectAll('path.violin') - .style('stroke-width', trace.line.width + 'px') - .call(Color.stroke, trace.line.color) - .call(Color.fill, trace.fillcolor); - - sel.selectAll('path.box') - .style('stroke-width', boxLine.width + 'px') - .call(Color.stroke, boxLine.color) - .call(Color.fill, box.fillcolor); - - sel.selectAll('g.points path') - .call(Drawing.pointStyle, trace, gd); - - sel.selectAll('path.mean') - .style({ - 'stroke-width': meanLineWidth + 'px', - 'stroke-dasharray': (2 * meanLineWidth) + 'px,' + meanLineWidth + 'px' - }) - .call(Color.stroke, meanline.color); - }); +var stylePoints = require('../scatter/style').stylePoints; + +module.exports = function style(gd, cd) { + var s = cd ? cd[0].node3 : d3.select(gd).selectAll('g.trace.violins'); + + s.style('opacity', function(d) { return d[0].trace.opacity; }); + + s.each(function(d) { + var trace = d[0].trace; + var sel = d3.select(this); + var box = trace.box || {}; + var boxLine = box.line || {}; + var meanline = trace.meanline || {}; + var meanLineWidth = meanline.width; + + sel.selectAll('path.violin') + .style('stroke-width', trace.line.width + 'px') + .call(Color.stroke, trace.line.color) + .call(Color.fill, trace.fillcolor); + + sel.selectAll('path.box') + .style('stroke-width', boxLine.width + 'px') + .call(Color.stroke, boxLine.color) + .call(Color.fill, box.fillcolor); + + sel.selectAll('path.mean') + .style({ + 'stroke-width': meanLineWidth + 'px', + 'stroke-dasharray': (2 * meanLineWidth) + 'px,' + meanLineWidth + 'px' + }) + .call(Color.stroke, meanline.color); + + stylePoints(sel, trace, gd); + }); }; diff --git a/test/image/baselines/geo_point-selection.png b/test/image/baselines/geo_point-selection.png new file mode 100644 index 00000000000..c74d0b67fae Binary files /dev/null and b/test/image/baselines/geo_point-selection.png differ diff --git a/test/image/baselines/mapbox_bubbles.png b/test/image/baselines/mapbox_bubbles.png index e0544baa69a..ae38027605e 100644 Binary files a/test/image/baselines/mapbox_bubbles.png and b/test/image/baselines/mapbox_bubbles.png differ diff --git a/test/image/baselines/point-selection.png b/test/image/baselines/point-selection.png new file mode 100644 index 00000000000..396cf75cad1 Binary files /dev/null and b/test/image/baselines/point-selection.png differ diff --git a/test/image/baselines/point-selection2.png b/test/image/baselines/point-selection2.png new file mode 100644 index 00000000000..8a898869141 Binary files /dev/null and b/test/image/baselines/point-selection2.png differ diff --git a/test/image/baselines/scattercarpet.png b/test/image/baselines/scattercarpet.png index 441327dca9a..0de047bf419 100644 Binary files a/test/image/baselines/scattercarpet.png and b/test/image/baselines/scattercarpet.png differ diff --git a/test/image/baselines/ternary_array_styles.png b/test/image/baselines/ternary_array_styles.png index ab11c0f648d..2ed0c033b92 100644 Binary files a/test/image/baselines/ternary_array_styles.png and b/test/image/baselines/ternary_array_styles.png differ diff --git a/test/image/mocks/geo_point-selection.json b/test/image/mocks/geo_point-selection.json new file mode 100644 index 00000000000..a74f498dcf0 --- /dev/null +++ b/test/image/mocks/geo_point-selection.json @@ -0,0 +1,51 @@ +{ + "data": [{ + "selectedpoints": [1, 2, 4], + "type": "choropleth", + "locations": ["CAN", "USA", "RUS", "AUS", "FRA"], + "z": [0, 1, 1, 0, 1], + "colorscale": [[0, "blue"], [1, "orange"]], + "showscale": false, + "selected": { + "marker": { + "opacity": 0.8 + } + }, + "unselected": { + "marker": { + "opacity": 0.5 + } + } + }, { + "selectedpoints": [0, 2], + "type": "scattergeo", + "mode": "markers+text", + "lon": [0, -75, 100], + "lat": [0, 45, -30], + "selected": { + "marker": { + "color": "green", + "opacity": 1, + "size": 20 + } + }, + "unselected": { + "marker": { + "color": "red", + "opacity": 0.5, + "size": 10 + } + } + }], + "layout": { + "dragmode": "lasso", + "showlegend": false, + "width": 700, + "height": 500, + "geo": { + "projection": { + "type": "miller" + } + } + } +} diff --git a/test/image/mocks/mapbox_bubbles.json b/test/image/mocks/mapbox_bubbles.json index 3f72cc2dbce..c35b8eb247f 100644 --- a/test/image/mocks/mapbox_bubbles.json +++ b/test/image/mocks/mapbox_bubbles.json @@ -28,6 +28,24 @@ [1, "rgb(178,10,28)"] ] } + }, + { + "type": "scattermapbox", + "selectedpoints": [1], + "mode": "markers", + "lon": [-10, -20, -30], + "lat": [10, 20, 30], + "marker": {"size": 20}, + "selected": { + "marker": { + "opacity": 0.8 + } + }, + "unselected": { + "marker": { + "opacity": 0.5 + } + } } ], "layout": { diff --git a/test/image/mocks/point-selection.json b/test/image/mocks/point-selection.json new file mode 100644 index 00000000000..d7f6f2c2139 --- /dev/null +++ b/test/image/mocks/point-selection.json @@ -0,0 +1,72 @@ +{ + "data": [{ + "mode": "lines+markers+text", + "x": [1, 2, 3, 4, 5, 6], + "y": [1, 3, 2, 4, 5, 7], + "ids": ["a", "b", "c", "d", "e", "f"], + "text": ["a", "b", "c", "d", "e", "f"], + "marker": { + "color": "#67353E", + "size": 12 + }, + "textposition": "top left", + "selectedpoints": [1, 2, 3], + "selected": { + "marker": { + "color": "blue", + "size": 20 + } + }, + "unselected": { + "marker": { + "color": "green", + "opacity": 0.5 + } + }, + "transforms": [{ + "type": "filter", + "target": "x", + "operation": "][", + "value": [3.5, 4.5] + }] + }, { + "mode": "lines+markers+text", + "x": [1, 2, 3, 4, 5, 6], + "y": [6, 5, 6, 5, 4, 3], + "ids": ["a", "b", "c", "d", "e", "f"], + "text": ["a", "b", "c", "d", "e", "f"], + "marker": { + "color": "#34ABA2", + "size": 12 + }, + "textposition": "top left", + "selectedpoints": [], + "selected": { + "marker": { + "color": "green" + } + }, + "unselected": { + "marker": { + "color": "blue", + "opacity": 0.5 + } + }, + "transforms": [{ + "type": "filter", + "target": "x", + "operation": "][", + "value": [3.5, 4.5] + }] + }], + "layout": { + "dragmode": "lasso", + "legend": { + "x": 0, + "y": 1, + "xanchor": "left", + "yanchor": "bottom" + }, + "width": 600 + } +} diff --git a/test/image/mocks/point-selection2.json b/test/image/mocks/point-selection2.json new file mode 100644 index 00000000000..3c4857bf620 --- /dev/null +++ b/test/image/mocks/point-selection2.json @@ -0,0 +1,112 @@ +{ + "data": [{ + "selectedpoints": [1, 2, 3], + "type": "bar", + "y": [1, 3, 2, 4, 5, 7], + "selected": { + "marker": { + "opacity": 1, + "color": "green" + } + }, + "unselected": { + "marker": { + "opacity": 0.5, + "color": "red" + } + } + }, { + "selectedpoints": [1, 2], + "type": "histogram", + "xaxis": "x2", + "yaxis": "y2", + "x": [0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3], + "selected": { + "marker": { + "opacity": 1, + "color": "green" + } + }, + "unselected": { + "marker": { + "opacity": 0.5, + "color": "red" + } + } + }, { + "selectedpoints": [1, 2, 3], + "type": "box", + "y": [0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3], + "boxpoints": "all", + "xaxis": "x3", + "yaxis": "y3", + "selected": { + "marker": { + "opacity": 1, + "color": "green" + } + }, + "unselected": { + "marker": { + "opacity": 0.5, + "color": "red" + } + } + }, { + "selectedpoints": [1, 2, 3], + "type": "violin", + "y": [0.2, 0.2, 0.6, 1, 0.5, 0.4, 0.2, 0.7, 0.9, 0.1, 0.5, 0.3], + "points": "all", + "xaxis": "x4", + "yaxis": "y4", + "selected": { + "marker": { + "opacity": 1, + "color": "green" + } + }, + "unselected": { + "marker": { + "opacity": 0.5, + "color": "red" + } + } + }], + "layout": { + "dragmode": "select", + "showlegend": false, + "width": 500, + "height": 500, + "margin": {"l": 20, "b": 0, "t": 0, "r": 20}, + "xaxis": { + "domain": [0, 0.48] + }, + "yaxis": { + "domain": [0, 0.48] + }, + "xaxis2": { + "domain": [0.52, 1], + "anchor": "y2" + }, + "yaxis2": { + "domain": [0, 0.48], + "anchor": "x2" + }, + "xaxis3": { + "domain": [0, 0.48], + "anchor": "y3" + }, + "yaxis3": { + "domain": [0.52, 1], + "anchor": "x3" + }, + "xaxis4": { + "domain": [0.52, 1], + "anchor": "y4" + }, + "yaxis4": { + "domain": [0.52, 1], + "anchor": "x4" + } + } +} diff --git a/test/image/mocks/scattercarpet.json b/test/image/mocks/scattercarpet.json index 09756b08a41..ea9f00b5ec5 100644 --- a/test/image/mocks/scattercarpet.json +++ b/test/image/mocks/scattercarpet.json @@ -75,6 +75,21 @@ "line": { "smoothing": 1, "shape": "spline" + }, + "selectedpoints": [1], + "selected": { + "marker": { + "size": 20, + "color": "green", + "opacity": 1 + } + }, + "unselected": { + "marker": { + "size": 20, + "color": "red", + "opacity": 0.5 + } } } ], diff --git a/test/image/mocks/ternary_array_styles.json b/test/image/mocks/ternary_array_styles.json index 755b8ebbde1..6b4f00d389e 100644 --- a/test/image/mocks/ternary_array_styles.json +++ b/test/image/mocks/ternary_array_styles.json @@ -84,6 +84,28 @@ "width": 3 }, "connectgaps": true + }, + { + "type": "scatterternary", + "mode": "markers", + "a": [0.1, 0.3, 0.2], + "b": [0.1, 0.9, 0.4], + "c": [0.1, 0.2, 0.1], + "selectedpoints": [0, 2], + "selected": { + "marker": { + "color": "green", + "opacity": 1, + "size": 20 + } + }, + "unselected": { + "marker": { + "color": "red", + "opacity": 0.5, + "size": 10 + } + } } ], "layout": { diff --git a/test/jasmine/tests/click_test.js b/test/jasmine/tests/click_test.js index deb54b228f4..0841b5ce895 100644 --- a/test/jasmine/tests/click_test.js +++ b/test/jasmine/tests/click_test.js @@ -86,7 +86,7 @@ describe('Test click interactions:', function() { var pt = futureData.points[0]; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'x', 'y', 'xaxis', 'yaxis' ]); expect(pt.curveNumber).toEqual(0); @@ -128,7 +128,7 @@ describe('Test click interactions:', function() { var pt = futureData.points[0]; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'x', 'y', 'xaxis', 'yaxis' ]); expect(pt.curveNumber).toEqual(0); @@ -208,7 +208,7 @@ describe('Test click interactions:', function() { var pt = futureData.points[0]; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'x', 'y', 'xaxis', 'yaxis' ]); expect(pt.curveNumber).toEqual(0); @@ -239,7 +239,7 @@ describe('Test click interactions:', function() { var pt = futureData.points[0]; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'x', 'y', 'xaxis', 'yaxis' ]); expect(pt.curveNumber).toEqual(0); @@ -274,7 +274,7 @@ describe('Test click interactions:', function() { var pt = futureData.points[0]; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'x', 'y', 'xaxis', 'yaxis' ]); expect(pt.curveNumber).toEqual(0); diff --git a/test/jasmine/tests/geo_test.js b/test/jasmine/tests/geo_test.js index bca4661146e..51f8b1e901a 100644 --- a/test/jasmine/tests/geo_test.js +++ b/test/jasmine/tests/geo_test.js @@ -569,7 +569,7 @@ describe('Test geo interactions', function() { it('should contain the correct fields', function() { expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat', 'location', 'marker.size' ]); expect(cnt).toEqual(1); @@ -632,7 +632,7 @@ describe('Test geo interactions', function() { it('should contain the correct fields', function() { expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat', 'location', 'marker.size' ]); }); @@ -664,7 +664,7 @@ describe('Test geo interactions', function() { it('should contain the correct fields', function() { expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat', 'location', 'marker.size' ]); }); @@ -693,7 +693,7 @@ describe('Test geo interactions', function() { it('should contain the correct fields', function() { expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'location', 'z' ]); }); @@ -721,7 +721,7 @@ describe('Test geo interactions', function() { it('should contain the correct fields', function() { expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'location', 'z' ]); }); @@ -753,7 +753,7 @@ describe('Test geo interactions', function() { it('should contain the correct fields', function() { expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'location', 'z' ]); }); @@ -1303,7 +1303,8 @@ describe('Test event property of interactions on a geo plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'lon', 'lat', 'location', 'text', 'marker.size' ]); @@ -1351,7 +1352,8 @@ describe('Test event property of interactions on a geo plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'lon', 'lat', 'location', 'text', 'marker.size' ]); @@ -1392,7 +1394,8 @@ describe('Test event property of interactions on a geo plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'lon', 'lat', 'location', 'text', 'marker.size' ]); @@ -1428,7 +1431,8 @@ describe('Test event property of interactions on a geo plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'lon', 'lat', 'location', 'text', 'marker.size' ]); diff --git a/test/jasmine/tests/histogram_test.js b/test/jasmine/tests/histogram_test.js index e9ae22bc786..60f4d57abd0 100644 --- a/test/jasmine/tests/histogram_test.js +++ b/test/jasmine/tests/histogram_test.js @@ -206,10 +206,10 @@ describe('Test histogram', function() { expect(out).toEqual([ // full calcdata has x and y too (and t in the first one), // but those come later from setPositions. - {b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70}, - {b: 0, p: d71, s: 1, pts: [2], p0: d71, p1: d71}, - {b: 0, p: d72, s: 0, pts: [], p0: d72, p1: d72}, - {b: 0, p: d73, s: 1, pts: [3], p0: d73, p1: d73} + {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70}, + {i: 1, b: 0, p: d71, s: 1, pts: [2], p0: d71, p1: d71}, + {i: 2, b: 0, p: d72, s: 0, pts: [], p0: d72, p1: d72}, + {i: 3, b: 0, p: d73, s: 1, pts: [3], p0: d73, p1: d73} ]); // All data on exact months: shift so bin center is on (31-day months) @@ -223,10 +223,10 @@ describe('Test histogram', function() { var d70mar = Date.UTC(1970, 2, 2, 12); var d70apr = Date.UTC(1970, 3, 1); expect(out).toEqual([ - {b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70}, - {b: 0, p: d70feb, s: 1, pts: [2], p0: d70feb, p1: d70feb}, - {b: 0, p: d70mar, s: 0, pts: [], p0: d70mar, p1: d70mar}, - {b: 0, p: d70apr, s: 1, pts: [3], p0: d70apr, p1: d70apr} + {i: 0, b: 0, p: d70, s: 2, pts: [0, 1], p0: d70, p1: d70}, + {i: 1, b: 0, p: d70feb, s: 1, pts: [2], p0: d70feb, p1: d70feb}, + {i: 2, b: 0, p: d70mar, s: 0, pts: [], p0: d70mar, p1: d70mar}, + {i: 3, b: 0, p: d70apr, s: 1, pts: [3], p0: d70apr, p1: d70apr} ]); // data on exact days: shift so each bin goes from noon to noon @@ -248,11 +248,11 @@ describe('Test histogram', function() { expect(out).toEqual([ // dec 31 12:00 -> jan 31 12:00, middle is jan 16 - {b: 0, p: Date.UTC(1970, 0, 16), s: 2, pts: [0, 1], p0: Date.UTC(1970, 0, 1), p1: Date.UTC(1970, 0, 31)}, + {i: 0, b: 0, p: Date.UTC(1970, 0, 16), s: 2, pts: [0, 1], p0: Date.UTC(1970, 0, 1), p1: Date.UTC(1970, 0, 31)}, // jan 31 12:00 -> feb 28 12:00, middle is feb 14 12:00 - {b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1, pts: [2], p0: Date.UTC(1970, 1, 1), p1: Date.UTC(1970, 1, 28)}, - {b: 0, p: Date.UTC(1970, 2, 16), s: 0, pts: [], p0: Date.UTC(1970, 2, 1), p1: Date.UTC(1970, 2, 31)}, - {b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1, pts: [3], p0: Date.UTC(1970, 3, 1), p1: Date.UTC(1970, 3, 30)} + {i: 1, b: 0, p: Date.UTC(1970, 1, 14, 12), s: 1, pts: [2], p0: Date.UTC(1970, 1, 1), p1: Date.UTC(1970, 1, 28)}, + {i: 2, b: 0, p: Date.UTC(1970, 2, 16), s: 0, pts: [], p0: Date.UTC(1970, 2, 1), p1: Date.UTC(1970, 2, 31)}, + {i: 3, b: 0, p: Date.UTC(1970, 3, 15, 12), s: 1, pts: [3], p0: Date.UTC(1970, 3, 1), p1: Date.UTC(1970, 3, 30)} ]); }); @@ -268,10 +268,10 @@ describe('Test histogram', function() { x3 = x2 + oneDay; expect(out).toEqual([ - {b: 0, p: x0, s: 2, pts: [0, 1], p0: x0, p1: x0}, - {b: 0, p: x1, s: 1, pts: [2], p0: x1, p1: x1}, - {b: 0, p: x2, s: 0, pts: [], p0: x2, p1: x2}, - {b: 0, p: x3, s: 1, pts: [3], p0: x3, p1: x3} + {i: 0, b: 0, p: x0, s: 2, pts: [0, 1], p0: x0, p1: x0}, + {i: 1, b: 0, p: x1, s: 1, pts: [2], p0: x1, p1: x1}, + {i: 2, b: 0, p: x2, s: 0, pts: [], p0: x2, p1: x2}, + {i: 3, b: 0, p: x3, s: 1, pts: [3], p0: x3, p1: x3} ]); }); @@ -295,7 +295,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {b: 0, p: 3, s: 3, width1: 2, pts: [0, 1, 2], p0: 2, p1: 3.9} + {i: 0, b: 0, p: 3, s: 3, width1: 2, pts: [0, 1, 2], p0: 2, p1: 3.9} ]); }); @@ -308,7 +308,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {b: 0, p: 1.1, s: 3, width1: 0.5, pts: [0, 1, 2], p0: 1.1, p1: 1.1} + {i: 0, b: 0, p: 1.1, s: 3, width1: 0.5, pts: [0, 1, 2], p0: 1.1, p1: 1.1} ]); }); @@ -321,7 +321,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {b: 0, p: 17, s: 2, width1: 2, pts: [2, 4], p0: 17, p1: 17} + {i: 0, b: 0, p: 17, s: 2, width1: 2, pts: [2, 4], p0: 17, p1: 17} ]); }); @@ -334,7 +334,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {b: 0, p: 13, s: 2, width1: 8, pts: [1, 3], p0: 13, p1: 13} + {i: 0, b: 0, p: 13, s: 2, width1: 8, pts: [1, 3], p0: 13, p1: 13} ]); }); @@ -348,7 +348,7 @@ describe('Test histogram', function() { var p = 1296691200000; expect(out).toEqual([ - {b: 0, p: p, s: 2, width1: 2 * 24 * 3600 * 1000, pts: [1, 3], p0: p, p1: p} + {i: 0, b: 0, p: p, s: 2, width1: 2 * 24 * 3600 * 1000, pts: [1, 3], p0: p, p1: p} ]); }); @@ -361,7 +361,7 @@ describe('Test histogram', function() { }); expect(out).toEqual([ - {b: 0, p: 97, s: 2, width1: 1, pts: [1, 3], p0: 97, p1: 97} + {i: 0, b: 0, p: 97, s: 2, width1: 1, pts: [1, 3], p0: 97, p1: 97} ]); }); @@ -453,10 +453,10 @@ describe('Test histogram', function() { it('makes the right base histogram', function() { var baseOut = _calc(base); expect(baseOut).toEqual([ - {b: 0, p: 2, s: 1, pts: [0], p0: 0, p1: 0}, - {b: 0, p: 7, s: 2, pts: [1, 4], p0: 5, p1: 5}, - {b: 0, p: 12, s: 3, pts: [2, 5, 7], p0: 10, p1: 10}, - {b: 0, p: 17, s: 4, pts: [3, 6, 8, 9], p0: 15, p1: 15}, + {i: 0, b: 0, p: 2, s: 1, pts: [0], p0: 0, p1: 0}, + {i: 1, b: 0, p: 7, s: 2, pts: [1, 4], p0: 5, p1: 5}, + {i: 2, b: 0, p: 12, s: 3, pts: [2, 5, 7], p0: 10, p1: 10}, + {i: 3, b: 0, p: 17, s: 4, pts: [3, 6, 8, 9], p0: 15, p1: 15}, ]); }); diff --git a/test/jasmine/tests/mapbox_test.js b/test/jasmine/tests/mapbox_test.js index ae7ec7a9cf3..68590653f1f 100644 --- a/test/jasmine/tests/mapbox_test.js +++ b/test/jasmine/tests/mapbox_test.js @@ -768,7 +768,7 @@ describe('@noCI, mapbox plots', function() { return _mouseEvent('mousemove', pointPos, function() { expect(hoverData).not.toBe(undefined, 'firing on data points'); expect(Object.keys(hoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ], 'returning the correct event data keys'); expect(hoverData.curveNumber).toEqual(0, 'returning the correct curve number'); expect(hoverData.pointNumber).toEqual(0, 'returning the correct point number'); @@ -778,7 +778,7 @@ describe('@noCI, mapbox plots', function() { return _mouseEvent('mousemove', blankPos, function() { expect(unhoverData).not.toBe(undefined, 'firing on data points'); expect(Object.keys(unhoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ], 'returning the correct event data keys'); expect(unhoverData.curveNumber).toEqual(0, 'returning the correct curve number'); expect(unhoverData.pointNumber).toEqual(0, 'returning the correct point number'); @@ -859,7 +859,7 @@ describe('@noCI, mapbox plots', function() { return _click(pointPos, function() { expect(ptData).not.toBe(undefined, 'firing on data points'); expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ], 'returning the correct event data keys'); expect(ptData.curveNumber).toEqual(0, 'returning the correct curve number'); expect(ptData.pointNumber).toEqual(0, 'returning the correct point number'); diff --git a/test/jasmine/tests/scatter_test.js b/test/jasmine/tests/scatter_test.js index d61a2e6ce62..fed15714c31 100644 --- a/test/jasmine/tests/scatter_test.js +++ b/test/jasmine/tests/scatter_test.js @@ -128,6 +128,52 @@ describe('Test scatter', function() { expect(traceOut.xcalendar).toBe('coptic'); expect(traceOut.ycalendar).toBe('ethiopian'); }); + + describe('selected / unselected attribute containers', function() { + function _supply(patch) { traceIn = Lib.extendFlat({ + mode: 'markers', + x: [1, 2, 3], + y: [2, 1, 2] + }, patch); + traceOut = {visible: true}; + supplyDefaults(traceIn, traceOut, defaultColor, layout); + } + + it('should fill in [un]selected.marker.opacity default when no other [un]selected is set', function() { + _supply({}); + expect(traceOut.selected.marker.opacity).toBe(1); + expect(traceOut.unselected.marker.opacity).toBe(0.2); + + _supply({ marker: {opacity: 0.6} }); + expect(traceOut.selected.marker.opacity).toBe(0.6); + expect(traceOut.unselected.marker.opacity).toBe(0.12); + }); + + it('should not fill in [un]selected.marker.opacity default when some other [un]selected is set', function() { + _supply({ + selected: {marker: {size: 20}} + }); + expect(traceOut.selected.marker.opacity).toBeUndefined(); + expect(traceOut.selected.marker.size).toBe(20); + expect(traceOut.unselected).toBeUndefined(); + + _supply({ + unselected: {marker: {color: 'red'}} + }); + expect(traceOut.selected).toBeUndefined(); + expect(traceOut.unselected.marker.opacity).toBeUndefined(); + expect(traceOut.unselected.marker.color).toBe('red'); + + _supply({ + mode: 'markers+text', + selected: {textfont: {color: 'blue'}} + }); + expect(traceOut.selected.marker).toBeUndefined(); + expect(traceOut.selected.textfont.color).toBe('blue'); + expect(traceOut.unselected).toBeUndefined(); + }); + }); + }); describe('isBubble', function() { @@ -738,6 +784,347 @@ describe('scatter hoverPoints', function() { }); }); +describe('Test Scatter.style', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function makeCheckFn(attr, getterFn) { + return function(update, expectation, msg) { + var msg2 = ' (' + msg + ')'; + var promise = update ? Plotly.restyle(gd, update) : Promise.resolve(); + var selector = attr.indexOf('textfont') === 0 ? '.textpoint > text' : '.point'; + + return promise.then(function() { + d3.selectAll('.trace').each(function(_, i) { + var pts = d3.select(this).selectAll(selector); + var expi = expectation[i]; + + expect(pts.size()) + .toBe(expi.length, '# of pts for trace ' + i + msg2); + + pts.each(function(_, j) { + var msg3 = ' for pt ' + j + ' in trace ' + i + msg2; + expect(getterFn(this)).toBe(expi[j], attr + msg3); + }); + }); + }); + }; + } + + var getOpacity = function(node) { return Number(node.style.opacity); }; + var getFillOpacity = function(node) { return Number(node.style['fill-opacity']); }; + var getColor = function(node) { return node.style.fill; }; + var getMarkerSize = function(node) { + // find path arc multiply by 2 to get the corresponding marker.size value + // (works for circles only) + return d3.select(node).attr('d').split('A')[1].split(',')[0] * 2; + }; + + var r = 'rgb(255, 0, 0)'; + var g = 'rgb(0, 255, 0)'; + var b = 'rgb(0, 0, 255)'; + var y = 'rgb(255, 255, 0)'; + var c = 'rgb(0, 255, 255)'; + + it('should style selected point marker opacity correctly', function(done) { + var check = makeCheckFn('marker.opacity', getOpacity); + + Plotly.plot(gd, [{ + mode: 'markers', + y: [1, 2, 1], + marker: {opacity: 0.6} + }, { + mode: 'markers', + y: [2, 1, 2], + marker: {opacity: [0.5, 0.5, 0.5]} + }]) + .then(function() { + return check( + null, + [[0.6, 0.6, 0.6], [0.5, 0.5, 0.5]], + 'base case' + ); + }) + .then(function() { + return check( + {selectedpoints: [[1]]}, + [[0.12, 0.6, 0.12], [0.1, 0.5, 0.1]], + 'selected pt 1 w/o [un]selected setting' + ); + }) + .then(function() { + return check( + {'selected.marker.opacity': 1}, + [[0.12, 1, 0.12], [0.1, 1, 0.1]], + 'selected pt 1 w/ set selected.marker.opacity' + ); + }) + .then(function() { + return check( + {selectedpoints: [[1, 2]]}, + [[0.12, 1, 1], [0.1, 1, 1]], + 'selected pt 1-2 w/ set selected.marker.opacity' + ); + }) + .then(function() { + return check( + {selectedpoints: [[2]]}, + [[0.12, 0.12, 1], [0.1, 0.1, 1]], + 'selected pt 2 w/ set selected.marker.opacity' + ); + }) + .then(function() { + return check( + {selectedpoints: null}, + [[0.6, 0.6, 0.6], [0.5, 0.5, 0.5]], + 'no selected pts w/ set selected.marker.opacity' + ); + }) + .then(function() { + return check( + {selectedpoints: [[1]]}, + [[0.12, 1, 0.12], [0.1, 1, 0.1]], + 'selected pt 1 w/o [un]selected setting (take 2)' + ); + }) + .then(function() { + return check( + {'unselected.marker.opacity': 0}, + [[0, 1, 0], [0, 1, 0]], + 'selected pt 1 w/ set [un]selected.marker.opacity' + ); + }) + .then(function() { + return check( + {'selected.marker.opacity': null}, + [[0, 0.6, 0], [0, 0.5, 0]], + 'selected pt 1 w/ set unselected.marker.opacity' + ); + }) + .catch(fail) + .then(done); + }); + + it('should style selected point marker color correctly', function(done) { + var check = makeCheckFn('marker.color', getColor); + var checkOpacity = makeCheckFn('marker.opacity', getOpacity); + + Plotly.plot(gd, [{ + mode: 'markers', + y: [1, 2, 1], + marker: {color: b} + }, { + mode: 'markers', + y: [2, 1, 2], + marker: {color: [r, g, b]} + }]) + .then(function() { + return check( + null, + [[b, b, b], [r, g, b]], + 'base case' + ); + }) + .then(function() { + return check( + {selectedpoints: [[0, 2]]}, + [[b, b, b], [r, g, b]], + 'selected pts 0-2 w/o [un]selected setting' + ); + }) + .then(function() { + return checkOpacity( + null, + [[1, 0.2, 1], [1, 0.2, 1]], + 'selected pts 0-2 w/o [un]selected setting [should just change opacity]' + ); + }) + .then(function() { + return check( + {'selected.marker.color': y}, + [[y, b, y], [y, g, y]], + 'selected pts 0-2 w/ set selected.marker.color' + ); + }) + .then(function() { + return checkOpacity( + null, + [[1, 1, 1], [1, 1, 1]], + 'selected pts 0-2 w/o [un]selected setting [should NOT change opacity]' + ); + }) + .then(function() { + return check( + {selectedpoints: [[1, 2]]}, + [[b, y, y], [r, y, y]], + 'selected pt 1-2 w/ set selected.marker.color' + ); + }) + .then(function() { + return check( + {selectedpoints: null}, + [[b, b, b], [r, g, b]], + 'no selected pts w/ set selected.marker.color' + ); + }) + .then(function() { + return check( + {selectedpoints: [[0, 2]]}, + [[y, b, y], [y, g, y]], + 'selected pts 0-2 w/ set selected.marker.color (take 2)' + ); + }) + .then(function() { + return check( + {'unselected.marker.color': c}, + [[y, c, y], [y, c, y]], + 'selected pts 0-2 w/ set [un]selected.marker.color' + ); + }) + .then(function() { + return check( + {'selected.marker.color': null}, + [[b, c, b], [r, c, b]], + 'selected pts 0-2 w/ set selected.marker.color' + ); + }) + .catch(fail) + .then(done); + }); + + it('should style selected point marker size correctly', function(done) { + var check = makeCheckFn('marker.size', getMarkerSize); + + Plotly.plot(gd, [{ + mode: 'markers', + y: [1, 2, 1], + marker: {size: 20} + }, { + mode: 'markers', + y: [2, 1, 2], + marker: {size: [15, 25, 35]} + }]) + .then(function() { + return check( + null, + [[20, 20, 20], [15, 25, 35]], + 'base case' + ); + }) + .then(function() { + return check( + {selectedpoints: [[0]], 'selected.marker.size': 40}, + [[40, 20, 20], [40, 25, 35]], + 'selected pt 0 w/ set selected.marker.size' + ); + }) + .then(function() { + return check( + {'unselected.marker.size': 0}, + [[40, 0, 0], [40, 0, 0]], + 'selected pt 0 w/ set [un]selected.marker.size' + ); + }) + .then(function() { + return check( + {'selected.marker.size': null}, + [[20, 0, 0], [15, 0, 0]], + 'selected pt 0 w/ set unselected.marker.size' + ); + }) + .catch(fail) + .then(done); + }); + + it('should style selected point textfont correctly', function(done) { + var checkFontColor = makeCheckFn('textfont.color', getColor); + var checkFontOpacity = makeCheckFn('textfont.color (alpha channel)', getFillOpacity); + var checkPtOpacity = makeCheckFn('marker.opacity', getOpacity); + + Plotly.plot(gd, [{ + mode: 'markers+text', + y: [1, 2, 1], + text: 'TEXT', + textfont: {color: b} + }, { + mode: 'markers+text', + y: [2, 1, 2], + text: ['A', 'B', 'C'], + textfont: {color: [r, g, b]} + }]) + .then(function() { + return checkFontColor( + null, + [[b, b, b], [r, g, b]], + 'base case' + ); + }) + .then(function() { + return checkFontColor( + {selectedpoints: [[0, 2]]}, + [[b, b, b], [r, g, b]], + 'selected pts 0-2 w/o [un]selected setting' + ); + }) + .then(function() { + return checkFontOpacity( + null, + [[1, 0.2, 1], [1, 0.2, 1]], + 'selected pts 0-2 w/o [un]selected setting [should change font color alpha]' + ); + }) + .then(function() { + return checkPtOpacity( + null, + [[1, 0.2, 1], [1, 0.2, 1]], + 'selected pts 0-2 w/o [un]selected setting [should change pt opacity]' + ); + }) + .then(function() { + return checkFontColor( + {'selected.textfont.color': y}, + [[y, b, y], [y, g, y]], + 'selected pts 0-2 w/ set selected.textfont.color' + ); + }) + .then(function() { + return checkFontOpacity( + null, + [[1, 1, 1], [1, 1, 1]], + 'selected pts 0-2 w set selected.textfont.color [should NOT change font color alpha]' + ); + }) + .then(function() { + return checkPtOpacity( + null, + [[1, 1, 1], [1, 1, 1]], + 'selected pts 0-2 w/o [un]selected setting [should NOT change opacity]' + ); + }) + .then(function() { + return checkFontColor( + {'unselected.textfont.color': c}, + [[y, c, y], [y, c, y]], + 'selected pts 0-2 w/ set [un]selected.textfont.color' + ); + }) + .then(function() { + return checkFontColor( + {'selected.textfont.color': null}, + [[b, c, b], [r, c, b]], + 'selected pts 0-2 w/ set selected.textfont.color' + ); + }) + .catch(fail) + .then(done); + }); +}); + describe('Test scatter *clipnaxis*:', function() { afterEach(destroyGraphDiv); diff --git a/test/jasmine/tests/scattermapbox_test.js b/test/jasmine/tests/scattermapbox_test.js index 3c8b98e12d0..729b4cd6abc 100644 --- a/test/jasmine/tests/scattermapbox_test.js +++ b/test/jasmine/tests/scattermapbox_test.js @@ -110,7 +110,7 @@ describe('scattermapbox defaults', function() { describe('scattermapbox convert', function() { 'use strict'; - function _convert(trace, selected) { + function _convert(trace) { var gd = { data: [trace] }; Plots.supplyDefaults(gd); @@ -118,18 +118,6 @@ describe('scattermapbox convert', function() { Plots.doCalcdata(gd, fullTrace); var calcTrace = gd.calcdata[0]; - - if(selected) { - var hasDimmedPts = false; - - selected.forEach(function(v, i) { - if(v) hasDimmedPts = true; - calcTrace[i].dim = v; - }); - - fullTrace._hasDimmedPts = hasDimmedPts; - } - return convert(calcTrace); } @@ -226,36 +214,41 @@ describe('scattermapbox convert', function() { }; var specs = [{ - patch: {}, - selected: [0, 1, 1], - expected: {stops: [[0, 1], [1, 0.2]], props: [0, 1, 1]} + patch: { + selectedpoints: [1, 2] + }, + expected: {stops: [[0, 0.2], [1, 1]], props: [0, 1, 1]} }, { - patch: {opacity: 0.5}, - selected: [0, 1, 1], - expected: {stops: [[0, 0.5], [1, 0.1]], props: [0, 1, 1]} + patch: { + opacity: 0.5, + selectedpoints: [1, 2] + }, + expected: {stops: [[0, 0.1], [1, 0.5]], props: [0, 1, 1]} }, { patch: { - marker: {opacity: 0.6} + marker: {opacity: 0.6}, + selectedpoints: [1, 2] }, - selected: [1, 0, 1], - expected: {stops: [[0, 0.12], [1, 0.6]], props: [0, 1, 0]} + expected: {stops: [[0, 0.12], [1, 0.6]], props: [0, 1, 1]} }, { patch: { - marker: {opacity: [0.5, 1, 0.6]} + marker: { + opacity: [0.5, 1, 0.6], + }, + selectedpoints: [0, 2] }, - selected: [1, 0, 1], - expected: {stops: [[0, 0.1], [1, 1], [2, 0.12]], props: [0, 1, 2]} + expected: {stops: [[0, 0.5], [1, 0.2], [2, 0.6]], props: [0, 1, 2]} }, { patch: { - marker: {opacity: [2, null, -0.6]} + marker: {opacity: [2, null, -0.6]}, + selectedpoints: [0, 1, 2] }, - selected: [1, 1, 1], - expected: {stops: [[0, 0.2], [1, 0]], props: [0, 1, 1]} + expected: {stops: [[0, 1], [1, 0]], props: [0, 1, 1]} }]; specs.forEach(function(s, i) { var msg0 = '- case ' + i + ' '; - var opts = _convert(Lib.extendDeep({}, _base, s.patch), s.selected); + var opts = _convert(Lib.extendDeep({}, _base, s.patch)); expect(opts.circle.paint['circle-opacity'].stops) .toEqual(s.expected.stops, msg0 + 'stops'); @@ -704,7 +697,7 @@ describe('@noCI Test plotly events on a scattermapbox plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ]); expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber'); @@ -748,7 +741,7 @@ describe('@noCI Test plotly events on a scattermapbox plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ]); expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber'); @@ -785,7 +778,7 @@ describe('@noCI Test plotly events on a scattermapbox plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ]); expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber'); @@ -817,7 +810,7 @@ describe('@noCI Test plotly events on a scattermapbox plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'lon', 'lat' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'lon', 'lat' ]); expect(pt.curveNumber).toEqual(0, 'points[0].curveNumber'); diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index db16fe098a6..38bf19fb1ba 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -162,6 +162,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectedData.points, [{ curveNumber: 0, pointNumber: 0, + pointIndex: 0, x: 0.002, y: 16.25, id: 'id-0.002', @@ -169,6 +170,7 @@ describe('Test select box and lasso in general:', function() { }, { curveNumber: 0, pointNumber: 1, + pointIndex: 1, x: 0.004, y: 12.5, id: 'id-0.004', @@ -199,6 +201,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectingData.points, [{ curveNumber: 0, pointNumber: 0, + pointIndex: 0, x: 0.002, y: 16.25, id: 'id-0.002', @@ -206,6 +209,7 @@ describe('Test select box and lasso in general:', function() { }, { curveNumber: 0, pointNumber: 1, + pointIndex: 1, x: 0.004, y: 12.5, id: 'id-0.004', @@ -225,6 +229,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectingData.points, [{ curveNumber: 0, pointNumber: 0, + pointIndex: 0, x: 0.002, y: 16.25, id: 'id-0.002', @@ -232,6 +237,7 @@ describe('Test select box and lasso in general:', function() { }, { curveNumber: 0, pointNumber: 1, + pointIndex: 1, x: 0.004, y: 12.5, id: 'id-0.004', @@ -239,6 +245,7 @@ describe('Test select box and lasso in general:', function() { }, { curveNumber: 0, pointNumber: 4, + pointIndex: 4, x: 0.013, y: 6.875, id: 'id-0.013', @@ -252,6 +259,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectingData.points, [{ curveNumber: 0, pointNumber: 0, + pointIndex: 0, x: 0.002, y: 16.25, id: 'id-0.002', @@ -259,6 +267,7 @@ describe('Test select box and lasso in general:', function() { }, { curveNumber: 0, pointNumber: 1, + pointIndex: 1, x: 0.004, y: 12.5, id: 'id-0.004', @@ -299,6 +308,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectingData.points, [{ curveNumber: 0, pointNumber: 10, + pointIndex: 10, x: 0.099, y: 2.75 }], 'with the correct selecting points (1)'); @@ -307,6 +317,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectedData.points, [{ curveNumber: 0, pointNumber: 10, + pointIndex: 10, x: 0.099, y: 2.75, }], 'with the correct selected points (2)'); @@ -326,6 +337,44 @@ describe('Test select box and lasso in general:', function() { .then(done); }); + it('should set selected points in graph data', function(done) { + resetEvents(gd); + + drag(lassoPath); + + selectedPromise.then(function() { + expect(selectingCnt).toBe(3, 'with the correct selecting count'); + expect(gd.data[0].selectedpoints).toEqual([10]); + + return doubleClick(250, 200); + }) + .then(deselectPromise) + .then(function() { + expect(gd.data[0].selectedpoints).toBeUndefined(); + }) + .catch(fail) + .then(done); + }); + + it('should set selected points in full data', function(done) { + resetEvents(gd); + + drag(lassoPath); + + selectedPromise.then(function() { + expect(selectingCnt).toBe(3, 'with the correct selecting count'); + expect(gd._fullData[0].selectedpoints).toEqual([10]); + + return doubleClick(250, 200); + }) + .then(deselectPromise) + .then(function() { + expect(gd._fullData[0].selectedpoints).toBeUndefined(); + }) + .catch(fail) + .then(done); + }); + it('should trigger selecting/selected/deselect events for touches', function(done) { resetEvents(gd); @@ -336,6 +385,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectingData.points, [{ curveNumber: 0, pointNumber: 10, + pointIndex: 10, x: 0.099, y: 2.75 }], 'with the correct selecting points (1)'); @@ -344,6 +394,7 @@ describe('Test select box and lasso in general:', function() { assertEventData(selectedData.points, [{ curveNumber: 0, pointNumber: 10, + pointIndex: 10, x: 0.099, y: 2.75, }], 'with the correct selected points (2)'); @@ -503,6 +554,8 @@ describe('Test select box and lasso per trace:', function() { if(typeof e[j] === 'number') { expect(p[k]).toBeCloseTo(e[j], 1, msgFull); + } else if(Array.isArray(e[j])) { + expect(p[k]).toBeCloseToArray(e[j], 1, msgFull); } else { expect(p[k]).toBe(e[j], msgFull); } @@ -513,6 +566,27 @@ describe('Test select box and lasso per trace:', function() { }; } + function makeAssertSelectedPoints() { + var callNumber = 0; + + return function(expected) { + var msg = '(call #' + callNumber + ') '; + + gd.data.forEach(function(trace, i) { + var msgFull = msg + 'selectedpoints array for trace ' + i; + var actual = trace.selectedpoints; + + if(expected[i]) { + expect(actual).toBeCloseToArray(expected[i], 1, msgFull); + } else { + expect(actual).toBe(undefined, 1, msgFull); + } + }); + + callNumber++; + }; + } + function makeAssertRanges(subplot, tol) { tol = tol || 1; var callNumber = 0; @@ -583,6 +657,7 @@ describe('Test select box and lasso per trace:', function() { it('should work on scatterternary traces', function(done) { var assertPoints = makeAssertPoints(['a', 'b', 'c']); + var assertSelectedPoints = makeAssertSelectedPoints(); var fig = Lib.extendDeep({}, require('@mocks/ternary_simple')); fig.layout.width = 800; @@ -594,6 +669,7 @@ describe('Test select box and lasso per trace:', function() { [[400, 200], [445, 235]], function() { assertPoints([[0.5, 0.25, 0.25]]); + assertSelectedPoints({0: [0]}); }, [380, 180], BOXEVENTS, 'scatterternary select' @@ -605,7 +681,10 @@ describe('Test select box and lasso per trace:', function() { .then(function() { return _run( [[400, 200], [445, 200], [445, 235], [400, 235], [400, 200]], - function() { assertPoints([[0.5, 0.25, 0.25]]); }, + function() { + assertPoints([[0.5, 0.25, 0.25]]); + assertSelectedPoints({0: [0]}); + }, [380, 180], LASSOEVENTS, 'scatterternary lasso' ); @@ -617,7 +696,10 @@ describe('Test select box and lasso per trace:', function() { .then(function() { return _run( [[200, 200], [230, 200], [230, 230], [200, 230], [200, 200]], - function() { assertPoints([[0.5, 0.25, 0.25]]); }, + function() { + assertPoints([[0.5, 0.25, 0.25]]); + assertSelectedPoints({0: [0]}); + }, [180, 180], LASSOEVENTS, 'scatterternary lasso after relayout' ); @@ -628,15 +710,20 @@ describe('Test select box and lasso per trace:', function() { it('should work on scattercarpet traces', function(done) { var assertPoints = makeAssertPoints(['a', 'b']); + var assertSelectedPoints = makeAssertSelectedPoints(); var fig = Lib.extendDeep({}, require('@mocks/scattercarpet')); + delete fig.data[6].selectedpoints; fig.layout.dragmode = 'select'; addInvisible(fig); Plotly.plot(gd, fig).then(function() { return _run( [[300, 200], [400, 250]], - function() { assertPoints([[0.2, 1.5]]); }, + function() { + assertPoints([[0.2, 1.5]]); + assertSelectedPoints({1: [], 2: [], 3: [], 4: [], 5: [1], 6: []}); + }, null, BOXEVENTS, 'scattercarpet select' ); }) @@ -646,7 +733,10 @@ describe('Test select box and lasso per trace:', function() { .then(function() { return _run( [[300, 200], [400, 200], [400, 250], [300, 250], [300, 200]], - function() { assertPoints([[0.2, 1.5]]); }, + function() { + assertPoints([[0.2, 1.5]]); + assertSelectedPoints({1: [], 2: [], 3: [], 4: [], 5: [1], 6: []}); + }, null, LASSOEVENTS, 'scattercarpet lasso' ); }) @@ -658,6 +748,7 @@ describe('Test select box and lasso per trace:', function() { var assertPoints = makeAssertPoints(['lon', 'lat']); var assertRanges = makeAssertRanges('mapbox'); var assertLassoPoints = makeAssertLassoPoints('mapbox'); + var assertSelectedPoints = makeAssertSelectedPoints(); var fig = Lib.extendDeep({}, require('@mocks/mapbox_bubbles-text')); fig.layout.dragmode = 'select'; @@ -672,6 +763,7 @@ describe('Test select box and lasso per trace:', function() { function() { assertPoints([[30, 30]]); assertRanges([[21.99, 34.55], [38.14, 25.98]]); + assertSelectedPoints({0: [2]}); }, null, BOXEVENTS, 'scattermapbox select' ); @@ -684,6 +776,7 @@ describe('Test select box and lasso per trace:', function() { [[300, 200], [300, 300], [400, 300], [400, 200], [300, 200]], function() { assertPoints([[20, 20]]); + assertSelectedPoints({0: [1]}); assertLassoPoints([ [13.28, 25.97], [13.28, 14.33], [25.71, 14.33], [25.71, 25.97], [13.28, 25.97] ]); @@ -706,8 +799,10 @@ describe('Test select box and lasso per trace:', function() { it('should work on scattergeo traces', function(done) { var assertPoints = makeAssertPoints(['lon', 'lat']); + var assertSelectedPoints = makeAssertSelectedPoints(); var assertRanges = makeAssertRanges('geo'); var assertLassoPoints = makeAssertLassoPoints('geo'); + var fig = { data: [{ type: 'scattergeo', @@ -733,6 +828,7 @@ describe('Test select box and lasso per trace:', function() { [[350, 200], [450, 400]], function() { assertPoints([[10, 10], [20, 20], [-10, 10], [-20, 20]]); + assertSelectedPoints({0: [0, 1], 1: [0, 1]}); assertRanges([[-28.13, 61.88], [28.13, -50.64]]); }, null, BOXEVENTS, 'scattergeo select' @@ -746,6 +842,7 @@ describe('Test select box and lasso per trace:', function() { [[300, 200], [300, 300], [400, 300], [400, 200], [300, 200]], function() { assertPoints([[-10, 10], [-20, 20], [-30, 30]]); + assertSelectedPoints({0: [], 1: [0, 1, 2]}); assertLassoPoints([ [-56.25, 61.88], [-56.24, 5.63], [0, 5.63], [0, 61.88], [-56.25, 61.88] ]); @@ -753,10 +850,7 @@ describe('Test select box and lasso per trace:', function() { null, LASSOEVENTS, 'scattergeo lasso' ); }) - // .then(deselectPromise) .then(function() { - // assertEventCounts(4, 2, 1, 'de-lasso'); - // make sure selection handlers don't get called in 'pan' dragmode return Plotly.relayout(gd, 'dragmode', 'pan'); }) @@ -771,6 +865,7 @@ describe('Test select box and lasso per trace:', function() { it('should work on choropleth traces', function(done) { var assertPoints = makeAssertPoints(['location', 'z']); + var assertSelectedPoints = makeAssertSelectedPoints(); var assertRanges = makeAssertRanges('geo', -0.5); var assertLassoPoints = makeAssertLassoPoints('geo', -0.5); @@ -794,6 +889,7 @@ describe('Test select box and lasso per trace:', function() { [[350, 200], [400, 250]], function() { assertPoints([['GBR', 26.507354205352502], ['IRL', 86.4125147625692]]); + assertSelectedPoints({0: [54, 68]}); assertRanges([[-19.11, 63.06], [7.31, 53.72]]); }, [280, 190], @@ -808,6 +904,7 @@ describe('Test select box and lasso per trace:', function() { [[350, 200], [400, 200], [400, 250], [350, 250], [350, 200]], function() { assertPoints([['GBR', 26.507354205352502], ['IRL', 86.4125147625692]]); + assertSelectedPoints({0: [54, 68]}); assertLassoPoints([ [-19.11, 63.06], [5.50, 65.25], [7.31, 53.72], [-12.90, 51.70], [-19.11, 63.06] ]); @@ -831,6 +928,7 @@ describe('Test select box and lasso per trace:', function() { it('should work for bar traces', function(done) { var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']); + var assertSelectedPoints = makeAssertSelectedPoints(); var assertRanges = makeAssertRanges(); var assertLassoPoints = makeAssertLassoPoints(); @@ -852,6 +950,11 @@ describe('Test select box and lasso per trace:', function() { [2, 4.9, 0.473], [2, 5, 0.368], [2, 5.1, 0.258], [2, 5.2, 0.146], [2, 5.3, 0.036] ]); + assertSelectedPoints({ + 0: [49, 50, 51, 52, 53, 54, 55, 56, 57], + 1: [51, 52, 53, 54, 55, 56], + 2: [49, 50, 51, 52, 53] + }); assertLassoPoints([ [4.87, 5.74, 5.74, 4.87, 4.87], [0.53, 0.53, -0.02, -0.02, 0.53] @@ -863,6 +966,16 @@ describe('Test select box and lasso per trace:', function() { .then(function() { return Plotly.relayout(gd, 'dragmode', 'select'); }) + .then(function() { + // For some reason we need this to make the following tests pass + // on CI consistently. It appears that a double-click action + // is being confused with a mere click. See + // https://github.com/plotly/plotly.js/pull/2135#discussion_r148897529 + // for more info. + return new Promise(function(resolve) { + setTimeout(resolve, 100); + }); + }) .then(function() { return _run( [[350, 200], [370, 220]], @@ -872,6 +985,11 @@ describe('Test select box and lasso per trace:', function() { [1, 5.1, 0.485], [1, 5.2, 0.41], [2, 4.9, 0.473], [2, 5, 0.37] ]); + assertSelectedPoints({ + 0: [49, 50, 51, 52], + 1: [51, 52], + 2: [49, 50] + }); assertRanges([[4.87, 5.22], [0.31, 0.53]]); }, null, BOXEVENTS, 'bar select' @@ -883,6 +1001,7 @@ describe('Test select box and lasso per trace:', function() { it('should work for date/category traces', function(done) { var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']); + var assertSelectedPoints = makeAssertSelectedPoints(); var fig = { data: [{ @@ -915,6 +1034,7 @@ describe('Test select box and lasso per trace:', function() { [0, '2017-02-01', 'b'], [1, '2017-02-02', 'b'] ]); + assertSelectedPoints({0: [1], 1: [1]}); }, null, LASSOEVENTS, 'date/category lasso' ); @@ -930,6 +1050,7 @@ describe('Test select box and lasso per trace:', function() { [0, '2017-02-01', 'b'], [1, '2017-02-02', 'b'] ]); + assertSelectedPoints({0: [1], 1: [1]}); }, null, BOXEVENTS, 'date/category select' ); @@ -939,7 +1060,8 @@ describe('Test select box and lasso per trace:', function() { }); it('should work for histogram traces', function(done) { - var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y']); + var assertPoints = makeAssertPoints(['curveNumber', 'x', 'y', 'pointIndices']); + var assertSelectedPoints = makeAssertSelectedPoints(); var assertRanges = makeAssertRanges(); var assertLassoPoints = makeAssertLassoPoints(); @@ -955,8 +1077,9 @@ describe('Test select box and lasso per trace:', function() { [[200, 200], [400, 200], [400, 350], [200, 350], [200, 200]], function() { assertPoints([ - [0, 1.8, 2], [1, 2.2, 1], [1, 3.2, 1] + [0, 1.8, 2, [3, 4]], [1, 2.2, 1, [1]], [1, 3.2, 1, [2]] ]); + assertSelectedPoints({0: [3, 4], 1: [1, 2]}); assertLassoPoints([ [1.66, 3.59, 3.59, 1.66, 1.66], [2.17, 2.17, 0.69, 0.69, 2.17] @@ -973,8 +1096,9 @@ describe('Test select box and lasso per trace:', function() { [[200, 200], [400, 350]], function() { assertPoints([ - [0, 1.8, 2], [1, 2.2, 1], [1, 3.2, 1] + [0, 1.8, 2, [3, 4]], [1, 2.2, 1, [1]], [1, 3.2, 1, [2]] ]); + assertSelectedPoints({0: [3, 4], 1: [1, 2]}); assertRanges([[1.66, 3.59], [0.69, 2.17]]); }, null, BOXEVENTS, 'histogram select' @@ -986,6 +1110,7 @@ describe('Test select box and lasso per trace:', function() { it('should work for box traces', function(done) { var assertPoints = makeAssertPoints(['curveNumber', 'y', 'x']); + var assertSelectedPoints = makeAssertSelectedPoints(); var assertRanges = makeAssertRanges(); var assertLassoPoints = makeAssertLassoPoints(); @@ -1008,6 +1133,11 @@ describe('Test select box and lasso per trace:', function() { [1, 0.2, 'day 2'], [1, 0.5, 'day 2'], [1, 0.7, 'day 2'], [1, 0.7, 'day 2'], [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'] ]); + assertSelectedPoints({ + 0: [6, 11, 10, 7], + 1: [11, 8, 6, 10], + 2: [1, 4, 5] + }); assertLassoPoints([ ['day 1', 'day 2', 'day 2', 'day 1', 'day 1'], [0.71, 0.71, 0.1875, 0.1875, 0.71] @@ -1028,6 +1158,11 @@ describe('Test select box and lasso per trace:', function() { [1, 0.2, 'day 2'], [1, 0.5, 'day 2'], [1, 0.7, 'day 2'], [1, 0.7, 'day 2'], [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'] ]); + assertSelectedPoints({ + 0: [6, 11, 10, 7], + 1: [11, 8, 6, 10], + 2: [1, 4, 5] + }); assertRanges([['day 1', 'day 2'], [0.1875, 0.71]]); }, null, BOXEVENTS, 'box select' @@ -1039,6 +1174,7 @@ describe('Test select box and lasso per trace:', function() { it('should work for violin traces', function(done) { var assertPoints = makeAssertPoints(['curveNumber', 'y', 'x']); + var assertSelectedPoints = makeAssertSelectedPoints(); var assertRanges = makeAssertRanges(); var assertLassoPoints = makeAssertLassoPoints(); @@ -1059,6 +1195,11 @@ describe('Test select box and lasso per trace:', function() { [1, 0.9, 'day 2'], [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'], [2, 0.9, 'day 1'] ]); + assertSelectedPoints({ + 0: [11, 10, 7, 8], + 1: [8, 6, 10, 9, 7], + 2: [1, 4, 5, 3] + }); assertLassoPoints([ ['day 1', 'day 2', 'day 2', 'day 1', 'day 1'], [1.02, 1.02, 0.27, 0.27, 1.02] @@ -1080,6 +1221,11 @@ describe('Test select box and lasso per trace:', function() { [1, 0.9, 'day 2'], [2, 0.3, 'day 1'], [2, 0.6, 'day 1'], [2, 0.6, 'day 1'], [2, 0.9, 'day 1'] ]); + assertSelectedPoints({ + 0: [11, 10, 7, 8], + 1: [8, 6, 10, 9, 7], + 2: [1, 4, 5, 3] + }); assertRanges([['day 1', 'day 2'], [0.27, 1.02]]); }, null, BOXEVENTS, 'violin select' @@ -1088,6 +1234,194 @@ describe('Test select box and lasso per trace:', function() { .catch(fail) .then(done); }); + + it('should work on traces with enabled transforms', function(done) { + var assertSelectedPoints = makeAssertSelectedPoints(); + + Plotly.plot(gd, [{ + x: [1, 2, 3, 4, 5], + y: [2, 3, 1, 7, 9], + marker: {size: [10, 20, 20, 20, 10]}, + transforms: [{ + type: 'filter', + operation: '>', + value: 2, + target: 'y' + }, { + type: 'aggregate', + groups: 'marker.size', + aggregations: [ + // 20: 6, 10: 5 + {target: 'x', func: 'sum'}, + // 20: 5, 10: 9 + {target: 'y', func: 'avg'} + ] + }] + }], { + dragmode: 'select', + showlegend: false, + width: 400, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0} + }) + .then(function() { + return _run( + [[5, 5], [395, 395]], + function() { + assertSelectedPoints({0: [1, 3, 4]}); + }, + [380, 180], + BOXEVENTS, 'transformed trace select (all points selected)' + ); + }) + .catch(fail) + .then(done); + }); +}); + +describe('Test that selections persist:', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + function assertPtOpacity(selector, expected) { + d3.selectAll(selector).each(function(_, i) { + var style = Number(this.style.opacity); + expect(style).toBe(expected.style[i], 'style for pt ' + i); + }); + } + + it('should persist for scatter', function(done) { + function _assert(expected) { + var selected = gd.calcdata[0].map(function(d) { return d.selected; }); + expect(selected).toBeCloseToArray(expected.selected, 'selected vals'); + assertPtOpacity('.point', expected); + } + + Plotly.plot(gd, [{ + x: [1, 2, 3], + y: [1, 2, 1] + }], { + dragmode: 'select', + width: 400, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0} + }) + .then(function() { + resetEvents(gd); + drag([[5, 5], [250, 350]]); + return selectedPromise; + }) + .then(function() { + _assert({ + selected: [0, 1, 0], + style: [0.2, 1, 0.2] + }); + + // trigger a recalc + Plotly.restyle(gd, 'x', [[10, 20, 30]]); + }) + .then(function() { + _assert({ + selected: [undefined, 1, undefined], + style: [0.2, 1, 0.2] + }); + }) + .catch(fail) + .then(done); + }); + + it('should persist for box', function(done) { + function _assert(expected) { + var selected = gd.calcdata[0][0].pts.map(function(d) { return d.selected; }); + expect(selected).toBeCloseToArray(expected.cd, 'selected calcdata vals'); + expect(gd.data[0].selectedpoints).toBeCloseToArray(expected.selectedpoints, 'selectedpoints array'); + assertPtOpacity('.point', expected); + } + + Plotly.plot(gd, [{ + type: 'box', + x0: 0, + y: [5, 4, 4, 1, 2, 2, 2, 2, 2, 3, 3, 3], + boxpoints: 'all' + }], { + dragmode: 'select', + width: 400, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0} + }) + .then(function() { + resetEvents(gd); + drag([[5, 5], [400, 150]]); + return selectedPromise; + }) + .then(function() { + _assert({ + // N.B. pts in calcdata are sorted + cd: [0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1], + selectedpoints: [1, 2, 0], + style: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 1, 1, 1], + }); + + // trigger a recalc + Plotly.restyle(gd, 'x0', 1); + }) + .then(function() { + _assert({ + cd: [undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 1, 1, 1], + selectedpoints: [1, 2, 0], + style: [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 1, 1, 1], + }); + }) + .catch(fail) + .then(done); + }); + + it('should persist for histogram', function(done) { + function _assert(expected) { + var selected = gd.calcdata[0].map(function(d) { return d.selected; }); + expect(selected).toBeCloseToArray(expected.selected, 'selected vals'); + assertPtOpacity('.point > path', expected); + } + + Plotly.plot(gd, [{ + type: 'histogram', + x: [1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5], + boxpoints: 'all' + }], { + dragmode: 'select', + width: 400, + height: 400, + margin: {l: 0, t: 0, r: 0, b: 0} + }) + .then(function() { + resetEvents(gd); + drag([[5, 5], [400, 150]]); + return selectedPromise; + }) + .then(function() { + _assert({ + selected: [0, 1, 0, 0, 0], + style: [0.2, 1, 0.2, 0.2, 0.2], + }); + + // trigger a recalc + Plotly.restyle(gd, 'histfunc', 'sum'); + }) + .then(done) + .then(function() { + _assert({ + selected: [undefined, 1, undefined, undefined, undefined], + style: [0.2, 1, 0.2, 0.2, 0.2], + }); + }) + .catch(fail) + .then(done); + }); }); // to make sure none of the above tests fail with extraneous invisible traces, diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js index 45a4ee75c86..5715e48499a 100644 --- a/test/jasmine/tests/ternary_test.js +++ b/test/jasmine/tests/ternary_test.js @@ -184,8 +184,8 @@ describe('ternary plots', function() { mouseEvent('mousemove', pointPos[0], pointPos[1]); expect(hoverData).not.toBe(undefined, 'firing on data points'); expect(Object.keys(hoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis', 'a', 'b', 'c' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'xaxis', 'yaxis', 'a', 'b', 'c' ], 'returning the correct event data keys'); expect(hoverData.curveNumber).toEqual(0, 'returning the correct curve number'); expect(hoverData.pointNumber).toEqual(0, 'returning the correct point number'); @@ -193,8 +193,8 @@ describe('ternary plots', function() { mouseEvent('mouseout', pointPos[0], pointPos[1]); expect(unhoverData).not.toBe(undefined, 'firing on data points'); expect(Object.keys(unhoverData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis', 'a', 'b', 'c' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'xaxis', 'yaxis', 'a', 'b', 'c' ], 'returning the correct event data keys'); expect(unhoverData.curveNumber).toEqual(0, 'returning the correct curve number'); expect(unhoverData.pointNumber).toEqual(0, 'returning the correct point number'); @@ -216,8 +216,8 @@ describe('ternary plots', function() { click(pointPos[0], pointPos[1]); expect(ptData).not.toBe(undefined, 'firing on data points'); expect(Object.keys(ptData)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', - 'x', 'y', 'xaxis', 'yaxis', 'a', 'b', 'c' + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', + 'xaxis', 'yaxis', 'a', 'b', 'c' ], 'returning the correct event data keys'); expect(ptData.curveNumber).toEqual(0, 'returning the correct curve number'); expect(ptData.pointNumber).toEqual(0, 'returning the correct point number'); @@ -491,7 +491,7 @@ describe('Test event property of interactions on a ternary plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'x', 'y', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'xaxis', 'yaxis', 'a', 'b', 'c' ]); @@ -499,13 +499,11 @@ describe('Test event property of interactions on a ternary plot:', function() { expect(typeof pt.data).toEqual(typeof {}, 'points[0].data'); expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData'); expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber'); - expect(pt.x).toEqual(undefined, 'points[0].x'); - expect(pt.y).toEqual(undefined, 'points[0].y'); expect(typeof pt.xaxis).toEqual(typeof {}, 'points[0].xaxis'); expect(typeof pt.yaxis).toEqual(typeof {}, 'points[0].yaxis'); - expect(pt.a).toEqual(2, 'points[0].a'); - expect(pt.b).toEqual(1, 'points[0].b'); - expect(pt.c).toEqual(1, 'points[0].c'); + expect(pt.a).toEqual(0.5, 'points[0].a'); + expect(pt.b).toEqual(0.25, 'points[0].b'); + expect(pt.c).toEqual(0.25, 'points[0].c'); expect(evt.clientX).toEqual(pointPos[0], 'event.clientX'); expect(evt.clientY).toEqual(pointPos[1], 'event.clientY'); @@ -541,7 +539,7 @@ describe('Test event property of interactions on a ternary plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'x', 'y', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'xaxis', 'yaxis', 'a', 'b', 'c' ]); @@ -549,13 +547,11 @@ describe('Test event property of interactions on a ternary plot:', function() { expect(typeof pt.data).toEqual(typeof {}, 'points[0].data'); expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData'); expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber'); - expect(pt.x).toEqual(undefined, 'points[0].x'); - expect(pt.y).toEqual(undefined, 'points[0].y'); expect(typeof pt.xaxis).toEqual(typeof {}, 'points[0].xaxis'); expect(typeof pt.yaxis).toEqual(typeof {}, 'points[0].yaxis'); - expect(pt.a).toEqual(2, 'points[0].a'); - expect(pt.b).toEqual(1, 'points[0].b'); - expect(pt.c).toEqual(1, 'points[0].c'); + expect(pt.a).toEqual(0.5, 'points[0].a'); + expect(pt.b).toEqual(0.25, 'points[0].b'); + expect(pt.c).toEqual(0.25, 'points[0].c'); expect(evt.clientX).toEqual(pointPos[0], 'event.clientX'); expect(evt.clientY).toEqual(pointPos[1], 'event.clientY'); @@ -588,7 +584,7 @@ describe('Test event property of interactions on a ternary plot:', function() { yvals0 = futureData.yvals[0]; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'x', 'y', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'xaxis', 'yaxis', 'a', 'b', 'c' ]); @@ -596,13 +592,11 @@ describe('Test event property of interactions on a ternary plot:', function() { expect(typeof pt.data).toEqual(typeof {}, 'points[0].data'); expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData'); expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber'); - expect(pt.x).toEqual(undefined, 'points[0].x'); - expect(pt.y).toEqual(undefined, 'points[0].y'); expect(typeof pt.xaxis).toEqual(typeof {}, 'points[0].xaxis'); expect(typeof pt.yaxis).toEqual(typeof {}, 'points[0].yaxis'); - expect(pt.a).toEqual(2, 'points[0].a'); - expect(pt.b).toEqual(1, 'points[0].b'); - expect(pt.c).toEqual(1, 'points[0].c'); + expect(pt.a).toEqual(0.5, 'points[0].a'); + expect(pt.b).toEqual(0.25, 'points[0].b'); + expect(pt.c).toEqual(0.25, 'points[0].c'); expect(xaxes0).toEqual(pt.xaxis, 'xaxes[0]'); expect(xvals0).toEqual(-0.0016654247744483342, 'xaxes[0]'); @@ -634,7 +628,7 @@ describe('Test event property of interactions on a ternary plot:', function() { evt = futureData.event; expect(Object.keys(pt)).toEqual([ - 'data', 'fullData', 'curveNumber', 'pointNumber', 'x', 'y', + 'data', 'fullData', 'curveNumber', 'pointNumber', 'pointIndex', 'xaxis', 'yaxis', 'a', 'b', 'c' ]); @@ -642,13 +636,11 @@ describe('Test event property of interactions on a ternary plot:', function() { expect(typeof pt.data).toEqual(typeof {}, 'points[0].data'); expect(typeof pt.fullData).toEqual(typeof {}, 'points[0].fullData'); expect(pt.pointNumber).toEqual(0, 'points[0].pointNumber'); - expect(pt.x).toEqual(undefined, 'points[0].x'); - expect(pt.y).toEqual(undefined, 'points[0].y'); expect(typeof pt.xaxis).toEqual(typeof {}, 'points[0].xaxis'); expect(typeof pt.yaxis).toEqual(typeof {}, 'points[0].yaxis'); - expect(pt.a).toEqual(2, 'points[0].a'); - expect(pt.b).toEqual(1, 'points[0].b'); - expect(pt.c).toEqual(1, 'points[0].c'); + expect(pt.a).toEqual(0.5, 'points[0].a'); + expect(pt.b).toEqual(0.25, 'points[0].b'); + expect(pt.c).toEqual(0.25, 'points[0].c'); expect(evt.clientX).toEqual(pointPos[0], 'event.clientX'); expect(evt.clientY).toEqual(pointPos[1], 'event.clientY');