From e008c0bdb80512fb35f5a1bdcd960dacc42c9619 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Wed, 18 May 2022 10:19:44 -0400 Subject: [PATCH 001/105] move selection code into components/selections --- src/components/selections/constants.js | 15 +++++++++++++++ .../selections}/handle_outline.js | 0 .../selections}/helpers.js | 0 .../selections}/select.js | 18 +++++++++--------- src/components/shapes/draw.js | 2 +- .../shapes/draw_newshape/display_outlines.js | 2 +- src/components/shapes/draw_newshape/helpers.js | 2 +- .../shapes/draw_newshape/newshapes.js | 4 ++-- src/plot_api/plot_api.js | 2 +- src/plots/cartesian/constants.js | 12 ------------ src/plots/cartesian/dragbox.js | 6 +++--- src/plots/geo/geo.js | 6 +++--- src/plots/mapbox/mapbox.js | 8 ++++---- src/plots/plots.js | 2 +- src/plots/polar/polar.js | 6 +++--- src/plots/ternary/ternary.js | 8 ++++---- src/traces/sankey/base_plot.js | 2 +- 17 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 src/components/selections/constants.js rename src/{plots/cartesian => components/selections}/handle_outline.js (100%) rename src/{plots/cartesian => components/selections}/helpers.js (100%) rename src/{plots/cartesian => components/selections}/select.js (98%) diff --git a/src/components/selections/constants.js b/src/components/selections/constants.js new file mode 100644 index 00000000000..eec3d375259 --- /dev/null +++ b/src/components/selections/constants.js @@ -0,0 +1,15 @@ +'use strict'; + +module.exports = { + // max pixels off straight before a lasso select line counts as bent + BENDPX: 1.5, + + // smallest dimension allowed for a select box + MINSELECT: 12, + + // throttling limit (ms) for selectPoints calls + SELECTDELAY: 100, + + // cache ID suffix for throttle + SELECTID: '-select', +}; diff --git a/src/plots/cartesian/handle_outline.js b/src/components/selections/handle_outline.js similarity index 100% rename from src/plots/cartesian/handle_outline.js rename to src/components/selections/handle_outline.js diff --git a/src/plots/cartesian/helpers.js b/src/components/selections/helpers.js similarity index 100% rename from src/plots/cartesian/helpers.js rename to src/components/selections/helpers.js diff --git a/src/plots/cartesian/select.js b/src/components/selections/select.js similarity index 98% rename from src/plots/cartesian/select.js rename to src/components/selections/select.js index ebab65a69a8..832807535d0 100644 --- a/src/plots/cartesian/select.js +++ b/src/components/selections/select.js @@ -3,25 +3,25 @@ var polybool = require('polybooljs'); var Registry = require('../../registry'); -var dashStyle = require('../../components/drawing').dashStyle; -var Color = require('../../components/color'); -var Fx = require('../../components/fx'); -var makeEventData = require('../../components/fx/helpers').makeEventData; -var dragHelpers = require('../../components/dragelement/helpers'); +var dashStyle = require('../drawing').dashStyle; +var Color = require('../color'); +var Fx = require('../fx'); +var makeEventData = require('../fx/helpers').makeEventData; +var dragHelpers = require('../dragelement/helpers'); var freeMode = dragHelpers.freeMode; var rectMode = dragHelpers.rectMode; var drawMode = dragHelpers.drawMode; var openMode = dragHelpers.openMode; var selectMode = dragHelpers.selectMode; -var displayOutlines = require('../../components/shapes/draw_newshape/display_outlines'); -var handleEllipse = require('../../components/shapes/draw_newshape/helpers').handleEllipse; -var newShapes = require('../../components/shapes/draw_newshape/newshapes'); +var displayOutlines = require('../shapes/draw_newshape/display_outlines'); +var handleEllipse = require('../shapes/draw_newshape/helpers').handleEllipse; +var newShapes = require('../shapes/draw_newshape/newshapes'); var Lib = require('../../lib'); var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); -var getFromId = require('./axis_ids').getFromId; +var getFromId = require('../../plots/cartesian/axis_ids').getFromId; var clearGlCanvases = require('../../lib/clear_gl_canvases'); var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 06050ca72c5..f68258d4903 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -7,7 +7,7 @@ var Axes = require('../../plots/cartesian/axes'); var readPaths = require('./draw_newshape/helpers').readPaths; var displayOutlines = require('./draw_newshape/display_outlines'); -var clearOutlineControllers = require('../../plots/cartesian/handle_outline').clearOutlineControllers; +var clearOutlineControllers = require('../selections/handle_outline').clearOutlineControllers; var Color = require('../color'); var Drawing = require('../drawing'); diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 4d855b3bc51..bb49cbbdce0 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -12,7 +12,7 @@ var i090 = constants.i090; var i180 = constants.i180; var i270 = constants.i270; -var handleOutline = require('../../../plots/cartesian/handle_outline'); +var handleOutline = require('../../selections/handle_outline'); var clearOutlineControllers = handleOutline.clearOutlineControllers; var helpers = require('./helpers'); diff --git a/src/components/shapes/draw_newshape/helpers.js b/src/components/shapes/draw_newshape/helpers.js index 9e3a6f34fc3..8f3b8b09637 100644 --- a/src/components/shapes/draw_newshape/helpers.js +++ b/src/components/shapes/draw_newshape/helpers.js @@ -6,7 +6,7 @@ var constants = require('./constants'); var CIRCLE_SIDES = constants.CIRCLE_SIDES; var SQRT2 = constants.SQRT2; -var cartesianHelpers = require('../../../plots/cartesian/helpers'); +var cartesianHelpers = require('../../selections/helpers'); var p2r = cartesianHelpers.p2r; var r2p = cartesianHelpers.r2p; diff --git a/src/components/shapes/draw_newshape/newshapes.js b/src/components/shapes/draw_newshape/newshapes.js index 8934e2a5cb7..19ba2f162c8 100644 --- a/src/components/shapes/draw_newshape/newshapes.js +++ b/src/components/shapes/draw_newshape/newshapes.js @@ -12,11 +12,11 @@ var i270 = constants.i270; var cos45 = constants.cos45; var sin45 = constants.sin45; -var cartesianHelpers = require('../../../plots/cartesian/helpers'); +var cartesianHelpers = require('../../selections/helpers'); var p2r = cartesianHelpers.p2r; var r2p = cartesianHelpers.r2p; -var handleOutline = require('../../../plots/cartesian/handle_outline'); +var handleOutline = require('../../selections/handle_outline'); var clearSelect = handleOutline.clearSelect; var helpers = require('./helpers'); diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 727ef1c9238..b4793be66a7 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -19,7 +19,7 @@ var Drawing = require('../components/drawing'); var Color = require('../components/color'); var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); -var clearSelect = require('../plots/cartesian/select').clearSelect; +var clearSelect = require('../components/selections/select').clearSelect; var dfltConfig = require('./plot_config').dfltConfig; var manageArrays = require('./manage_arrays'); diff --git a/src/plots/cartesian/constants.js b/src/plots/cartesian/constants.js index dacdf300b61..f741ecd5599 100644 --- a/src/plots/cartesian/constants.js +++ b/src/plots/cartesian/constants.js @@ -29,27 +29,15 @@ module.exports = { // pixels to move mouse before you stop clamping to starting point MINDRAG: 8, - // smallest dimension allowed for a select box - MINSELECT: 12, - // smallest dimension allowed for a zoombox MINZOOM: 20, // width of axis drag regions DRAGGERSIZE: 20, - // max pixels off straight before a lasso select line counts as bent - BENDPX: 1.5, - // delay before a redraw (relayout) after smooth panning and zooming REDRAWDELAY: 50, - // throttling limit (ms) for selectPoints calls - SELECTDELAY: 100, - - // cache ID suffix for throttle - SELECTID: '-select', - // last resort axis ranges for x and y axes if we have no data DFLTRANGEX: [-1, 6], DFLTRANGEY: [-1, 4], diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index b12489e399c..bfab70745f0 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -26,9 +26,9 @@ var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var Plots = require('../plots'); var getFromId = require('./axis_ids').getFromId; -var prepSelect = require('./select').prepSelect; -var clearSelect = require('./select').clearSelect; -var selectOnClick = require('./select').selectOnClick; +var prepSelect = require('../../components/selections/select').prepSelect; +var clearSelect = require('../../components/selections/select').clearSelect; +var selectOnClick = require('../../components/selections/select').selectOnClick; var scaleZoom = require('./scale_zoom'); var constants = require('./constants'); diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 1a147f9b128..666d0222753 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -18,9 +18,9 @@ var Plots = require('../plots'); var Axes = require('../cartesian/axes'); var getAutoRange = require('../cartesian/autorange').getAutoRange; var dragElement = require('../../components/dragelement'); -var prepSelect = require('../cartesian/select').prepSelect; -var clearSelect = require('../cartesian/select').clearSelect; -var selectOnClick = require('../cartesian/select').selectOnClick; +var prepSelect = require('../../components/selections/select').prepSelect; +var clearSelect = require('../../components/selections/select').clearSelect; +var selectOnClick = require('../../components/selections/select').selectOnClick; var createGeoZoom = require('./zoom'); var constants = require('./constants'); diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index ad728f3fd42..030f4ec288c 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -14,10 +14,10 @@ var rectMode = dragHelpers.rectMode; var drawMode = dragHelpers.drawMode; var selectMode = dragHelpers.selectMode; -var prepSelect = require('../cartesian/select').prepSelect; -var clearSelect = require('../cartesian/select').clearSelect; -var clearSelectionsCache = require('../cartesian/select').clearSelectionsCache; -var selectOnClick = require('../cartesian/select').selectOnClick; +var prepSelect = require('../../components/selections/select').prepSelect; +var clearSelect = require('../../components/selections/select').clearSelect; +var clearSelectionsCache = require('../../components/selections/select').clearSelectionsCache; +var selectOnClick = require('../../components/selections/select').selectOnClick; var constants = require('./constants'); var createMapboxLayer = require('./layers'); diff --git a/src/plots/plots.js b/src/plots/plots.js index cda76cf8f37..d76e66d63d3 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -13,7 +13,7 @@ var Color = require('../components/color'); var BADNUM = require('../constants/numerical').BADNUM; var axisIDs = require('./cartesian/axis_ids'); -var clearSelect = require('./cartesian/handle_outline').clearSelect; +var clearSelect = require('../components/selections/handle_outline').clearSelect; var animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 5c359a8a15f..6e65e70505b 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -18,9 +18,9 @@ var dragBox = require('../cartesian/dragbox'); var dragElement = require('../../components/dragelement'); var Fx = require('../../components/fx'); var Titles = require('../../components/titles'); -var prepSelect = require('../cartesian/select').prepSelect; -var selectOnClick = require('../cartesian/select').selectOnClick; -var clearSelect = require('../cartesian/select').clearSelect; +var prepSelect = require('../../components/selections/select').prepSelect; +var selectOnClick = require('../../components/selections/select').selectOnClick; +var clearSelect = require('../../components/selections/select').clearSelect; var setCursor = require('../../lib/setcursor'); var clearGlCanvases = require('../../lib/clear_gl_canvases'); var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 004cc8ef202..80344982e23 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -19,10 +19,10 @@ var dragHelpers = require('../../components/dragelement/helpers'); var freeMode = dragHelpers.freeMode; var rectMode = dragHelpers.rectMode; var Titles = require('../../components/titles'); -var prepSelect = require('../cartesian/select').prepSelect; -var selectOnClick = require('../cartesian/select').selectOnClick; -var clearSelect = require('../cartesian/select').clearSelect; -var clearSelectionsCache = require('../cartesian/select').clearSelectionsCache; +var prepSelect = require('../../components/selections/select').prepSelect; +var selectOnClick = require('../../components/selections/select').selectOnClick; +var clearSelect = require('../../components/selections/select').clearSelect; +var clearSelectionsCache = require('../../components/selections/select').clearSelectionsCache; var constants = require('../cartesian/constants'); function Ternary(options, fullLayout) { diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index e9558289d8b..5befa698ea7 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -7,7 +7,7 @@ var fxAttrs = require('../../components/fx/layout_attributes'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../../components/dragelement'); -var prepSelect = require('../../plots/cartesian/select').prepSelect; +var prepSelect = require('../../components/selections/select').prepSelect; var Lib = require('../../lib'); var Registry = require('../../registry'); From c09a693d8689c66933c2935057c09d0a80634f77 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 09:45:04 -0400 Subject: [PATCH 002/105] add selections, newselection & activeselection --- src/components/selections/attributes.js | 84 +++++ src/components/selections/defaults.js | 81 +++++ src/components/selections/draw.js | 187 ++++++++++ .../draw_newselection/attributes.js | 54 +++ .../selections/draw_newselection/defaults.js | 12 + .../draw_newselection/newselections.js | 119 ++++++ src/components/selections/handle_outline.js | 2 +- src/components/selections/index.js | 23 ++ src/components/selections/select.js | 343 ++++++++++++++---- 9 files changed, 828 insertions(+), 77 deletions(-) create mode 100644 src/components/selections/attributes.js create mode 100644 src/components/selections/defaults.js create mode 100644 src/components/selections/draw.js create mode 100644 src/components/selections/draw_newselection/attributes.js create mode 100644 src/components/selections/draw_newselection/defaults.js create mode 100644 src/components/selections/draw_newselection/newselections.js create mode 100644 src/components/selections/index.js diff --git a/src/components/selections/attributes.js b/src/components/selections/attributes.js new file mode 100644 index 00000000000..d1a325752c5 --- /dev/null +++ b/src/components/selections/attributes.js @@ -0,0 +1,84 @@ +'use strict'; + +var annAttrs = require('../annotations/attributes'); +var scatterLineAttrs = require('../../traces/scatter/attributes').line; +var dash = require('../drawing/attributes').dash; +var extendFlat = require('../../lib/extend').extendFlat; +var overrideAll = require('../../plot_api/edit_types').overrideAll; +var templatedArray = require('../../plot_api/plot_template').templatedArray; +var axisPlaceableObjs = require('../../constants/axis_placeable_objects'); + +module.exports = overrideAll(templatedArray('selection', { + type: { + valType: 'enumerated', + values: ['rect', 'path'], + description: [ + 'Specifies the selection type to be drawn.', + + 'If *rect*, a rectangle is drawn linking', + '(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`) and (`x0`,`y1`).', + + 'If *path*, draw a custom SVG path using `path`.' + ].join(' ') + }, + + xref: extendFlat({}, annAttrs.xref, { + description: [ + 'Sets the selection\'s x coordinate axis.', + axisPlaceableObjs.axisRefDescription('x', 'left', 'right') + ].join(' ') + }), + + yref: extendFlat({}, annAttrs.yref, { + description: [ + 'Sets the selection\'s x coordinate axis.', + axisPlaceableObjs.axisRefDescription('y', 'bottom', 'top') + ].join(' ') + }), + + x0: { + valType: 'any', + description: 'Sets the selection\'s starting x position.' + }, + x1: { + valType: 'any', + description: 'Sets the selection\'s end x position.' + }, + + y0: { + valType: 'any', + description: 'Sets the selection\'s starting y position.' + }, + y1: { + valType: 'any', + description: 'Sets the selection\'s end y position.' + }, + + path: { + valType: 'string', + editType: 'arraydraw', + description: [ + 'For `type` *path* - a valid SVG path with the pixel values similar to `shapes.path`.' + ].join(' ') + }, + + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.7, + editType: 'arraydraw', + description: 'Sets the opacity of the selection.' + }, + + line: { + color: scatterLineAttrs.color, + width: extendFlat({}, scatterLineAttrs.width, { + min: 1, + dflt: 1 + }), + dash: extendFlat({}, dash, { + dflt: 'dot' + }) + }, +}), 'arraydraw', 'from-root'); diff --git a/src/components/selections/defaults.js b/src/components/selections/defaults.js new file mode 100644 index 00000000000..7ef8fae7817 --- /dev/null +++ b/src/components/selections/defaults.js @@ -0,0 +1,81 @@ +'use strict'; + +var Lib = require('../../lib'); +var Axes = require('../../plots/cartesian/axes'); +var handleArrayContainerDefaults = require('../../plots/array_container_defaults'); + +var attributes = require('./attributes'); +var helpers = require('../shapes/helpers'); + +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut) { + handleArrayContainerDefaults(layoutIn, layoutOut, { + name: 'selections', + handleItemDefaults: handleSelectionDefaults + }); +}; + +function handleSelectionDefaults(selectionIn, selectionOut, fullLayout) { + function coerce(attr, dflt) { + return Lib.coerce(selectionIn, selectionOut, attributes, attr, dflt); + } + + var path = coerce('path'); + var dfltType = path ? 'path' : 'rect'; + var selectionType = coerce('type', dfltType); + var noPath = selectionType !== 'path'; + if(noPath) delete selectionOut.path; + + coerce('opacity'); + coerce('line.color'); + coerce('line.width'); + coerce('line.dash'); + + // positioning + var axLetters = ['x', 'y']; + for(var i = 0; i < 2; i++) { + var axLetter = axLetters[i]; + var gdMock = {_fullLayout: fullLayout}; + var ax; + var pos2r; + var r2pos; + + // xref, yref + var axRef = Axes.coerceRef(selectionIn, selectionOut, gdMock, axLetter); + + // axRefType is 'range' for selections + ax = Axes.getFromId(gdMock, axRef); + ax._selectionIndices.push(selectionOut._index); + r2pos = helpers.rangeToShapePosition(ax); + pos2r = helpers.shapePositionToRange(ax); + + // Coerce x0, x1, y0, y1 + if(noPath) { + var dflt0 = 0.25; + var dflt1 = 0.75; + + // hack until V3.0 when log has regular range behavior - make it look like other + // ranges to send to coerce, then put it back after + // this is all to give reasonable default position behavior on log axes, which is + // a pretty unimportant edge case so we could just ignore this. + var attr0 = axLetter + '0'; + var attr1 = axLetter + '1'; + var in0 = selectionIn[attr0]; + var in1 = selectionIn[attr1]; + selectionIn[attr0] = pos2r(selectionIn[attr0], true); + selectionIn[attr1] = pos2r(selectionIn[attr1], true); + + Axes.coercePosition(selectionOut, gdMock, coerce, axRef, attr0, dflt0); + Axes.coercePosition(selectionOut, gdMock, coerce, axRef, attr1, dflt1); + + // hack part 2 + selectionOut[attr0] = r2pos(selectionOut[attr0]); + selectionOut[attr1] = r2pos(selectionOut[attr1]); + selectionIn[attr0] = in0; + selectionIn[attr1] = in1; + } + } // TODO: centralize. This is similar to shapes. + + if(noPath) { + Lib.noneOrAll(selectionIn, selectionOut, ['x0', 'x1', 'y0', 'y1']); + } +} diff --git a/src/components/selections/draw.js b/src/components/selections/draw.js new file mode 100644 index 00000000000..20a6234dcd7 --- /dev/null +++ b/src/components/selections/draw.js @@ -0,0 +1,187 @@ +'use strict'; + +var Registry = require('../../registry'); + +var readPaths = require('../shapes/draw_newshape/helpers').readPaths; +var displayOutlines = require('../shapes/draw_newshape/display_outlines'); + +var clearOutlineControllers = require('./handle_outline').clearOutlineControllers; + +var Color = require('../color'); +var Drawing = require('../drawing'); +var arrayEditor = require('../../plot_api/plot_template').arrayEditor; + +var helpers = require('../shapes/helpers'); +var getPathString = helpers.getPathString; + + +// Selections are stored in gd.layout.selections, an array of objects +// index can point to one item in this array, +// or non-numeric to simply add a new one +// or -1 to modify all existing +// opt can be the full options object, or one key (to be set to value) +// or undefined to simply redraw +// if opt is blank, val can be 'add' or a full options object to add a new +// annotation at that point in the array, or 'remove' to delete this one + +module.exports = { + draw: draw, + drawOne: drawOne, + eraseActiveSelection: eraseActiveSelection +}; + +function draw(gd) { + var fullLayout = gd._fullLayout; + + // Remove previous selections before drawing new selections in fullLayout.selections + fullLayout._selectionLayer.selectAll('path').remove(); + + for(var k in fullLayout._plots) { + var selectionLayer = fullLayout._plots[k].selectionLayer; + if(selectionLayer) selectionLayer.selectAll('path').remove(); + } + + for(var i = 0; i < fullLayout.selections.length; i++) { + drawOne(gd, i); + } +} + +function drawOne(gd, index) { + // remove the existing selection if there is one. + // because indices can change, we need to look in all selection layers + gd._fullLayout._paperdiv + .selectAll('.selectionlayer [data-index="' + index + '"]') + .remove(); + + var o = helpers.makeSelectionsOptionsAndPlotinfo(gd, index); + var options = o.options; + var plotinfo = o.plotinfo; + + // this selection is gone - quit now after deleting it + // TODO: use d3 idioms instead of deleting and redrawing every time + if(!options._input) return; + + drawSelection(gd._fullLayout._selectionLayer); + + function drawSelection(selectionLayer) { + var d = getPathString(gd, options); + var attrs = { + 'data-index': index, + 'fill-rule': options.fillrule, + d: d + }; + + var opacity = options.opacity; + var fillColor = 'rgba(0,0,0,0)'; + var lineColor = options.line.color || Color.contrast(gd._fullLayout.plot_bgcolor); + var lineWidth = options.line.width; + var lineDash = options.line.dash; + if(!lineWidth) { + // ensure invisible border to activate the selection + lineWidth = 5; + lineDash = 'solid'; + } + + var isOpen = d[d.length - 1] !== 'Z'; + + var isActiveSelection = + gd._fullLayout._activeSelectionIndex === index; + + if(isActiveSelection) { + fillColor = isOpen ? 'rgba(0,0,0,0)' : + gd._fullLayout.activeselection.fillcolor; + + opacity = gd._fullLayout.activeselection.opacity; + } + + var path = selectionLayer.append('path') + .attr(attrs) + .style('opacity', opacity) + .call(Color.stroke, lineColor) + .call(Color.fill, fillColor) + .call(Drawing.dashLine, lineDash, lineWidth); + + setClipPath(path, gd, options); + + if(isActiveSelection) { + var editHelpers = arrayEditor(gd.layout, 'selections', options); + + path.style({ + 'cursor': 'move', + }); + + var dragOptions = { + element: path.node(), + plotinfo: plotinfo, + gd: gd, + editHelpers: editHelpers, + isActiveSelection: true // i.e. to enable controllers + }; + + var polygons = readPaths(d, gd); + // display polygons on the screen + displayOutlines(polygons, path, dragOptions); + } else { + path.style('pointer-events', 'stroke'); + } + + path.node().addEventListener('click', function() { return activateSelection(gd, path); }); + } +} + +function setClipPath(selectionPath, gd, selectionOptions) { + var clipAxes = selectionOptions.xref + selectionOptions.yref; + + Drawing.setClipUrl( + selectionPath, + 'clip' + gd._fullLayout._uid + clipAxes, + gd + ); +} + + +function activateSelection(gd, path) { + var element = path.node(); + var id = +element.getAttribute('data-index'); + if(id >= 0) { + // deactivate if already active + if(id === gd._fullLayout._activeSelectionIndex) { + deactivateSelection(gd); + return; + } + + gd._fullLayout._activeSelectionIndex = id; + gd._fullLayout._deactivateSelection = deactivateSelection; + draw(gd); + } +} + +function deactivateSelection(gd) { + var id = gd._fullLayout._activeSelectionIndex; + if(id >= 0) { + clearOutlineControllers(gd); + delete gd._fullLayout._activeSelectionIndex; + draw(gd); + } +} + +function eraseActiveSelection(gd) { + clearOutlineControllers(gd); + + var id = gd._fullLayout._activeSelectionIndex; + var selections = (gd.layout || {}).selections || []; + if(id < selections.length) { + var newSelections = []; + for(var q = 0; q < selections.length; q++) { + if(q !== id) { + newSelections.push(selections[q]); + } + } + + delete gd._fullLayout._activeSelectionIndex; + + Registry.call('_guiRelayout', gd, { + selections: newSelections + }); + } +} diff --git a/src/components/selections/draw_newselection/attributes.js b/src/components/selections/draw_newselection/attributes.js new file mode 100644 index 00000000000..8eb01af8b7e --- /dev/null +++ b/src/components/selections/draw_newselection/attributes.js @@ -0,0 +1,54 @@ +'use strict'; + +var dash = require('../../drawing/attributes').dash; +var extendFlat = require('../../../lib/extend').extendFlat; + +module.exports = { + newselection: { + line: { + color: { + valType: 'color', + editType: 'none', + description: [ + 'Sets the line color.', + 'By default uses either dark grey or white', + 'to increase contrast with background color.' + ].join(' ') + }, + width: { + valType: 'number', + min: 1, + dflt: 1, + editType: 'none', + description: 'Sets the line width (in px).' + }, + dash: extendFlat({}, dash, { + dflt: 'dot', + editType: 'none' + }), + editType: 'none' + }, + + // no drawdirection here noting that layout.selectdirection is used instead. + + editType: 'none' + }, + + activeselection: { + fillcolor: { + valType: 'color', + dflt: '#7f7f7f', + editType: 'none', + description: 'Sets the color filling the active selection\' interior.' + }, + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.5, + editType: 'none', + description: 'Sets the opacity of the active selection.' + }, + editType: 'none' + } +}; diff --git a/src/components/selections/draw_newselection/defaults.js b/src/components/selections/draw_newselection/defaults.js new file mode 100644 index 00000000000..7cd9712a51c --- /dev/null +++ b/src/components/selections/draw_newselection/defaults.js @@ -0,0 +1,12 @@ +'use strict'; + +module.exports = function supplyDrawNewSelectionDefaults(layoutIn, layoutOut, coerce) { + var newselectionLineWidth = coerce('newselection.line.width'); + if(newselectionLineWidth) { + coerce('newselection.line.color'); + coerce('newselection.line.dash'); + } + + coerce('activeselection.fillcolor'); + coerce('activeselection.opacity'); +}; diff --git a/src/components/selections/draw_newselection/newselections.js b/src/components/selections/draw_newselection/newselections.js new file mode 100644 index 00000000000..478c2a8b1d0 --- /dev/null +++ b/src/components/selections/draw_newselection/newselections.js @@ -0,0 +1,119 @@ +'use strict'; + +var dragHelpers = require('../../dragelement/helpers'); +var selectMode = dragHelpers.selectMode; + +var handleOutline = require('../handle_outline'); +var clearSelect = handleOutline.clearSelect; + +var helpers = require('../../shapes/draw_newshape/helpers'); +var readPaths = helpers.readPaths; +var writePaths = helpers.writePaths; +var fixDatesForPaths = helpers.fixDatesForPaths; + +module.exports = function newSelections(outlines, dragOptions) { + if(!outlines.length) return; + var e = outlines[0][0]; // pick first + if(!e) return; + var d = e.getAttribute('d'); + + var gd = dragOptions.gd; + var newStyle = gd._fullLayout.newselection; + + var plotinfo = dragOptions.plotinfo; + var xaxis = plotinfo.xaxis; + var yaxis = plotinfo.yaxis; + + var isActiveSelection = dragOptions.isActiveSelection; + var dragmode = dragOptions.dragmode; + + var selections = (gd.layout || {}).selections || []; + + if(!selectMode(dragmode) && isActiveSelection !== undefined) { + var id = gd._fullLayout._activeSelectionIndex; + if(id < selections.length) { + switch(gd._fullLayout.selections[id].type) { + case 'rect': + dragmode = 'select'; + break; + case 'path': + dragmode = 'lasso'; + break; + } + } + } + + var polygons = readPaths(d, gd, plotinfo, isActiveSelection); + + var newSelection = { + xref: xaxis._id, + yref: yaxis._id, + + opacity: newStyle.opacity, + line: { + color: newStyle.line.color, + width: newStyle.line.width, + dash: newStyle.line.dash + } + }; + + var cell; + // rect can be in one cell + // only define cell if there is single cell + if(polygons.length === 1) cell = polygons[0]; + + if( + cell && + cell.length === 5 && // ensure we only have 4 corners for a rect + dragmode === 'select' + ) { + newSelection.type = 'rect'; + newSelection.x0 = cell[0][1]; + newSelection.y0 = cell[0][2]; + newSelection.x1 = cell[2][1]; + newSelection.y1 = cell[2][2]; + } else { + newSelection.type = 'path'; + if(xaxis && yaxis) fixDatesForPaths(polygons, xaxis, yaxis); + newSelection.path = writePaths(polygons); + cell = null; + } + + clearSelect(gd); + + var editHelpers = dragOptions.editHelpers; + var modifyItem = (editHelpers || {}).modifyItem; + + var allSelections = []; + for(var q = 0; q < selections.length; q++) { + var beforeEdit = gd._fullLayout.selections[q]; + allSelections[q] = beforeEdit._input; + + if( + isActiveSelection !== undefined && + q === gd._fullLayout._activeSelectionIndex + ) { + var afterEdit = newSelection; + + switch(beforeEdit.type) { + case 'rect': + modifyItem('x0', afterEdit.x0); + modifyItem('x1', afterEdit.x1); + modifyItem('y0', afterEdit.y0); + modifyItem('y1', afterEdit.y1); + break; + + case 'path': + modifyItem('path', afterEdit.path); + break; + } + } + } + + if(isActiveSelection === undefined) { + allSelections.push(newSelection); // add new selection + return allSelections; + } + + return editHelpers ? editHelpers.getUpdateObj() : {}; +}; diff --git a/src/components/selections/handle_outline.js b/src/components/selections/handle_outline.js index 23d39408dc9..9ce383d35aa 100644 --- a/src/components/selections/handle_outline.js +++ b/src/components/selections/handle_outline.js @@ -16,7 +16,7 @@ function clearSelect(gd) { zoomLayer.selectAll('.select-outline').remove(); } - gd._fullLayout._drawing = false; + gd._fullLayout._outlining = false; } module.exports = { diff --git a/src/components/selections/index.js b/src/components/selections/index.js new file mode 100644 index 00000000000..4323edae6e6 --- /dev/null +++ b/src/components/selections/index.js @@ -0,0 +1,23 @@ +'use strict'; + +var drawModule = require('./draw'); +var select = require('./select'); + +module.exports = { + moduleType: 'component', + name: 'selections', + + layoutAttributes: require('./attributes'), + supplyLayoutDefaults: require('./defaults'), + supplyDrawNewSelectionDefaults: require('./draw_newselection/defaults'), + includeBasePlot: require('../../plots/cartesian/include_components')('selections'), + + draw: drawModule.draw, + drawOne: drawModule.drawOne, + + reselect: select.reselect, + prepSelect: select.prepSelect, + clearSelect: select.clearSelect, + clearSelectionsCache: select.clearSelectionsCache, + selectOnClick: select.selectOnClick +}; diff --git a/src/components/selections/select.js b/src/components/selections/select.js index 832807535d0..ed58bfa0e14 100644 --- a/src/components/selections/select.js +++ b/src/components/selections/select.js @@ -14,10 +14,15 @@ var drawMode = dragHelpers.drawMode; var openMode = dragHelpers.openMode; var selectMode = dragHelpers.selectMode; +var shapeHelpers = require('../shapes/helpers'); +var shapeConstants = require('../shapes/constants'); + var displayOutlines = require('../shapes/draw_newshape/display_outlines'); var handleEllipse = require('../shapes/draw_newshape/helpers').handleEllipse; var newShapes = require('../shapes/draw_newshape/newshapes'); +var newSelections = require('./draw_newselection/newselections'); + var Lib = require('../../lib'); var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); @@ -39,7 +44,7 @@ var p2r = helpers.p2r; var axValue = helpers.axValue; var getTransform = helpers.getTransform; -function prepSelect(e, startX, startY, dragOptions, mode) { +function prepSelect(evt, startX, startY, dragOptions, mode) { var isFreeMode = freeMode(mode); var isRectMode = rectMode(mode); var isOpenMode = openMode(mode); @@ -69,35 +74,45 @@ function prepSelect(e, startX, startY, dragOptions, mode) { var x1 = x0; var y1 = y0; var path0 = 'M' + x0 + ',' + y0; - var pw = dragOptions.xaxes[0]._length; - var ph = dragOptions.yaxes[0]._length; + var xAxis = dragOptions.xaxes[0]; + var yAxis = dragOptions.yaxes[0]; + var pw = xAxis._length; + var ph = yAxis._length; + var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes); - var subtract = e.altKey && + var subtract = evt.altKey && !(drawMode(mode) && isOpenMode); - var filterPoly, selectionTester, mergedPolygons, currentPolygon; + var filterPoly, selectionTesters, mergedPolygons, currentPolygon; var i, searchInfo, eventData; - coerceSelectionsCache(e, gd, dragOptions); + coerceSelectionsCache(evt, gd, dragOptions); if(isFreeMode) { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); } var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data(isDrawMode ? [0] : [1, 2]); - var drwStyle = fullLayout.newshape; + var newStyle = isDrawMode ? + fullLayout.newshape : + fullLayout.newselection; outlines.enter() .append('path') .attr('class', function(d) { return 'select-outline select-outline-' + d + ' select-outline-' + plotinfo.id; }) - .style(isDrawMode ? { - opacity: drwStyle.opacity / 2, - fill: isOpenMode ? undefined : drwStyle.fillcolor, - stroke: drwStyle.line.color, - 'stroke-dasharray': dashStyle(drwStyle.line.dash, drwStyle.line.width), - 'stroke-width': drwStyle.line.width + 'px' - } : {}) - .attr('fill-rule', drwStyle.fillrule) + .style({ + opacity: isDrawMode ? newStyle.opacity / 2 : 1, + fill: (isDrawMode && !isOpenMode) ? newStyle.fillcolor : 'none', + stroke: newStyle.line.color || ( + dragOptions.subplot !== undefined ? + '#7f7f7f' : // non-cartesian subplot + Color.contrast(gd._fullLayout.plot_bgcolor) // cartesian subplot + ), + 'stroke-dasharray': dashStyle(newStyle.line.dash, newStyle.line.width), + 'stroke-width': newStyle.line.width + 'px', + 'shape-rendering': 'crispEdges' + }) + .attr('fill-rule', newStyle.fillrule) .classed('cursor-move', isDrawMode ? true : false) .attr('transform', transform) .attr('d', path0 + 'Z'); @@ -269,43 +284,28 @@ function prepSelect(e, startX, startY, dragOptions, mode) { // create outline & tester if(dragOptions.selectionDefs && dragOptions.selectionDefs.length) { mergedPolygons = mergePolygons(dragOptions.mergedPolygons, currentPolygon, subtract); + currentPolygon.subtract = subtract; - selectionTester = multiTester(dragOptions.selectionDefs.concat([currentPolygon])); + selectionTesters = multiTester(dragOptions.selectionDefs.concat([currentPolygon]), selectionTesters); } else { mergedPolygons = [currentPolygon]; - selectionTester = polygonTester(currentPolygon); + selectionTesters = polygonTester(currentPolygon); + selectionTesters.testers = [selectionTesters]; } // display polygons on the screen displayOutlines(convertPoly(mergedPolygons, isOpenMode), outlines, dragOptions); if(isSelectMode) { + selectionTesters = reselect(gd, xAxis._id, yAxis._id, selectionTesters, searchTraces); + throttle.throttle( throttleID, constants.SELECTDELAY, function() { - selection = []; - - var thisSelection; - var traceSelections = []; - var traceSelection; - for(i = 0; i < searchTraces.length; i++) { - searchInfo = searchTraces[i]; - - traceSelection = searchInfo._module.selectPoints(searchInfo, selectionTester); - traceSelections.push(traceSelection); - - thisSelection = fillSelectionItem(traceSelection, searchInfo); - - if(selection.length) { - for(var j = 0; j < thisSelection.length; j++) { - selection.push(thisSelection[j]); - } - } else selection = thisSelection; - } + selection = _doSelect(selectionTesters, searchTraces); eventData = {points: selection}; - updateSelectedState(gd, searchTraces, eventData); fillRangeItems(eventData, currentPolygon, filterPoly); dragOptions.gd.emit('plotly_selecting', eventData); } @@ -339,6 +339,30 @@ function prepSelect(e, startX, startY, dragOptions, mode) { clearSelectionsCache(dragOptions); gd.emit('plotly_deselect', null); + + if(searchTraces.length) { + var clickedXaxis = searchTraces[0].xaxis; + var clickedYaxis = searchTraces[0].yaxis; + + if(clickedXaxis && clickedYaxis) { + // drop selections in the clicked subplot + var newSelections = []; + var oldSelections = gd._fullLayout.selections; + for(var k = 0; k < oldSelections.length; k++) { + var s = oldSelections[k]; + if( + s.xref !== clickedXaxis._id || + s.yref !== clickedYaxis._id + ) { + newSelections.push(s); + } + } + + Registry.call('_guiRelayout', gd, { + selections: newSelections + }); + } + } } else { if(clickmode.indexOf('select') > -1) { selectOnClick(evt, gd, dragOptions.xaxes, dragOptions.yaxes, @@ -392,7 +416,7 @@ function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutli var clickmode = fullLayout.clickmode; var sendEvents = clickmode.indexOf('event') > -1; var selection = []; - var searchTraces, searchInfo, currentSelectionDef, selectionTester, traceSelection; + var searchTraces, searchInfo, currentSelectionDef, selectionTesters, traceSelection; var thisTracesSelection, pointOrBinSelected, subtract, eventData, i; if(isHoverDataSet(hoverData)) { @@ -430,10 +454,10 @@ function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutli currentSelectionDef = newPointSelectionDef(clickedPtInfo.pointNumber, clickedPtInfo.searchInfo, subtract); var allSelectionDefs = dragOptions.selectionDefs.concat([currentSelectionDef]); - selectionTester = multiTester(allSelectionDefs); + selectionTesters = multiTester(allSelectionDefs, selectionTesters); for(i = 0; i < searchTraces.length; i++) { - traceSelection = searchTraces[i]._module.selectPoints(searchTraces[i], selectionTester); + traceSelection = searchTraces[i]._module.selectPoints(searchTraces[i], selectionTesters); thisTracesSelection = fillSelectionItem(traceSelection, searchTraces[i]); if(selection.length) { @@ -511,8 +535,9 @@ function newPointNumTester(pointSelectionDef) { * that can be called to evaluate a point against all wrapped * selection testers that were passed in list. */ -function multiTester(list) { - var testers = []; +function multiTester(list, prevOut) { + if(!prevOut) prevOut = {}; + var testers = prevOut.testers || []; var xmin = isPointSelectionDef(list[0]) ? 0 : list[0][0][0]; var xmax = xmin; var ymin = isPointSelectionDef(list[0]) ? 0 : list[0][0][1]; @@ -547,7 +572,7 @@ function multiTester(list) { for(var i = 0; i < testers.length; i++) { if(testers[i].contains(pt, arg, pointNumber, searchInfo)) { // if contained by subtract tester - exclude the point - contained = testers[i].subtract === false; + contained = !testers[i].subtract; } } @@ -562,13 +587,12 @@ function multiTester(list) { pts: [], contains: contains, isRect: false, - degenerate: false + degenerate: false, + testers: testers }; } function coerceSelectionsCache(evt, gd, dragOptions) { - gd._fullLayout._drawing = false; - var fullLayout = gd._fullLayout; var plotinfo = dragOptions.plotinfo; var dragmode = dragOptions.dragmode; @@ -581,8 +605,13 @@ function coerceSelectionsCache(evt, gd, dragOptions) { var hasModifierKey = (evt.shiftKey || evt.altKey) && !(drawMode(dragmode) && openMode(dragmode)); - if(selectingOnSameSubplot && hasModifierKey && - (plotinfo.selection && plotinfo.selection.selectionDefs) && !dragOptions.selectionDefs) { + if( + selectingOnSameSubplot && + hasModifierKey && + plotinfo.selection && + plotinfo.selection.selectionDefs && + !dragOptions.selectionDefs + ) { // take over selection definitions from prev mode, if any dragOptions.selectionDefs = plotinfo.selection.selectionDefs; dragOptions.mergedPolygons = plotinfo.selection.mergedPolygons; @@ -605,22 +634,45 @@ function clearSelectionsCache(dragOptions) { if(gd._fullLayout._activeShapeIndex >= 0) { gd._fullLayout._deactivateShape(gd); } + if(gd._fullLayout._activeSelectionIndex >= 0) { + gd._fullLayout._deactivateSelection(gd); + } + + var fullLayout = gd._fullLayout; + var zoomLayer = fullLayout._zoomlayer; - if(drawMode(dragmode)) { - var fullLayout = gd._fullLayout; - var zoomLayer = fullLayout._zoomlayer; + var isDrawMode = drawMode(dragmode); + var isSelectMode = selectMode(dragmode); + if(isDrawMode || isSelectMode) { var outlines = zoomLayer.selectAll('.select-outline-' + plotinfo.id); - if(outlines && gd._fullLayout._drawing) { + if(outlines && gd._fullLayout._outlining) { // add shape - var shapes = newShapes(outlines, dragOptions); + var shapes; + if(isDrawMode) { + shapes = newShapes(outlines, dragOptions); + } if(shapes) { Registry.call('_guiRelayout', gd, { shapes: shapes }); } - gd._fullLayout._drawing = false; + // add selection + var selections; + if( + isSelectMode && + !dragOptions.subplot // only allow cartesian - no mapbox for now + ) { + selections = newSelections(outlines, dragOptions); + } + if(selections) { + Registry.call('_guiRelayout', gd, { + selections: selections + }); + } + + gd._fullLayout._outlining = false; } } @@ -630,6 +682,8 @@ function clearSelectionsCache(dragOptions) { } function determineSearchTraces(gd, xAxes, yAxes, subplot) { + if(!gd.calcdata) return []; + var searchTraces = []; var xAxisIds = xAxes.map(function(ax) { return ax._id; }); var yAxisIds = yAxes.map(function(ax) { return ax._id; }); @@ -666,15 +720,15 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) { } return searchTraces; +} - function createSearchInfo(module, calcData, xaxis, yaxis) { - return { - _module: module, - cd: calcData, - xaxis: xaxis, - yaxis: yaxis - }; - } +function createSearchInfo(module, calcData, xaxis, yaxis) { + return { + _module: module, + cd: calcData, + xaxis: xaxis, + yaxis: yaxis + }; } function isHoverDataSet(hoverData) { @@ -860,21 +914,11 @@ function updateSelectedState(gd, searchTraces, eventData) { } function mergePolygons(list, poly, subtract) { - var res; - - if(subtract) { - res = polybool.difference({ - regions: list, - inverted: false - }, { - regions: [poly], - inverted: false - }); - - return res.regions; - } + var fn = subtract ? + polybool.difference : + polybool.union; - res = polybool.union({ + var res = fn({ regions: list, inverted: false }, { @@ -924,7 +968,154 @@ function convertPoly(polygonsIn, isOpenMode) { // add M and L command to draft p return polygonsOut; } +function _doSelect(selectionTesters, searchTraces) { + var selection = []; + + var thisSelection; + var traceSelections = []; + var traceSelection; + for(var i = 0; i < searchTraces.length; i++) { + var searchInfo = searchTraces[i]; + + traceSelection = searchInfo._module.selectPoints(searchInfo, selectionTesters); + traceSelections.push(traceSelection); + + thisSelection = fillSelectionItem(traceSelection, searchInfo); + + if(selection.length) { + for(var j = 0; j < thisSelection.length; j++) { + selection.push(thisSelection[j]); + } + } else selection = thisSelection; + } + + return selection; +} + +function reselect(gd, xRef, yRef, selectionTesters, searchTraces) { + var hadSearchTraces = !!searchTraces; + + // select layout.selection polygons + var layoutPolygons = getLayoutPolygons(gd); + var i; + var subplots = (xRef && yRef) ? [xRef + yRef] : + gd._fullLayout._subplots.cartesian; + + for(i = 0; i < subplots.length; i++) { + var subplot = subplots[i]; + var yAt = subplot.indexOf('y'); + var _xRef = subplot.slice(0, yAt); + var _yRef = subplot.slice(yAt); + + var _selectionTesters = (xRef && yRef) ? selectionTesters : undefined; + _selectionTesters = addTester(layoutPolygons, _xRef, _yRef, _selectionTesters); + + var _searchTraces = searchTraces; + if(!hadSearchTraces) { + _searchTraces = determineSearchTraces( + gd, + [getFromId(gd, _xRef, 'x')], + [getFromId(gd, _yRef, 'y')], + subplot + ); + } + + if(_selectionTesters) { + var selection = _doSelect(_selectionTesters, _searchTraces); + + updateSelectedState(gd, _searchTraces, {points: selection}); + } + } + + return selectionTesters; +} + +function addTester(layoutPolygons, xRef, yRef, selectionTesters) { + for(var m = 0; m < layoutPolygons.length; m++) { + var p = layoutPolygons[m]; + if( + xRef === p.xref && + yRef === p.yref + ) { + selectionTesters = multiTester([p], selectionTesters); + } + } + + return selectionTesters; +} + +function getLayoutPolygons(gd) { + var allPolygons = []; + var allSelections = gd._fullLayout.selections; + var len = allSelections.length; + + for(var i = 0; i < len; i++) { + var selection = allSelections[i]; + + var xref = selection.xref; + var yref = selection.yref; + + var xaxis = getFromId(gd, xref, 'x'); + var yaxis = getFromId(gd, yref, 'y'); + + var xmin, xmax, ymin, ymax; + + var polygon = []; + if(selection.type === 'rect') { + var x0 = convert(xaxis, selection.x0); + var x1 = convert(xaxis, selection.x1); + var y0 = convert(yaxis, selection.y0); + var y1 = convert(yaxis, selection.y1); + polygon = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]; + + xmin = Math.min(x0, x1); + xmax = Math.max(x0, x1); + ymin = Math.min(y0, y1); + ymax = Math.max(y0, y1); + } else if(selection.type === 'path') { + var path = selection.path; + var allX = shapeHelpers.extractPathCoords(path, shapeConstants.paramIsX, 'raw'); + var allY = shapeHelpers.extractPathCoords(path, shapeConstants.paramIsY, 'raw'); + + xmin = Infinity; + xmax = -Infinity; + ymin = Infinity; + ymax = -Infinity; + + for(var q = 0; q < allX.length; q++) { + var x = convert(xaxis, allX[q]); + var y = convert(yaxis, allY[q]); + + polygon.push([x, y]); + + xmin = Math.min(x, xmin); + xmax = Math.max(x, xmax); + ymin = Math.min(y, ymin); + ymax = Math.max(y, ymax); + } + } + + polygon.xmin = xmin; + polygon.xmax = xmax; + polygon.ymin = ymin; + polygon.ymax = ymax; + + polygon.xref = xref; + polygon.yref = yref; + + allPolygons.push(polygon); + } + + return allPolygons; +} + +function convert(ax, d) { + if(ax.type === 'date') d = d.replace('_', ' '); + return ax.type === 'log' ? ax.c2p(d) : ax.r2p(d, null, ax.calendar); +} + module.exports = { + reselect: reselect, prepSelect: prepSelect, clearSelect: clearSelect, clearSelectionsCache: clearSelectionsCache, From 49c2d2865c274c50d6e7568029c0286e2754b4c9 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 09:47:09 -0400 Subject: [PATCH 003/105] adjustments in shapes for selections --- src/components/shapes/draw.js | 114 +------------- .../shapes/draw_newshape/display_outlines.js | 81 ++++++---- .../shapes/draw_newshape/helpers.js | 21 ++- .../shapes/draw_newshape/newshapes.js | 36 ++--- src/components/shapes/helpers.js | 140 +++++++++++++++++- 5 files changed, 219 insertions(+), 173 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index f68258d4903..ae1a2ba160f 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -18,6 +18,7 @@ var setCursor = require('../../lib/setcursor'); var constants = require('./constants'); var helpers = require('./helpers'); +var getPathString = helpers.getPathString; // Shapes are stored in gd.layout.shapes, an array of objects @@ -58,7 +59,7 @@ function draw(gd) { } function shouldSkipEdits(gd) { - return !!gd._fullLayout._drawing; + return !!gd._fullLayout._outlining; } function couldHaveActiveShape(gd) { @@ -73,7 +74,7 @@ function drawOne(gd, index) { .selectAll('.shapelayer [data-index="' + index + '"]') .remove(); - var o = helpers.makeOptionsAndPlotinfo(gd, index); + var o = helpers.makeShapesOptionsAndPlotinfo(gd, index); var options = o.options; var plotinfo = o.plotinfo; @@ -578,115 +579,6 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHe } } -function getPathString(gd, options) { - var type = options.type; - var xRefType = Axes.getRefType(options.xref); - var yRefType = Axes.getRefType(options.yref); - var xa = Axes.getFromId(gd, options.xref); - var ya = Axes.getFromId(gd, options.yref); - var gs = gd._fullLayout._size; - var x2r, x2p, y2r, y2p; - var x0, x1, y0, y1; - - if(xa) { - if(xRefType === 'domain') { - x2p = function(v) { return xa._offset + xa._length * v; }; - } else { - x2r = helpers.shapePositionToRange(xa); - x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; - } - } else { - x2p = function(v) { return gs.l + gs.w * v; }; - } - - if(ya) { - if(yRefType === 'domain') { - y2p = function(v) { return ya._offset + ya._length * (1 - v); }; - } else { - y2r = helpers.shapePositionToRange(ya); - y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; - } - } else { - y2p = function(v) { return gs.t + gs.h * (1 - v); }; - } - - if(type === 'path') { - if(xa && xa.type === 'date') x2p = helpers.decodeDate(x2p); - if(ya && ya.type === 'date') y2p = helpers.decodeDate(y2p); - return convertPath(options, x2p, y2p); - } - - if(options.xsizemode === 'pixel') { - var xAnchorPos = x2p(options.xanchor); - x0 = xAnchorPos + options.x0; - x1 = xAnchorPos + options.x1; - } else { - x0 = x2p(options.x0); - x1 = x2p(options.x1); - } - - if(options.ysizemode === 'pixel') { - var yAnchorPos = y2p(options.yanchor); - y0 = yAnchorPos - options.y0; - y1 = yAnchorPos - options.y1; - } else { - y0 = y2p(options.y0); - y1 = y2p(options.y1); - } - - if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1; - if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z'; - - // circle - var cx = (x0 + x1) / 2; - var cy = (y0 + y1) / 2; - var rx = Math.abs(cx - x0); - var ry = Math.abs(cy - y0); - var rArc = 'A' + rx + ',' + ry; - var rightPt = (cx + rx) + ',' + cy; - var topPt = cx + ',' + (cy - ry); - return 'M' + rightPt + rArc + ' 0 1,1 ' + topPt + - rArc + ' 0 0,1 ' + rightPt + 'Z'; -} - - -function convertPath(options, x2p, y2p) { - var pathIn = options.path; - var xSizemode = options.xsizemode; - var ySizemode = options.ysizemode; - var xAnchor = options.xanchor; - var yAnchor = options.yanchor; - - return pathIn.replace(constants.segmentRE, function(segment) { - var paramNumber = 0; - var segmentType = segment.charAt(0); - var xParams = constants.paramIsX[segmentType]; - var yParams = constants.paramIsY[segmentType]; - var nParams = constants.numParams[segmentType]; - - var paramString = segment.substr(1).replace(constants.paramRE, function(param) { - if(xParams[paramNumber]) { - if(xSizemode === 'pixel') param = x2p(xAnchor) + Number(param); - else param = x2p(param); - } else if(yParams[paramNumber]) { - if(ySizemode === 'pixel') param = y2p(yAnchor) - Number(param); - else param = y2p(param); - } - paramNumber++; - - if(paramNumber > nParams) param = 'X'; - return param; - }); - - if(paramNumber > nParams) { - paramString = paramString.replace(/[\s,]*X.*/, ''); - Lib.log('Ignoring extra params in segment ' + segment); - } - - return segmentType + paramString; - }); -} - function movePath(pathIn, moveX, moveY) { return pathIn.replace(constants.segmentRE, function(segment) { var paramNumber = 0; diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index bb49cbbdce0..6f69e4bdf95 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -3,6 +3,7 @@ var dragElement = require('../../dragelement'); var dragHelpers = require('../../dragelement/helpers'); var drawMode = dragHelpers.drawMode; +var selectMode = dragHelpers.selectMode; var Registry = require('../../../registry'); @@ -16,10 +17,11 @@ var handleOutline = require('../../selections/handle_outline'); var clearOutlineControllers = handleOutline.clearOutlineControllers; var helpers = require('./helpers'); -var pointsShapeRectangle = helpers.pointsShapeRectangle; -var pointsShapeEllipse = helpers.pointsShapeEllipse; +var pointsOnRectangle = helpers.pointsOnRectangle; +var pointsOnEllipse = helpers.pointsOnEllipse; var writePaths = helpers.writePaths; var newShapes = require('./newshapes'); +var newSelections = require('../../selections/draw_newselection/newselections'); module.exports = function displayOutlines(polygons, outlines, dragOptions, nCalls) { if(!nCalls) nCalls = 0; @@ -30,47 +32,64 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall // recursive call displayOutlines(polygons, outlines, dragOptions, nCalls++); - if(pointsShapeEllipse(polygons[0])) { + if(pointsOnEllipse(polygons[0])) { update({redrawing: true}); } } function update(opts) { - dragOptions.isActiveShape = false; // i.e. to disable controllers + var updateObject = {}; + + if(dragOptions.isActiveShape !== undefined) { + dragOptions.isActiveShape = false; // i.e. to disable shape controllers + updateObject = newShapes(outlines, dragOptions); + } + + if(dragOptions.isActiveSelection !== undefined) { + dragOptions.isActiveSelection = false; // i.e. to disable selection controllers + updateObject = newSelections(outlines, dragOptions); + } - var updateObject = newShapes(outlines, dragOptions); if(Object.keys(updateObject).length) { Registry.call((opts || {}).redrawing ? 'relayout' : '_guiRelayout', gd, updateObject); } } - - var isActiveShape = dragOptions.isActiveShape; var fullLayout = gd._fullLayout; var zoomLayer = fullLayout._zoomlayer; var dragmode = dragOptions.dragmode; var isDrawMode = drawMode(dragmode); - - if(isDrawMode) gd._fullLayout._drawing = true; - else if(gd._fullLayout._activeShapeIndex >= 0) clearOutlineControllers(gd); + var isSelectMode = selectMode(dragmode); + + if(isDrawMode || isSelectMode) { + gd._fullLayout._outlining = true; + } else if( + gd._fullLayout._activeShapeIndex >= 0 || + gd._fullLayout._activeSelectionIndex >= 0 + ) { + clearOutlineControllers(gd); + } // make outline outlines.attr('d', writePaths(polygons)); // add controllers var vertexDragOptions; - var shapeDragOptions; + var regionDragOptions; var indexI; // cell index var indexJ; // vertex or cell-controller index var copyPolygons; - if(isActiveShape && !nCalls) { + if(!nCalls && ( + dragOptions.isActiveShape || + dragOptions.isActiveSelection + )) { copyPolygons = recordPositions([], polygons); var g = zoomLayer.append('g').attr('class', 'outline-controllers'); addVertexControllers(g); - addShapeControllers(); + addRegionControllers(); } function startDragVertex(evt) { @@ -88,7 +107,7 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var cell = polygons[indexI]; var len = cell.length; - if(pointsShapeRectangle(cell)) { + if(pointsOnRectangle(cell)) { for(var q = 0; q < len; q++) { if(q === indexJ) continue; @@ -107,7 +126,7 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall cell[indexJ][1] = x0 + dx; cell[indexJ][2] = y0 + dy; - if(!pointsShapeRectangle(cell)) { + if(!pointsOnRectangle(cell)) { // reject result to rectangles with ensure areas for(var j = 0; j < len; j++) { for(var k = 0; k < cell[j].length; k++) { @@ -162,8 +181,8 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var cell = polygons[indexI]; if( - !pointsShapeRectangle(cell) && - !pointsShapeEllipse(cell) + !pointsOnRectangle(cell) && + !pointsOnEllipse(cell) ) { removeVertex(); } @@ -176,8 +195,8 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall for(var i = 0; i < polygons.length; i++) { var cell = polygons[i]; - var onRect = pointsShapeRectangle(cell); - var onEllipse = !onRect && pointsShapeEllipse(cell); + var onRect = pointsOnRectangle(cell); + var onEllipse = !onRect && pointsOnEllipse(cell); vertexDragOptions[i] = []; for(var j = 0; j < cell.length; j++) { @@ -222,7 +241,7 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall } } - function moveShape(dx, dy) { + function moveRegion(dx, dy) { if(!polygons.length) return; for(var i = 0; i < polygons.length; i++) { @@ -235,37 +254,37 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall } } - function moveShapeController(dx, dy) { - moveShape(dx, dy); + function moveRegionController(dx, dy) { + moveRegion(dx, dy); redraw(); } - function startDragShapeController(evt) { + function startDragRegionController(evt) { indexI = +evt.srcElement.getAttribute('data-i'); if(!indexI) indexI = 0; // ensure non-existing move button get zero index - shapeDragOptions[indexI].moveFn = moveShapeController; + regionDragOptions[indexI].moveFn = moveRegionController; } - function endDragShapeController() { + function endDragRegionController() { update(); } - function addShapeControllers() { - shapeDragOptions = []; + function addRegionControllers() { + regionDragOptions = []; if(!polygons.length) return; var i = 0; - shapeDragOptions[i] = { + regionDragOptions[i] = { element: outlines[0][0], gd: gd, - prepFn: startDragShapeController, - doneFn: endDragShapeController + prepFn: startDragRegionController, + doneFn: endDragRegionController }; - dragElement.init(shapeDragOptions[i]); + dragElement.init(regionDragOptions[i]); } }; diff --git a/src/components/shapes/draw_newshape/helpers.js b/src/components/shapes/draw_newshape/helpers.js index 8f3b8b09637..e31d83c99cd 100644 --- a/src/components/shapes/draw_newshape/helpers.js +++ b/src/components/shapes/draw_newshape/helpers.js @@ -221,7 +221,7 @@ function dist(a, b) { ); } -exports.pointsShapeRectangle = function(cell) { +exports.pointsOnRectangle = function(cell) { var len = cell.length; if(len !== 5) return false; @@ -249,7 +249,7 @@ exports.pointsShapeRectangle = function(cell) { ); }; -exports.pointsShapeEllipse = function(cell) { +exports.pointsOnEllipse = function(cell) { var len = cell.length; if(len !== CIRCLE_SIDES + 1) return false; @@ -325,3 +325,20 @@ exports.ellipseOver = function(pos) { y1: cy + dy }; }; + +exports.fixDatesForPaths = function(polygons, xaxis, yaxis) { + var xIsDate = xaxis.type === 'date'; + var yIsDate = yaxis.type === 'date'; + if(!xIsDate && !yIsDate) return polygons; + + for(var i = 0; i < polygons.length; i++) { + for(var j = 0; j < polygons[i].length; j++) { + for(var k = 0; k + 2 < polygons[i][j].length; k += 2) { + if(xIsDate) polygons[i][j][k + 1] = polygons[i][j][k + 1].replace(' ', '_'); + if(yIsDate) polygons[i][j][k + 2] = polygons[i][j][k + 2].replace(' ', '_'); + } + } + } + + return polygons; +}; diff --git a/src/components/shapes/draw_newshape/newshapes.js b/src/components/shapes/draw_newshape/newshapes.js index 19ba2f162c8..24c7355da2b 100644 --- a/src/components/shapes/draw_newshape/newshapes.js +++ b/src/components/shapes/draw_newshape/newshapes.js @@ -23,7 +23,7 @@ var helpers = require('./helpers'); var readPaths = helpers.readPaths; var writePaths = helpers.writePaths; var ellipseOver = helpers.ellipseOver; - +var fixDatesForPaths = helpers.fixDatesForPaths; module.exports = function newShapes(outlines, dragOptions) { if(!outlines.length) return; @@ -32,7 +32,7 @@ module.exports = function newShapes(outlines, dragOptions) { var d = e.getAttribute('d'); var gd = dragOptions.gd; - var drwStyle = gd._fullLayout.newshape; + var newStyle = gd._fullLayout.newshape; var plotinfo = dragOptions.plotinfo; var xaxis = plotinfo.xaxis; @@ -80,18 +80,18 @@ module.exports = function newShapes(outlines, dragOptions) { xref: xPaper ? 'paper' : xaxis._id, yref: yPaper ? 'paper' : yaxis._id, - layer: drwStyle.layer, - opacity: drwStyle.opacity, + layer: newStyle.layer, + opacity: newStyle.opacity, line: { - color: drwStyle.line.color, - width: drwStyle.line.width, - dash: drwStyle.line.dash + color: newStyle.line.color, + width: newStyle.line.width, + dash: newStyle.line.dash } }; if(!isOpenMode) { - newShape.fillcolor = drwStyle.fillcolor; - newShape.fillrule = drwStyle.fillrule; + newShape.fillcolor = newStyle.fillcolor; + newShape.fillrule = newStyle.fillrule; } var cell; @@ -101,6 +101,7 @@ module.exports = function newShapes(outlines, dragOptions) { if( cell && + cell.length === 5 && // ensure we only have 4 corners for a rect dragmode === 'drawrect' ) { newShape.type = 'rect'; @@ -229,20 +230,3 @@ module.exports = function newShapes(outlines, dragOptions) { return editHelpers ? editHelpers.getUpdateObj() : {}; }; - -function fixDatesForPaths(polygons, xaxis, yaxis) { - var xIsDate = xaxis.type === 'date'; - var yIsDate = yaxis.type === 'date'; - if(!xIsDate && !yIsDate) return polygons; - - for(var i = 0; i < polygons.length; i++) { - for(var j = 0; j < polygons[i].length; j++) { - for(var k = 0; k + 2 < polygons[i][j].length; k += 2) { - if(xIsDate) polygons[i][j][k + 1] = polygons[i][j][k + 1].replace(' ', '_'); - if(yIsDate) polygons[i][j][k + 2] = polygons[i][j][k + 2].replace(' ', '_'); - } - } - } - - return polygons; -} diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index ca03b9aa935..d07fe6948eb 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -3,6 +3,7 @@ var constants = require('./constants'); var Lib = require('../../lib'); +var Axes = require('../../plots/cartesian/axes'); // special position conversion functions... category axis positions can't be // specified by their data values, because they don't make a continuous mapping. @@ -32,7 +33,7 @@ exports.encodeDate = function(convertToDate) { return function(v) { return convertToDate(v).replace(' ', '_'); }; }; -exports.extractPathCoords = function(path, paramsToUse) { +exports.extractPathCoords = function(path, paramsToUse, isRaw) { var extractedCoordinates = []; var segments = path.match(constants.segmentRE); @@ -43,7 +44,10 @@ exports.extractPathCoords = function(path, paramsToUse) { var params = segment.substr(1).match(constants.paramRE); if(!params || params.length < relevantParamIdx) return; - extractedCoordinates.push(Lib.cleanNumber(params[relevantParamIdx])); + var str = params[relevantParamIdx]; + var pos = isRaw ? str : Lib.cleanNumber(str); + + extractedCoordinates.push(pos); }); return extractedCoordinates; @@ -122,7 +126,7 @@ exports.roundPositionForSharpStrokeRendering = function(pos, strokeWidth) { return strokeWidthIsOdd ? posValAsInt + 0.5 : posValAsInt; }; -exports.makeOptionsAndPlotinfo = function(gd, index) { +exports.makeShapesOptionsAndPlotinfo = function(gd, index) { var options = gd._fullLayout.shapes[index] || {}; var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; @@ -145,3 +149,133 @@ exports.makeOptionsAndPlotinfo = function(gd, index) { plotinfo: plotinfo }; }; + +// TODO: move to selections helpers? +exports.makeSelectionsOptionsAndPlotinfo = function(gd, index) { + var options = gd._fullLayout.selections[index] || {}; + + var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; + var hasPlotinfo = !!plotinfo; + if(hasPlotinfo) { + plotinfo._hadPlotinfo = true; + } else { + plotinfo = {}; + if(options.xref) plotinfo.xaxis = gd._fullLayout[options.xref + 'axis']; + if(options.yref) plotinfo.yaxis = gd._fullLayout[options.yref + 'axis']; + } + + return { + options: options, + plotinfo: plotinfo + }; +}; + + +exports.getPathString = function(gd, options) { + var type = options.type; + var xRefType = Axes.getRefType(options.xref); + var yRefType = Axes.getRefType(options.yref); + var xa = Axes.getFromId(gd, options.xref); + var ya = Axes.getFromId(gd, options.yref); + var gs = gd._fullLayout._size; + var x2r, x2p, y2r, y2p; + var x0, x1, y0, y1; + + if(xa) { + if(xRefType === 'domain') { + x2p = function(v) { return xa._offset + xa._length * v; }; + } else { + x2r = exports.shapePositionToRange(xa); + x2p = function(v) { return xa._offset + xa.r2p(x2r(v, true)); }; + } + } else { + x2p = function(v) { return gs.l + gs.w * v; }; + } + + if(ya) { + if(yRefType === 'domain') { + y2p = function(v) { return ya._offset + ya._length * (1 - v); }; + } else { + y2r = exports.shapePositionToRange(ya); + y2p = function(v) { return ya._offset + ya.r2p(y2r(v, true)); }; + } + } else { + y2p = function(v) { return gs.t + gs.h * (1 - v); }; + } + + if(type === 'path') { + if(xa && xa.type === 'date') x2p = exports.decodeDate(x2p); + if(ya && ya.type === 'date') y2p = exports.decodeDate(y2p); + return convertPath(options, x2p, y2p); + } + + if(options.xsizemode === 'pixel') { + var xAnchorPos = x2p(options.xanchor); + x0 = xAnchorPos + options.x0; + x1 = xAnchorPos + options.x1; + } else { + x0 = x2p(options.x0); + x1 = x2p(options.x1); + } + + if(options.ysizemode === 'pixel') { + var yAnchorPos = y2p(options.yanchor); + y0 = yAnchorPos - options.y0; + y1 = yAnchorPos - options.y1; + } else { + y0 = y2p(options.y0); + y1 = y2p(options.y1); + } + + if(type === 'line') return 'M' + x0 + ',' + y0 + 'L' + x1 + ',' + y1; + if(type === 'rect') return 'M' + x0 + ',' + y0 + 'H' + x1 + 'V' + y1 + 'H' + x0 + 'Z'; + + // circle + var cx = (x0 + x1) / 2; + var cy = (y0 + y1) / 2; + var rx = Math.abs(cx - x0); + var ry = Math.abs(cy - y0); + var rArc = 'A' + rx + ',' + ry; + var rightPt = (cx + rx) + ',' + cy; + var topPt = cx + ',' + (cy - ry); + return 'M' + rightPt + rArc + ' 0 1,1 ' + topPt + + rArc + ' 0 0,1 ' + rightPt + 'Z'; +}; + + +function convertPath(options, x2p, y2p) { + var pathIn = options.path; + var xSizemode = options.xsizemode; + var ySizemode = options.ysizemode; + var xAnchor = options.xanchor; + var yAnchor = options.yanchor; + + return pathIn.replace(constants.segmentRE, function(segment) { + var paramNumber = 0; + var segmentType = segment.charAt(0); + var xParams = constants.paramIsX[segmentType]; + var yParams = constants.paramIsY[segmentType]; + var nParams = constants.numParams[segmentType]; + + var paramString = segment.substr(1).replace(constants.paramRE, function(param) { + if(xParams[paramNumber]) { + if(xSizemode === 'pixel') param = x2p(xAnchor) + Number(param); + else param = x2p(param); + } else if(yParams[paramNumber]) { + if(ySizemode === 'pixel') param = y2p(yAnchor) - Number(param); + else param = y2p(param); + } + paramNumber++; + + if(paramNumber > nParams) param = 'X'; + return param; + }); + + if(paramNumber > nParams) { + paramString = paramString.replace(/[\s,]*X.*/, ''); + Lib.log('Ignoring extra params in segment ' + segment); + } + + return segmentType + paramString; + }); +} From e71617dd951a5b52605674e3c9c850f3cd6dcbbc Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 09:48:03 -0400 Subject: [PATCH 004/105] drag css no londer needed --- src/css/_drag.scss | 12 ------------ src/css/style.scss | 1 - 2 files changed, 13 deletions(-) delete mode 100644 src/css/_drag.scss diff --git a/src/css/_drag.scss b/src/css/_drag.scss deleted file mode 100644 index 0896a794f95..00000000000 --- a/src/css/_drag.scss +++ /dev/null @@ -1,12 +0,0 @@ -.select-outline { - fill: none; - stroke-width: 1; - shape-rendering: crispEdges; -} -.select-outline-1 { - stroke: white; -} -.select-outline-2 { - stroke: black; - stroke-dasharray: 2px 2px; -} \ No newline at end of file diff --git a/src/css/style.scss b/src/css/style.scss index 146a68e4afe..8305dfe36b4 100644 --- a/src/css/style.scss +++ b/src/css/style.scss @@ -6,6 +6,5 @@ @import "cursor.scss"; @import "modebar.scss"; @import "tooltip.scss"; - @import "drag.scss"; } @import "notifier.scss"; From c7e49e783724cf128c57fae4be27bca155445ac4 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 09:50:37 -0400 Subject: [PATCH 005/105] selections in core and plot_api --- src/core.js | 1 + src/plot_api/plot_api.js | 28 +++++++++++++++++++++++----- src/plot_api/subroutines.js | 1 + 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/core.js b/src/core.js index d9ea5c74115..4089e582dea 100644 --- a/src/core.js +++ b/src/core.js @@ -35,6 +35,7 @@ register([ require('./components/fx'), // fx needs to come after legend require('./components/annotations'), require('./components/annotations3d'), + require('./components/selections'), require('./components/shapes'), require('./components/images'), require('./components/updatemenus'), diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index b4793be66a7..f9313369dcd 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -19,7 +19,7 @@ var Drawing = require('../components/drawing'); var Color = require('../components/color'); var initInteractions = require('../plots/cartesian/graph_interact').initInteractions; var xmlnsNamespaces = require('../constants/xmlns_namespaces'); -var clearSelect = require('../components/selections/select').clearSelect; +var clearSelect = require('../components/selections').clearSelect; var dfltConfig = require('./plot_config').dfltConfig; var manageArrays = require('./manage_arrays'); @@ -369,6 +369,7 @@ function _doPlot(gd, data, layout, config) { Plots.addLinks, Plots.rehover, Plots.redrag, + Plots.reselect, // TODO: doAutoMargin is only needed here for axis automargin, which // happens outside of marginPushers where all the other automargins are // calculated. Would be much better to separate margin calculations from @@ -1299,7 +1300,11 @@ function restyle(gd, astr, val, _traces) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover, Plots.redrag); + seq.push( + Plots.rehover, + Plots.redrag, + Plots.reselect + ); Queue.add(gd, restyle, [gd, specs.undoit, specs.traces], @@ -1801,7 +1806,11 @@ function relayout(gd, astr, val) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover, Plots.redrag); + seq.push( + Plots.rehover, + Plots.redrag, + Plots.reselect + ); Queue.add(gd, relayout, [gd, specs.undoit], @@ -2314,7 +2323,11 @@ function update(gd, traceUpdate, layoutUpdate, _traces) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover, Plots.redrag); + seq.push( + Plots.rehover, + Plots.redrag, + Plots.reselect + ); Queue.add(gd, update, [gd, restyleSpecs.undoit, relayoutSpecs.undoit, restyleSpecs.traces], @@ -2750,7 +2763,11 @@ function react(gd, data, layout, config) { seq.push(emitAfterPlot); } - seq.push(Plots.rehover, Plots.redrag); + seq.push( + Plots.rehover, + Plots.redrag, + Plots.reselect + ); plotDone = Lib.syncOrAsync(seq, gd); if(!plotDone || !plotDone.then) plotDone = Promise.resolve(gd); @@ -3801,6 +3818,7 @@ function makePlotFramework(gd) { fullLayout._shapeUpperLayer = layerAbove.append('g') .classed('shapelayer', true); + fullLayout._selectionLayer = fullLayout._toppaper.append('g').classed('selectionlayer', true); fullLayout._infolayer = fullLayout._toppaper.append('g').classed('infolayer', true); fullLayout._menulayer = fullLayout._toppaper.append('g').classed('menulayer', true); fullLayout._zoomlayer = fullLayout._toppaper.append('g').classed('zoomlayer', true); diff --git a/src/plot_api/subroutines.js b/src/plot_api/subroutines.js index 20a7a59f85a..03e775879a7 100644 --- a/src/plot_api/subroutines.js +++ b/src/plot_api/subroutines.js @@ -592,6 +592,7 @@ exports.drawData = function(gd) { // draw components that can be drawn on axes, // and that do not push the margins + Registry.getComponentMethod('selections', 'draw')(gd); Registry.getComponentMethod('shapes', 'draw')(gd); Registry.getComponentMethod('annotations', 'draw')(gd); Registry.getComponentMethod('images', 'draw')(gd); From c6d954df8bd8f05db1fdbc9e251c6576e6804eb7 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 09:51:16 -0400 Subject: [PATCH 006/105] selections in plots --- src/plots/cartesian/axes.js | 1 + src/plots/cartesian/dragbox.js | 9 +++------ src/plots/cartesian/layout_defaults.js | 1 + src/plots/geo/geo.js | 6 +++--- src/plots/layout_attributes.js | 4 ++++ src/plots/mapbox/mapbox.js | 8 ++++---- src/plots/plots.js | 10 ++++++++++ src/plots/polar/polar.js | 6 +++--- src/plots/ternary/ternary.js | 8 ++++---- src/traces/sankey/base_plot.js | 2 +- 10 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index a53d42cd8fa..4a5bed02b15 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -213,6 +213,7 @@ axes.redrawComponents = function(gd, axIds) { _redrawOneComp('annotations', 'drawOne', '_annIndices'); _redrawOneComp('shapes', 'drawOne', '_shapeIndices'); _redrawOneComp('images', 'draw', '_imgIndices', true); + _redrawOneComp('selections', 'drawOne', '_selectionIndices'); }; var getDataConversions = axes.getDataConversions = function(gd, trace, target, targetArray) { diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index bfab70745f0..a48f3f83835 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -26,9 +26,9 @@ var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; var Plots = require('../plots'); var getFromId = require('./axis_ids').getFromId; -var prepSelect = require('../../components/selections/select').prepSelect; -var clearSelect = require('../../components/selections/select').clearSelect; -var selectOnClick = require('../../components/selections/select').selectOnClick; +var prepSelect = require('../../components/selections').prepSelect; +var clearSelect = require('../../components/selections').clearSelect; +var selectOnClick = require('../../components/selections').selectOnClick; var scaleZoom = require('./scale_zoom'); var constants = require('./constants'); @@ -231,9 +231,6 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { updateSubplots([0, 0, pw, ph]); dragOptions.moveFn(dragDataNow.dx, dragDataNow.dy); } - - // TODO should we try to "re-select" under select/lasso modes? - // probably best to wait for https://github.com/plotly/plotly.js/issues/1851 } }; }; diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index c24a7c34f1a..1c07600ad6a 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -146,6 +146,7 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { axLayoutOut._traceIndices = traces.map(function(t) { return t._expandedIndex; }); axLayoutOut._annIndices = []; axLayoutOut._shapeIndices = []; + axLayoutOut._selectionIndices = []; axLayoutOut._imgIndices = []; axLayoutOut._subplotsWith = []; axLayoutOut._counterAxes = []; diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 666d0222753..b50ee6fb102 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -18,9 +18,9 @@ var Plots = require('../plots'); var Axes = require('../cartesian/axes'); var getAutoRange = require('../cartesian/autorange').getAutoRange; var dragElement = require('../../components/dragelement'); -var prepSelect = require('../../components/selections/select').prepSelect; -var clearSelect = require('../../components/selections/select').clearSelect; -var selectOnClick = require('../../components/selections/select').selectOnClick; +var prepSelect = require('../../components/selections').prepSelect; +var clearSelect = require('../../components/selections').clearSelect; +var selectOnClick = require('../../components/selections').selectOnClick; var createGeoZoom = require('./zoom'); var constants = require('./constants'); diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index ee955e5bfc7..cc6e4d973a5 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -4,6 +4,7 @@ var fontAttrs = require('./font_attributes'); var animationAttrs = require('./animation_attributes'); var colorAttrs = require('../components/color/attributes'); var drawNewShapeAttrs = require('../components/shapes/draw_newshape/attributes'); +var drawNewSelectionAttrs = require('../components/selections/draw_newselection/attributes'); var padAttrs = require('./pad_attributes'); var extendFlat = require('../lib/extend').extendFlat; @@ -393,6 +394,9 @@ module.exports = { newshape: drawNewShapeAttrs.newshape, activeshape: drawNewShapeAttrs.activeshape, + newselection: drawNewSelectionAttrs.newselection, + activeselection: drawNewSelectionAttrs.activeselection, + meta: { valType: 'any', arrayOk: true, diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 030f4ec288c..ecd87a56927 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -14,10 +14,10 @@ var rectMode = dragHelpers.rectMode; var drawMode = dragHelpers.drawMode; var selectMode = dragHelpers.selectMode; -var prepSelect = require('../../components/selections/select').prepSelect; -var clearSelect = require('../../components/selections/select').clearSelect; -var clearSelectionsCache = require('../../components/selections/select').clearSelectionsCache; -var selectOnClick = require('../../components/selections/select').selectOnClick; +var prepSelect = require('../../components/selections').prepSelect; +var clearSelect = require('../../components/selections').clearSelect; +var clearSelectionsCache = require('../../components/selections').clearSelectionsCache; +var selectOnClick = require('../../components/selections').selectOnClick; var constants = require('./constants'); var createMapboxLayer = require('./layers'); diff --git a/src/plots/plots.js b/src/plots/plots.js index d76e66d63d3..632a17c643c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1539,6 +1539,11 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { 'supplyDrawNewShapeDefaults' )(layoutIn, layoutOut, coerce); + Registry.getComponentMethod( + 'selections', + 'supplyDrawNewSelectionDefaults' + )(layoutIn, layoutOut, coerce); + coerce('meta'); // do not include defaults in fullLayout when users do not set transition @@ -2901,6 +2906,7 @@ function _transition(gd, transitionOpts, opts) { interruptPreviousTransitions, opts.prepareFn, plots.rehover, + plots.reselect, executeTransitions ]; @@ -3357,6 +3363,10 @@ plots.redrag = function(gd) { } }; +plots.reselect = function(gd) { + Registry.getComponentMethod('selections', 'reselect')(gd); +}; + plots.generalUpdatePerTraceModule = function(gd, subplot, subplotCalcData, subplotLayout) { var traceHashOld = subplot.traceHash; var traceHash = {}; diff --git a/src/plots/polar/polar.js b/src/plots/polar/polar.js index 6e65e70505b..36153bae62e 100644 --- a/src/plots/polar/polar.js +++ b/src/plots/polar/polar.js @@ -18,9 +18,9 @@ var dragBox = require('../cartesian/dragbox'); var dragElement = require('../../components/dragelement'); var Fx = require('../../components/fx'); var Titles = require('../../components/titles'); -var prepSelect = require('../../components/selections/select').prepSelect; -var selectOnClick = require('../../components/selections/select').selectOnClick; -var clearSelect = require('../../components/selections/select').clearSelect; +var prepSelect = require('../../components/selections').prepSelect; +var selectOnClick = require('../../components/selections').selectOnClick; +var clearSelect = require('../../components/selections').clearSelect; var setCursor = require('../../lib/setcursor'); var clearGlCanvases = require('../../lib/clear_gl_canvases'); var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 80344982e23..c7478536259 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -19,10 +19,10 @@ var dragHelpers = require('../../components/dragelement/helpers'); var freeMode = dragHelpers.freeMode; var rectMode = dragHelpers.rectMode; var Titles = require('../../components/titles'); -var prepSelect = require('../../components/selections/select').prepSelect; -var selectOnClick = require('../../components/selections/select').selectOnClick; -var clearSelect = require('../../components/selections/select').clearSelect; -var clearSelectionsCache = require('../../components/selections/select').clearSelectionsCache; +var prepSelect = require('../../components/selections').prepSelect; +var selectOnClick = require('../../components/selections').selectOnClick; +var clearSelect = require('../../components/selections').clearSelect; +var clearSelectionsCache = require('../../components/selections').clearSelectionsCache; var constants = require('../cartesian/constants'); function Ternary(options, fullLayout) { diff --git a/src/traces/sankey/base_plot.js b/src/traces/sankey/base_plot.js index 5befa698ea7..65dc467cb85 100644 --- a/src/traces/sankey/base_plot.js +++ b/src/traces/sankey/base_plot.js @@ -7,7 +7,7 @@ var fxAttrs = require('../../components/fx/layout_attributes'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../../components/dragelement'); -var prepSelect = require('../../components/selections/select').prepSelect; +var prepSelect = require('../../components/selections').prepSelect; var Lib = require('../../lib'); var Registry = require('../../registry'); From b952125d9f5cf326d94a7831df4362e3214a31a9 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 09:51:51 -0400 Subject: [PATCH 007/105] selections in plot schema --- test/plot-schema.json | 163 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) diff --git a/test/plot-schema.json b/test/plot-schema.json index 5fda370259d..860871eee56 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -615,6 +615,24 @@ } } }, + "activeselection": { + "editType": "none", + "fillcolor": { + "description": "Sets the color filling the active selection' interior.", + "dflt": "#7f7f7f", + "editType": "none", + "valType": "color" + }, + "opacity": { + "description": "Sets the opacity of the active selection.", + "dflt": 0.5, + "editType": "none", + "max": 1, + "min": 0, + "valType": "number" + }, + "role": "object" + }, "activeshape": { "editType": "none", "fillcolor": { @@ -3421,6 +3439,40 @@ "valType": "any" } }, + "newselection": { + "editType": "none", + "line": { + "color": { + "description": "Sets the line color. By default uses either dark grey or white to increase contrast with background color.", + "editType": "none", + "valType": "color" + }, + "dash": { + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "dot", + "editType": "none", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "editType": "none", + "role": "object", + "width": { + "description": "Sets the line width (in px).", + "dflt": 1, + "editType": "none", + "min": 1, + "valType": "number" + } + }, + "role": "object" + }, "newshape": { "drawdirection": { "description": "When `dragmode` is set to *drawrect*, *drawline* or *drawcircle* this limits the drag to be horizontal, vertical or diagonal. Using *diagonal* there is no limit e.g. in drawing lines in any direction. *ortho* limits the draw to be either horizontal or vertical. *horizontal* allows horizontal extend. *vertical* allows vertical extend.", @@ -6845,6 +6897,117 @@ "editType": "none", "valType": "any" }, + "selections": { + "items": { + "selection": { + "editType": "arraydraw", + "line": { + "color": { + "anim": true, + "description": "Sets the line color.", + "editType": "arraydraw", + "valType": "color" + }, + "dash": { + "description": "Sets the dash style of lines. Set to a dash type string (*solid*, *dot*, *dash*, *longdash*, *dashdot*, or *longdashdot*) or a dash length list in px (eg *5px,10px,2px,2px*).", + "dflt": "dot", + "editType": "arraydraw", + "valType": "string", + "values": [ + "solid", + "dot", + "dash", + "longdash", + "dashdot", + "longdashdot" + ] + }, + "editType": "arraydraw", + "role": "object", + "width": { + "anim": true, + "description": "Sets the line width (in px).", + "dflt": 1, + "editType": "arraydraw", + "min": 1, + "valType": "number" + } + }, + "name": { + "description": "When used in a template, named items are created in the output figure in addition to any items the figure already has in this array. You can modify these items in the output figure by making your own item with `templateitemname` matching this `name` alongside your modifications (including `visible: false` or `enabled: false` to hide it). Has no effect outside of a template.", + "editType": "arraydraw", + "valType": "string" + }, + "opacity": { + "description": "Sets the opacity of the selection.", + "dflt": 0.7, + "editType": "arraydraw", + "max": 1, + "min": 0, + "valType": "number" + }, + "path": { + "description": "For `type` *path* - a valid SVG path with the pixel values similar to `shapes.path`.", + "editType": "arraydraw", + "valType": "string" + }, + "role": "object", + "templateitemname": { + "description": "Used to refer to a named item in this array in the template. Named items from the template will be created even without a matching item in the input figure, but you can modify one by making an item with `templateitemname` matching its `name`, alongside your modifications (including `visible: false` or `enabled: false` to hide it). If there is no template or no matching item, this item will be hidden unless you explicitly show it with `visible: true`.", + "editType": "arraydraw", + "valType": "string" + }, + "type": { + "description": "Specifies the selection type to be drawn. If *rect*, a rectangle is drawn linking (`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`) and (`x0`,`y1`). If *path*, draw a custom SVG path using `path`.", + "editType": "arraydraw", + "valType": "enumerated", + "values": [ + "rect", + "path" + ] + }, + "x0": { + "description": "Sets the selection's starting x position.", + "editType": "arraydraw", + "valType": "any" + }, + "x1": { + "description": "Sets the selection's end x position.", + "editType": "arraydraw", + "valType": "any" + }, + "xref": { + "description": "Sets the selection's x coordinate axis. If set to a x axis id (e.g. *x* or *x2*), the `x` position refers to a x coordinate. If set to *paper*, the `x` position refers to the distance from the left of the plotting area in normalized coordinates where *0* (*1*) corresponds to the left (right). If set to a x axis ID followed by *domain* (separated by a space), the position behaves like for *paper*, but refers to the distance in fractions of the domain length from the left of the domain of that axis: e.g., *x2 domain* refers to the domain of the second x axis and a x position of 0.5 refers to the point between the left and the right of the domain of the second x axis.", + "editType": "arraydraw", + "valType": "enumerated", + "values": [ + "paper", + "/^x([2-9]|[1-9][0-9]+)?( domain)?$/" + ] + }, + "y0": { + "description": "Sets the selection's starting y position.", + "editType": "arraydraw", + "valType": "any" + }, + "y1": { + "description": "Sets the selection's end y position.", + "editType": "arraydraw", + "valType": "any" + }, + "yref": { + "description": "Sets the selection's x coordinate axis. If set to a y axis id (e.g. *y* or *y2*), the `y` position refers to a y coordinate. If set to *paper*, the `y` position refers to the distance from the bottom of the plotting area in normalized coordinates where *0* (*1*) corresponds to the bottom (top). If set to a y axis ID followed by *domain* (separated by a space), the position behaves like for *paper*, but refers to the distance in fractions of the domain length from the bottom of the domain of that axis: e.g., *y2 domain* refers to the domain of the second y axis and a y position of 0.5 refers to the point between the bottom and the top of the domain of the second y axis.", + "editType": "arraydraw", + "valType": "enumerated", + "values": [ + "paper", + "/^y([2-9]|[1-9][0-9]+)?( domain)?$/" + ] + } + } + }, + "role": "object" + }, "separators": { "description": "Sets the decimal and thousand separators. For example, *. * puts a '.' before decimals and a space between thousands. In English locales, dflt is *.,* but other locales may alter this default.", "editType": "plot", From b8c1b6f7d71f13ddaa42ca2cef5e68cd67ae3f95 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 14:48:04 -0400 Subject: [PATCH 008/105] TODO comment for x0, x1 dflt --- src/components/selections/defaults.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/selections/defaults.js b/src/components/selections/defaults.js index 7ef8fae7817..3a34531c676 100644 --- a/src/components/selections/defaults.js +++ b/src/components/selections/defaults.js @@ -50,6 +50,7 @@ function handleSelectionDefaults(selectionIn, selectionOut, fullLayout) { // Coerce x0, x1, y0, y1 if(noPath) { + // FIXME: Are these the best dflts for selections? var dflt0 = 0.25; var dflt1 = 0.75; From 11d5a71f86bf30b63680851c4719f22a953ffa4e Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 16:48:41 -0400 Subject: [PATCH 009/105] one outline --- src/components/selections/select.js | 4 ++-- test/jasmine/tests/select_test.js | 10 +++++----- test/jasmine/tests/splom_test.js | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/components/selections/select.js b/src/components/selections/select.js index ed58bfa0e14..3e48554c7fd 100644 --- a/src/components/selections/select.js +++ b/src/components/selections/select.js @@ -92,14 +92,14 @@ function prepSelect(evt, startX, startY, dragOptions, mode) { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); } - var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data(isDrawMode ? [0] : [1, 2]); + var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data([1]); var newStyle = isDrawMode ? fullLayout.newshape : fullLayout.newselection; outlines.enter() .append('path') - .attr('class', function(d) { return 'select-outline select-outline-' + d + ' select-outline-' + plotinfo.id; }) + .attr('class', 'select-outline select-outline-' + plotinfo.id) .style({ opacity: isDrawMode ? newStyle.opacity / 2 : 1, fill: (isDrawMode && !isOpenMode) ? newStyle.fillcolor : 'none', diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index d0f3b2027b8..9f2af741a0d 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -74,7 +74,7 @@ function resetEvents(gd) { // these event handlers was called (via assertEventCounts), // we no longer need separate tests that these nodes are created // and this way *all* subplot variants get the test. - assertSelectionNodes(1, 2); + assertSelectionNodes(1, 1); selectingCnt++; selectingData = data; }); @@ -85,7 +85,7 @@ function resetEvents(gd) { if(data && gd._fullLayout.dragmode.indexOf('select') > -1 && gd._fullLayout.dragmode.indexOf('lasso') > -1) { - assertSelectionNodes(0, 2); + assertSelectionNodes(0, 1); } selectedCnt++; selectedData = data; @@ -1353,11 +1353,11 @@ describe('Test select box and lasso in general:', function() { Plotly.newPlot(gd, fig) .then(_drag) - .then(function() { assertSelectionNodes(0, 2, 'after drag 1'); }) + .then(function() { assertSelectionNodes(0, 1, 'after drag 1'); }) .then(function() { return Plotly.relayout(gd, 'xaxis.range', [-5, 5]); }) .then(function() { assertSelectionNodes(0, 0, 'after axrange relayout'); }) .then(_drag) - .then(function() { assertSelectionNodes(0, 2, 'after drag 2'); }) + .then(function() { assertSelectionNodes(0, 1, 'after drag 2'); }) .then(done, done.fail); }); @@ -1592,7 +1592,7 @@ describe('Test select box and lasso in general:', function() { } function _assert(msg, exp) { - var outline = d3Select(gd).select('.zoomlayer').select('.select-outline-1'); + var outline = d3Select(gd).select('.zoomlayer').select('.select-outline'); if(exp.outline) { expect(outline2coords(outline)).toBeCloseTo2DArray(exp.outline, 2, msg); diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js index 656d4ced184..7fe28322d46 100644 --- a/test/jasmine/tests/splom_test.js +++ b/test/jasmine/tests/splom_test.js @@ -1637,7 +1637,7 @@ describe('Test splom select:', function() { {pointNumber: 2, x: 3, y: 3} ], { subplot: 'xy', - selectionOutlineCnt: 2 + selectionOutlineCnt: 1 }); }) .then(function() { return _select([[50, 50], [100, 100]]); }) @@ -1646,7 +1646,7 @@ describe('Test splom select:', function() { {pointNumber: 1, x: 2, y: 2} ], { subplot: 'xy', - selectionOutlineCnt: 2 + selectionOutlineCnt: 1 }); }) .then(function() { return _select([[5, 195], [100, 100]], {shiftKey: true}); }) @@ -1656,8 +1656,8 @@ describe('Test splom select:', function() { {pointNumber: 1, x: 2, y: 2} ], { subplot: 'xy', - // still '2' as the selection get merged - selectionOutlineCnt: 2 + // still '1' as the selection get merged + selectionOutlineCnt: 1 }); }) .then(function() { return _select([[205, 205], [395, 395]]); }) @@ -1669,7 +1669,7 @@ describe('Test splom select:', function() { ], { subplot: 'x2y2', // outlines from previous subplot are cleared! - selectionOutlineCnt: 2 + selectionOutlineCnt: 1 }); }) .then(function() { return _select([[50, 50], [100, 100]]); }) @@ -1679,7 +1679,7 @@ describe('Test splom select:', function() { ], { subplot: 'xy', // outlines from previous subplot are cleared! - selectionOutlineCnt: 2 + selectionOutlineCnt: 1 }); }) .then(done, done.fail); From 9976a76104ea5dfc10c7b0b2e1dcb97858d7e9b8 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 17:16:47 -0400 Subject: [PATCH 010/105] add optional eraseselection button to modebar --- src/components/modebar/buttons.js | 8 ++++++++ src/components/modebar/constants.js | 3 ++- src/fonts/ploticon.js | 7 +++++++ test/plot-schema.json | 2 +- 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 3428489b25b..2f26e7c8c7b 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -5,6 +5,7 @@ var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); var Icons = require('../../fonts/ploticon'); var eraseActiveShape = require('../shapes/draw').eraseActiveShape; +var eraseActiveSelection = require('../selections/draw').eraseActiveSelection; var Lib = require('../../lib'); var _ = Lib._; @@ -182,6 +183,13 @@ modeBarButtons.eraseshape = { click: eraseActiveShape }; +modeBarButtons.eraseselection = { + name: 'eraseselection', + title: function(gd) { return _(gd, 'Erase active selection'); }, + icon: Icons.eraseselection, + click: eraseActiveSelection +}; + modeBarButtons.zoomIn2d = { name: 'zoomIn2d', _cat: 'zoomin', diff --git a/src/components/modebar/constants.js b/src/components/modebar/constants.js index 788cbb5ae4e..e9ef62dc396 100644 --- a/src/components/modebar/constants.js +++ b/src/components/modebar/constants.js @@ -9,7 +9,8 @@ var DRAW_MODES = [ 'drawclosedpath', 'drawcircle', 'drawrect', - 'eraseshape' + 'eraseshape', + 'eraseselection' ]; var backButtons = [ diff --git a/src/fonts/ploticon.js b/src/fonts/ploticon.js index 7c368c69d42..a6300d00ee1 100644 --- a/src/fonts/ploticon.js +++ b/src/fonts/ploticon.js @@ -151,6 +151,13 @@ module.exports = { 'path': 'M82.77,78H31.85L6,49.57,31.85,21.14H82.77a8.72,8.72,0,0,1,8.65,8.77V69.24A8.72,8.72,0,0,1,82.77,78ZM35.46,69.84H82.77a.57.57,0,0,0,.49-.6V29.91a.57.57,0,0,0-.49-.61H35.46L17,49.57Zm32.68-34.7-24,24,5,5,24-24Zm-19,.53-5,5,24,24,5-5Z', 'transform': 'matrix(1 0 0 1 -10 -10)' }, + 'eraseselection': { + // TODO: use dot in path + 'width': 80, + 'height': 80, + 'path': 'M82.77,78H31.85L6,49.57,31.85,21.14H82.77a8.72,8.72,0,0,1,8.65,8.77V69.24A8.72,8.72,0,0,1,82.77,78ZM35.46,69.84H82.77a.57.57,0,0,0,.49-.6V29.91a.57.57,0,0,0-.49-.61H35.46L17,49.57Zm32.68-34.7-24,24,5,5,24-24Zm-19,.53-5,5,24,24,5-5Z', + 'transform': 'matrix(1 0 0 1 -10 -10)' + }, 'spikeline': { 'width': 1000, 'height': 1000, diff --git a/test/plot-schema.json b/test/plot-schema.json index 860871eee56..c2fcccdb35b 100644 --- a/test/plot-schema.json +++ b/test/plot-schema.json @@ -3389,7 +3389,7 @@ }, "add": { "arrayOk": true, - "description": "Determines which predefined modebar buttons to add. Please note that these buttons will only be shown if they are compatible with all trace types used in a graph. Similar to `config.modeBarButtonsToAdd` option. This may include *v1hovermode*, *hoverclosest*, *hovercompare*, *togglehover*, *togglespikelines*, *drawline*, *drawopenpath*, *drawclosedpath*, *drawcircle*, *drawrect*, *eraseshape*.", + "description": "Determines which predefined modebar buttons to add. Please note that these buttons will only be shown if they are compatible with all trace types used in a graph. Similar to `config.modeBarButtonsToAdd` option. This may include *v1hovermode*, *hoverclosest*, *hovercompare*, *togglehover*, *togglespikelines*, *drawline*, *drawopenpath*, *drawclosedpath*, *drawcircle*, *drawrect*, *eraseshape*, *eraseselection*.", "dflt": "", "editType": "modebar", "valType": "string" From 1107600f7a9be2c11cd31c800bd46675d1545acf Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi Date: Mon, 20 Jun 2022 20:44:50 -0400 Subject: [PATCH 011/105] test selections in axes_breaks-gridlines --- .../image/baselines/axes_breaks-gridlines.png | Bin 44901 -> 52195 bytes test/image/mocks/axes_breaks-gridlines.json | 26 +++++++++++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/test/image/baselines/axes_breaks-gridlines.png b/test/image/baselines/axes_breaks-gridlines.png index d1faf40e25ebb79b7bc84a4d78ebe977a3c7eb74..77bca083c88e8abe3af10bb2b780ec0ca31b36b3 100644 GIT binary patch literal 52195 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