From ac758506ce1cd9d6206fc11c752595961b468194 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 16 Jan 2020 13:21:40 -0500 Subject: [PATCH 01/71] should not coerce shape.line.color and line.dash when line.width is zero --- src/components/shapes/defaults.js | 8 +++++--- test/jasmine/tests/shapes_test.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index d11ee8eb604..edaee3795b0 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -36,9 +36,11 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { coerce('layer'); coerce('opacity'); coerce('fillcolor'); - coerce('line.color'); - coerce('line.width'); - coerce('line.dash'); + var lineWidth = coerce('line.width'); + if(lineWidth) { + coerce('line.color'); + coerce('line.dash'); + } var dfltType = shapeIn.path ? 'path' : 'rect'; var shapeType = coerce('type', dfltType); diff --git a/test/jasmine/tests/shapes_test.js b/test/jasmine/tests/shapes_test.js index b6390dba30c..1a0950088c7 100644 --- a/test/jasmine/tests/shapes_test.js +++ b/test/jasmine/tests/shapes_test.js @@ -126,6 +126,37 @@ describe('Test shapes defaults:', function() { expect(shape2Out.y0).toBeWithin(1.5, 0.001); expect(shape2Out.y1).toBeWithin(5.5, 0.001); }); + + it('should not coerce line.color and line.dash when line.width is zero', function() { + var fullLayout = { + xaxis: {type: 'linear', range: [0, 1], _shapeIndices: []}, + yaxis: {type: 'log', range: [0, 1], _shapeIndices: []}, + _subplots: {xaxis: ['x'], yaxis: ['y']} + }; + + Axes.setConvert(fullLayout.xaxis); + Axes.setConvert(fullLayout.yaxis); + + var layoutIn = { + shapes: [{ + type: 'line', + xref: 'xaxis', + yref: 'yaxis', + x0: 0, + x1: 1, + y0: 1, + y1: 10, + line: { + width: 0 + } + }] + }; + + var shapes = _supply(layoutIn, fullLayout); + + expect(shapes[0].line.color).toEqual(undefined); + expect(shapes[0].line.dash).toEqual(undefined); + }); }); function countShapesInLowerLayer(gd) { From d5ee9503a348b1e2108887a69dae448a6a1f7c0f Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 16 Jan 2020 15:48:52 -0500 Subject: [PATCH 02/71] use Math.max instead of complex logic --- src/components/shapes/draw.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index c1541be4966..8b940cd6fb4 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -188,7 +188,7 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { var circleStyle = { 'fill-opacity': '0' // ensure not visible }; - var circleRadius = sensoryWidth / 2 > minSensoryWidth ? sensoryWidth / 2 : minSensoryWidth; + var circleRadius = Math.max(sensoryWidth / 2, minSensoryWidth); g.append('circle') .attr({ From 3ecddcb1c471331cec5613208aeb159d0d203570 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 16 Jan 2020 16:39:11 -0500 Subject: [PATCH 03/71] refactor conditions - avoid using Bitwise NOT to invert -1 --- src/components/shapes/draw.js | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 8b940cd6fb4..ed510b688f5 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -383,20 +383,30 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { modifyItem('y1', shapeOptions.y1 = yPixelSized ? newY1 : p2y(newY1)); } } else { - var newN = (~dragMode.indexOf('n')) ? n0 + dy : n0; - var newS = (~dragMode.indexOf('s')) ? s0 + dy : s0; - var newW = (~dragMode.indexOf('w')) ? w0 + dx : w0; - var newE = (~dragMode.indexOf('e')) ? e0 + dx : e0; + var has = function(str) { return dragMode.indexOf(str) !== -1; }; + var hasN = has('n'); + var hasS = has('s'); + var hasW = has('w'); + var hasE = has('e'); - // Do things in opposing direction for y-axis. - // Hint: for data-sized shapes the reversal of axis direction is done in p2y. - if(~dragMode.indexOf('n') && yPixelSized) newN = n0 - dy; - if(~dragMode.indexOf('s') && yPixelSized) newS = s0 - dy; + var newN = hasN ? n0 + dy : n0; + var newS = hasS ? s0 + dy : s0; + var newW = hasW ? w0 + dx : w0; + var newE = hasE ? e0 + dx : e0; + + if(yPixelSized) { + // Do things in opposing direction for y-axis. + // Hint: for data-sized shapes the reversal of axis direction is done in p2y. + if(hasN) newN = n0 - dy; + if(hasS) newS = s0 - dy; + } // Update shape eventually. Again, be aware of the // opposing direction of the y-axis of fixed size shapes. - if((!yPixelSized && newS - newN > MINHEIGHT) || - (yPixelSized && newN - newS > MINHEIGHT)) { + if( + (!yPixelSized && newS - newN > MINHEIGHT) || + (yPixelSized && newN - newS > MINHEIGHT) + ) { modifyItem(optN, shapeOptions[optN] = yPixelSized ? newN : p2y(newN)); modifyItem(optS, shapeOptions[optS] = yPixelSized ? newS : p2y(newS)); } From eee814e26b1a724c448cbaa0f8232750ce307f18 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 13 Feb 2020 16:08:48 -0500 Subject: [PATCH 04/71] refactor list of dragmodes --- src/components/fx/layout_attributes.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index 64935ed59fb..e5c1024ecf2 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -44,7 +44,15 @@ module.exports = { dragmode: { valType: 'enumerated', role: 'info', - values: ['zoom', 'pan', 'select', 'lasso', 'orbit', 'turntable', false], + values: [ + 'zoom', + 'pan', + 'select', + 'lasso', + 'orbit', + 'turntable', + false + ], dflt: 'zoom', editType: 'modebar', description: [ From 0a0326ccd5f8a56f634dafb63128248e5315ffdc Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 14 Feb 2020 15:22:48 -0500 Subject: [PATCH 05/71] reuse cartesian clearSelect function in mapbox and geo --- src/plots/geo/geo.js | 3 ++- src/plots/mapbox/mapbox.js | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 3c74c1647d0..00088cf9b03 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -22,6 +22,7 @@ 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 createGeoZoom = require('./zoom'); @@ -489,7 +490,7 @@ proto.updateFx = function(fullLayout, geoLayout) { subplot: _this.id, clickFn: function(numClicks) { if(numClicks === 2) { - fullLayout._zoomlayer.selectAll('.select-outline').remove(); + clearSelect(gd); } } }; diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 4986c647a7f..6d602d82254 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -17,6 +17,7 @@ var Registry = require('../../registry'); var Axes = require('../cartesian/axes'); var dragElement = require('../../components/dragelement'); var prepSelect = require('../cartesian/select').prepSelect; +var clearSelect = require('../cartesian/select').clearSelect; var selectOnClick = require('../cartesian/select').selectOnClick; var constants = require('./constants'); @@ -506,9 +507,7 @@ proto.initFx = function(calcData, fullLayout) { // define event handlers on map creation, to keep one ref per map, // so that map.on / map.off in updateFx works as expected - self.clearSelect = function() { - gd._fullLayout._zoomlayer.selectAll('.select-outline').remove(); - }; + self.clearSelect = clearSelect; /** * Returns a click handler function that is supposed From 11879d16cab8ac4a44486cd0c7b1017b7e6d8195 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 3 Mar 2020 14:40:24 -0500 Subject: [PATCH 06/71] add new icons and modebars to draw new shapes --- src/components/modebar/buttons.js | 56 +++++++++++++++++++++++++++++-- src/fonts/ploticon.js | 36 ++++++++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 0e21d292802..e1e931cddf1 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -11,9 +11,9 @@ var Registry = require('../../registry'); var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); -var Lib = require('../../lib'); var Icons = require('../../fonts/ploticon'); - +var eraseActiveShape = require('../../plots/cartesian/handle_outline').eraseActiveShape; +var Lib = require('../../lib'); var _ = Lib._; var modeBarButtons = module.exports = {}; @@ -134,6 +134,58 @@ modeBarButtons.lasso2d = { click: handleCartesian }; +modeBarButtons.closedfreedraw = { + name: 'closedfreedraw', + title: function(gd) { return _(gd, 'Draw Closed Freeform'); }, + attr: 'dragmode', + val: 'closedfreedraw', + icon: Icons.closedfreedraw, + click: handleCartesian +}; + +modeBarButtons.openfreedraw = { + name: 'openfreedraw', + title: function(gd) { return _(gd, 'Draw Open Freeform'); }, + attr: 'dragmode', + val: 'openfreedraw', + icon: Icons.openfreedraw, + click: handleCartesian +}; + +modeBarButtons.linedraw = { + name: 'linedraw', + title: function(gd) { return _(gd, 'Draw Line'); }, + attr: 'dragmode', + val: 'linedraw', + icon: Icons.linedraw, + click: handleCartesian +}; + +modeBarButtons.rectdraw = { + name: 'rectdraw', + title: function(gd) { return _(gd, 'Draw Rectangle'); }, + attr: 'dragmode', + val: 'rectdraw', + icon: Icons.rectdraw, + click: handleCartesian +}; + +modeBarButtons.ellipsedraw = { + name: 'ellipsedraw', + title: function(gd) { return _(gd, 'Draw Ellipse'); }, + attr: 'dragmode', + val: 'ellipsedraw', + icon: Icons.ellipsedraw, + click: handleCartesian +}; + +modeBarButtons.eraseshape = { + name: 'eraseshape', + title: function(gd) { return _(gd, 'Erase Active Shape'); }, + icon: Icons.eraseshape, + click: eraseActiveShape +}; + modeBarButtons.zoomIn2d = { name: 'zoomIn2d', title: function(gd) { return _(gd, 'Zoom in'); }, diff --git a/src/fonts/ploticon.js b/src/fonts/ploticon.js index c7a33742cde..76597da89c8 100644 --- a/src/fonts/ploticon.js +++ b/src/fonts/ploticon.js @@ -111,6 +111,18 @@ module.exports = { 'path': 'm214-7h429v214h-429v-214z m500 0h72v500q0 8-6 21t-11 20l-157 156q-5 6-19 12t-22 5v-232q0-22-15-38t-38-16h-322q-22 0-37 16t-16 38v232h-72v-714h72v232q0 22 16 38t37 16h465q22 0 38-16t15-38v-232z m-214 518v178q0 8-5 13t-13 5h-107q-7 0-13-5t-5-13v-178q0-8 5-13t13-5h107q7 0 13 5t5 13z m357-18v-518q0-22-15-38t-38-16h-750q-23 0-38 16t-16 38v750q0 22 16 38t38 16h517q23 0 50-12t42-26l156-157q16-15 27-42t11-49z', 'transform': 'matrix(1 0 0 -1 0 850)' }, + 'openfreedraw': { + 'width': 70, + 'height': 70, + 'path': 'M33.21,85.65a7.31,7.31,0,0,1-2.59-.48c-8.16-3.11-9.27-19.8-9.88-41.3-.1-3.58-.19-6.68-.35-9-.15-2.1-.67-3.48-1.43-3.79-2.13-.88-7.91,2.32-12,5.86L3,32.38c1.87-1.64,11.55-9.66,18.27-6.9,2.13.87,4.75,3.14,5.17,9,.17,2.43.26,5.59.36,9.25a224.17,224.17,0,0,0,1.5,23.4c1.54,10.76,4,12.22,4.48,12.4.84.32,2.79-.46,5.76-3.59L43,80.07C41.53,81.57,37.68,85.64,33.21,85.65ZM74.81,69a11.34,11.34,0,0,0,6.09-6.72L87.26,44.5,74.72,32,56.9,38.35c-2.37.86-5.57,3.42-6.61,6L38.65,72.14l8.42,8.43ZM55,46.27a7.91,7.91,0,0,1,3.64-3.17l14.8-5.3,8,8L76.11,60.6l-.06.19a6.37,6.37,0,0,1-3,3.43L48.25,74.59,44.62,71Zm16.57,7.82A6.9,6.9,0,1,0,64.64,61,6.91,6.91,0,0,0,71.54,54.09Zm-4.05,0a2.85,2.85,0,1,1-2.85-2.85A2.86,2.86,0,0,1,67.49,54.09Zm-4.13,5.22L60.5,56.45,44.26,72.7l2.86,2.86ZM97.83,35.67,84.14,22l-8.57,8.57L89.26,44.24Zm-13.69-8,8,8-2.85,2.85-8-8Z', + 'transform': 'matrix(1 0 0 1 -15 -15)' + }, + 'closedfreedraw': { + 'width': 90, + 'height': 90, + 'path': 'M88.41,21.12a26.56,26.56,0,0,0-36.18,0l-2.07,2-2.07-2a26.57,26.57,0,0,0-36.18,0,23.74,23.74,0,0,0,0,34.8L48,90.12a3.22,3.22,0,0,0,4.42,0l36-34.21a23.73,23.73,0,0,0,0-34.79ZM84,51.24,50.16,83.35,16.35,51.25a17.28,17.28,0,0,1,0-25.47,20,20,0,0,1,27.3,0l4.29,4.07a3.23,3.23,0,0,0,4.44,0l4.29-4.07a20,20,0,0,1,27.3,0,17.27,17.27,0,0,1,0,25.46ZM66.76,47.68h-33v6.91h33ZM53.35,35H46.44V68h6.91Z', + 'transform': 'matrix(1 0 0 1 -5 -5)' + }, 'lasso': { 'width': 1031, 'height': 1000, @@ -123,6 +135,30 @@ module.exports = { 'path': 'm0 850l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-285l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z', 'transform': 'matrix(1 0 0 -1 0 850)' }, + 'linedraw': { + 'width': 70, + 'height': 70, + 'path': 'M60.64,62.3a11.29,11.29,0,0,0,6.09-6.72l6.35-17.72L60.54,25.31l-17.82,6.4c-2.36.86-5.57,3.41-6.6,6L24.48,65.5l8.42,8.42ZM40.79,39.63a7.89,7.89,0,0,1,3.65-3.17l14.79-5.31,8,8L61.94,54l-.06.19a6.44,6.44,0,0,1-3,3.43L34.07,68l-3.62-3.63Zm16.57,7.81a6.9,6.9,0,1,0-6.89,6.9A6.9,6.9,0,0,0,57.36,47.44Zm-4,0a2.86,2.86,0,1,1-2.85-2.85A2.86,2.86,0,0,1,53.32,47.44Zm-4.13,5.22L46.33,49.8,30.08,66.05l2.86,2.86ZM83.65,29,70,15.34,61.4,23.9,75.09,37.59ZM70,21.06l8,8-2.84,2.85-8-8ZM87,80.49H10.67V87H87Z', + 'transform': 'matrix(1 0 0 1 -15 -15)' + }, + 'rectdraw': { + 'width': 80, + 'height': 80, + 'path': 'M78,22V79H21V22H78m9-9H12V88H87V13ZM68,46.22H31V54H68ZM53,32H45.22V69H53Z', + 'transform': 'matrix(1 0 0 1 -10 -10)' + }, + 'ellipsedraw': { + 'width': 80, + 'height': 80, + 'path': 'M50,84.72C26.84,84.72,8,69.28,8,50.3S26.84,15.87,50,15.87,92,31.31,92,50.3,73.16,84.72,50,84.72Zm0-60.59c-18.6,0-33.74,11.74-33.74,26.17S31.4,76.46,50,76.46,83.74,64.72,83.74,50.3,68.6,24.13,50,24.13Zm17.15,22h-34v7.11h34Zm-13.8-13H46.24v34h7.11Z', + 'transform': 'matrix(1 0 0 1 -10 -10)' + }, + 'eraseshape': { + '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, From ddfc1bd1b30350343bf3df686fb6162786f40682 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 3 Mar 2020 15:08:33 -0500 Subject: [PATCH 07/71] add new dragmodes and helpers --- src/components/dragelement/helpers.js | 57 ++++++++++++++++++++++++++ src/components/fx/layout_attributes.js | 11 +++-- 2 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/components/dragelement/helpers.js diff --git a/src/components/dragelement/helpers.js b/src/components/dragelement/helpers.js new file mode 100644 index 00000000000..819b0accbf9 --- /dev/null +++ b/src/components/dragelement/helpers.js @@ -0,0 +1,57 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +exports.selectMode = function(dragmode) { + return ( + dragmode === 'lasso' || + dragmode === 'select' + ); +}; + +exports.drawMode = function(dragmode) { + return ( + dragmode === 'closedfreedraw' || + dragmode === 'openfreedraw' || + dragmode === 'linedraw' || + dragmode === 'rectdraw' || + dragmode === 'ellipsedraw' + ); +}; + +exports.openMode = function(dragmode) { + return ( + dragmode === 'linedraw' || + dragmode === 'openfreedraw' + ); +}; + +exports.rectMode = function(dragmode) { + return ( + dragmode === 'select' || + dragmode === 'linedraw' || + dragmode === 'rectdraw' || + dragmode === 'ellipsedraw' + ); +}; + +exports.freeMode = function(dragmode) { + return ( + dragmode === 'lasso' || + dragmode === 'closedfreedraw' || + dragmode === 'openfreedraw' + ); +}; + +exports.selectingOrDrawing = function(dragmode) { + return ( + exports.freeMode(dragmode) || + exports.rectMode(dragmode) + ); +}; diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index e5c1024ecf2..dbef7fc3398 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -49,6 +49,11 @@ module.exports = { 'pan', 'select', 'lasso', + 'closedfreedraw', + 'openfreedraw', + 'linedraw', + 'rectdraw', + 'ellipsedraw', 'orbit', 'turntable', false @@ -169,9 +174,9 @@ module.exports = { values: ['h', 'v', 'd', 'any'], dflt: 'any', description: [ - 'When "dragmode" is set to "select", this limits the selection of the drag to', - 'horizontal, vertical or diagonal. "h" only allows horizontal selection,', - '"v" only vertical, "d" only diagonal and "any" sets no limit.' + 'When `dragmode` is set to *select*, this limits the selection of the drag to', + 'horizontal, vertical or diagonal. *h* only allows horizontal selection,', + '*v* only vertical, *d* only diagonal and *any* sets no limit.' ].join(' '), editType: 'none' } From 3ea4a2533730f097b5b0a96535621cdda8654c03 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 3 Mar 2020 15:11:58 -0500 Subject: [PATCH 08/71] add new attributes for drawing new shapes --- src/components/shapes/attributes.js | 24 ++++++- src/components/shapes/defaults.js | 8 ++- src/plots/layout_attributes.js | 107 ++++++++++++++++++++++++++++ src/plots/plots.js | 20 +++++- 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index 013cc804fbe..bdd44b19218 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -235,8 +235,30 @@ module.exports = templatedArray('shape', { role: 'info', editType: 'arraydraw', description: [ - 'Sets the color filling the shape\'s interior.' + 'Sets the color filling the closed shape\'s interior.' ].join(' ') }, + fillrule: { + valType: 'enumerated', + values: ['evenodd', 'nonzero'], + dflt: 'evenodd', + role: 'info', + editType: 'arraydraw', + description: [ + 'Determines the path\'s interior.', + 'For more info please visit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule' + ].join(' ') + }, + editable: { + valType: 'boolean', + role: 'info', + dflt: false, + editType: 'calc+arraydraw', + description: [ + 'Determines whether the shape could be activated for edit or not.', + 'Please note that setting to *false* has no effect in case `config.editable` is set to true' + ].join(' ') + }, + editType: 'arraydraw' }); diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index edaee3795b0..f113828e1c7 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -30,20 +30,22 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { } var visible = coerce('visible'); - if(!visible) return; + var dfltType = shapeIn.path ? 'path' : 'rect'; + var shapeType = coerce('type', dfltType); + + coerce('editable'); coerce('layer'); coerce('opacity'); coerce('fillcolor'); + if(shapeType === 'path') coerce('fillrule'); var lineWidth = coerce('line.width'); if(lineWidth) { coerce('line.color'); coerce('line.dash'); } - var dfltType = shapeIn.path ? 'path' : 'rect'; - var shapeType = coerce('type', dfltType); var xSizeMode = coerce('xsizemode'); var ySizeMode = coerce('ysizemode'); diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index c90509d8bf0..531a909c2bf 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -11,6 +11,7 @@ var fontAttrs = require('./font_attributes'); var animationAttrs = require('./animation_attributes'); var colorAttrs = require('../components/color/attributes'); +var dash = require('../components/drawing/attributes').dash; var padAttrs = require('./pad_attributes'); var extendFlat = require('../lib/extend').extendFlat; @@ -444,6 +445,112 @@ module.exports = { editType: 'modebar' }, + newshape: { + line: { + color: { + valType: 'color', + editType: 'none', + role: 'info', + description: [ + 'Sets the line color.', + 'By default uses either dark grey or white', + 'to increase contrast with background color.' + ].join(' ') + }, + width: { + valType: 'number', + min: 0, + dflt: 4, + role: 'info', + editType: 'none', + description: 'Sets the line width (in px).' + }, + dash: extendFlat({}, dash, { + dflt: 'solid', + editType: 'none' + }), + role: 'info', + editType: 'none' + }, + fillcolor: { + valType: 'color', + dflt: 'rgba(0,0,0,0)', + role: 'info', + editType: 'none', + description: 'Sets the color filling new shapes\' interior.' + }, + fillrule: { + valType: 'enumerated', + values: ['evenodd', 'nonzero'], + dflt: 'evenodd', + role: 'info', + editType: 'none', + description: [ + 'Determines the path\'s interior.', + 'For more info please visit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule' + ].join(' ') + }, + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 1, + role: 'info', + editType: 'none', + description: 'Sets the opacity of new shapes.' + }, + layer: { + valType: 'enumerated', + values: ['below', 'above'], + dflt: 'above', + role: 'info', + editType: 'none', + description: 'Specifies whether new shapes are drawn below or above traces.' + }, + drawdirection: { + valType: 'enumerated', + role: 'info', + values: ['ortho', 'horizontal', 'vertical', 'diagonal'], + dflt: 'diagonal', + editType: 'none', + description: [ + 'When `dragmode` is set to *rectdraw*, *linedraw* or *ellipsedraw*', + '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.' + ].join(' ') + }, + + editType: 'none' + }, + + activeshape: { + fillcolor: { + valType: 'color', + dflt: 'rgb(255,0,255)', + role: 'style', + editType: 'none', + description: 'Sets the color filling the active shape\' interior.' + }, + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.5, + role: 'info', + editType: 'none', + description: [ + 'Sets the opacity of the active shape.', + 'If using a value greater than half,', + 'drag inside the active shape starts moving shape,', + 'otherwise it starts drawing a new shape.' + ].join(' ') + }, + editType: 'none' + }, + meta: { valType: 'any', arrayOk: true, diff --git a/src/plots/plots.js b/src/plots/plots.js index d91b4f89b88..8d558d01235 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -19,6 +19,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 animationAttrs = require('./animation_attributes'); var frameAttrs = require('./frame_attributes'); @@ -487,7 +488,9 @@ plots.supplyDefaults = function(gd, opts) { // we should try to come up with a better solution when implementing // https://github.com/plotly/plotly.js/issues/1851 if(oldFullLayout._zoomlayer && !gd._dragging) { - oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); + clearSelect({ // mock old gd + _fullLayout: oldFullLayout + }); } @@ -1524,6 +1527,21 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { coerce('modebar.activecolor', Color.addOpacity(modebarDefaultColor, 0.7)); coerce('modebar.uirevision', uirevision); + coerce('newshape.drawdirection'); + coerce('newshape.layer'); + coerce('newshape.fillcolor'); + coerce('newshape.fillrule'); + coerce('newshape.opacity'); + var newshapeLineWidth = coerce('newshape.line.width'); + if(newshapeLineWidth) { + var bgcolor = (layoutIn || {}).plot_bgcolor || '#FFF'; + coerce('newshape.line.color', Color.contrast(bgcolor)); + coerce('newshape.line.dash'); + } + + coerce('activeshape.fillcolor'); + coerce('activeshape.opacity'); + coerce('meta'); // do not include defaults in fullLayout when users do not set transition From d0ade8300f26cc6cb40f51c19252261cf6d10456 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 3 Mar 2020 15:16:11 -0500 Subject: [PATCH 09/71] handle scattergl and splom --- src/plots/gl2d/scene2d.js | 8 ++++++-- src/traces/scattergl/plot.js | 9 +++++---- src/traces/splom/plot.js | 5 +++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/plots/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js index bbfbe9cebfa..79d6bc48de0 100644 --- a/src/plots/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -26,6 +26,10 @@ var enforceAxisConstraints = axisConstraints.enforce; var cleanAxisConstraints = axisConstraints.clean; var doAutoRange = require('../cartesian/autorange').doAutoRange; +var dragHelpers = require('../../components/dragelement/helpers'); +var drawMode = dragHelpers.drawMode; +var selectMode = dragHelpers.selectMode; + var AXES = ['xaxis', 'yaxis']; var STATIC_CANVAS, STATIC_CONTEXT; @@ -524,8 +528,8 @@ proto.updateTraces = function(fullData, calcData) { }; proto.updateFx = function(dragmode) { - // switch to svg interactions in lasso/select mode - if(dragmode === 'lasso' || dragmode === 'select') { + // switch to svg interactions in lasso/select mode & shape drawing + if(selectMode(dragmode) || drawMode(dragmode)) { this.pickCanvas.style['pointer-events'] = 'none'; this.mouseContainer.style['pointer-events'] = 'none'; } else { diff --git a/src/traces/scattergl/plot.js b/src/traces/scattergl/plot.js index 2b0aec67917..a7d3c074f2e 100644 --- a/src/traces/scattergl/plot.js +++ b/src/traces/scattergl/plot.js @@ -14,6 +14,7 @@ var createError = require('regl-error2d'); var Text = require('gl-text'); var Lib = require('../../lib'); +var selectMode = require('../../components/dragelement/helpers').selectMode; var prepareRegl = require('../../lib/prepare_regl'); var subTypes = require('../scatter/subtypes'); @@ -246,7 +247,7 @@ module.exports = function plot(gd, subplot, cdata) { // form batch arrays, and check for selected points var dragmode = fullLayout.dragmode; - var selectMode = dragmode === 'lasso' || dragmode === 'select'; + var isSelectMode = selectMode(dragmode); var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1; for(i = 0; i < count; i++) { @@ -258,8 +259,8 @@ module.exports = function plot(gd, subplot, cdata) { var x = stash.x; var y = stash.y; - if(trace.selectedpoints || selectMode || clickSelectEnabled) { - if(!selectMode) selectMode = true; + if(trace.selectedpoints || isSelectMode || clickSelectEnabled) { + if(!isSelectMode) isSelectMode = true; // regenerate scene batch, if traces number changed during selection if(trace.selectedpoints) { @@ -292,7 +293,7 @@ module.exports = function plot(gd, subplot, cdata) { } } - if(selectMode) { + if(isSelectMode) { // create scatter instance by cloning scatter2d if(!scene.select2d) { scene.select2d = createScatter(fullLayout._glcanvas.data()[1].regl); diff --git a/src/traces/splom/plot.js b/src/traces/splom/plot.js index ed46850c5d3..94a6bb12de7 100644 --- a/src/traces/splom/plot.js +++ b/src/traces/splom/plot.js @@ -12,6 +12,7 @@ var createMatrix = require('regl-splom'); var Lib = require('../../lib'); var AxisIDs = require('../../plots/cartesian/axis_ids'); +var selectMode = require('../../components/dragelement/helpers').selectMode; module.exports = function plot(gd, _, splomCalcData) { if(!splomCalcData.length) return; @@ -78,11 +79,11 @@ function plotOne(gd, cd0) { } var clickSelectEnabled = fullLayout.clickmode.indexOf('select') > -1; - var selectMode = dragmode === 'lasso' || dragmode === 'select' || + var isSelectMode = selectMode(dragmode) || !!trace.selectedpoints || clickSelectEnabled; var needsBaseUpdate = true; - if(selectMode) { + if(isSelectMode) { var commonLength = trace._length; // regenerate scene batch, if traces number changed during selection From 8fd8b0c24aa4a02907e4deac127838c5c1e4b32e Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 3 Mar 2020 15:16:46 -0500 Subject: [PATCH 10/71] prep to handle mapbox and ternary --- src/plots/mapbox/mapbox.js | 20 ++++++++++++--- src/plots/ternary/ternary.js | 47 ++++++++++++++++++++++-------------- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index 6d602d82254..bead2ba697c 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -10,14 +10,21 @@ var mapboxgl = require('mapbox-gl'); -var Fx = require('../../components/fx'); var Lib = require('../../lib'); var geoUtils = require('../../lib/geo_location_utils'); var Registry = require('../../registry'); var Axes = require('../cartesian/axes'); var dragElement = require('../../components/dragelement'); + +var Fx = require('../../components/fx'); +var dragHelpers = require('../../components/dragelement/helpers'); +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 constants = require('./constants'); @@ -507,7 +514,10 @@ proto.initFx = function(calcData, fullLayout) { // define event handlers on map creation, to keep one ref per map, // so that map.on / map.off in updateFx works as expected - self.clearSelect = clearSelect; + self.clearSelect = function(e) { + clearSelectionsCache(self.dragOptions); + clearSelect(e); + }; /** * Returns a click handler function that is supposed @@ -548,7 +558,7 @@ proto.updateFx = function(fullLayout) { var dragMode = fullLayout.dragmode; var fillRangeItems; - if(dragMode === 'select') { + if(rectMode(dragMode)) { fillRangeItems = function(eventData, poly) { var ranges = eventData.range = {}; ranges[self.id] = [ @@ -569,10 +579,12 @@ proto.updateFx = function(fullLayout) { // persistent selection state. var oldDragOptions = self.dragOptions; self.dragOptions = Lib.extendDeep(oldDragOptions || {}, { + dragmode: fullLayout.dragmode, element: self.div, gd: gd, plotinfo: { id: self.id, + domain: fullLayout[self.id].domain, xaxis: self.xaxis, yaxis: self.yaxis, fillRangeItems: fillRangeItems @@ -586,7 +598,7 @@ proto.updateFx = function(fullLayout) { // a new one. Otherwise multiple click handlers might // be registered resulting in unwanted behavior. map.off('click', self.onClickInPanHandler); - if(dragMode === 'select' || dragMode === 'lasso') { + if(selectMode(dragMode) || drawMode(dragMode)) { map.dragPan.disable(); map.on('zoomstart', self.clearSelect); diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 400530c2d69..82c70258ef4 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -23,10 +23,14 @@ var Plots = require('../plots'); var Axes = require('../cartesian/axes'); var dragElement = require('../../components/dragelement'); var Fx = require('../../components/fx'); +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 constants = require('../cartesian/constants'); function Ternary(options, fullLayout) { @@ -488,6 +492,11 @@ var STARTMARKER = 'm0.5,0.5h5v-2h-5v-5h-2v5h-5v2h5v5h2Z'; // I guess this could be shared with cartesian... but for now it's separate. var SHOWZOOMOUTTIP = true; +proto.clearSelect = function(e) { + clearSelectionsCache(this.dragOptions); + clearSelect(e); +}; + proto.initInteractions = function() { var _this = this; var dragger = _this.layers.plotbg.select('path').node(); @@ -495,11 +504,12 @@ proto.initInteractions = function() { var zoomLayer = gd._fullLayout._zoomlayer; // use plotbg for the main interactions - var dragOptions = { + this.dragOptions = { element: dragger, gd: gd, plotinfo: { id: _this.id, + domain: gd._fullLayout[_this.id].domain, xaxis: _this.xaxis, yaxis: _this.yaxis }, @@ -507,26 +517,27 @@ proto.initInteractions = function() { prepFn: function(e, startX, startY) { // these aren't available yet when initInteractions // is called - dragOptions.xaxes = [_this.xaxis]; - dragOptions.yaxes = [_this.yaxis]; - var dragModeNow = gd._fullLayout.dragmode; + _this.dragOptions.xaxes = [_this.xaxis]; + _this.dragOptions.yaxes = [_this.yaxis]; + + var dragModeNow = _this.dragOptions.dragmode = gd._fullLayout.dragmode; - if(dragModeNow === 'lasso') dragOptions.minDrag = 1; - else dragOptions.minDrag = undefined; + if(freeMode(dragModeNow)) _this.dragOptions.minDrag = 1; + else _this.dragOptions.minDrag = undefined; if(dragModeNow === 'zoom') { - dragOptions.moveFn = zoomMove; - dragOptions.clickFn = clickZoomPan; - dragOptions.doneFn = zoomDone; + _this.dragOptions.moveFn = zoomMove; + _this.dragOptions.clickFn = clickZoomPan; + _this.dragOptions.doneFn = zoomDone; zoomPrep(e, startX, startY); } else if(dragModeNow === 'pan') { - dragOptions.moveFn = plotDrag; - dragOptions.clickFn = clickZoomPan; - dragOptions.doneFn = dragDone; + _this.dragOptions.moveFn = plotDrag; + _this.dragOptions.clickFn = clickZoomPan; + _this.dragOptions.doneFn = dragDone; panPrep(); - clearSelect(gd); - } else if(dragModeNow === 'select' || dragModeNow === 'lasso') { - prepSelect(e, startX, startY, dragOptions, dragModeNow); + _this.clearSelect(gd); + } else if(rectMode(dragModeNow) || freeMode(dragModeNow)) { + prepSelect(e, startX, startY, _this.dragOptions, dragModeNow); } } }; @@ -552,7 +563,7 @@ proto.initInteractions = function() { } if(clickMode.indexOf('select') > -1 && numClicks === 1) { - selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, dragOptions); + selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, this.dragOptions); } if(clickMode.indexOf('event') > -1) { @@ -595,7 +606,7 @@ proto.initInteractions = function() { }) .attr('d', 'M0,0Z'); - clearSelect(gd); + _this.clearSelect(gd); } function getAFrac(x, y) { return 1 - (y / _this.h); } @@ -745,7 +756,7 @@ proto.initInteractions = function() { dragElement.unhover(gd, evt); }; - dragElement.init(dragOptions); + dragElement.init(this.dragOptions); }; function removeZoombox(gd) { From 07b07dcb35fac9fe64e1495820c0302325c627c0 Mon Sep 17 00:00:00 2001 From: archmoj Date: Sun, 19 Apr 2020 19:06:34 -0400 Subject: [PATCH 11/71] add new shapes over cartesian traces - activate, resize, move and delete --- package.json | 1 + src/components/shapes/draw.js | 97 +++- src/plots/cartesian/dragbox.js | 18 +- src/plots/cartesian/handle_outline.js | 95 +++ src/plots/cartesian/helpers.js | 45 ++ src/plots/cartesian/new_shape.js | 808 ++++++++++++++++++++++++++ src/plots/cartesian/select.js | 311 ++++++---- 7 files changed, 1255 insertions(+), 120 deletions(-) create mode 100644 src/plots/cartesian/handle_outline.js create mode 100644 src/plots/cartesian/helpers.js create mode 100644 src/plots/cartesian/new_shape.js diff --git a/package.json b/package.json index 5e51a3b4e9f..7fde6cd1898 100644 --- a/package.json +++ b/package.json @@ -102,6 +102,7 @@ "ndarray": "^1.0.18", "ndarray-fill": "^1.0.2", "ndarray-homography": "^1.0.0", + "parse-svg-path": "^0.1.2", "point-cluster": "^3.1.8", "polybooljs": "^1.2.0", "regl": "^1.3.11", diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index ed510b688f5..e6a61eda5e0 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -12,6 +12,15 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); + +var newShape = require('../../plots/cartesian/new_shape'); +var readPaths = newShape.readPaths; +var displayOutlines = newShape.displayOutlines; + +var handleOutline = require('../../plots/cartesian/handle_outline'); +var activateShape = handleOutline.activateShape; +var eraseActiveShape = handleOutline.eraseActiveShape; + var Color = require('../color'); var Drawing = require('../drawing'); var arrayEditor = require('../../plot_api/plot_template').arrayEditor; @@ -34,7 +43,8 @@ var helpers = require('./helpers'); module.exports = { draw: draw, - drawOne: drawOne + drawOne: drawOne, + eraseActiveShape: eraseActiveShape }; function draw(gd) { @@ -72,13 +82,16 @@ function drawOne(gd, index) { // TODO: use d3 idioms instead of deleting and redrawing every time if(!options._input || options.visible === false) return; + var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; + var hasPlotinfo = !!plotinfo; + if(!hasPlotinfo) plotinfo = {}; + if(options.layer !== 'below') { drawShape(gd._fullLayout._shapeUpperLayer); } else if(options.xref === 'paper' || options.yref === 'paper') { drawShape(gd._fullLayout._shapeLowerLayer); } else { - var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; - if(plotinfo) { + if(hasPlotinfo) { var mainPlot = plotinfo.mainplotinfo || plotinfo; drawShape(mainPlot.shapelayer); } else { @@ -90,23 +103,73 @@ function drawOne(gd, index) { } function drawShape(shapeLayer) { + var d = getPathString(gd, options); var attrs = { 'data-index': index, - 'fill-rule': 'evenodd', - d: getPathString(gd, options) + 'fill-rule': options.fillrule, + d: d }; + + var opacity = options.opacity; + var fillColor = options.fillcolor; var lineColor = options.line.width ? options.line.color : 'rgba(0,0,0,0)'; + var lineWidth = options.line.width; + var lineDash = options.line.dash; + + var isOpen = d[d.length - 1] !== 'Z'; + + var isActiveShape = options.editable && gd._fullLayout._activeShapeIndex === index; + if(isActiveShape) { + fillColor = isOpen ? 'rgba(0,0,0,0)' : + gd._fullLayout.activeshape.fillcolor; + + opacity = gd._fullLayout.activeshape.opacity; + } var path = shapeLayer.append('path') .attr(attrs) - .style('opacity', options.opacity) + .style('opacity', opacity) .call(Color.stroke, lineColor) - .call(Color.fill, options.fillcolor) - .call(Drawing.dashLine, options.line.dash, options.line.width); + .call(Color.fill, fillColor) + .call(Drawing.dashLine, lineDash, lineWidth); setClipPath(path, gd, options); - if(gd._context.edits.shapePosition) setupDragElement(gd, path, options, index, shapeLayer); + if(isActiveShape) { + path.style({ + 'cursor': 'move', + }); + + var dragOptions = { + element: path.node(), + plotinfo: plotinfo, + gd: gd, + dragmode: gd._fullLayout.dragmode, + isActiveShape: true // i.e. to enable controllers + }; + + var polygons = readPaths(d); + // display polygons on the screen + displayOutlines(polygons, path, dragOptions); + } else { + if(gd._context.edits.shapePosition) { + setupDragElement(gd, path, options, index, shapeLayer); + } + + path.style('pointer-events', + !gd._context.edits.shapePosition && // for backward compatibility + (lineWidth >= 1) && ( // has border + (Color.opacity(fillColor) * opacity <= 0.5) || // too transparent + isOpen + ) ? + 'stroke' : 'all' + ); + path.node().addEventListener('click', function() { return clickFn(path); }); + } + } + + function clickFn(path) { + return activateShape(gd, path, draw); } } @@ -213,7 +276,16 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { return g; } + function shouldSkipEdits() { + return !!gd._fullLayout._drawing; + } + function updateDragMode(evt) { + if(shouldSkipEdits()) { + dragMode = null; + return; + } + if(isLine) { if(evt.target.tagName === 'path') { dragMode = 'move'; @@ -244,6 +316,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { } function startDrag(evt) { + if(shouldSkipEdits()) return; + // setup update strings and initial values if(xPixelSized) { xAnchor = x2p(shapeOptions.xanchor); @@ -292,9 +366,12 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { renderVisualCues(shapeLayer, shapeOptions); deactivateClipPathTemporarily(shapePath, shapeOptions, gd); dragOptions.moveFn = (dragMode === 'move') ? moveShape : resizeShape; + dragOptions.altKey = evt.altKey; } function endDrag() { + if(shouldSkipEdits()) return; + setCursor(shapePath); removeVisualCues(shapeLayer); @@ -304,6 +381,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { } function abortDrag() { + if(shouldSkipEdits()) return; + removeVisualCues(shapeLayer); } diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index be7bfd12dce..a88e6db956d 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -21,6 +21,10 @@ var Fx = require('../../components/fx'); var Axes = require('./axes'); var setCursor = require('../../lib/setcursor'); var dragElement = require('../../components/dragelement'); +var helpers = require('../../components/dragelement/helpers'); +var selectingOrDrawing = helpers.selectingOrDrawing; +var freeMode = helpers.freeMode; + var FROM_TL = require('../../constants/alignment').FROM_TL; var clearGlCanvases = require('../../lib/clear_gl_canvases'); var redrawReglTraces = require('../../plot_api/subroutines').redrawReglTraces; @@ -163,7 +167,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { // to pan (or to zoom if it already is pan) on shift if(e.shiftKey) { if(dragModeNow === 'pan') dragModeNow = 'zoom'; - else if(!isSelectOrLasso(dragModeNow)) dragModeNow = 'pan'; + else if(!selectingOrDrawing(dragModeNow)) dragModeNow = 'pan'; } else if(e.ctrlKey) { dragModeNow = 'pan'; } @@ -173,17 +177,17 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } } - if(dragModeNow === 'lasso') dragOptions.minDrag = 1; + if(freeMode(dragModeNow)) dragOptions.minDrag = 1; else dragOptions.minDrag = undefined; - if(isSelectOrLasso(dragModeNow)) { + if(selectingOrDrawing(dragModeNow)) { dragOptions.xaxes = xaxes; dragOptions.yaxes = yaxes; // this attaches moveFn, clickFn, doneFn on dragOptions prepSelect(e, startX, startY, dragOptions, dragModeNow); } else { dragOptions.clickFn = clickFn; - if(isSelectOrLasso(dragModePrev)) { + if(selectingOrDrawing(dragModePrev)) { // TODO Fix potential bug // Note: clearing / resetting selection state only happens, when user // triggers at least one interaction in pan/zoom mode. Otherwise, the @@ -221,7 +225,7 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { if(dragDataNow && dragDataNow.element === dragger) { var dragModeNow = gd._fullLayout.dragmode; - if(!isSelectOrLasso(dragModeNow)) { + if(!selectingOrDrawing(dragModeNow)) { recomputeAxisLists(); updateSubplots([0, 0, pw, ph]); dragOptions.moveFn(dragDataNow.dx, dragDataNow.dy); @@ -1111,10 +1115,6 @@ function showDoubleClickNotifier(gd) { } } -function isSelectOrLasso(dragmode) { - return dragmode === 'lasso' || dragmode === 'select'; -} - function xCorners(box, y0) { return 'M' + (box.l - 0.5) + ',' + (y0 - MINZOOM - 0.5) + diff --git a/src/plots/cartesian/handle_outline.js b/src/plots/cartesian/handle_outline.js new file mode 100644 index 00000000000..675d3b8efc4 --- /dev/null +++ b/src/plots/cartesian/handle_outline.js @@ -0,0 +1,95 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Registry = require('../../registry'); + + +function activateShape(gd, path, drawShapes) { + var element = path.node(); + var id = +element.getAttribute('data-index'); + + for(var q = 0; q < gd._fullLayout.shapes.length; q++) { + var shapeIn = gd._fullLayout.shapes[q]._input; + if(q === id && shapeIn.editable) { + gd._fullLayout._activeShapeIndex = q; + break; + } + } + + if(gd._fullLayout._activeShapeIndex >= 0) { + drawShapes(gd); + } +} + +function deactivateShape(gd) { + clearOutlineControllers(gd); + + var shapes = []; + for(var q = 0; q < gd._fullLayout.shapes.length; q++) { + var shapeIn = gd._fullLayout.shapes[q]._input; + shapes.push(shapeIn); + } + + delete gd._fullLayout._activeShapeIndex; + + Registry.call('_guiRelayout', gd, { + shapes: shapes + }); +} + +function eraseActiveShape(gd) { + clearOutlineControllers(gd); + + var id = gd._fullLayout._activeShapeIndex; + if(id < gd._fullLayout.shapes.length) { + var shapes = []; + for(var q = 0; q < gd._fullLayout.shapes.length; q++) { + var shapeIn = gd._fullLayout.shapes[q]._input; + + if(q !== id) { + shapes.push(shapeIn); + } + } + + delete gd._fullLayout._activeShapeIndex; + + Registry.call('_guiRelayout', gd, { + shapes: shapes + }); + } +} + +function clearOutlineControllers(gd) { + var zoomLayer = gd._fullLayout._zoomlayer; + if(zoomLayer) { + zoomLayer.selectAll('.outline-controllers').remove(); + } +} + +function clearSelect(gd) { + var zoomLayer = gd._fullLayout._zoomlayer; + if(zoomLayer) { + // until we get around to persistent selections, remove the outline + // here. The selection itself will be removed when the plot redraws + // at the end. + zoomLayer.selectAll('.select-outline').remove(); + } + + gd._fullLayout._drawing = false; +} + +module.exports = { + activateShape: activateShape, + deactivateShape: deactivateShape, + eraseActiveShape: eraseActiveShape, + clearOutlineControllers: clearOutlineControllers, + clearSelect: clearSelect +}; diff --git a/src/plots/cartesian/helpers.js b/src/plots/cartesian/helpers.js new file mode 100644 index 00000000000..824d4f75bb1 --- /dev/null +++ b/src/plots/cartesian/helpers.js @@ -0,0 +1,45 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +function getAxId(ax) { + return ax._id; +} + +// in v2 (once log ranges are fixed), +// we'll be able to p2r here for all axis types +function p2r(ax, v) { + switch(ax.type) { + case 'log': + return ax.p2d(v); + case 'date': + return ax.p2r(v, 0, ax.calendar); + default: + return ax.p2r(v); + } +} + +function axValue(ax) { + var index = (ax._id.charAt(0) === 'y') ? 1 : 0; + return function(v) { return p2r(ax, v[index]); }; +} + +function getTransform(plotinfo) { + return 'translate(' + + plotinfo.xaxis._offset + ',' + + plotinfo.yaxis._offset + ')'; +} + +module.exports = { + getAxId: getAxId, + p2r: p2r, + axValue: axValue, + getTransform: getTransform +}; diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js new file mode 100644 index 00000000000..f894902f4a5 --- /dev/null +++ b/src/plots/cartesian/new_shape.js @@ -0,0 +1,808 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var parseSvgPath = require('parse-svg-path'); + +var dragElement = require('../../components/dragelement'); +var dragHelpers = require('../../components/dragelement/helpers'); +var drawMode = dragHelpers.drawMode; +var openMode = dragHelpers.openMode; + +var Registry = require('../../registry'); +var Lib = require('../../lib'); +var setCursor = require('../../lib/setcursor'); + +var constants = require('./constants'); +var MINSELECT = constants.MINSELECT; +var CIRCLE_SIDES = 32; // should be divisible by 8 +var i000 = 0; +var i045 = CIRCLE_SIDES / 8; +var i090 = CIRCLE_SIDES / 4; +var i180 = CIRCLE_SIDES / 2; +var i270 = CIRCLE_SIDES / 4 * 3; +var cos45 = Math.cos(Math.PI / 4); +var sin45 = Math.sin(Math.PI / 4); +var SQRT2 = Math.sqrt(2); + +var helpers = require('./helpers'); +var p2r = helpers.p2r; + +var handleOutline = require('./handle_outline'); +var clearOutlineControllers = handleOutline.clearOutlineControllers; +var clearSelect = handleOutline.clearSelect; + +function recordPositions(polygonsOut, polygonsIn) { + for(var i = 0; i < polygonsIn.length; i++) { + polygonsOut[i] = []; + var len = polygonsIn[i].length; + for(var j = 0; j < len; j++) { + // skip close points + if(dist(polygonsIn[i][j], polygonsIn[i][(j + 1) % len]) < 1) continue; + + polygonsOut[i].push([ + polygonsIn[i][j][0], + polygonsIn[i][j][1] + ]); + } + } + return polygonsOut; +} + +function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { + var polygons = recordPositions([], polygonsIn); + + if(!nCalls) nCalls = 0; + + var gd = dragOptions.gd; + + function redraw() { + // recursive call + displayOutlines(polygons, outlines, dragOptions, nCalls++); + + dragOptions.isActiveShape = false; // i.e. to disable controllers + var shapes = addNewShapes(outlines, dragOptions); + if(shapes) { + Registry.call('_guiRelayout', gd, { + shapes: shapes // update active shape + }); + } + } + + + // remove previous controllers - only if there is an active shape + if(gd._fullLayout._activeShapeIndex >= 0) clearOutlineControllers(gd); + + var isActiveShape = dragOptions.isActiveShape; + var fullLayout = gd._fullLayout; + var zoomLayer = fullLayout._zoomlayer; + + var dragmode = dragOptions.dragmode; + var isDrawMode = drawMode(dragmode); + var isOpenMode = openMode(dragmode); + + if(isDrawMode) gd._fullLayout._drawing = true; + + var paths = []; + for(var k = 0; k < polygons.length; k++) { + // create outline path + paths.push( + providePath(polygons[k], isOpenMode) + ); + } + + // make outline + outlines.attr('d', writePaths(paths, isOpenMode)); + + // add controllers + var rVertexController = MINSELECT * 1.5; // bigger vertex buttons + var vertexDragOptions; + var shapeDragOptions; + var indexI; // cell index + var indexJ; // vertex or cell-controller index + var copyPolygons; + + copyPolygons = recordPositions([], polygons); + + if(isActiveShape) { + var g = zoomLayer.append('g').attr('class', 'outline-controllers'); + addVertexControllers(g); + addShapeControllers(); + } + + function startDragVertex(evt) { + indexI = +evt.srcElement.getAttribute('data-i'); + indexJ = +evt.srcElement.getAttribute('data-j'); + + vertexDragOptions[indexI][indexJ].moveFn = moveVertexController; + } + + function moveVertexController(dx, dy) { + if(!polygons.length) return; + + var x0 = copyPolygons[indexI][indexJ][0]; + var y0 = copyPolygons[indexI][indexJ][1]; + + var cell = polygons[indexI]; + var len = cell.length; + if(pointsShapeRectangle(cell)) { + for(var q = 0; q < len; q++) { + if(q === indexJ) continue; + + // move other corners of rectangle + var pos = cell[q]; + + if(pos[0] === cell[indexJ][0]) { + pos[0] = x0 + dx; + } + + if(pos[1] === cell[indexJ][1]) { + pos[1] = y0 + dy; + } + } + // move the corner + cell[indexJ][0] = x0 + dx; + cell[indexJ][1] = y0 + dy; + + if(!pointsShapeRectangle(cell)) { + // reject result to rectangles with ensure areas + for(var j = 0; j < len; j++) { + for(var k = 0; k < 2; k++) { + cell[j][k] = copyPolygons[indexI][j][k]; + } + } + } + } else { // other polylines + cell[indexJ][0] = x0 + dx; + cell[indexJ][1] = y0 + dy; + } + + redraw(); + } + + function endDragVertexController(evt) { + Lib.noop(evt); + } + + function removeVertex() { + if(!polygons.length) return; + + var newPolygon = []; + for(var j = 0; j < polygons[indexI].length; j++) { + if(j !== indexJ) { + newPolygon.push( + polygons[indexI][j] + ); + } + } + polygons[indexI] = newPolygon; + } + + function clickVertexController(numClicks) { + if(numClicks === 2) { + var cell = polygons[indexI]; + if(cell.length > 4) { + removeVertex(); + } + + redraw(); + } + } + + function addVertexControllers(g) { + vertexDragOptions = []; + + for(var i = 0; i < polygons.length; i++) { + var cell = polygons[i]; + + var onRect = pointsShapeRectangle(cell); + var onEllipse = !onRect && pointsShapeEllipse(cell); + + var minX; + var minY; + var maxX; + var maxY; + if(onRect) { + // compute bounding box + minX = calcMin(cell, 0); + minY = calcMin(cell, 1); + maxX = calcMax(cell, 0); + maxY = calcMax(cell, 1); + } + + vertexDragOptions[i] = []; + for(var j = 0; j < cell.length; j++) { + if(onEllipse && + j !== 0 && + j !== CIRCLE_SIDES * 0.25 && + j !== CIRCLE_SIDES * 0.5 && + j !== CIRCLE_SIDES * 0.75 + ) { + continue; + } + + var x = cell[j][0]; + var y = cell[j][1]; + + var rIcon = 3; + var button = g.append(onRect ? 'rect' : 'circle') + .style({ + 'mix-blend-mode': 'luminosity', + fill: 'black', + stroke: 'white', + 'stroke-width': 1 + }); + + if(onRect) { + button + .attr('x', x - rIcon) + .attr('y', y - rIcon) + .attr('width', 2 * rIcon) + .attr('height', 2 * rIcon); + } else { + button + .attr('cx', x) + .attr('cy', y) + .attr('r', rIcon); + } + + var vertex = g.append(onRect ? 'rect' : 'circle') + .attr('data-i', i) + .attr('data-j', j) + .style({ + opacity: 0 + }); + + if(onRect) { + var ratioX = (x - minX) / (maxX - minX); + var ratioY = (y - minY) / (maxY - minY); + if(isFinite(ratioX) && isFinite(ratioY)) { + setCursor( + vertex, + dragElement.getCursor(ratioX, 1 - ratioY) + ); + } + + vertex + .attr('x', x - rVertexController) + .attr('y', y - rVertexController) + .attr('width', 2 * rVertexController) + .attr('height', 2 * rVertexController); + } else { + vertex + .classed('cursor-grab', true) + .attr('cx', x) + .attr('cy', y) + .attr('r', rVertexController); + } + + vertexDragOptions[i][j] = { + element: vertex.node(), + gd: gd, + prepFn: startDragVertex, + doneFn: endDragVertexController, + clickFn: clickVertexController + }; + + dragElement.init(vertexDragOptions[i][j]); + } + } + } + + function moveShape(dx, dy) { + if(!polygons.length) return; + + for(var i = 0; i < polygons.length; i++) { + for(var j = 0; j < polygons[i].length; j++) { + var x0 = copyPolygons[i][j][0]; + var y0 = copyPolygons[i][j][1]; + + polygons[i][j][0] = x0 + dx; + polygons[i][j][1] = y0 + dy; + } + } + } + + function moveShapeController(dx, dy) { + moveShape(dx, dy); + + redraw(); + } + + function startDragShapeController(evt) { + indexI = +evt.srcElement.getAttribute('data-i'); + if(!indexI) indexI = 0; // ensure non-existing move button get zero index + + shapeDragOptions[indexI].moveFn = moveShapeController; + } + + function endDragShapeController(evt) { + Lib.noop(evt); + } + + function addShapeControllers() { + shapeDragOptions = []; + + if(!polygons.length) return; + + var i = 0; + shapeDragOptions[i] = { + element: outlines[0][0], + gd: gd, + prepFn: startDragShapeController, + doneFn: endDragShapeController + }; + + dragElement.init(shapeDragOptions[i]); + } +} + +function providePath(cell, isOpenMode) { + return cell.join('L') + ( + isOpenMode ? '' : 'L' + cell[0] + ); +} + +function writePaths(paths, isOpenMode) { + return paths.length > 0 ? 'M' + paths.join('M') + (isOpenMode ? '' : 'Z') : 'M0,0Z'; +} + +function readPaths(str, plotinfo, size, isActiveShape) { + var cmd = parseSvgPath(str); + + var polys = []; + var n = -1; + var newPoly = function() { + n++; + polys[n] = []; + }; + + var x = 0; + var y = 0; + var initX; + var initY; + var recStart = function() { + initX = x; + initY = y; + }; + + recStart(); + for(var i = 0; i < cmd.length; i++) { + var newPos = []; + + var c = cmd[i][0]; + switch(c) { + case 'M': + newPoly(); + x = +cmd[i][1]; + y = +cmd[i][2]; + newPos.push([x, y]); + + recStart(); + break; + + case 'L': + x = +cmd[i][1]; + y = +cmd[i][2]; + newPos.push([x, y]); + + break; + + case 'H': + x = +cmd[i][1]; + newPos.push([x, y]); + + break; + + case 'V': + y = +cmd[i][1]; + newPos.push([x, y]); + + break; + + case 'A': + var rx = +cmd[i][1]; + var ry = +cmd[i][2]; + if(!+cmd[i][4]) { + rx = -rx; + ry = -ry; + } + + var cenX = x - rx; + var cenY = y; + for(var k = 1; k <= CIRCLE_SIDES / 2; k++) { + var t = 2 * Math.PI * k / CIRCLE_SIDES; + newPos.push([ + cenX + rx * Math.cos(t), + cenY + ry * Math.sin(t) + ]); + } + + break; + } + + if(c === 'Z') { + x = initX; + y = initY; + } else { + for(var j = 0; j < newPos.length; j++) { + x = newPos[j][0]; + y = newPos[j][1]; + + if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { + polys[n].push([ + x, + y + ]); + } else if(plotinfo.domain) { + polys[n].push([ + plotinfo.domain.x[0] + x / size.w, + plotinfo.domain.y[1] - y / size.h + ]); + } else if(isActiveShape === false) { + polys[n].push([ + p2r(plotinfo.xaxis, x - plotinfo.xaxis._offset), + p2r(plotinfo.yaxis, y - plotinfo.yaxis._offset) + ]); + } else { + polys[n].push([ + p2r(plotinfo.xaxis, x), + p2r(plotinfo.yaxis, y) + ]); + } + } + } + } + + return polys; +} + +function fixDatesOnPaths(path, xaxis, yaxis) { + var xIsDate = xaxis.type === 'date'; + var yIsDate = yaxis.type === 'date'; + if(!xIsDate && !yIsDate) return path; + + for(var i = 0; i < path.length; i++) { + if(xIsDate) path[i][0] = path[i][0].replace(' ', '_'); + if(yIsDate) path[i][1] = path[i][1].replace(' ', '_'); + } + + return path; +} + +function almostEq(a, b) { + return Math.abs(a - b) <= 1e-6; +} + +function dist(a, b) { + var dx = b[0] - a[0]; + var dy = b[1] - a[1]; + return Math.sqrt( + dx * dx + + dy * dy + ); +} + +function calcMin(cell, dim) { + var v = Infinity; + for(var i = 0; i < cell.length; i++) { + v = Math.min(v, cell[i][dim]); + } + return v; +} + +function calcMax(cell, dim) { + var v = -Infinity; + for(var i = 0; i < cell.length; i++) { + v = Math.max(v, cell[i][dim]); + } + return v; +} + +function pointsShapeRectangle(cell, len) { + if(!len) len = cell.length; + if(len !== 4) return false; + for(var j = 0; j < 2; j++) { + var e01 = cell[0][j] - cell[1][j]; + var e32 = cell[3][j] - cell[2][j]; + + if(!almostEq(e01, e32)) return false; + + var e03 = cell[0][j] - cell[3][j]; + var e12 = cell[1][j] - cell[2][j]; + if(!almostEq(e03, e12)) return false; + } + + // N.B. rotated rectangles are not valid rects since rotation is not supported in shapes for now. + if( + !almostEq(cell[0][0], cell[1][0]) && + !almostEq(cell[0][0], cell[3][0]) + ) return false; + + // reject cases with zero area + return !!( + dist(cell[0], cell[1]) * + dist(cell[0], cell[3]) + ); +} + +function pointsShapeEllipse(cell, len) { + if(!len) len = cell.length; + if(len !== CIRCLE_SIDES) return false; + // opposite diagonals should be the same + for(var i = 0; i < len; i++) { + var k = (len * 2 - i) % len; + + var k2 = (len / 2 + k) % len; + var i2 = (len / 2 + i) % len; + + if(!almostEq( + dist(cell[i], cell[i2]), + dist(cell[k], cell[k2]) + )) return false; + } + return true; +} + +function handleEllipse(isEllipse, start, end) { + if(!isEllipse) return [start, end]; // i.e. case of line + + var pos = ellipseOver({ + x0: start[0], + y0: start[1], + x1: end[0], + y1: end[1] + }); + + var cx = (pos.x1 + pos.x0) / 2; + var cy = (pos.y1 + pos.y0) / 2; + var rx = (pos.x1 - pos.x0) / 2; + var ry = (pos.y1 - pos.y0) / 2; + + // make a circle when one dimension is zero + if(!rx) rx = ry = ry / SQRT2; + if(!ry) ry = rx = rx / SQRT2; + + var cell = []; + for(var i = 0; i < CIRCLE_SIDES; i++) { + var t = i * 2 * Math.PI / CIRCLE_SIDES; + cell.push([ + cx + rx * Math.cos(t), + cy + ry * Math.sin(t), + ]); + } + return cell; +} + +function ellipseOver(pos) { + var x0 = pos.x0; + var y0 = pos.y0; + var x1 = pos.x1; + var y1 = pos.y1; + + var dx = x1 - x0; + var dy = y1 - y0; + + x0 -= dx; + y0 -= dy; + + var cx = (x0 + x1) / 2; + var cy = (y0 + y1) / 2; + + var scale = SQRT2; + dx *= scale; + dy *= scale; + + return { + x0: cx - dx, + y0: cy - dy, + x1: cx + dx, + y1: cy + dy + }; +} + +function addNewShapes(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 drwStyle = gd._fullLayout.newshape; + + var plotinfo = dragOptions.plotinfo; + var xaxis = plotinfo.xaxis; + var yaxis = plotinfo.yaxis; + var onPaper = plotinfo.domain || !plotinfo || !(plotinfo.xaxis && plotinfo.yaxis); + + var isActiveShape = dragOptions.isActiveShape; + var dragmode = dragOptions.dragmode; + if(isActiveShape !== undefined) { + var id = gd._fullLayout._activeShapeIndex; + if(id < gd._fullLayout.shapes.length) { + switch(gd._fullLayout.shapes[id].type) { + case 'rect': + dragmode = 'rectdraw'; + break; + case 'circle': + dragmode = 'ellipsedraw'; + break; + case 'line': + dragmode = 'linedraw'; + break; + case 'path': + if(d[d.length - 1] === 'Z') { + dragmode = 'closedfreedraw'; + } else { + dragmode = 'openfreedraw'; + } + break; + } + } + } + var isOpenMode = openMode(dragmode); + + var newShapes = []; + var polygons = readPaths(d, plotinfo, gd._fullLayout._size, isActiveShape); + for(var i = 0; i < polygons.length; i++) { + var cell = polygons[i]; + var len = cell.length; + if( + cell[0][0] === cell[len - 1][0] && + cell[0][1] === cell[len - 1][1] + ) { + len -= 1; + } + if(len < 2) continue; + + var shape = { + editable: true, + + xref: onPaper ? 'paper' : xaxis._id, + yref: onPaper ? 'paper' : yaxis._id, + + layer: drwStyle.layer, + opacity: drwStyle.opacity, + line: { + color: drwStyle.line.color, + width: drwStyle.line.width, + dash: drwStyle.line.dash + } + }; + + if(!isOpenMode) { + shape.fillcolor = drwStyle.fillcolor; + shape.fillrule = drwStyle.fillrule; + } + + if( + dragmode === 'rectdraw' && + pointsShapeRectangle(cell, len) // should pass len here which is equal to cell.length - 1 i.e. because of the closing point + ) { + shape.type = 'rect'; + shape.x0 = cell[0][0]; + shape.y0 = cell[0][1]; + shape.x1 = cell[2][0]; + shape.y1 = cell[2][1]; + } else if( + dragmode === 'linedraw' + ) { + shape.type = 'line'; + shape.x0 = cell[0][0]; + shape.y0 = cell[0][1]; + shape.x1 = cell[1][0]; + shape.y1 = cell[1][1]; + } else if( + dragmode === 'ellipsedraw' && + (isActiveShape === false || pointsShapeEllipse(cell, len)) // should pass len here which is equal to cell.length - 1 i.e. because of the closing point + ) { + shape.type = 'circle'; // an ellipse! + var pos = {}; + if(isActiveShape === false) { + var x0 = (cell[i090][0] + cell[i270][0]) / 2; + var y0 = (cell[i000][1] + cell[i180][1]) / 2; + var rx = (cell[i270][0] - cell[i090][0] + cell[i180][0] - cell[i000][0]) / 2; + var ry = (cell[i270][1] - cell[i090][1] + cell[i180][1] - cell[i000][1]) / 2; + pos = ellipseOver({ + x0: x0, + y0: y0, + x1: x0 + rx * cos45, + y1: y0 + ry * sin45 + }); + } else { + pos = ellipseOver({ + x0: (cell[i000][0] + cell[i180][0]) / 2, + y0: (cell[i000][1] + cell[i180][1]) / 2, + x1: cell[i045][0], + y1: cell[i045][1] + }); + } + + shape.x0 = pos.x0; + shape.y0 = pos.y0; + shape.x1 = pos.x1; + shape.y1 = pos.y1; + } else { + shape.type = 'path'; + if(xaxis && yaxis) { + fixDatesOnPaths(cell, xaxis, yaxis); + } + + shape.path = writePaths([ + providePath(cell, isOpenMode) + ], isOpenMode); + } + + newShapes.push(shape); + } + + clearSelect(gd); + + var shapes; + if(newShapes.length) { + var updatedActiveShape = false; + shapes = []; + for(var q = 0; q < gd._fullLayout.shapes.length; q++) { + var beforeEdit = gd._fullLayout.shapes[q]; + shapes[q] = beforeEdit._input; + + if( + isActiveShape !== undefined && + q === gd._fullLayout._activeShapeIndex + ) { + var afterEdit = newShapes[0]; // pick first + + switch(beforeEdit.type) { + case 'line': + case 'rect': + case 'circle': + updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); + if(updatedActiveShape) { // update active shape + shapes[q].x0 = afterEdit.x0; + shapes[q].x1 = afterEdit.x1; + shapes[q].y0 = afterEdit.y0; + shapes[q].y1 = afterEdit.y1; + } + break; + + case 'path': + updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); + if(updatedActiveShape) { // update active shape + shapes[q].path = afterEdit.path; + } + break; + } + } + } + + if(isActiveShape === undefined) { + shapes = shapes.concat(newShapes); // add new shapes + } + } + + return shapes; +} + +function hasChanged(beforeEdit, afterEdit, keys) { + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + if(beforeEdit[k] !== afterEdit[k]) { + return true; + } + } + return false; +} + +module.exports = { + displayOutlines: displayOutlines, + handleEllipse: handleEllipse, + addNewShapes: addNewShapes, + readPaths: readPaths +}; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 78d00c2e94c..d1b2234e05a 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -12,13 +12,20 @@ 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 freeMode = dragHelpers.freeMode; +var rectMode = dragHelpers.rectMode; +var drawMode = dragHelpers.drawMode; +var openMode = dragHelpers.openMode; +var selectMode = dragHelpers.selectMode; var Lib = require('../../lib'); var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); -var makeEventData = require('../../components/fx/helpers').makeEventData; var getFromId = require('./axis_ids').getFromId; var clearGlCanvases = require('../../lib/clear_gl_canvases'); @@ -30,16 +37,38 @@ var MINSELECT = constants.MINSELECT; var filteredPolygon = polygon.filter; var polygonTester = polygon.tester; -function getAxId(ax) { return ax._id; } +var handleOutline = require('./handle_outline'); +var clearSelect = handleOutline.clearSelect; +var deactivateShape = handleOutline.deactivateShape; + +var newShape = require('./new_shape'); +var displayOutlines = newShape.displayOutlines; +var handleEllipse = newShape.handleEllipse; +var addNewShapes = newShape.addNewShapes; + +var helpers = require('./helpers'); +var getAxId = helpers.getAxId; +var p2r = helpers.p2r; +var axValue = helpers.axValue; +var getTransform = helpers.getTransform; function prepSelect(e, startX, startY, dragOptions, mode) { + var isFreeMode = freeMode(mode); + var isRectMode = rectMode(mode); + var isOpenMode = openMode(mode); + var isDrawMode = drawMode(mode); + var isSelectMode = selectMode(mode); + + var isLine = mode === 'linedraw'; + var isEllipse = mode === 'ellipsedraw'; + var isLineOrEllipse = isLine || isEllipse; // cases with two start & end positions + var gd = dragOptions.gd; var fullLayout = gd._fullLayout; var zoomLayer = fullLayout._zoomlayer; var dragBBox = dragOptions.element.getBoundingClientRect(); var plotinfo = dragOptions.plotinfo; - var xs = plotinfo.xaxis._offset; - var ys = plotinfo.yaxis._offset; + var transform = getTransform(plotinfo); var x0 = startX - dragBBox.left; var y0 = startY - dragBBox.top; var x1 = x0; @@ -48,23 +77,34 @@ function prepSelect(e, startX, startY, dragOptions, mode) { var pw = dragOptions.xaxes[0]._length; var ph = dragOptions.yaxes[0]._length; var allAxes = dragOptions.xaxes.concat(dragOptions.yaxes); - var subtract = e.altKey; + var subtract = e.altKey && + !(drawMode(mode) && isOpenMode); var filterPoly, selectionTester, mergedPolygons, currentPolygon; var i, searchInfo, eventData; coerceSelectionsCache(e, gd, dragOptions); - if(mode === 'lasso') { + if(isFreeMode) { filterPoly = filteredPolygon([[x0, y0]], constants.BENDPX); } - var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data([1, 2]); + var outlines = zoomLayer.selectAll('path.select-outline-' + plotinfo.id).data(isDrawMode ? [0] : [1, 2]); + var drwStyle = fullLayout.newshape; outlines.enter() .append('path') .attr('class', function(d) { return 'select-outline select-outline-' + d + ' select-outline-' + plotinfo.id; }) - .attr('transform', 'translate(' + xs + ', ' + ys + ')') + .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) + .classed('cursor-move', isDrawMode ? true : false) + .attr('transform', transform) .attr('d', path0 + 'Z'); var corners = zoomLayer.append('path') @@ -74,7 +114,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) { stroke: Color.defaultLine, 'stroke-width': 1 }) - .attr('transform', 'translate(' + xs + ', ' + ys + ')') + .attr('transform', transform) .attr('d', 'M0,0Z'); @@ -85,17 +125,6 @@ function prepSelect(e, startX, startY, dragOptions, mode) { var searchTraces = determineSearchTraces(gd, dragOptions.xaxes, dragOptions.yaxes, dragOptions.subplot); - // in v2 (once log ranges are fixed), - // we'll be able to p2r here for all axis types - function p2r(ax, v) { - return ax.type === 'log' ? ax.p2d(v) : ax.p2r(v); - } - - function axValue(ax) { - var index = (ax._id.charAt(0) === 'y') ? 1 : 0; - return function(v) { return p2r(ax, v[index]); }; - } - function ascending(a, b) { return a - b; } // allow subplots to override fillRangeItems routine @@ -104,7 +133,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) { if(plotinfo.fillRangeItems) { fillRangeItems = plotinfo.fillRangeItems; } else { - if(mode === 'select') { + if(isRectMode) { fillRangeItems = function(eventData, poly) { var ranges = eventData.range = {}; @@ -118,7 +147,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) { ].sort(ascending); } }; - } else { + } else { // case of isFreeMode fillRangeItems = function(eventData, poly, filterPoly) { var dataPts = eventData.lassoPoints = {}; @@ -137,50 +166,107 @@ function prepSelect(e, startX, startY, dragOptions, mode) { var dx = Math.abs(x1 - x0); var dy = Math.abs(y1 - y0); - if(mode === 'select') { - var direction = fullLayout.selectdirection; + if(isRectMode) { + var direction; + var start, end; - if(fullLayout.selectdirection === 'any') { - if(dy < Math.min(dx * 0.6, MINSELECT)) direction = 'h'; - else if(dx < Math.min(dy * 0.6, MINSELECT)) direction = 'v'; - else direction = 'd'; - } else { - direction = fullLayout.selectdirection; + if(isSelectMode) { + var q = fullLayout.selectdirection; + + if(q === 'any') { + if(dy < Math.min(dx * 0.6, MINSELECT)) { + direction = 'h'; + } else if(dx < Math.min(dy * 0.6, MINSELECT)) { + direction = 'v'; + } else { + direction = 'd'; + } + } else { + direction = q; + } + + switch(direction) { + case 'h': + start = isEllipse ? ph / 2 : 0; + end = ph; + break; + case 'v': + start = isEllipse ? pw / 2 : 0; + end = pw; + break; + } + } + + if(isDrawMode) { + switch(fullLayout.newshape.drawdirection) { + case 'vertical': + direction = 'h'; + start = isEllipse ? ph / 2 : 0; + end = ph; + break; + case 'horizontal': + direction = 'v'; + start = isEllipse ? pw / 2 : 0; + end = pw; + break; + case 'ortho': + if(dx < dy) { + direction = 'h'; + start = y0; + end = y1; + } else { + direction = 'v'; + start = x0; + end = x1; + } + break; + default: // i.e. case of 'diagonal' + direction = 'd'; + } } if(direction === 'h') { - // horizontal motion: make a vertical box - currentPolygon = [[x0, 0], [x0, ph], [x1, ph], [x1, 0]]; - currentPolygon.xmin = Math.min(x0, x1); - currentPolygon.xmax = Math.max(x0, x1); - currentPolygon.ymin = Math.min(0, ph); - currentPolygon.ymax = Math.max(0, ph); + // horizontal motion + currentPolygon = isLineOrEllipse ? + handleEllipse(isEllipse, [x1, start], [x1, end]) : // using x1 instead of x0 allows adjusting the line while drawing + [[x0, start], [x0, end], [x1, end], [x1, start]]; // make a vertical box + + currentPolygon.xmin = isLineOrEllipse ? x1 : Math.min(x0, x1); + currentPolygon.xmax = isLineOrEllipse ? x1 : Math.max(x0, x1); + currentPolygon.ymin = Math.min(start, end); + currentPolygon.ymax = Math.max(start, end); // extras to guide users in keeping a straight selection corners.attr('d', 'M' + currentPolygon.xmin + ',' + (y0 - MINSELECT) + 'h-4v' + (2 * MINSELECT) + 'h4Z' + 'M' + (currentPolygon.xmax - 1) + ',' + (y0 - MINSELECT) + 'h4v' + (2 * MINSELECT) + 'h-4Z'); } else if(direction === 'v') { - // vertical motion: make a horizontal box - currentPolygon = [[0, y0], [0, y1], [pw, y1], [pw, y0]]; - currentPolygon.xmin = Math.min(0, pw); - currentPolygon.xmax = Math.max(0, pw); - currentPolygon.ymin = Math.min(y0, y1); - currentPolygon.ymax = Math.max(y0, y1); + // vertical motion + currentPolygon = isLineOrEllipse ? + handleEllipse(isEllipse, [start, y1], [end, y1]) : // using y1 instead of y0 allows adjusting the line while drawing + [[start, y0], [start, y1], [end, y1], [end, y0]]; // make a horizontal box + + currentPolygon.xmin = Math.min(start, end); + currentPolygon.xmax = Math.max(start, end); + currentPolygon.ymin = isLineOrEllipse ? y1 : Math.min(y0, y1); + currentPolygon.ymax = isLineOrEllipse ? y1 : Math.max(y0, y1); corners.attr('d', 'M' + (x0 - MINSELECT) + ',' + currentPolygon.ymin + 'v-4h' + (2 * MINSELECT) + 'v4Z' + 'M' + (x0 - MINSELECT) + ',' + (currentPolygon.ymax - 1) + 'v4h' + (2 * MINSELECT) + 'v-4Z'); } else if(direction === 'd') { // diagonal motion - currentPolygon = [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]; + currentPolygon = isLineOrEllipse ? + handleEllipse(isEllipse, [x0, y0], [x1, y1]) : + [[x0, y0], [x0, y1], [x1, y1], [x1, y0]]; + currentPolygon.xmin = Math.min(x0, x1); currentPolygon.xmax = Math.max(x0, x1); currentPolygon.ymin = Math.min(y0, y1); currentPolygon.ymax = Math.max(y0, y1); corners.attr('d', 'M0,0Z'); } - } else if(mode === 'lasso') { + } else if(isFreeMode) { filterPoly.addPt([x1, y1]); currentPolygon = filterPoly.filtered; } @@ -195,47 +281,54 @@ function prepSelect(e, startX, startY, dragOptions, mode) { selectionTester = polygonTester(currentPolygon); } - // draw selection - drawSelection(mergedPolygons, outlines); + // display polygons on the screen + displayOutlines(mergedPolygons, outlines, dragOptions); + if(isSelectMode) { + throttle.throttle( + throttleID, + constants.SELECTDELAY, + function() { + selection = []; - throttle.throttle( - throttleID, - constants.SELECTDELAY, - function() { - selection = []; + var thisSelection; + var traceSelections = []; + var traceSelection; + for(i = 0; i < searchTraces.length; i++) { + searchInfo = searchTraces[i]; - 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); - traceSelection = searchInfo._module.selectPoints(searchInfo, selectionTester); - traceSelections.push(traceSelection); + thisSelection = fillSelectionItem(traceSelection, searchInfo); - thisSelection = fillSelectionItem(traceSelection, searchInfo); + if(selection.length) { + for(var j = 0; j < thisSelection.length; j++) { + selection.push(thisSelection[j]); + } + } else selection = thisSelection; + } - if(selection.length) { - for(var j = 0; j < thisSelection.length; j++) { - selection.push(thisSelection[j]); - } - } else selection = thisSelection; + eventData = {points: selection}; + updateSelectedState(gd, searchTraces, eventData); + fillRangeItems(eventData, currentPolygon, filterPoly); + dragOptions.gd.emit('plotly_selecting', eventData); } - - eventData = {points: selection}; - updateSelectedState(gd, searchTraces, eventData); - fillRangeItems(eventData, currentPolygon, filterPoly); - dragOptions.gd.emit('plotly_selecting', eventData); - } - ); + ); + } }; dragOptions.clickFn = function(numClicks, evt) { - var clickmode = fullLayout.clickmode; - corners.remove(); + if(gd._fullLayout._activeShapeIndex >= 0) { + deactivateShape(gd); + return; + } + if(isDrawMode) return; + + var clickmode = fullLayout.clickmode; + throttle.done(throttleID).then(function() { throttle.clear(throttleID); if(numClicks === 2) { @@ -291,12 +384,17 @@ function prepSelect(e, startX, startY, dragOptions, mode) { dragOptions.doneFnCompleted(selection); } }).catch(Lib.error); + + if(isDrawMode) { + clearSelectionsCache(dragOptions); + } }; } function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutlines) { var hoverData = gd._hoverdata; - var clickmode = gd._fullLayout.clickmode; + var fullLayout = gd._fullLayout; + var clickmode = fullLayout.clickmode; var sendEvents = clickmode.indexOf('event') > -1; var selection = []; var searchTraces, searchInfo, currentSelectionDef, selectionTester, traceSelection; @@ -357,7 +455,12 @@ function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutli dragOptions.selectionDefs.push(currentSelectionDef); } - if(polygonOutlines) drawSelection(dragOptions.mergedPolygons, polygonOutlines); + if(polygonOutlines) { + var polygons = dragOptions.mergedPolygons; + + // display polygons on the screen + displayOutlines(polygons, polygonOutlines, dragOptions); + } if(sendEvents) { gd.emit('plotly_selected', eventData); @@ -468,14 +571,19 @@ function multiTester(list) { } function coerceSelectionsCache(evt, gd, dragOptions) { + gd._fullLayout._drawing = false; + var fullLayout = gd._fullLayout; var plotinfo = dragOptions.plotinfo; + var dragmode = dragOptions.dragmode; var selectingOnSameSubplot = ( fullLayout._lastSelectedSubplot && fullLayout._lastSelectedSubplot === plotinfo.id ); - var hasModifierKey = evt.shiftKey || evt.altKey; + + var hasModifierKey = (evt.shiftKey || evt.altKey) && + !(drawMode(dragmode) && openMode(dragmode)); if(selectingOnSameSubplot && hasModifierKey && (plotinfo.selection && plotinfo.selection.selectionDefs) && !dragOptions.selectionDefs) { @@ -494,8 +602,32 @@ function coerceSelectionsCache(evt, gd, dragOptions) { } function clearSelectionsCache(dragOptions) { + var dragmode = dragOptions.dragmode; var plotinfo = dragOptions.plotinfo; + if(drawMode(dragmode)) { + var gd = dragOptions.gd; + if(gd._fullLayout._activeShapeIndex >= 0) { + deactivateShape(gd); + } + + var fullLayout = gd._fullLayout; + var zoomLayer = fullLayout._zoomlayer; + + var outlines = zoomLayer.selectAll('.select-outline-' + plotinfo.id); + if(outlines && gd._fullLayout._drawing) { + // add shape + var shapes = addNewShapes(outlines, dragOptions); + if(shapes) { + Registry.call('relayout', gd, { + shapes: shapes + }); + } + + gd._fullLayout._drawing = false; + } + } + plotinfo.selection = {}; plotinfo.selection.selectionDefs = dragOptions.selectionDefs = []; plotinfo.selection.mergedPolygons = dragOptions.mergedPolygons = []; @@ -549,21 +681,6 @@ function determineSearchTraces(gd, xAxes, yAxes, subplot) { } } -function drawSelection(polygons, outlines) { - var paths = []; - var i, d; - - for(i = 0; i < polygons.length; i++) { - var ppts = polygons[i]; - paths.push(ppts.join('L') + 'L' + ppts[0]); - } - - d = polygons.length > 0 ? - 'M' + paths.join('M') + 'Z' : - 'M0,0Z'; - outlines.attr('d', d); -} - function isHoverDataSet(hoverData) { return hoverData && Array.isArray(hoverData) && @@ -785,19 +902,9 @@ function fillSelectionItem(selection, searchInfo) { return selection; } -// until we get around to persistent selections, remove the outline -// here. The selection itself will be removed when the plot redraws -// at the end. -function clearSelect(gd) { - var fullLayout = gd._fullLayout || {}; - var zoomlayer = fullLayout._zoomlayer; - if(zoomlayer) { - zoomlayer.selectAll('.select-outline').remove(); - } -} - module.exports = { prepSelect: prepSelect, clearSelect: clearSelect, + clearSelectionsCache: clearSelectionsCache, selectOnClick: selectOnClick }; From 0c495e176834079ec3501b9b702fa19da213c587 Mon Sep 17 00:00:00 2001 From: archmoj Date: Sun, 19 Apr 2020 19:40:43 -0400 Subject: [PATCH 12/71] enable new modebars by default for now i.e. to test --- src/components/modebar/manage.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 267f2117392..30237448d2c 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -170,6 +170,21 @@ function getButtonGroups(gd) { dragModeGroup.push('select2d', 'lasso2d'); } + if( + // fullLayout._has('ternary') || + // fullLayout._has('mapbox') || + fullLayout._has('cartesian') + ) { + dragModeGroup.push( + 'linedraw', + 'openfreedraw', + 'closedfreedraw', + 'ellipsedraw', + 'rectdraw', + 'eraseshape' + ); + } + addGroup(dragModeGroup); addGroup(zoomGroup.concat(resetGroup)); addGroup(hoverGroup); From 104078675addbbb83c5c9f38a58e61781a0c15f7 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 11:08:41 -0400 Subject: [PATCH 13/71] improve edit open paths --- src/plots/cartesian/new_shape.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index f894902f4a5..052e1e5d76c 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -638,7 +638,8 @@ function addNewShapes(outlines, dragOptions) { dragmode = 'linedraw'; break; case 'path': - if(d[d.length - 1] === 'Z') { + var path = gd._fullLayout.shapes[id].path || ''; + if(path[path.length - 1] === 'Z') { dragmode = 'closedfreedraw'; } else { dragmode = 'openfreedraw'; @@ -651,6 +652,15 @@ function addNewShapes(outlines, dragOptions) { var newShapes = []; var polygons = readPaths(d, plotinfo, gd._fullLayout._size, isActiveShape); + if(isOpenMode) { + var last = polygons[0].length - 1; + if( // ensure first and last positions are not the same on an open path + polygons[0][0][0] === polygons[0][last][0] && + polygons[0][0][1] === polygons[0][last][1] + ) { + polygons[0].pop(); + } + } for(var i = 0; i < polygons.length; i++) { var cell = polygons[i]; var len = cell.length; From 47870fa26e6c35e96398d8c8857153071bda1a9b Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 15:19:12 -0400 Subject: [PATCH 14/71] prep to edit curves - preserve first element of points in polygons for path command e.g. M, L, etc. --- src/plots/cartesian/new_shape.js | 163 +++++++++++++++++-------------- src/plots/cartesian/select.js | 21 +++- 2 files changed, 106 insertions(+), 78 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 052e1e5d76c..205104e424a 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -39,18 +39,19 @@ var handleOutline = require('./handle_outline'); var clearOutlineControllers = handleOutline.clearOutlineControllers; var clearSelect = handleOutline.clearSelect; -function recordPositions(polygonsOut, polygonsIn) { +function recordPositions(polygonsOut, polygonsIn) { // copy & clean (i.e. skip duplicates) for(var i = 0; i < polygonsIn.length; i++) { polygonsOut[i] = []; var len = polygonsIn[i].length; - for(var j = 0; j < len; j++) { + for(var newJ = 0, j = 0; j < len; j++) { // skip close points - if(dist(polygonsIn[i][j], polygonsIn[i][(j + 1) % len]) < 1) continue; + if(j > 0 && dist(polygonsIn[i][j], polygonsIn[i][(j + 1) % len]) < 1) continue; - polygonsOut[i].push([ - polygonsIn[i][j][0], - polygonsIn[i][j][1] - ]); + polygonsOut[i][newJ] = []; + for(var k = 0; k < polygonsIn[i][newJ].length; k++) { + polygonsOut[i][newJ][k] = polygonsIn[i][j][k]; + } + newJ++; } } return polygonsOut; @@ -90,16 +91,8 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { if(isDrawMode) gd._fullLayout._drawing = true; - var paths = []; - for(var k = 0; k < polygons.length; k++) { - // create outline path - paths.push( - providePath(polygons[k], isOpenMode) - ); - } - // make outline - outlines.attr('d', writePaths(paths, isOpenMode)); + outlines.attr('d', writePaths(polygons, isOpenMode)); // add controllers var rVertexController = MINSELECT * 1.5; // bigger vertex buttons @@ -127,8 +120,8 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { function moveVertexController(dx, dy) { if(!polygons.length) return; - var x0 = copyPolygons[indexI][indexJ][0]; - var y0 = copyPolygons[indexI][indexJ][1]; + var x0 = copyPolygons[indexI][indexJ][1]; + var y0 = copyPolygons[indexI][indexJ][2]; var cell = polygons[indexI]; var len = cell.length; @@ -139,29 +132,29 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { // move other corners of rectangle var pos = cell[q]; - if(pos[0] === cell[indexJ][0]) { - pos[0] = x0 + dx; + if(pos[1] === cell[indexJ][1]) { + pos[1] = x0 + dx; } - if(pos[1] === cell[indexJ][1]) { - pos[1] = y0 + dy; + if(pos[2] === cell[indexJ][2]) { + pos[2] = y0 + dy; } } // move the corner - cell[indexJ][0] = x0 + dx; - cell[indexJ][1] = y0 + dy; + cell[indexJ][1] = x0 + dx; + cell[indexJ][2] = y0 + dy; if(!pointsShapeRectangle(cell)) { // reject result to rectangles with ensure areas for(var j = 0; j < len; j++) { - for(var k = 0; k < 2; k++) { + for(var k = 0; k < 3; k++) { cell[j][k] = copyPolygons[indexI][j][k]; } } } } else { // other polylines - cell[indexJ][0] = x0 + dx; - cell[indexJ][1] = y0 + dy; + cell[indexJ][1] = x0 + dx; + cell[indexJ][2] = y0 + dy; } redraw(); @@ -211,10 +204,10 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { var maxY; if(onRect) { // compute bounding box - minX = calcMin(cell, 0); - minY = calcMin(cell, 1); - maxX = calcMax(cell, 0); - maxY = calcMax(cell, 1); + minX = calcMin(cell, 1); + minY = calcMin(cell, 2); + maxX = calcMax(cell, 1); + maxY = calcMax(cell, 2); } vertexDragOptions[i] = []; @@ -228,8 +221,8 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { continue; } - var x = cell[j][0]; - var y = cell[j][1]; + var x = cell[j][1]; + var y = cell[j][2]; var rIcon = 3; var button = g.append(onRect ? 'rect' : 'circle') @@ -301,11 +294,11 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { for(var i = 0; i < polygons.length; i++) { for(var j = 0; j < polygons[i].length; j++) { - var x0 = copyPolygons[i][j][0]; - var y0 = copyPolygons[i][j][1]; + var x0 = copyPolygons[i][j][1]; + var y0 = copyPolygons[i][j][2]; - polygons[i][j][0] = x0 + dx; - polygons[i][j][1] = y0 + dy; + polygons[i][j][1] = x0 + dx; + polygons[i][j][2] = y0 + dy; } } } @@ -344,14 +337,25 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { } } -function providePath(cell, isOpenMode) { - return cell.join('L') + ( - isOpenMode ? '' : 'L' + cell[0] - ); -} - -function writePaths(paths, isOpenMode) { - return paths.length > 0 ? 'M' + paths.join('M') + (isOpenMode ? '' : 'Z') : 'M0,0Z'; +function writePaths(polygons, isOpenMode) { + var nI = polygons.length; + if(!nI) return 'M0,0Z'; + + var str = ''; + for(var i = 0; i < nI; i++) { + var nJ = polygons[i].length; + for(var j = 0; j < nJ; j++) { + var nK = polygons[i][j].length; + for(var k = 0; k < nK; k++) { + str += polygons[i][j][k]; + if(k > 0 && k < nK - 1) { + str += ','; + } + } + } + if(!isOpenMode) str += 'Z'; + } + return str; } function readPaths(str, plotinfo, size, isActiveShape) { @@ -378,6 +382,7 @@ function readPaths(str, plotinfo, size, isActiveShape) { var newPos = []; var c = cmd[i][0]; + var w = c; switch(c) { case 'M': newPoly(); @@ -396,18 +401,21 @@ function readPaths(str, plotinfo, size, isActiveShape) { break; case 'H': + w = 'L'; // convert to line x = +cmd[i][1]; newPos.push([x, y]); break; case 'V': + w = 'L'; // convert to line y = +cmd[i][1]; newPos.push([x, y]); break; case 'A': + w = 'L'; // convert to line (for now) var rx = +cmd[i][1]; var ry = +cmd[i][2]; if(!+cmd[i][4]) { @@ -438,21 +446,25 @@ function readPaths(str, plotinfo, size, isActiveShape) { if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { polys[n].push([ + w, x, y ]); } else if(plotinfo.domain) { polys[n].push([ + w, plotinfo.domain.x[0] + x / size.w, plotinfo.domain.y[1] - y / size.h ]); } else if(isActiveShape === false) { polys[n].push([ + w, p2r(plotinfo.xaxis, x - plotinfo.xaxis._offset), p2r(plotinfo.yaxis, y - plotinfo.yaxis._offset) ]); } else { polys[n].push([ + w, p2r(plotinfo.xaxis, x), p2r(plotinfo.yaxis, y) ]); @@ -470,8 +482,8 @@ function fixDatesOnPaths(path, xaxis, yaxis) { if(!xIsDate && !yIsDate) return path; for(var i = 0; i < path.length; i++) { - if(xIsDate) path[i][0] = path[i][0].replace(' ', '_'); - if(yIsDate) path[i][1] = path[i][1].replace(' ', '_'); + if(xIsDate) path[i][1] = path[i][1].replace(' ', '_'); + if(yIsDate) path[i][2] = path[i][2].replace(' ', '_'); } return path; @@ -482,8 +494,8 @@ function almostEq(a, b) { } function dist(a, b) { - var dx = b[0] - a[0]; - var dy = b[1] - a[1]; + var dx = b[1] - a[1]; + var dy = b[2] - a[2]; return Math.sqrt( dx * dx + dy * dy @@ -509,7 +521,7 @@ function calcMax(cell, dim) { function pointsShapeRectangle(cell, len) { if(!len) len = cell.length; if(len !== 4) return false; - for(var j = 0; j < 2; j++) { + for(var j = 1; j < 3; j++) { var e01 = cell[0][j] - cell[1][j]; var e32 = cell[3][j] - cell[2][j]; @@ -522,8 +534,8 @@ function pointsShapeRectangle(cell, len) { // N.B. rotated rectangles are not valid rects since rotation is not supported in shapes for now. if( - !almostEq(cell[0][0], cell[1][0]) && - !almostEq(cell[0][0], cell[3][0]) + !almostEq(cell[0][1], cell[1][1]) && + !almostEq(cell[0][1], cell[3][1]) ) return false; // reject cases with zero area @@ -650,23 +662,24 @@ function addNewShapes(outlines, dragOptions) { } var isOpenMode = openMode(dragmode); - var newShapes = []; var polygons = readPaths(d, plotinfo, gd._fullLayout._size, isActiveShape); if(isOpenMode) { var last = polygons[0].length - 1; if( // ensure first and last positions are not the same on an open path - polygons[0][0][0] === polygons[0][last][0] && - polygons[0][0][1] === polygons[0][last][1] + polygons[0][0][1] === polygons[0][last][1] && + polygons[0][0][2] === polygons[0][last][2] ) { polygons[0].pop(); } } + + var newShapes = []; for(var i = 0; i < polygons.length; i++) { var cell = polygons[i]; var len = cell.length; if( - cell[0][0] === cell[len - 1][0] && - cell[0][1] === cell[len - 1][1] + cell[0][1] === cell[len - 1][1] && + cell[0][2] === cell[len - 1][2] ) { len -= 1; } @@ -697,18 +710,18 @@ function addNewShapes(outlines, dragOptions) { pointsShapeRectangle(cell, len) // should pass len here which is equal to cell.length - 1 i.e. because of the closing point ) { shape.type = 'rect'; - shape.x0 = cell[0][0]; - shape.y0 = cell[0][1]; - shape.x1 = cell[2][0]; - shape.y1 = cell[2][1]; + shape.x0 = cell[0][1]; + shape.y0 = cell[0][2]; + shape.x1 = cell[2][1]; + shape.y1 = cell[2][2]; } else if( dragmode === 'linedraw' ) { shape.type = 'line'; - shape.x0 = cell[0][0]; - shape.y0 = cell[0][1]; - shape.x1 = cell[1][0]; - shape.y1 = cell[1][1]; + shape.x0 = cell[0][1]; + shape.y0 = cell[0][2]; + shape.x1 = cell[1][1]; + shape.y1 = cell[1][2]; } else if( dragmode === 'ellipsedraw' && (isActiveShape === false || pointsShapeEllipse(cell, len)) // should pass len here which is equal to cell.length - 1 i.e. because of the closing point @@ -716,10 +729,10 @@ function addNewShapes(outlines, dragOptions) { shape.type = 'circle'; // an ellipse! var pos = {}; if(isActiveShape === false) { - var x0 = (cell[i090][0] + cell[i270][0]) / 2; - var y0 = (cell[i000][1] + cell[i180][1]) / 2; - var rx = (cell[i270][0] - cell[i090][0] + cell[i180][0] - cell[i000][0]) / 2; - var ry = (cell[i270][1] - cell[i090][1] + cell[i180][1] - cell[i000][1]) / 2; + var x0 = (cell[i090][1] + cell[i270][1]) / 2; + var y0 = (cell[i000][2] + cell[i180][2]) / 2; + var rx = (cell[i270][1] - cell[i090][1] + cell[i180][1] - cell[i000][1]) / 2; + var ry = (cell[i270][2] - cell[i090][2] + cell[i180][2] - cell[i000][2]) / 2; pos = ellipseOver({ x0: x0, y0: y0, @@ -728,10 +741,10 @@ function addNewShapes(outlines, dragOptions) { }); } else { pos = ellipseOver({ - x0: (cell[i000][0] + cell[i180][0]) / 2, - y0: (cell[i000][1] + cell[i180][1]) / 2, - x1: cell[i045][0], - y1: cell[i045][1] + x0: (cell[i000][1] + cell[i180][1]) / 2, + y0: (cell[i000][2] + cell[i180][2]) / 2, + x1: cell[i045][1], + y1: cell[i045][2] }); } @@ -745,9 +758,7 @@ function addNewShapes(outlines, dragOptions) { fixDatesOnPaths(cell, xaxis, yaxis); } - shape.path = writePaths([ - providePath(cell, isOpenMode) - ], isOpenMode); + shape.path = writePaths([cell], isOpenMode); } newShapes.push(shape); diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index d1b2234e05a..7ec21d128f4 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -282,7 +282,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) { } // display polygons on the screen - displayOutlines(mergedPolygons, outlines, dragOptions); + displayOutlines(convertPoly(mergedPolygons), outlines, dragOptions); if(isSelectMode) { throttle.throttle( @@ -459,7 +459,7 @@ function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutli var polygons = dragOptions.mergedPolygons; // display polygons on the screen - displayOutlines(polygons, polygonOutlines, dragOptions); + displayOutlines(convertPoly(polygons), polygonOutlines, dragOptions); } if(sendEvents) { @@ -902,6 +902,23 @@ function fillSelectionItem(selection, searchInfo) { return selection; } +function convertPoly(polygonsIn) { // add M and L command to draft positions + var polygonsOut = []; + for(var i = 0; i < polygonsIn.length; i++) { + polygonsOut[i] = []; + for(var j = 0; j < polygonsIn[i].length; j++) { + polygonsOut[i][j] = []; + polygonsOut[i][j][0] = j ? 'L' : 'M'; + for(var k = 0; k < polygonsIn[i][j].length; k++) { + polygonsOut[i][j].push( + polygonsIn[i][j][k] + ); + } + } + } + return polygonsOut; +} + module.exports = { prepSelect: prepSelect, clearSelect: clearSelect, From 110618091c483129cdf13ec1ac8d053a2f19549d Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 17:19:41 -0400 Subject: [PATCH 15/71] towards better handling of closing point --- src/components/shapes/draw.js | 1 - src/plots/cartesian/new_shape.js | 151 ++++++++---------- src/plots/cartesian/select.js | 16 +- test/jasmine/tests/cartesian_interact_test.js | 4 +- test/jasmine/tests/select_test.js | 24 +-- 5 files changed, 97 insertions(+), 99 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index e6a61eda5e0..8b4e5d2d078 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -144,7 +144,6 @@ function drawOne(gd, index) { element: path.node(), plotinfo: plotinfo, gd: gd, - dragmode: gd._fullLayout.dragmode, isActiveShape: true // i.e. to enable controllers }; diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 205104e424a..b63ec17174a 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -39,27 +39,21 @@ var handleOutline = require('./handle_outline'); var clearOutlineControllers = handleOutline.clearOutlineControllers; var clearSelect = handleOutline.clearSelect; -function recordPositions(polygonsOut, polygonsIn) { // copy & clean (i.e. skip duplicates) +function recordPositions(polygonsOut, polygonsIn) { for(var i = 0; i < polygonsIn.length; i++) { + var cell = polygonsIn[i]; polygonsOut[i] = []; - var len = polygonsIn[i].length; - for(var newJ = 0, j = 0; j < len; j++) { - // skip close points - if(j > 0 && dist(polygonsIn[i][j], polygonsIn[i][(j + 1) % len]) < 1) continue; - - polygonsOut[i][newJ] = []; - for(var k = 0; k < polygonsIn[i][newJ].length; k++) { - polygonsOut[i][newJ][k] = polygonsIn[i][j][k]; + for(var j = 0; j < cell.length; j++) { + polygonsOut[i][j] = []; + for(var k = 0; k < cell[j].length; k++) { + polygonsOut[i][j][k] = cell[j][k]; } - newJ++; } } return polygonsOut; } -function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { - var polygons = recordPositions([], polygonsIn); - +function displayOutlines(polygons, outlines, dragOptions, nCalls) { if(!nCalls) nCalls = 0; var gd = dragOptions.gd; @@ -87,12 +81,11 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { var dragmode = dragOptions.dragmode; var isDrawMode = drawMode(dragmode); - var isOpenMode = openMode(dragmode); if(isDrawMode) gd._fullLayout._drawing = true; // make outline - outlines.attr('d', writePaths(polygons, isOpenMode)); + outlines.attr('d', writePaths(polygons)); // add controllers var rVertexController = MINSELECT * 1.5; // bigger vertex buttons @@ -337,7 +330,7 @@ function displayOutlines(polygonsIn, outlines, dragOptions, nCalls) { } } -function writePaths(polygons, isOpenMode) { +function writePaths(polygons) { var nI = polygons.length; if(!nI) return 'M0,0Z'; @@ -345,16 +338,21 @@ function writePaths(polygons, isOpenMode) { for(var i = 0; i < nI; i++) { var nJ = polygons[i].length; for(var j = 0; j < nJ; j++) { - var nK = polygons[i][j].length; - for(var k = 0; k < nK; k++) { - str += polygons[i][j][k]; - if(k > 0 && k < nK - 1) { - str += ','; + if(polygons[i][j][0] === 'Z') { + str += 'Z'; + break; + } else { + var nK = polygons[i][j].length; + for(var k = 0; k < nK; k++) { + str += polygons[i][j][k]; + if(k > 0 && k < nK - 1) { + str += ','; + } } } } - if(!isOpenMode) str += 'Z'; } + return str; } @@ -434,41 +432,44 @@ function readPaths(str, plotinfo, size, isActiveShape) { } break; - } - if(c === 'Z') { - x = initX; - y = initY; - } else { - for(var j = 0; j < newPos.length; j++) { - x = newPos[j][0]; - y = newPos[j][1]; - - if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { - polys[n].push([ - w, - x, - y - ]); - } else if(plotinfo.domain) { - polys[n].push([ - w, - plotinfo.domain.x[0] + x / size.w, - plotinfo.domain.y[1] - y / size.h - ]); - } else if(isActiveShape === false) { - polys[n].push([ - w, - p2r(plotinfo.xaxis, x - plotinfo.xaxis._offset), - p2r(plotinfo.yaxis, y - plotinfo.yaxis._offset) - ]); - } else { - polys[n].push([ - w, - p2r(plotinfo.xaxis, x), - p2r(plotinfo.yaxis, y) - ]); + case 'Z': + if(x !== initX || y !== initY) { + x = initX; + y = initY; + newPos.push([x, y]); } + break; + } + + for(var j = 0; j < newPos.length; j++) { + x = newPos[j][0]; + y = newPos[j][1]; + + if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { + polys[n].push([ + w, + x, + y + ]); + } else if(plotinfo.domain) { + polys[n].push([ + w, + plotinfo.domain.x[0] + x / size.w, + plotinfo.domain.y[1] - y / size.h + ]); + } else if(isActiveShape === false) { + polys[n].push([ + w, + p2r(plotinfo.xaxis, x - plotinfo.xaxis._offset), + p2r(plotinfo.yaxis, y - plotinfo.yaxis._offset) + ]); + } else { + polys[n].push([ + w, + p2r(plotinfo.xaxis, x), + p2r(plotinfo.yaxis, y) + ]); } } } @@ -518,9 +519,10 @@ function calcMax(cell, dim) { return v; } -function pointsShapeRectangle(cell, len) { - if(!len) len = cell.length; - if(len !== 4) return false; +function pointsShapeRectangle(cell) { + var len = cell.length; + if(len !== 5) return false; + for(var j = 1; j < 3; j++) { var e01 = cell[0][j] - cell[1][j]; var e32 = cell[3][j] - cell[2][j]; @@ -545,10 +547,12 @@ function pointsShapeRectangle(cell, len) { ); } -function pointsShapeEllipse(cell, len) { - if(!len) len = cell.length; - if(len !== CIRCLE_SIDES) return false; +function pointsShapeEllipse(cell) { + var len = cell.length; + if(len !== CIRCLE_SIDES + 1) return false; + // opposite diagonals should be the same + len = CIRCLE_SIDES; for(var i = 0; i < len; i++) { var k = (len * 2 - i) % len; @@ -660,30 +664,15 @@ function addNewShapes(outlines, dragOptions) { } } } + var isOpenMode = openMode(dragmode); var polygons = readPaths(d, plotinfo, gd._fullLayout._size, isActiveShape); - if(isOpenMode) { - var last = polygons[0].length - 1; - if( // ensure first and last positions are not the same on an open path - polygons[0][0][1] === polygons[0][last][1] && - polygons[0][0][2] === polygons[0][last][2] - ) { - polygons[0].pop(); - } - } var newShapes = []; for(var i = 0; i < polygons.length; i++) { var cell = polygons[i]; - var len = cell.length; - if( - cell[0][1] === cell[len - 1][1] && - cell[0][2] === cell[len - 1][2] - ) { - len -= 1; - } - if(len < 2) continue; + if(cell.length < 2) continue; var shape = { editable: true, @@ -707,7 +696,7 @@ function addNewShapes(outlines, dragOptions) { if( dragmode === 'rectdraw' && - pointsShapeRectangle(cell, len) // should pass len here which is equal to cell.length - 1 i.e. because of the closing point + pointsShapeRectangle(cell) ) { shape.type = 'rect'; shape.x0 = cell[0][1]; @@ -724,7 +713,7 @@ function addNewShapes(outlines, dragOptions) { shape.y1 = cell[1][2]; } else if( dragmode === 'ellipsedraw' && - (isActiveShape === false || pointsShapeEllipse(cell, len)) // should pass len here which is equal to cell.length - 1 i.e. because of the closing point + (isActiveShape === false || pointsShapeEllipse(cell)) ) { shape.type = 'circle'; // an ellipse! var pos = {}; @@ -758,7 +747,7 @@ function addNewShapes(outlines, dragOptions) { fixDatesOnPaths(cell, xaxis, yaxis); } - shape.path = writePaths([cell], isOpenMode); + shape.path = writePaths([cell]); } newShapes.push(shape); diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 7ec21d128f4..14b60b603b8 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -282,7 +282,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) { } // display polygons on the screen - displayOutlines(convertPoly(mergedPolygons), outlines, dragOptions); + displayOutlines(convertPoly(mergedPolygons, isOpenMode), outlines, dragOptions); if(isSelectMode) { throttle.throttle( @@ -457,9 +457,10 @@ function selectOnClick(evt, gd, xAxes, yAxes, subplot, dragOptions, polygonOutli if(polygonOutlines) { var polygons = dragOptions.mergedPolygons; + var isOpenMode = openMode(dragOptions.dragmode); // display polygons on the screen - displayOutlines(convertPoly(polygons), polygonOutlines, dragOptions); + displayOutlines(convertPoly(polygons, isOpenMode), polygonOutlines, dragOptions); } if(sendEvents) { @@ -902,7 +903,7 @@ function fillSelectionItem(selection, searchInfo) { return selection; } -function convertPoly(polygonsIn) { // add M and L command to draft positions +function convertPoly(polygonsIn, isOpenMode) { // add M and L command to draft positions var polygonsOut = []; for(var i = 0; i < polygonsIn.length; i++) { polygonsOut[i] = []; @@ -915,7 +916,16 @@ function convertPoly(polygonsIn) { // add M and L command to draft positions ); } } + + if(!isOpenMode) { + polygonsOut[i].push([ + 'Z', + polygonsOut[i][0][1], // initial x + polygonsOut[i][0][2] // initial y + ]); + } } + return polygonsOut; } diff --git a/test/jasmine/tests/cartesian_interact_test.js b/test/jasmine/tests/cartesian_interact_test.js index e3f2ea51cb6..2fdd891f83b 100644 --- a/test/jasmine/tests/cartesian_interact_test.js +++ b/test/jasmine/tests/cartesian_interact_test.js @@ -1946,7 +1946,7 @@ describe('axis zoom/pan and main plot zoom', function() { hasDragData: true, selectingCnt: 1, selectedCnt: 0, - selectOutline: 'M20,20L20,220L220,220L220,20L20,20Z' + selectOutline: 'M20,20L20,220L220,220L220,20Z' })) .then(delay(100)) .then(_assert('while holding on mouse', { @@ -1954,7 +1954,7 @@ describe('axis zoom/pan and main plot zoom', function() { hasDragData: true, selectingCnt: 1, selectedCnt: 0, - selectOutline: 'M20,20L20,220L220,220L220,20L20,20Z' + selectOutline: 'M20,20L20,220L220,220L220,20Z' })) .then(drag.end); }) diff --git a/test/jasmine/tests/select_test.js b/test/jasmine/tests/select_test.js index ecf4f0689e3..fbcc5261fa3 100644 --- a/test/jasmine/tests/select_test.js +++ b/test/jasmine/tests/select_test.js @@ -1646,13 +1646,13 @@ describe('Test select box and lasso in general:', function() { .then(_drag(path1)) .then(function() { _assert('select path1', { - outline: [[150, 150], [150, 170], [170, 170], [170, 150], [150, 150]] + outline: [[150, 150], [150, 170], [170, 170], [170, 150]] }); }) .then(_drag(path2)) .then(function() { _assert('select path2', { - outline: [[193, 0], [193, 500], [213, 500], [213, 0], [193, 0]] + outline: [[193, 0], [193, 500], [213, 500], [213, 0]] }); }) .then(_drag(path1)) @@ -1660,8 +1660,8 @@ describe('Test select box and lasso in general:', function() { .then(function() { _assert('select path1+path2', { outline: [ - [170, 170], [170, 150], [150, 150], [150, 170], [170, 170], - [213, 500], [213, 0], [193, 0], [193, 500], [213, 500] + [170, 170], [170, 150], [150, 150], [150, 170], + [213, 500], [213, 0], [193, 0], [193, 500] ] }); }) @@ -1678,16 +1678,16 @@ describe('Test select box and lasso in general:', function() { // merged with previous 'select' polygon _assert('after shift lasso', { outline: [ - [170, 170], [170, 150], [150, 150], [150, 170], [170, 170], - [213, 500], [213, 0], [193, 0], [193, 500], [213, 500], - [335, 243], [328, 169], [316, 171], [318, 239], [335, 243] + [170, 170], [170, 150], [150, 150], [150, 170], + [213, 500], [213, 0], [193, 0], [193, 500], + [335, 243], [328, 169], [316, 171], [318, 239] ] }); }) .then(_drag(lassoPath)) .then(function() { _assert('after lasso (no-shift)', { - outline: [[316, 171], [318, 239], [335, 243], [328, 169], [316, 171]] + outline: [[316, 171], [318, 239], [335, 243], [328, 169]] }); }) .then(function() { @@ -1706,15 +1706,15 @@ describe('Test select box and lasso in general:', function() { .then(function() { // this used to merged 'lasso' polygons before (see #2669) _assert('shift select path1 after pan', { - outline: [[150, 150], [150, 170], [170, 170], [170, 150], [150, 150]] + outline: [[150, 150], [150, 170], [170, 170], [170, 150]] }); }) .then(_drag(path2, {shiftKey: true})) .then(function() { _assert('shift select path1+path2 after pan', { outline: [ - [170, 170], [170, 150], [150, 150], [150, 170], [170, 170], - [213, 500], [213, 0], [193, 0], [193, 500], [213, 500] + [170, 170], [170, 150], [150, 150], [150, 170], + [213, 500], [213, 0], [193, 0], [193, 500] ] }); }) @@ -1725,7 +1725,7 @@ describe('Test select box and lasso in general:', function() { .then(_drag(path1, {shiftKey: true})) .then(function() { _assert('shift select path1 after scroll', { - outline: [[150, 150], [150, 170], [170, 170], [170, 150], [150, 150]] + outline: [[150, 150], [150, 170], [170, 170], [170, 150]] }); }) .catch(failTest) From a58539412618d497cd4d24e7f6912431f2e9328d Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 17:45:22 -0400 Subject: [PATCH 16/71] rename new dragmodes as well as modebar button names --- src/components/dragelement/helpers.js | 24 ++++++++-------- src/components/fx/layout_attributes.js | 10 +++---- src/components/modebar/buttons.js | 40 +++++++++++++------------- src/components/modebar/manage.js | 10 +++---- src/fonts/ploticon.js | 10 +++---- src/plots/cartesian/new_shape.js | 16 +++++------ src/plots/cartesian/select.js | 4 +-- src/plots/layout_attributes.js | 2 +- 8 files changed, 58 insertions(+), 58 deletions(-) diff --git a/src/components/dragelement/helpers.js b/src/components/dragelement/helpers.js index 819b0accbf9..ce10ba5b9f2 100644 --- a/src/components/dragelement/helpers.js +++ b/src/components/dragelement/helpers.js @@ -17,35 +17,35 @@ exports.selectMode = function(dragmode) { exports.drawMode = function(dragmode) { return ( - dragmode === 'closedfreedraw' || - dragmode === 'openfreedraw' || - dragmode === 'linedraw' || - dragmode === 'rectdraw' || - dragmode === 'ellipsedraw' + dragmode === 'drawclosedpath' || + dragmode === 'drawopenpath' || + dragmode === 'drawline' || + dragmode === 'drawrect' || + dragmode === 'drawcircle' ); }; exports.openMode = function(dragmode) { return ( - dragmode === 'linedraw' || - dragmode === 'openfreedraw' + dragmode === 'drawline' || + dragmode === 'drawopenpath' ); }; exports.rectMode = function(dragmode) { return ( dragmode === 'select' || - dragmode === 'linedraw' || - dragmode === 'rectdraw' || - dragmode === 'ellipsedraw' + dragmode === 'drawline' || + dragmode === 'drawrect' || + dragmode === 'drawcircle' ); }; exports.freeMode = function(dragmode) { return ( dragmode === 'lasso' || - dragmode === 'closedfreedraw' || - dragmode === 'openfreedraw' + dragmode === 'drawclosedpath' || + dragmode === 'drawopenpath' ); }; diff --git a/src/components/fx/layout_attributes.js b/src/components/fx/layout_attributes.js index dbef7fc3398..8e6735f092f 100644 --- a/src/components/fx/layout_attributes.js +++ b/src/components/fx/layout_attributes.js @@ -49,11 +49,11 @@ module.exports = { 'pan', 'select', 'lasso', - 'closedfreedraw', - 'openfreedraw', - 'linedraw', - 'rectdraw', - 'ellipsedraw', + 'drawclosedpath', + 'drawopenpath', + 'drawline', + 'drawrect', + 'drawcircle', 'orbit', 'turntable', false diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index e1e931cddf1..3e156b09af2 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -134,48 +134,48 @@ modeBarButtons.lasso2d = { click: handleCartesian }; -modeBarButtons.closedfreedraw = { - name: 'closedfreedraw', +modeBarButtons.drawclosedpath = { + name: 'drawclosedpath', title: function(gd) { return _(gd, 'Draw Closed Freeform'); }, attr: 'dragmode', - val: 'closedfreedraw', - icon: Icons.closedfreedraw, + val: 'drawclosedpath', + icon: Icons.drawclosedpath, click: handleCartesian }; -modeBarButtons.openfreedraw = { - name: 'openfreedraw', +modeBarButtons.drawopenpath = { + name: 'drawopenpath', title: function(gd) { return _(gd, 'Draw Open Freeform'); }, attr: 'dragmode', - val: 'openfreedraw', - icon: Icons.openfreedraw, + val: 'drawopenpath', + icon: Icons.drawopenpath, click: handleCartesian }; -modeBarButtons.linedraw = { - name: 'linedraw', +modeBarButtons.drawline = { + name: 'drawline', title: function(gd) { return _(gd, 'Draw Line'); }, attr: 'dragmode', - val: 'linedraw', - icon: Icons.linedraw, + val: 'drawline', + icon: Icons.drawline, click: handleCartesian }; -modeBarButtons.rectdraw = { - name: 'rectdraw', +modeBarButtons.drawrect = { + name: 'drawrect', title: function(gd) { return _(gd, 'Draw Rectangle'); }, attr: 'dragmode', - val: 'rectdraw', - icon: Icons.rectdraw, + val: 'drawrect', + icon: Icons.drawrect, click: handleCartesian }; -modeBarButtons.ellipsedraw = { - name: 'ellipsedraw', +modeBarButtons.drawcircle = { + name: 'drawcircle', title: function(gd) { return _(gd, 'Draw Ellipse'); }, attr: 'dragmode', - val: 'ellipsedraw', - icon: Icons.ellipsedraw, + val: 'drawcircle', + icon: Icons.drawcircle, click: handleCartesian }; diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 30237448d2c..71437d7f757 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -176,11 +176,11 @@ function getButtonGroups(gd) { fullLayout._has('cartesian') ) { dragModeGroup.push( - 'linedraw', - 'openfreedraw', - 'closedfreedraw', - 'ellipsedraw', - 'rectdraw', + 'drawline', + 'drawopenpath', + 'drawclosedpath', + 'drawcircle', + 'drawrect', 'eraseshape' ); } diff --git a/src/fonts/ploticon.js b/src/fonts/ploticon.js index 76597da89c8..814cb16364d 100644 --- a/src/fonts/ploticon.js +++ b/src/fonts/ploticon.js @@ -111,13 +111,13 @@ module.exports = { 'path': 'm214-7h429v214h-429v-214z m500 0h72v500q0 8-6 21t-11 20l-157 156q-5 6-19 12t-22 5v-232q0-22-15-38t-38-16h-322q-22 0-37 16t-16 38v232h-72v-714h72v232q0 22 16 38t37 16h465q22 0 38-16t15-38v-232z m-214 518v178q0 8-5 13t-13 5h-107q-7 0-13-5t-5-13v-178q0-8 5-13t13-5h107q7 0 13 5t5 13z m357-18v-518q0-22-15-38t-38-16h-750q-23 0-38 16t-16 38v750q0 22 16 38t38 16h517q23 0 50-12t42-26l156-157q16-15 27-42t11-49z', 'transform': 'matrix(1 0 0 -1 0 850)' }, - 'openfreedraw': { + 'drawopenpath': { 'width': 70, 'height': 70, 'path': 'M33.21,85.65a7.31,7.31,0,0,1-2.59-.48c-8.16-3.11-9.27-19.8-9.88-41.3-.1-3.58-.19-6.68-.35-9-.15-2.1-.67-3.48-1.43-3.79-2.13-.88-7.91,2.32-12,5.86L3,32.38c1.87-1.64,11.55-9.66,18.27-6.9,2.13.87,4.75,3.14,5.17,9,.17,2.43.26,5.59.36,9.25a224.17,224.17,0,0,0,1.5,23.4c1.54,10.76,4,12.22,4.48,12.4.84.32,2.79-.46,5.76-3.59L43,80.07C41.53,81.57,37.68,85.64,33.21,85.65ZM74.81,69a11.34,11.34,0,0,0,6.09-6.72L87.26,44.5,74.72,32,56.9,38.35c-2.37.86-5.57,3.42-6.61,6L38.65,72.14l8.42,8.43ZM55,46.27a7.91,7.91,0,0,1,3.64-3.17l14.8-5.3,8,8L76.11,60.6l-.06.19a6.37,6.37,0,0,1-3,3.43L48.25,74.59,44.62,71Zm16.57,7.82A6.9,6.9,0,1,0,64.64,61,6.91,6.91,0,0,0,71.54,54.09Zm-4.05,0a2.85,2.85,0,1,1-2.85-2.85A2.86,2.86,0,0,1,67.49,54.09Zm-4.13,5.22L60.5,56.45,44.26,72.7l2.86,2.86ZM97.83,35.67,84.14,22l-8.57,8.57L89.26,44.24Zm-13.69-8,8,8-2.85,2.85-8-8Z', 'transform': 'matrix(1 0 0 1 -15 -15)' }, - 'closedfreedraw': { + 'drawclosedpath': { 'width': 90, 'height': 90, 'path': 'M88.41,21.12a26.56,26.56,0,0,0-36.18,0l-2.07,2-2.07-2a26.57,26.57,0,0,0-36.18,0,23.74,23.74,0,0,0,0,34.8L48,90.12a3.22,3.22,0,0,0,4.42,0l36-34.21a23.73,23.73,0,0,0,0-34.79ZM84,51.24,50.16,83.35,16.35,51.25a17.28,17.28,0,0,1,0-25.47,20,20,0,0,1,27.3,0l4.29,4.07a3.23,3.23,0,0,0,4.44,0l4.29-4.07a20,20,0,0,1,27.3,0,17.27,17.27,0,0,1,0,25.46ZM66.76,47.68h-33v6.91h33ZM53.35,35H46.44V68h6.91Z', @@ -135,19 +135,19 @@ module.exports = { 'path': 'm0 850l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-285l0-143 143 0 0 143-143 0z m857 0l0-143 143 0 0 143-143 0z m-857-286l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z m285 0l0-143 143 0 0 143-143 0z m286 0l0-143 143 0 0 143-143 0z', 'transform': 'matrix(1 0 0 -1 0 850)' }, - 'linedraw': { + 'drawline': { 'width': 70, 'height': 70, 'path': 'M60.64,62.3a11.29,11.29,0,0,0,6.09-6.72l6.35-17.72L60.54,25.31l-17.82,6.4c-2.36.86-5.57,3.41-6.6,6L24.48,65.5l8.42,8.42ZM40.79,39.63a7.89,7.89,0,0,1,3.65-3.17l14.79-5.31,8,8L61.94,54l-.06.19a6.44,6.44,0,0,1-3,3.43L34.07,68l-3.62-3.63Zm16.57,7.81a6.9,6.9,0,1,0-6.89,6.9A6.9,6.9,0,0,0,57.36,47.44Zm-4,0a2.86,2.86,0,1,1-2.85-2.85A2.86,2.86,0,0,1,53.32,47.44Zm-4.13,5.22L46.33,49.8,30.08,66.05l2.86,2.86ZM83.65,29,70,15.34,61.4,23.9,75.09,37.59ZM70,21.06l8,8-2.84,2.85-8-8ZM87,80.49H10.67V87H87Z', 'transform': 'matrix(1 0 0 1 -15 -15)' }, - 'rectdraw': { + 'drawrect': { 'width': 80, 'height': 80, 'path': 'M78,22V79H21V22H78m9-9H12V88H87V13ZM68,46.22H31V54H68ZM53,32H45.22V69H53Z', 'transform': 'matrix(1 0 0 1 -10 -10)' }, - 'ellipsedraw': { + 'drawcircle': { 'width': 80, 'height': 80, 'path': 'M50,84.72C26.84,84.72,8,69.28,8,50.3S26.84,15.87,50,15.87,92,31.31,92,50.3,73.16,84.72,50,84.72Zm0-60.59c-18.6,0-33.74,11.74-33.74,26.17S31.4,76.46,50,76.46,83.74,64.72,83.74,50.3,68.6,24.13,50,24.13Zm17.15,22h-34v7.11h34Zm-13.8-13H46.24v34h7.11Z', diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index b63ec17174a..4ab59428099 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -645,20 +645,20 @@ function addNewShapes(outlines, dragOptions) { if(id < gd._fullLayout.shapes.length) { switch(gd._fullLayout.shapes[id].type) { case 'rect': - dragmode = 'rectdraw'; + dragmode = 'drawrect'; break; case 'circle': - dragmode = 'ellipsedraw'; + dragmode = 'drawcircle'; break; case 'line': - dragmode = 'linedraw'; + dragmode = 'drawline'; break; case 'path': var path = gd._fullLayout.shapes[id].path || ''; if(path[path.length - 1] === 'Z') { - dragmode = 'closedfreedraw'; + dragmode = 'drawclosedpath'; } else { - dragmode = 'openfreedraw'; + dragmode = 'drawopenpath'; } break; } @@ -695,7 +695,7 @@ function addNewShapes(outlines, dragOptions) { } if( - dragmode === 'rectdraw' && + dragmode === 'drawrect' && pointsShapeRectangle(cell) ) { shape.type = 'rect'; @@ -704,7 +704,7 @@ function addNewShapes(outlines, dragOptions) { shape.x1 = cell[2][1]; shape.y1 = cell[2][2]; } else if( - dragmode === 'linedraw' + dragmode === 'drawline' ) { shape.type = 'line'; shape.x0 = cell[0][1]; @@ -712,7 +712,7 @@ function addNewShapes(outlines, dragOptions) { shape.x1 = cell[1][1]; shape.y1 = cell[1][2]; } else if( - dragmode === 'ellipsedraw' && + dragmode === 'drawcircle' && (isActiveShape === false || pointsShapeEllipse(cell)) ) { shape.type = 'circle'; // an ellipse! diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 14b60b603b8..ab51d9af774 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -59,8 +59,8 @@ function prepSelect(e, startX, startY, dragOptions, mode) { var isDrawMode = drawMode(mode); var isSelectMode = selectMode(mode); - var isLine = mode === 'linedraw'; - var isEllipse = mode === 'ellipsedraw'; + var isLine = mode === 'drawline'; + var isEllipse = mode === 'drawcircle'; var isLineOrEllipse = isLine || isEllipse; // cases with two start & end positions var gd = dragOptions.gd; diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 531a909c2bf..825d133d785 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -514,7 +514,7 @@ module.exports = { dflt: 'diagonal', editType: 'none', description: [ - 'When `dragmode` is set to *rectdraw*, *linedraw* or *ellipsedraw*', + '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.', From 092a80e5976566ab15211e14926affb9ca99033b Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 17:51:24 -0400 Subject: [PATCH 17/71] improve shape type defaults considering path in template --- src/components/shapes/defaults.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index f113828e1c7..183d93ea9b2 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -32,8 +32,10 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { var visible = coerce('visible'); if(!visible) return; - var dfltType = shapeIn.path ? 'path' : 'rect'; + var path = coerce('path'); + var dfltType = path ? 'path' : 'rect'; var shapeType = coerce('type', dfltType); + if(shapeOut.type !== 'path') delete shapeOut.path; coerce('editable'); coerce('layer'); From 31209e7bcb96ce72d524e3e18ad2bb31bc92dfc2 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 18:46:17 -0400 Subject: [PATCH 18/71] init handle svg curves edits --- src/plots/cartesian/new_shape.js | 118 ++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 43 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 4ab59428099..492995556c7 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -140,7 +140,7 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { if(!pointsShapeRectangle(cell)) { // reject result to rectangles with ensure areas for(var j = 0; j < len; j++) { - for(var k = 0; k < 3; k++) { + for(var k = 0; k < cell[j].length; k++) { cell[j][k] = copyPolygons[indexI][j][k]; } } @@ -330,6 +330,9 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { } } +var iC = [0, 3, 4, 5, 6, 1, 2]; +var iQS = [0, 3, 4, 1, 2]; + function writePaths(polygons) { var nI = polygons.length; if(!nI) return 'M0,0Z'; @@ -338,13 +341,21 @@ function writePaths(polygons) { for(var i = 0; i < nI; i++) { var nJ = polygons[i].length; for(var j = 0; j < nJ; j++) { - if(polygons[i][j][0] === 'Z') { + var w = polygons[i][j][0]; + if(w === 'Z') { str += 'Z'; break; } else { var nK = polygons[i][j].length; for(var k = 0; k < nK; k++) { - str += polygons[i][j][k]; + var realK = k; + if(w === 'Q' || w === 'S') { + realK = iQS[k]; + } else if(w === 'C') { + realK = iC[k]; + } + + str += polygons[i][j][realK]; if(k > 0 && k < nK - 1) { str += ','; } @@ -366,6 +377,7 @@ function readPaths(str, plotinfo, size, isActiveShape) { polys[n] = []; }; + var k; var x = 0; var y = 0; var initX; @@ -379,6 +391,8 @@ function readPaths(str, plotinfo, size, isActiveShape) { for(var i = 0; i < cmd.length; i++) { var newPos = []; + var x1, x2, y1, y2; // i.e. extra params for curves + var c = cmd[i][0]; var w = c; switch(c) { @@ -386,34 +400,51 @@ function readPaths(str, plotinfo, size, isActiveShape) { newPoly(); x = +cmd[i][1]; y = +cmd[i][2]; - newPos.push([x, y]); + newPos.push([w, x, y]); recStart(); break; + case 'Q': + case 'S': + x1 = +cmd[i][1]; + y1 = +cmd[i][2]; + x = +cmd[i][3]; + y = +cmd[i][4]; + newPos.push([w, x, y, x1, y1]); // -> iQS order + break; + + case 'C': + x1 = +cmd[i][1]; + y1 = +cmd[i][2]; + x2 = +cmd[i][3]; + y2 = +cmd[i][4]; + x = +cmd[i][5]; + y = +cmd[i][6]; + newPos.push([w, x, y, x1, y1, x2, y2]); // -> iC order + break; + + case 'T': case 'L': x = +cmd[i][1]; y = +cmd[i][2]; - newPos.push([x, y]); - + newPos.push([w, x, y]); break; case 'H': - w = 'L'; // convert to line + w = 'L'; // convert to line (for now) x = +cmd[i][1]; - newPos.push([x, y]); - + newPos.push([w, x, y]); break; case 'V': - w = 'L'; // convert to line + w = 'L'; // convert to line (for now) y = +cmd[i][1]; - newPos.push([x, y]); - + newPos.push([w, x, y]); break; case 'A': - w = 'L'; // convert to line (for now) + w = 'L'; // convert to line to handle circle var rx = +cmd[i][1]; var ry = +cmd[i][2]; if(!+cmd[i][4]) { @@ -423,54 +454,55 @@ function readPaths(str, plotinfo, size, isActiveShape) { var cenX = x - rx; var cenY = y; - for(var k = 1; k <= CIRCLE_SIDES / 2; k++) { + for(k = 1; k <= CIRCLE_SIDES / 2; k++) { var t = 2 * Math.PI * k / CIRCLE_SIDES; newPos.push([ + w, cenX + rx * Math.cos(t), cenY + ry * Math.sin(t) ]); } - break; case 'Z': if(x !== initX || y !== initY) { x = initX; y = initY; - newPos.push([x, y]); + newPos.push([w, x, y]); } break; } for(var j = 0; j < newPos.length; j++) { - x = newPos[j][0]; - y = newPos[j][1]; - - if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { - polys[n].push([ - w, - x, - y - ]); - } else if(plotinfo.domain) { - polys[n].push([ - w, - plotinfo.domain.x[0] + x / size.w, - plotinfo.domain.y[1] - y / size.h - ]); - } else if(isActiveShape === false) { - polys[n].push([ - w, - p2r(plotinfo.xaxis, x - plotinfo.xaxis._offset), - p2r(plotinfo.yaxis, y - plotinfo.yaxis._offset) - ]); - } else { - polys[n].push([ - w, - p2r(plotinfo.xaxis, x), - p2r(plotinfo.yaxis, y) - ]); + for(k = 0; k <= 4; k += 2) { + var _x = newPos[j][k + 1]; + var _y = newPos[j][k + 2]; + + if(_x === undefined || _y === undefined) continue; + if(k === 0) { + x = _x; + y = _y; + } + + if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { + // no change + } else if(plotinfo.domain) { + _x = plotinfo.domain.x[0] + _x / size.w; + _y = plotinfo.domain.y[1] - _y / size.h; + } else if(isActiveShape === false) { + _x = p2r(plotinfo.xaxis, _x - plotinfo.xaxis._offset); + _y = p2r(plotinfo.yaxis, _y - plotinfo.yaxis._offset); + } else { + _x = p2r(plotinfo.xaxis, _x); + _y = p2r(plotinfo.yaxis, _y); + } + + newPos[j][k + 1] = _x; + newPos[j][k + 2] = _y; } + polys[n].push( + newPos[j].slice() + ); } } From feb7ab12fa3539cd355b2e6a62e58c4b95bb6fea Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 20 Apr 2020 22:45:42 -0400 Subject: [PATCH 19/71] fix moving curve and handle multi-path e.g. smiley face on shapes mock --- src/plots/cartesian/new_shape.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 492995556c7..871925686da 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -287,11 +287,10 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { for(var i = 0; i < polygons.length; i++) { for(var j = 0; j < polygons[i].length; j++) { - var x0 = copyPolygons[i][j][1]; - var y0 = copyPolygons[i][j][2]; - - polygons[i][j][1] = x0 + dx; - polygons[i][j][2] = y0 + dy; + for(var k = 0; k < polygons[i][j].length - 1; k += 2) { + polygons[i][j][k + 1] = copyPolygons[i][j][k + 1] + dx; + polygons[i][j][k + 2] = copyPolygons[i][j][k + 2] + dy; + } } } } @@ -800,6 +799,11 @@ function addNewShapes(outlines, dragOptions) { q === gd._fullLayout._activeShapeIndex ) { var afterEdit = newShapes[0]; // pick first + if(beforeEdit.type === 'path') { // add other paths + for(var k = 1; k < newShapes.length; k++) { + afterEdit.path += newShapes[k].path; + } + } switch(beforeEdit.type) { case 'line': From 396d7d3288e9325615c4b88971f1659dc67bbe29 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 09:24:40 -0400 Subject: [PATCH 20/71] fix drag start points on closed paths --- src/plots/cartesian/new_shape.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 871925686da..a6f9ce8fbc4 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -205,6 +205,8 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { vertexDragOptions[i] = []; for(var j = 0; j < cell.length; j++) { + if(cell[j][0] === 'Z') continue; + if(onEllipse && j !== 0 && j !== CIRCLE_SIDES * 0.25 && @@ -287,7 +289,7 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { for(var i = 0; i < polygons.length; i++) { for(var j = 0; j < polygons[i].length; j++) { - for(var k = 0; k < polygons[i][j].length - 1; k += 2) { + for(var k = 0; k + 2 < polygons[i][j].length; k += 2) { polygons[i][j][k + 1] = copyPolygons[i][j][k + 1] + dx; polygons[i][j][k + 2] = copyPolygons[i][j][k + 2] + dy; } @@ -343,7 +345,6 @@ function writePaths(polygons) { var w = polygons[i][j][0]; if(w === 'Z') { str += 'Z'; - break; } else { var nK = polygons[i][j].length; for(var k = 0; k < nK; k++) { @@ -473,7 +474,7 @@ function readPaths(str, plotinfo, size, isActiveShape) { } for(var j = 0; j < newPos.length; j++) { - for(k = 0; k <= 4; k += 2) { + for(k = 0; k + 2 < 7; k += 2) { var _x = newPos[j][k + 1]; var _y = newPos[j][k + 2]; From 838bda91346430351e0f705d1a8f452693a6f6b5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 09:42:15 -0400 Subject: [PATCH 21/71] revise description and use pre computed indices for circles --- src/components/shapes/attributes.js | 4 ++-- src/plots/cartesian/new_shape.js | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index bdd44b19218..0d736e9ed52 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -235,7 +235,7 @@ module.exports = templatedArray('shape', { role: 'info', editType: 'arraydraw', description: [ - 'Sets the color filling the closed shape\'s interior.' + 'Sets the color filling the shape\'s interior. Only applies to closed shapes.' ].join(' ') }, fillrule: { @@ -245,7 +245,7 @@ module.exports = templatedArray('shape', { role: 'info', editType: 'arraydraw', description: [ - 'Determines the path\'s interior.', + 'Determines which regions of complex paths constitute the interior.', 'For more info please visit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule' ].join(' ') }, diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index a6f9ce8fbc4..e153e2ccf79 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -208,10 +208,10 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { if(cell[j][0] === 'Z') continue; if(onEllipse && - j !== 0 && - j !== CIRCLE_SIDES * 0.25 && - j !== CIRCLE_SIDES * 0.5 && - j !== CIRCLE_SIDES * 0.75 + j !== i000 && + j !== i090 && + j !== i180 && + j !== i270 ) { continue; } From 90a7ec7dd1cfc776b880904c7c095a3b342cd0c7 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 10:16:22 -0400 Subject: [PATCH 22/71] simplify shape activation --- src/plots/cartesian/handle_outline.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/plots/cartesian/handle_outline.js b/src/plots/cartesian/handle_outline.js index 675d3b8efc4..76e431b4bc7 100644 --- a/src/plots/cartesian/handle_outline.js +++ b/src/plots/cartesian/handle_outline.js @@ -15,18 +15,8 @@ var Registry = require('../../registry'); function activateShape(gd, path, drawShapes) { var element = path.node(); var id = +element.getAttribute('data-index'); - - for(var q = 0; q < gd._fullLayout.shapes.length; q++) { - var shapeIn = gd._fullLayout.shapes[q]._input; - if(q === id && shapeIn.editable) { - gd._fullLayout._activeShapeIndex = q; - break; - } - } - - if(gd._fullLayout._activeShapeIndex >= 0) { - drawShapes(gd); - } + gd._fullLayout._activeShapeIndex = id; + if(id >= 0) drawShapes(gd); } function deactivateShape(gd) { From 618d138240baee2d39197420c704768435f7e208 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 13:23:41 -0400 Subject: [PATCH 23/71] fix date extra curve positions --- src/plots/cartesian/new_shape.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index e153e2ccf79..412a98a28b8 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -515,8 +515,10 @@ function fixDatesOnPaths(path, xaxis, yaxis) { if(!xIsDate && !yIsDate) return path; for(var i = 0; i < path.length; i++) { - if(xIsDate) path[i][1] = path[i][1].replace(' ', '_'); - if(yIsDate) path[i][2] = path[i][2].replace(' ', '_'); + for(var j = 1; j + 2 < path[i].length; j += 2) { + if(xIsDate) path[i][j + 1] = path[i][j + 1].replace(' ', '_'); + if(yIsDate) path[i][j + 2] = path[i][j + 2].replace(' ', '_'); + } } return path; From c22bc775745a6b3124ef3f36062b93eab141d38f Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 14:31:12 -0400 Subject: [PATCH 24/71] simplify handle plotinfo axes and dates --- src/plots/cartesian/helpers.js | 2 +- src/plots/cartesian/new_shape.js | 47 +++++++++++--------------------- 2 files changed, 17 insertions(+), 32 deletions(-) diff --git a/src/plots/cartesian/helpers.js b/src/plots/cartesian/helpers.js index 824d4f75bb1..68bf646cfa6 100644 --- a/src/plots/cartesian/helpers.js +++ b/src/plots/cartesian/helpers.js @@ -20,7 +20,7 @@ function p2r(ax, v) { case 'log': return ax.p2d(v); case 'date': - return ax.p2r(v, 0, ax.calendar); + return ax.p2r(v, 0, ax.calendar).replace(' ', '_'); default: return ax.p2r(v); } diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 412a98a28b8..9e3db4fea96 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -479,22 +479,26 @@ function readPaths(str, plotinfo, size, isActiveShape) { var _y = newPos[j][k + 2]; if(_x === undefined || _y === undefined) continue; - if(k === 0) { + if(k === 0) { // record end point x = _x; y = _y; } - if(!plotinfo || !(plotinfo.xaxis && plotinfo.yaxis)) { - // no change - } else if(plotinfo.domain) { - _x = plotinfo.domain.x[0] + _x / size.w; - _y = plotinfo.domain.y[1] - _y / size.h; - } else if(isActiveShape === false) { - _x = p2r(plotinfo.xaxis, _x - plotinfo.xaxis._offset); - _y = p2r(plotinfo.yaxis, _y - plotinfo.yaxis._offset); - } else { - _x = p2r(plotinfo.xaxis, _x); - _y = p2r(plotinfo.yaxis, _y); + if(plotinfo) { + if(plotinfo.domain) { + _x = plotinfo.domain.x[0] + _x / size.w; + _y = plotinfo.domain.y[1] - _y / size.h; + } else { + if(plotinfo.xaxis) { + if(isActiveShape === false) _x -= plotinfo.xaxis._offset; + _x = p2r(plotinfo.xaxis, _x); + } + + if(plotinfo.yaxis) { + if(isActiveShape === false) _y -= plotinfo.yaxis._offset; + _y = p2r(plotinfo.yaxis, _y); + } + } } newPos[j][k + 1] = _x; @@ -509,21 +513,6 @@ function readPaths(str, plotinfo, size, isActiveShape) { return polys; } -function fixDatesOnPaths(path, xaxis, yaxis) { - var xIsDate = xaxis.type === 'date'; - var yIsDate = yaxis.type === 'date'; - if(!xIsDate && !yIsDate) return path; - - for(var i = 0; i < path.length; i++) { - for(var j = 1; j + 2 < path[i].length; j += 2) { - if(xIsDate) path[i][j + 1] = path[i][j + 1].replace(' ', '_'); - if(yIsDate) path[i][j + 2] = path[i][j + 2].replace(' ', '_'); - } - } - - return path; -} - function almostEq(a, b) { return Math.abs(a - b) <= 1e-6; } @@ -777,10 +766,6 @@ function addNewShapes(outlines, dragOptions) { shape.y1 = pos.y1; } else { shape.type = 'path'; - if(xaxis && yaxis) { - fixDatesOnPaths(cell, xaxis, yaxis); - } - shape.path = writePaths([cell]); } From 5e1c9d910bdcd011496421a645278351967701cc Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 15:56:39 -0400 Subject: [PATCH 25/71] fix edits with paper anchor --- src/components/shapes/draw.js | 2 +- src/plots/cartesian/new_shape.js | 34 +++++++++++++++++++------------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 8b4e5d2d078..32c417c982e 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -147,7 +147,7 @@ function drawOne(gd, index) { isActiveShape: true // i.e. to enable controllers }; - var polygons = readPaths(d); + var polygons = readPaths(d, gd); // display polygons on the screen displayOutlines(polygons, path, dragOptions); } else { diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 9e3db4fea96..6a3389f4062 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -367,7 +367,7 @@ function writePaths(polygons) { return str; } -function readPaths(str, plotinfo, size, isActiveShape) { +function readPaths(str, gd, plotinfo, isActiveShape) { var cmd = parseSvgPath(str); var polys = []; @@ -473,31 +473,37 @@ function readPaths(str, plotinfo, size, isActiveShape) { break; } + var domain = (plotinfo || {}).domain; + var size = gd._fullLayout._size; + for(var j = 0; j < newPos.length; j++) { for(k = 0; k + 2 < 7; k += 2) { var _x = newPos[j][k + 1]; var _y = newPos[j][k + 2]; if(_x === undefined || _y === undefined) continue; - if(k === 0) { // record end point + if(k === 0) { // keep track of end point for Z x = _x; y = _y; } if(plotinfo) { - if(plotinfo.domain) { - _x = plotinfo.domain.x[0] + _x / size.w; - _y = plotinfo.domain.y[1] - _y / size.h; + if(plotinfo.xaxis) { + if(isActiveShape === false) _x -= plotinfo.xaxis._offset; + _x = p2r(plotinfo.xaxis, _x); } else { - if(plotinfo.xaxis) { - if(isActiveShape === false) _x -= plotinfo.xaxis._offset; - _x = p2r(plotinfo.xaxis, _x); - } + if(isActiveShape === false) _x -= size.l; + if(domain) _x = domain.x[0] + _x / size.w; + else _x = _x / size.w; + } - if(plotinfo.yaxis) { - if(isActiveShape === false) _y -= plotinfo.yaxis._offset; - _y = p2r(plotinfo.yaxis, _y); - } + if(plotinfo.yaxis) { + if(isActiveShape === false) _y -= plotinfo.yaxis._offset; + _y = p2r(plotinfo.yaxis, _y); + } else { + if(isActiveShape === false) _y -= size.t; + if(domain) _y = domain.y[1] - _y / size.h; + else _y = 1 - _y / size.h; } } @@ -690,7 +696,7 @@ function addNewShapes(outlines, dragOptions) { var isOpenMode = openMode(dragmode); - var polygons = readPaths(d, plotinfo, gd._fullLayout._size, isActiveShape); + var polygons = readPaths(d, gd, plotinfo, isActiveShape); var newShapes = []; for(var i = 0; i < polygons.length; i++) { From adb65f6f7670ca6a298932b7a6cf11f05b89923a Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 16:19:24 -0400 Subject: [PATCH 26/71] fixup for traces that has select but not x and y axes --- src/components/modebar/manage.js | 2 +- src/plots/cartesian/new_shape.js | 4 ++-- src/plots/mapbox/mapbox.js | 4 ++-- src/plots/ternary/ternary.js | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 71437d7f757..b0a159ead14 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -172,7 +172,7 @@ function getButtonGroups(gd) { if( // fullLayout._has('ternary') || - // fullLayout._has('mapbox') || + fullLayout._has('mapbox') || fullLayout._has('cartesian') ) { dragModeGroup.push( diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 6a3389f4062..e230fc1a91f 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -488,7 +488,7 @@ function readPaths(str, gd, plotinfo, isActiveShape) { } if(plotinfo) { - if(plotinfo.xaxis) { + if(plotinfo.xaxis && plotinfo.xaxis.p2r) { if(isActiveShape === false) _x -= plotinfo.xaxis._offset; _x = p2r(plotinfo.xaxis, _x); } else { @@ -497,7 +497,7 @@ function readPaths(str, gd, plotinfo, isActiveShape) { else _x = _x / size.w; } - if(plotinfo.yaxis) { + if(plotinfo.yaxis && plotinfo.yaxis.p2r) { if(isActiveShape === false) _y -= plotinfo.yaxis._offset; _y = p2r(plotinfo.yaxis, _y); } else { diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js index bead2ba697c..b3268b9a5cb 100644 --- a/src/plots/mapbox/mapbox.js +++ b/src/plots/mapbox/mapbox.js @@ -514,9 +514,9 @@ proto.initFx = function(calcData, fullLayout) { // define event handlers on map creation, to keep one ref per map, // so that map.on / map.off in updateFx works as expected - self.clearSelect = function(e) { + self.clearSelect = function() { clearSelectionsCache(self.dragOptions); - clearSelect(e); + clearSelect(self.dragOptions.gd); }; /** diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 82c70258ef4..8d8eaab1dca 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -492,9 +492,9 @@ var STARTMARKER = 'm0.5,0.5h5v-2h-5v-5h-2v5h-5v2h5v5h2Z'; // I guess this could be shared with cartesian... but for now it's separate. var SHOWZOOMOUTTIP = true; -proto.clearSelect = function(e) { +proto.clearSelect = function() { clearSelectionsCache(this.dragOptions); - clearSelect(e); + clearSelect(self.dragOptions); }; proto.initInteractions = function() { From 38aef9ec5b728a30ec3fbdddb00f9e26f664d8cf Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 17:39:32 -0400 Subject: [PATCH 27/71] do not change template shapes for now --- src/plots/cartesian/handle_outline.js | 22 +++++++------------- src/plots/cartesian/new_shape.js | 29 +++++++++++++++------------ 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/plots/cartesian/handle_outline.js b/src/plots/cartesian/handle_outline.js index 76e431b4bc7..9d7d566255b 100644 --- a/src/plots/cartesian/handle_outline.js +++ b/src/plots/cartesian/handle_outline.js @@ -22,16 +22,9 @@ function activateShape(gd, path, drawShapes) { function deactivateShape(gd) { clearOutlineControllers(gd); - var shapes = []; - for(var q = 0; q < gd._fullLayout.shapes.length; q++) { - var shapeIn = gd._fullLayout.shapes[q]._input; - shapes.push(shapeIn); - } - delete gd._fullLayout._activeShapeIndex; - Registry.call('_guiRelayout', gd, { - shapes: shapes + shapes: (gd.layout || {}).shapes || [] }); } @@ -39,20 +32,19 @@ function eraseActiveShape(gd) { clearOutlineControllers(gd); var id = gd._fullLayout._activeShapeIndex; - if(id < gd._fullLayout.shapes.length) { - var shapes = []; - for(var q = 0; q < gd._fullLayout.shapes.length; q++) { - var shapeIn = gd._fullLayout.shapes[q]._input; - + var shapes = (gd.layout || {}).shapes || []; + if(id < shapes.length) { + var allShapes = []; + for(var q = 0; q < shapes.length; q++) { if(q !== id) { - shapes.push(shapeIn); + allShapes.push(shapes[q]); } } delete gd._fullLayout._activeShapeIndex; Registry.call('_guiRelayout', gd, { - shapes: shapes + shapes: allShapes }); } } diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index e230fc1a91f..17fcf249ef8 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -669,9 +669,12 @@ function addNewShapes(outlines, dragOptions) { var isActiveShape = dragOptions.isActiveShape; var dragmode = dragOptions.dragmode; + + var shapes = (gd.layout || {}).shapes || []; + if(isActiveShape !== undefined) { var id = gd._fullLayout._activeShapeIndex; - if(id < gd._fullLayout.shapes.length) { + if(id < shapes.length) { switch(gd._fullLayout.shapes[id].type) { case 'rect': dragmode = 'drawrect'; @@ -683,7 +686,7 @@ function addNewShapes(outlines, dragOptions) { dragmode = 'drawline'; break; case 'path': - var path = gd._fullLayout.shapes[id].path || ''; + var path = shapes[id].path || ''; if(path[path.length - 1] === 'Z') { dragmode = 'drawclosedpath'; } else { @@ -780,13 +783,13 @@ function addNewShapes(outlines, dragOptions) { clearSelect(gd); - var shapes; + var allShapes; if(newShapes.length) { var updatedActiveShape = false; - shapes = []; - for(var q = 0; q < gd._fullLayout.shapes.length; q++) { + allShapes = []; + for(var q = 0; q < shapes.length; q++) { var beforeEdit = gd._fullLayout.shapes[q]; - shapes[q] = beforeEdit._input; + allShapes[q] = beforeEdit._input; if( isActiveShape !== undefined && @@ -805,17 +808,17 @@ function addNewShapes(outlines, dragOptions) { case 'circle': updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); if(updatedActiveShape) { // update active shape - shapes[q].x0 = afterEdit.x0; - shapes[q].x1 = afterEdit.x1; - shapes[q].y0 = afterEdit.y0; - shapes[q].y1 = afterEdit.y1; + allShapes[q].x0 = afterEdit.x0; + allShapes[q].x1 = afterEdit.x1; + allShapes[q].y0 = afterEdit.y0; + allShapes[q].y1 = afterEdit.y1; } break; case 'path': updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); if(updatedActiveShape) { // update active shape - shapes[q].path = afterEdit.path; + allShapes[q].path = afterEdit.path; } break; } @@ -823,11 +826,11 @@ function addNewShapes(outlines, dragOptions) { } if(isActiveShape === undefined) { - shapes = shapes.concat(newShapes); // add new shapes + allShapes = allShapes.concat(newShapes); // add new shapes } } - return shapes; + return allShapes; } function hasChanged(beforeEdit, afterEdit, keys) { From 98c7e9ad44b8934e849fcf7911140ef23010cbcc Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 18:08:38 -0400 Subject: [PATCH 28/71] better handle for multiple polygons - only one newShape at a time --- src/plots/cartesian/new_shape.js | 154 +++++++++++++++---------------- 1 file changed, 74 insertions(+), 80 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 17fcf249ef8..5f4ae958a95 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -701,90 +701,89 @@ function addNewShapes(outlines, dragOptions) { var polygons = readPaths(d, gd, plotinfo, isActiveShape); - var newShapes = []; - for(var i = 0; i < polygons.length; i++) { - var cell = polygons[i]; - if(cell.length < 2) continue; - - var shape = { - editable: true, - - xref: onPaper ? 'paper' : xaxis._id, - yref: onPaper ? 'paper' : yaxis._id, - - layer: drwStyle.layer, - opacity: drwStyle.opacity, - line: { - color: drwStyle.line.color, - width: drwStyle.line.width, - dash: drwStyle.line.dash - } - }; - - if(!isOpenMode) { - shape.fillcolor = drwStyle.fillcolor; - shape.fillrule = drwStyle.fillrule; + var newShape = { + editable: true, + + xref: onPaper ? 'paper' : xaxis._id, + yref: onPaper ? 'paper' : yaxis._id, + + layer: drwStyle.layer, + opacity: drwStyle.opacity, + line: { + color: drwStyle.line.color, + width: drwStyle.line.width, + dash: drwStyle.line.dash } + }; - if( - dragmode === 'drawrect' && - pointsShapeRectangle(cell) - ) { - shape.type = 'rect'; - shape.x0 = cell[0][1]; - shape.y0 = cell[0][2]; - shape.x1 = cell[2][1]; - shape.y1 = cell[2][2]; - } else if( - dragmode === 'drawline' - ) { - shape.type = 'line'; - shape.x0 = cell[0][1]; - shape.y0 = cell[0][2]; - shape.x1 = cell[1][1]; - shape.y1 = cell[1][2]; - } else if( - dragmode === 'drawcircle' && - (isActiveShape === false || pointsShapeEllipse(cell)) - ) { - shape.type = 'circle'; // an ellipse! - var pos = {}; - if(isActiveShape === false) { - var x0 = (cell[i090][1] + cell[i270][1]) / 2; - var y0 = (cell[i000][2] + cell[i180][2]) / 2; - var rx = (cell[i270][1] - cell[i090][1] + cell[i180][1] - cell[i000][1]) / 2; - var ry = (cell[i270][2] - cell[i090][2] + cell[i180][2] - cell[i000][2]) / 2; - pos = ellipseOver({ - x0: x0, - y0: y0, - x1: x0 + rx * cos45, - y1: y0 + ry * sin45 - }); - } else { - pos = ellipseOver({ - x0: (cell[i000][1] + cell[i180][1]) / 2, - y0: (cell[i000][2] + cell[i180][2]) / 2, - x1: cell[i045][1], - y1: cell[i045][2] - }); - } + if(!isOpenMode) { + newShape.fillcolor = drwStyle.fillcolor; + newShape.fillrule = drwStyle.fillrule; + } + + var cell; + // only define cell if there is single cell + if(polygons.length === 1) cell = polygons[0]; - shape.x0 = pos.x0; - shape.y0 = pos.y0; - shape.x1 = pos.x1; - shape.y1 = pos.y1; + if( + cell && + dragmode === 'drawrect' && + pointsShapeRectangle(cell) + ) { + newShape.type = 'rect'; + newShape.x0 = cell[0][1]; + newShape.y0 = cell[0][2]; + newShape.x1 = cell[2][1]; + newShape.y1 = cell[2][2]; + } else if( + cell && + dragmode === 'drawline' + ) { + newShape.type = 'line'; + newShape.x0 = cell[0][1]; + newShape.y0 = cell[0][2]; + newShape.x1 = cell[1][1]; + newShape.y1 = cell[1][2]; + } else if( + cell && + dragmode === 'drawcircle' && + (isActiveShape === false || pointsShapeEllipse(cell)) + ) { + newShape.type = 'circle'; // an ellipse! + var pos = {}; + if(isActiveShape === false) { + var x0 = (cell[i090][1] + cell[i270][1]) / 2; + var y0 = (cell[i000][2] + cell[i180][2]) / 2; + var rx = (cell[i270][1] - cell[i090][1] + cell[i180][1] - cell[i000][1]) / 2; + var ry = (cell[i270][2] - cell[i090][2] + cell[i180][2] - cell[i000][2]) / 2; + pos = ellipseOver({ + x0: x0, + y0: y0, + x1: x0 + rx * cos45, + y1: y0 + ry * sin45 + }); } else { - shape.type = 'path'; - shape.path = writePaths([cell]); + pos = ellipseOver({ + x0: (cell[i000][1] + cell[i180][1]) / 2, + y0: (cell[i000][2] + cell[i180][2]) / 2, + x1: cell[i045][1], + y1: cell[i045][2] + }); } - newShapes.push(shape); + newShape.x0 = pos.x0; + newShape.y0 = pos.y0; + newShape.x1 = pos.x1; + newShape.y1 = pos.y1; + } else { + newShape.type = 'path'; + newShape.path = writePaths(polygons); } clearSelect(gd); var allShapes; - if(newShapes.length) { + if(newShape) { var updatedActiveShape = false; allShapes = []; for(var q = 0; q < shapes.length; q++) { @@ -795,12 +794,7 @@ function addNewShapes(outlines, dragOptions) { isActiveShape !== undefined && q === gd._fullLayout._activeShapeIndex ) { - var afterEdit = newShapes[0]; // pick first - if(beforeEdit.type === 'path') { // add other paths - for(var k = 1; k < newShapes.length; k++) { - afterEdit.path += newShapes[k].path; - } - } + var afterEdit = newShape; switch(beforeEdit.type) { case 'line': @@ -826,7 +820,7 @@ function addNewShapes(outlines, dragOptions) { } if(isActiveShape === undefined) { - allShapes = allShapes.concat(newShapes); // add new shapes + allShapes.push(newShape); // add new shape } } From fc1d34773c70162f1c07aac07bfca4172cb59aca Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 21 Apr 2020 20:13:37 -0400 Subject: [PATCH 29/71] improve type detection on edit --- src/plots/cartesian/helpers.js | 14 ++- src/plots/cartesian/new_shape.js | 165 +++++++++++++++++++------------ 2 files changed, 116 insertions(+), 63 deletions(-) diff --git a/src/plots/cartesian/helpers.js b/src/plots/cartesian/helpers.js index 68bf646cfa6..23a1b4799c3 100644 --- a/src/plots/cartesian/helpers.js +++ b/src/plots/cartesian/helpers.js @@ -20,12 +20,23 @@ function p2r(ax, v) { case 'log': return ax.p2d(v); case 'date': - return ax.p2r(v, 0, ax.calendar).replace(' ', '_'); + return ax.p2r(v, 0, ax.calendar); default: return ax.p2r(v); } } +function r2p(ax, v) { + switch(ax.type) { + case 'log': + return ax.d2p(v); + case 'date': + return ax.r2p(v, 0, ax.calendar); + default: + return ax.r2p(v); + } +} + function axValue(ax) { var index = (ax._id.charAt(0) === 'y') ? 1 : 0; return function(v) { return p2r(ax, v[index]); }; @@ -40,6 +51,7 @@ function getTransform(plotinfo) { module.exports = { getAxId: getAxId, p2r: p2r, + r2p: r2p, axValue: axValue, getTransform: getTransform }; diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 5f4ae958a95..26838e7e7c8 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -22,9 +22,8 @@ var setCursor = require('../../lib/setcursor'); var constants = require('./constants'); var MINSELECT = constants.MINSELECT; -var CIRCLE_SIDES = 32; // should be divisible by 8 +var CIRCLE_SIDES = 32; // should be divisible by 4 var i000 = 0; -var i045 = CIRCLE_SIDES / 8; var i090 = CIRCLE_SIDES / 4; var i180 = CIRCLE_SIDES / 2; var i270 = CIRCLE_SIDES / 4 * 3; @@ -34,6 +33,7 @@ var SQRT2 = Math.sqrt(2); var helpers = require('./helpers'); var p2r = helpers.p2r; +var r2p = helpers.r2p; var handleOutline = require('./handle_outline'); var clearOutlineControllers = handleOutline.clearOutlineControllers; @@ -519,6 +519,23 @@ function readPaths(str, gd, plotinfo, isActiveShape) { return polys; } +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; +} + function almostEq(a, b) { return Math.abs(a - b) <= 1e-6; } @@ -672,7 +689,7 @@ function addNewShapes(outlines, dragOptions) { var shapes = (gd.layout || {}).shapes || []; - if(isActiveShape !== undefined) { + if(!drawMode(dragmode) && isActiveShape !== undefined) { var id = gd._fullLayout._activeShapeIndex; if(id < shapes.length) { switch(gd._fullLayout.shapes[id].type) { @@ -722,13 +739,13 @@ function addNewShapes(outlines, dragOptions) { } var cell; + // line, rect and circle can be in one cell // only define cell if there is single cell if(polygons.length === 1) cell = polygons[0]; if( cell && - dragmode === 'drawrect' && - pointsShapeRectangle(cell) + dragmode === 'drawrect' ) { newShape.type = 'rect'; newShape.x0 = cell[0][1]; @@ -746,29 +763,53 @@ function addNewShapes(outlines, dragOptions) { newShape.y1 = cell[1][2]; } else if( cell && - dragmode === 'drawcircle' && - (isActiveShape === false || pointsShapeEllipse(cell)) + dragmode === 'drawcircle' ) { newShape.type = 'circle'; // an ellipse! - var pos = {}; - if(isActiveShape === false) { - var x0 = (cell[i090][1] + cell[i270][1]) / 2; - var y0 = (cell[i000][2] + cell[i180][2]) / 2; - var rx = (cell[i270][1] - cell[i090][1] + cell[i180][1] - cell[i000][1]) / 2; - var ry = (cell[i270][2] - cell[i090][2] + cell[i180][2] - cell[i000][2]) / 2; - pos = ellipseOver({ - x0: x0, - y0: y0, - x1: x0 + rx * cos45, - y1: y0 + ry * sin45 - }); - } else { - pos = ellipseOver({ - x0: (cell[i000][1] + cell[i180][1]) / 2, - y0: (cell[i000][2] + cell[i180][2]) / 2, - x1: cell[i045][1], - y1: cell[i045][2] - }); + + var xA = cell[i000][1]; + var xB = cell[i090][1]; + var xC = cell[i180][1]; + var xD = cell[i270][1]; + + var yA = cell[i000][2]; + var yB = cell[i090][2]; + var yC = cell[i180][2]; + var yD = cell[i270][2]; + + if(plotinfo.xaxis && plotinfo.xaxis.type === 'date') { + xA = r2p(plotinfo.xaxis, xA); + xB = r2p(plotinfo.xaxis, xB); + xC = r2p(plotinfo.xaxis, xC); + xD = r2p(plotinfo.xaxis, xD); + } + + if(plotinfo.yaxis && plotinfo.yaxis.type === 'date') { + yA = r2p(plotinfo.yaxis, yA); + yB = r2p(plotinfo.yaxis, yB); + yC = r2p(plotinfo.yaxis, yC); + yD = r2p(plotinfo.yaxis, yD); + } + + var x0 = (xB + xD) / 2; + var y0 = (yA + yC) / 2; + var rx = (xD - xB + xC - xA) / 2; + var ry = (yD - yB + yC - yA) / 2; + var pos = ellipseOver({ + x0: x0, + y0: y0, + x1: x0 + rx * cos45, + y1: y0 + ry * sin45 + }); + + if(plotinfo.xaxis && plotinfo.xaxis.type === 'date') { + pos.x0 = p2r(plotinfo.xaxis, pos.x0); + pos.x1 = p2r(plotinfo.xaxis, pos.x1); + } + + if(plotinfo.yaxis && plotinfo.yaxis.type === 'date') { + pos.y0 = p2r(plotinfo.yaxis, pos.y0); + pos.y1 = p2r(plotinfo.yaxis, pos.y1); } newShape.x0 = pos.x0; @@ -777,51 +818,51 @@ function addNewShapes(outlines, dragOptions) { newShape.y1 = pos.y1; } else { newShape.type = 'path'; + if(xaxis && yaxis) fixDatesForPaths(polygons, xaxis, yaxis); newShape.path = writePaths(polygons); + cell = null; } clearSelect(gd); var allShapes; - if(newShape) { - var updatedActiveShape = false; - allShapes = []; - for(var q = 0; q < shapes.length; q++) { - var beforeEdit = gd._fullLayout.shapes[q]; - allShapes[q] = beforeEdit._input; - - if( - isActiveShape !== undefined && - q === gd._fullLayout._activeShapeIndex - ) { - var afterEdit = newShape; - - switch(beforeEdit.type) { - case 'line': - case 'rect': - case 'circle': - updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); - if(updatedActiveShape) { // update active shape - allShapes[q].x0 = afterEdit.x0; - allShapes[q].x1 = afterEdit.x1; - allShapes[q].y0 = afterEdit.y0; - allShapes[q].y1 = afterEdit.y1; - } - break; - - case 'path': - updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); - if(updatedActiveShape) { // update active shape - allShapes[q].path = afterEdit.path; - } - break; - } + var updatedActiveShape = false; + allShapes = []; + for(var q = 0; q < shapes.length; q++) { + var beforeEdit = gd._fullLayout.shapes[q]; + allShapes[q] = beforeEdit._input; + + if( + isActiveShape !== undefined && + q === gd._fullLayout._activeShapeIndex + ) { + var afterEdit = newShape; + + switch(beforeEdit.type) { + case 'line': + case 'rect': + case 'circle': + updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); + if(updatedActiveShape) { // update active shape + allShapes[q].x0 = afterEdit.x0; + allShapes[q].x1 = afterEdit.x1; + allShapes[q].y0 = afterEdit.y0; + allShapes[q].y1 = afterEdit.y1; + } + break; + + case 'path': + updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); + if(updatedActiveShape) { // update active shape + allShapes[q].path = afterEdit.path; + } + break; } } + } - if(isActiveShape === undefined) { - allShapes.push(newShape); // add new shape - } + if(isActiveShape === undefined) { + allShapes.push(newShape); // add new shape } return allShapes; From 569ce1c57a2eeb54a3b353d763f0b52ca969f1a3 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 09:37:07 -0400 Subject: [PATCH 30/71] fix circle on log --- src/plots/cartesian/new_shape.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 26838e7e7c8..edb82130eda 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -777,14 +777,24 @@ function addNewShapes(outlines, dragOptions) { var yC = cell[i180][2]; var yD = cell[i270][2]; - if(plotinfo.xaxis && plotinfo.xaxis.type === 'date') { + var xDateOrLog = plotinfo.xaxis && ( + plotinfo.xaxis.type === 'date' || + plotinfo.xaxis.type === 'log' + ); + + var yDateOrLog = plotinfo.yaxis && ( + plotinfo.yaxis.type === 'date' || + plotinfo.yaxis.type === 'log' + ); + + if(xDateOrLog) { xA = r2p(plotinfo.xaxis, xA); xB = r2p(plotinfo.xaxis, xB); xC = r2p(plotinfo.xaxis, xC); xD = r2p(plotinfo.xaxis, xD); } - if(plotinfo.yaxis && plotinfo.yaxis.type === 'date') { + if(yDateOrLog) { yA = r2p(plotinfo.yaxis, yA); yB = r2p(plotinfo.yaxis, yB); yC = r2p(plotinfo.yaxis, yC); @@ -802,12 +812,12 @@ function addNewShapes(outlines, dragOptions) { y1: y0 + ry * sin45 }); - if(plotinfo.xaxis && plotinfo.xaxis.type === 'date') { + if(xDateOrLog) { pos.x0 = p2r(plotinfo.xaxis, pos.x0); pos.x1 = p2r(plotinfo.xaxis, pos.x1); } - if(plotinfo.yaxis && plotinfo.yaxis.type === 'date') { + if(yDateOrLog) { pos.y0 = p2r(plotinfo.yaxis, pos.y0); pos.y1 = p2r(plotinfo.yaxis, pos.y1); } From a20b290b3427c39cf203498d7233749c5f42a0c6 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 10:27:36 -0400 Subject: [PATCH 31/71] fix closing point on path --- src/plots/cartesian/new_shape.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index edb82130eda..42b49920bf3 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -482,10 +482,9 @@ function readPaths(str, gd, plotinfo, isActiveShape) { var _y = newPos[j][k + 2]; if(_x === undefined || _y === undefined) continue; - if(k === 0) { // keep track of end point for Z - x = _x; - y = _y; - } + // keep track of end point for Z + x = _x; + y = _y; if(plotinfo) { if(plotinfo.xaxis && plotinfo.xaxis.p2r) { From 77bf2032636f5ad42cca711fb5088a0c764e15c7 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 11:26:42 -0400 Subject: [PATCH 32/71] fix edit shapes with refs to one cartesian axis as well as paper --- src/components/shapes/draw.js | 6 +++++- src/plots/cartesian/new_shape.js | 7 ++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 32c417c982e..af09ceaaa80 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -84,7 +84,11 @@ function drawOne(gd, index) { var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; var hasPlotinfo = !!plotinfo; - if(!hasPlotinfo) plotinfo = {}; + if(!hasPlotinfo) { + plotinfo = {}; + if(options.xref && options.xref !== 'paper') plotinfo.xaxis = gd._fullLayout[options.xref + 'axis']; + if(options.yref && options.yref !== 'paper') plotinfo.yaxis = gd._fullLayout[options.yref + 'axis']; + } if(options.layer !== 'below') { drawShape(gd._fullLayout._shapeUpperLayer); diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index 42b49920bf3..b7dd9d82d67 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -681,7 +681,8 @@ function addNewShapes(outlines, dragOptions) { var plotinfo = dragOptions.plotinfo; var xaxis = plotinfo.xaxis; var yaxis = plotinfo.yaxis; - var onPaper = plotinfo.domain || !plotinfo || !(plotinfo.xaxis && plotinfo.yaxis); + var xPaper = !!plotinfo.domain || !plotinfo || !plotinfo.xaxis; + var yPaper = !!plotinfo.domain || !plotinfo || !plotinfo.yaxis; var isActiveShape = dragOptions.isActiveShape; var dragmode = dragOptions.dragmode; @@ -720,8 +721,8 @@ function addNewShapes(outlines, dragOptions) { var newShape = { editable: true, - xref: onPaper ? 'paper' : xaxis._id, - yref: onPaper ? 'paper' : yaxis._id, + xref: xPaper ? 'paper' : xaxis._id, + yref: yPaper ? 'paper' : yaxis._id, layer: drwStyle.layer, opacity: drwStyle.opacity, From 5f57d5c12fb8145ff7ec315f6bbf3e8ecd673d15 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 12:19:44 -0400 Subject: [PATCH 33/71] create a reusable function to prepare options and plotinfo for arrayEditor --- src/components/shapes/draw.js | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index af09ceaaa80..6b18b3f96da 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -69,13 +69,7 @@ function draw(gd) { // return Plots.previousPromises(gd); } -function drawOne(gd, index) { - // remove the existing shape if there is one. - // because indices can change, we need to look in all shape layers - gd._fullLayout._paperdiv - .selectAll('.shapelayer [data-index="' + index + '"]') - .remove(); - +function makeOptionsAndPlotinfo(gd, index) { var options = gd._fullLayout.shapes[index] || {}; // this shape is gone - quit now after deleting it @@ -84,18 +78,37 @@ function drawOne(gd, index) { var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; var hasPlotinfo = !!plotinfo; - if(!hasPlotinfo) { + if(hasPlotinfo) { + plotinfo._hadPlotinfo = true; + } else { plotinfo = {}; if(options.xref && options.xref !== 'paper') plotinfo.xaxis = gd._fullLayout[options.xref + 'axis']; if(options.yref && options.yref !== 'paper') plotinfo.yaxis = gd._fullLayout[options.yref + 'axis']; } + return { + options: options, + plotinfo: plotinfo + }; +} + +function drawOne(gd, index) { + // remove the existing shape if there is one. + // because indices can change, we need to look in all shape layers + gd._fullLayout._paperdiv + .selectAll('.shapelayer [data-index="' + index + '"]') + .remove(); + + var o = makeOptionsAndPlotinfo(gd, index); + var options = o.options; + var plotinfo = o.plotinfo; + if(options.layer !== 'below') { drawShape(gd._fullLayout._shapeUpperLayer); } else if(options.xref === 'paper' || options.yref === 'paper') { drawShape(gd._fullLayout._shapeLowerLayer); } else { - if(hasPlotinfo) { + if(plotinfo._hadPlotinfo) { var mainPlot = plotinfo.mainplotinfo || plotinfo; drawShape(mainPlot.shapelayer); } else { From 9a4f4bfea78c83028c378d282e4a5e6948ec75a5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 12:43:42 -0400 Subject: [PATCH 34/71] move makeOptionsAndPlotinfo to shape helpers --- src/components/shapes/draw.js | 25 +------------------------ src/components/shapes/helpers.js | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 6b18b3f96da..de9ef35d657 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -69,29 +69,6 @@ function draw(gd) { // return Plots.previousPromises(gd); } -function makeOptionsAndPlotinfo(gd, index) { - var options = gd._fullLayout.shapes[index] || {}; - - // this shape is gone - quit now after deleting it - // TODO: use d3 idioms instead of deleting and redrawing every time - if(!options._input || options.visible === false) return; - - var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; - var hasPlotinfo = !!plotinfo; - if(hasPlotinfo) { - plotinfo._hadPlotinfo = true; - } else { - plotinfo = {}; - if(options.xref && options.xref !== 'paper') plotinfo.xaxis = gd._fullLayout[options.xref + 'axis']; - if(options.yref && options.yref !== 'paper') plotinfo.yaxis = gd._fullLayout[options.yref + 'axis']; - } - - return { - options: options, - plotinfo: plotinfo - }; -} - function drawOne(gd, index) { // remove the existing shape if there is one. // because indices can change, we need to look in all shape layers @@ -99,7 +76,7 @@ function drawOne(gd, index) { .selectAll('.shapelayer [data-index="' + index + '"]') .remove(); - var o = makeOptionsAndPlotinfo(gd, index); + var o = helpers.makeOptionsAndPlotinfo(gd, index); var options = o.options; var plotinfo = o.plotinfo; diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index ae95bddef02..0bd0ba1c7cc 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -117,3 +117,26 @@ exports.roundPositionForSharpStrokeRendering = function(pos, strokeWidth) { return strokeWidthIsOdd ? posValAsInt + 0.5 : posValAsInt; }; + +exports.makeOptionsAndPlotinfo = function(gd, index) { + var options = gd._fullLayout.shapes[index] || {}; + + // this shape is gone - quit now after deleting it + // TODO: use d3 idioms instead of deleting and redrawing every time + if(!options._input || options.visible === false) return; + + var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; + var hasPlotinfo = !!plotinfo; + if(hasPlotinfo) { + plotinfo._hadPlotinfo = true; + } else { + plotinfo = {}; + if(options.xref && options.xref !== 'paper') plotinfo.xaxis = gd._fullLayout[options.xref + 'axis']; + if(options.yref && options.yref !== 'paper') plotinfo.yaxis = gd._fullLayout[options.yref + 'axis']; + } + + return { + options: options, + plotinfo: plotinfo + }; +}; From 18dd4f80b5f1e6c071039cd95ccf09a1c93a5caf Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 14:21:36 -0400 Subject: [PATCH 35/71] deactivate shape from pan and zoom dragmodes - move shape activation & deactivation functions to shape --- src/components/modebar/buttons.js | 2 +- src/components/shapes/draw.js | 46 ++++++++++++++++++++++++--- src/plots/cartesian/dragbox.js | 6 ++++ src/plots/cartesian/handle_outline.js | 43 ------------------------- src/plots/cartesian/select.js | 16 ++++------ 5 files changed, 56 insertions(+), 57 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 3e156b09af2..a30f79bcb84 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -12,7 +12,7 @@ var Registry = require('../../registry'); var Plots = require('../../plots/plots'); var axisIds = require('../../plots/cartesian/axis_ids'); var Icons = require('../../fonts/ploticon'); -var eraseActiveShape = require('../../plots/cartesian/handle_outline').eraseActiveShape; +var eraseActiveShape = require('../shapes/draw').eraseActiveShape; var Lib = require('../../lib'); var _ = Lib._; diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index de9ef35d657..16792950436 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -17,9 +17,7 @@ var newShape = require('../../plots/cartesian/new_shape'); var readPaths = newShape.readPaths; var displayOutlines = newShape.displayOutlines; -var handleOutline = require('../../plots/cartesian/handle_outline'); -var activateShape = handleOutline.activateShape; -var eraseActiveShape = handleOutline.eraseActiveShape; +var clearOutlineControllers = require('../../plots/cartesian/handle_outline').clearOutlineControllers; var Color = require('../color'); var Drawing = require('../drawing'); @@ -162,7 +160,7 @@ function drawOne(gd, index) { } function clickFn(path) { - return activateShape(gd, path, draw); + return activateShape(gd, path); } } @@ -695,3 +693,43 @@ function movePath(pathIn, moveX, moveY) { return segmentType + paramString; }); } + +function activateShape(gd, path) { + var element = path.node(); + var id = +element.getAttribute('data-index'); + if(id >= 0) { + gd._fullLayout._activeShapeIndex = id; + gd._fullLayout._deactivateShape = deactivateShape; + draw(gd); + } +} + +function deactivateShape(gd) { + var id = gd._fullLayout._activeShapeIndex; + if(id >= 0) { + clearOutlineControllers(gd); + delete gd._fullLayout._activeShapeIndex; + draw(gd); + } +} + +function eraseActiveShape(gd) { + clearOutlineControllers(gd); + + var id = gd._fullLayout._activeShapeIndex; + var shapes = (gd.layout || {}).shapes || []; + if(id < shapes.length) { + var allShapes = []; + for(var q = 0; q < shapes.length; q++) { + if(q !== id) { + allShapes.push(shapes[q]); + } + } + + delete gd._fullLayout._activeShapeIndex; + + Registry.call('_guiRelayout', gd, { + shapes: allShapes + }); + } +} diff --git a/src/plots/cartesian/dragbox.js b/src/plots/cartesian/dragbox.js index a88e6db956d..5b7130785f9 100644 --- a/src/plots/cartesian/dragbox.js +++ b/src/plots/cartesian/dragbox.js @@ -245,6 +245,12 @@ function makeDragBox(gd, plotinfo, x, y, w, h, ns, ew) { } function clickFn(numClicks, evt) { + var gd = dragOptions.gd; + if(gd._fullLayout._activeShapeIndex >= 0) { + gd._fullLayout._deactivateShape(gd); + return; + } + var clickmode = gd._fullLayout.clickmode; removeZoombox(gd); diff --git a/src/plots/cartesian/handle_outline.js b/src/plots/cartesian/handle_outline.js index 9d7d566255b..0f10d1438d9 100644 --- a/src/plots/cartesian/handle_outline.js +++ b/src/plots/cartesian/handle_outline.js @@ -9,46 +9,6 @@ 'use strict'; -var Registry = require('../../registry'); - - -function activateShape(gd, path, drawShapes) { - var element = path.node(); - var id = +element.getAttribute('data-index'); - gd._fullLayout._activeShapeIndex = id; - if(id >= 0) drawShapes(gd); -} - -function deactivateShape(gd) { - clearOutlineControllers(gd); - - delete gd._fullLayout._activeShapeIndex; - Registry.call('_guiRelayout', gd, { - shapes: (gd.layout || {}).shapes || [] - }); -} - -function eraseActiveShape(gd) { - clearOutlineControllers(gd); - - var id = gd._fullLayout._activeShapeIndex; - var shapes = (gd.layout || {}).shapes || []; - if(id < shapes.length) { - var allShapes = []; - for(var q = 0; q < shapes.length; q++) { - if(q !== id) { - allShapes.push(shapes[q]); - } - } - - delete gd._fullLayout._activeShapeIndex; - - Registry.call('_guiRelayout', gd, { - shapes: allShapes - }); - } -} - function clearOutlineControllers(gd) { var zoomLayer = gd._fullLayout._zoomlayer; if(zoomLayer) { @@ -69,9 +29,6 @@ function clearSelect(gd) { } module.exports = { - activateShape: activateShape, - deactivateShape: deactivateShape, - eraseActiveShape: eraseActiveShape, clearOutlineControllers: clearOutlineControllers, clearSelect: clearSelect }; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index ab51d9af774..224a932c269 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -37,9 +37,7 @@ var MINSELECT = constants.MINSELECT; var filteredPolygon = polygon.filter; var polygonTester = polygon.tester; -var handleOutline = require('./handle_outline'); -var clearSelect = handleOutline.clearSelect; -var deactivateShape = handleOutline.deactivateShape; +var clearSelect = require('./handle_outline').clearSelect; var newShape = require('./new_shape'); var displayOutlines = newShape.displayOutlines; @@ -322,7 +320,7 @@ function prepSelect(e, startX, startY, dragOptions, mode) { corners.remove(); if(gd._fullLayout._activeShapeIndex >= 0) { - deactivateShape(gd); + gd._fullLayout._deactivateShape(gd); return; } if(isDrawMode) return; @@ -606,12 +604,12 @@ function clearSelectionsCache(dragOptions) { var dragmode = dragOptions.dragmode; var plotinfo = dragOptions.plotinfo; - if(drawMode(dragmode)) { - var gd = dragOptions.gd; - if(gd._fullLayout._activeShapeIndex >= 0) { - deactivateShape(gd); - } + var gd = dragOptions.gd; + if(gd._fullLayout._activeShapeIndex >= 0) { + gd._fullLayout._deactivateShape(gd); + } + if(drawMode(dragmode)) { var fullLayout = gd._fullLayout; var zoomLayer = fullLayout._zoomlayer; From 1fcd55a8cca9a28a90bc08bdbdace0fb22445251 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 16:33:55 -0400 Subject: [PATCH 36/71] handle other deactivation scenarios --- src/components/shapes/draw.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 16792950436..c4f7a930685 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -155,12 +155,13 @@ function drawOne(gd, index) { ) ? 'stroke' : 'all' ); - path.node().addEventListener('click', function() { return clickFn(path); }); } + + path.node().addEventListener('click', function() { return clickFn(path); }); } function clickFn(path) { - return activateShape(gd, path); + activateShape(gd, path); } } @@ -698,6 +699,12 @@ function activateShape(gd, path) { var element = path.node(); var id = +element.getAttribute('data-index'); if(id >= 0) { + // deactivate if already active + if(id === gd._fullLayout._activeShapeIndex) { + deactivateShape(gd); + return; + } + gd._fullLayout._activeShapeIndex = id; gd._fullLayout._deactivateShape = deactivateShape; draw(gd); From 281666ee897591f235ddd8ab950707c9103991a5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 17:31:07 -0400 Subject: [PATCH 37/71] accept pre-defined draw buttons as string --- src/components/modebar/manage.js | 40 +++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index b0a159ead14..12234ca8f1b 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -67,6 +67,15 @@ module.exports = function manageModeBar(gd) { else fullLayout._modeBar = createModeBar(gd, buttonGroups); }; +var DRAW_MODES = [ + 'drawline', + 'drawopenpath', + 'drawclosedpath', + 'drawcircle', + 'drawrect', + 'eraseshape' +]; + // logic behind which buttons are displayed by default function getButtonGroups(gd) { var fullLayout = gd._fullLayout; @@ -170,19 +179,24 @@ function getButtonGroups(gd) { dragModeGroup.push('select2d', 'lasso2d'); } - if( - // fullLayout._has('ternary') || - fullLayout._has('mapbox') || - fullLayout._has('cartesian') - ) { - dragModeGroup.push( - 'drawline', - 'drawopenpath', - 'drawclosedpath', - 'drawcircle', - 'drawrect', - 'eraseshape' - ); + // accept pre-defined buttons as string + if(Array.isArray(buttonsToAdd)) { + var newList = []; + for(var i = 0; i < buttonsToAdd.length; i++) { + var b = buttonsToAdd[i]; + if(typeof b === 'string') { + if(DRAW_MODES.indexOf(b) !== -1) { + if( + // fullLayout._has('ternary') || + fullLayout._has('mapbox') || + fullLayout._has('cartesian') + ) { + dragModeGroup.push(b); + } + } + } else newList.push(b); + } + buttonsToAdd = newList; } addGroup(dragModeGroup); From 83b6cd241878a43415e75015551be4ef4771c30e Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 17:56:57 -0400 Subject: [PATCH 38/71] apply _guiRelayout instead of relayout when adding a new shape --- src/plots/cartesian/select.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 224a932c269..155b83af6ba 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -618,7 +618,7 @@ function clearSelectionsCache(dragOptions) { // add shape var shapes = addNewShapes(outlines, dragOptions); if(shapes) { - Registry.call('relayout', gd, { + Registry.call('_guiRelayout', gd, { shapes: shapes }); } From cedad524b52d2b373f3d801f9ce354804f0e3c0a Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 17:58:46 -0400 Subject: [PATCH 39/71] improve description --- src/plots/layout_attributes.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 825d133d785..4c9160a160c 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -477,7 +477,12 @@ module.exports = { dflt: 'rgba(0,0,0,0)', role: 'info', editType: 'none', - description: 'Sets the color filling new shapes\' interior.' + description: [ + 'Sets the color filling new shapes\' interior.', + 'Please note that if using a fillcolor with alpha greater than half,', + 'drag inside the active shape starts moving the shape underneath,', + 'otherwise a new shape could be started over.' + ].join(' ') }, fillrule: { valType: 'enumerated', @@ -541,12 +546,7 @@ module.exports = { dflt: 0.5, role: 'info', editType: 'none', - description: [ - 'Sets the opacity of the active shape.', - 'If using a value greater than half,', - 'drag inside the active shape starts moving shape,', - 'otherwise it starts drawing a new shape.' - ].join(' ') + description: 'Sets the opacity of the active shape.' }, editType: 'none' }, From b90d1799256e264fdc87fa39c3a7ac7c2c75283b Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 21:36:38 -0400 Subject: [PATCH 40/71] Ensure config.editable:true works as it was before - i.e. with no active shape and erase shape for now. - Later on we could possibly enable shape deletion with - config.editable:true when eraseshape button activated. --- src/components/shapes/draw.js | 42 ++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index c4f7a930685..553a61ce960 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -67,6 +67,15 @@ function draw(gd) { // return Plots.previousPromises(gd); } +function shouldSkipEdits(gd) { + return !!gd._fullLayout._drawing; +} + +function couldHaveActiveShape(gd) { + // for now keep config.editable: true as it was before shape-drawing PR + return !gd._context.edits.shapePosition; +} + function drawOne(gd, index) { // remove the existing shape if there is one. // because indices can change, we need to look in all shape layers @@ -110,7 +119,9 @@ function drawOne(gd, index) { var isOpen = d[d.length - 1] !== 'Z'; - var isActiveShape = options.editable && gd._fullLayout._activeShapeIndex === index; + var isActiveShape = couldHaveActiveShape(gd) && + options.editable && gd._fullLayout._activeShapeIndex === index; + if(isActiveShape) { fillColor = isOpen ? 'rgba(0,0,0,0)' : gd._fullLayout.activeshape.fillcolor; @@ -148,12 +159,11 @@ function drawOne(gd, index) { } path.style('pointer-events', - !gd._context.edits.shapePosition && // for backward compatibility - (lineWidth >= 1) && ( // has border - (Color.opacity(fillColor) * opacity <= 0.5) || // too transparent - isOpen - ) ? - 'stroke' : 'all' + !couldHaveActiveShape(gd) || ( + lineWidth < 2 || ( // not has a remarkable border + !isOpen && Color.opacity(fillColor) * opacity > 0.5 // not too transparent + ) + ) ? 'all' : 'stroke' ); } @@ -268,12 +278,8 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { return g; } - function shouldSkipEdits() { - return !!gd._fullLayout._drawing; - } - function updateDragMode(evt) { - if(shouldSkipEdits()) { + if(shouldSkipEdits(gd)) { dragMode = null; return; } @@ -308,7 +314,7 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { } function startDrag(evt) { - if(shouldSkipEdits()) return; + if(shouldSkipEdits(gd)) return; // setup update strings and initial values if(xPixelSized) { @@ -362,7 +368,7 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { } function endDrag() { - if(shouldSkipEdits()) return; + if(shouldSkipEdits(gd)) return; setCursor(shapePath); removeVisualCues(shapeLayer); @@ -373,7 +379,7 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { } function abortDrag() { - if(shouldSkipEdits()) return; + if(shouldSkipEdits(gd)) return; removeVisualCues(shapeLayer); } @@ -696,6 +702,8 @@ function movePath(pathIn, moveX, moveY) { } function activateShape(gd, path) { + if(!couldHaveActiveShape(gd)) return; + var element = path.node(); var id = +element.getAttribute('data-index'); if(id >= 0) { @@ -712,6 +720,8 @@ function activateShape(gd, path) { } function deactivateShape(gd) { + if(!couldHaveActiveShape(gd)) return; + var id = gd._fullLayout._activeShapeIndex; if(id >= 0) { clearOutlineControllers(gd); @@ -721,6 +731,8 @@ function deactivateShape(gd) { } function eraseActiveShape(gd) { + if(!couldHaveActiveShape(gd)) return; + clearOutlineControllers(gd); var id = gd._fullLayout._activeShapeIndex; From 7c26cd193d3433636e6f9d0ebe2ef4866a0f5bb6 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 22:23:16 -0400 Subject: [PATCH 41/71] corce fillrule for all shape types - so that the template test simply passes as before --- src/components/shapes/defaults.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shapes/defaults.js b/src/components/shapes/defaults.js index 183d93ea9b2..85b18ad3463 100644 --- a/src/components/shapes/defaults.js +++ b/src/components/shapes/defaults.js @@ -41,7 +41,7 @@ function handleShapeDefaults(shapeIn, shapeOut, fullLayout) { coerce('layer'); coerce('opacity'); coerce('fillcolor'); - if(shapeType === 'path') coerce('fillrule'); + coerce('fillrule'); var lineWidth = coerce('line.width'); if(lineWidth) { coerce('line.color'); From 4bcc06ab0043f354196c29d3702ebe9865f7dbb5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 23:07:52 -0400 Subject: [PATCH 42/71] correct early return from shape drawOne --- src/components/shapes/draw.js | 4 ++++ src/components/shapes/helpers.js | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 553a61ce960..3cfe743d644 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -87,6 +87,10 @@ function drawOne(gd, index) { var options = o.options; var plotinfo = o.plotinfo; + // this shape is gone - quit now after deleting it + // TODO: use d3 idioms instead of deleting and redrawing every time + if(!options._input || options.visible === false) return; + if(options.layer !== 'below') { drawShape(gd._fullLayout._shapeUpperLayer); } else if(options.xref === 'paper' || options.yref === 'paper') { diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index 0bd0ba1c7cc..e69528d1f7c 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -121,10 +121,6 @@ exports.roundPositionForSharpStrokeRendering = function(pos, strokeWidth) { exports.makeOptionsAndPlotinfo = function(gd, index) { var options = gd._fullLayout.shapes[index] || {}; - // this shape is gone - quit now after deleting it - // TODO: use d3 idioms instead of deleting and redrawing every time - if(!options._input || options.visible === false) return; - var plotinfo = gd._fullLayout._plots[options.xref + options.yref]; var hasPlotinfo = !!plotinfo; if(hasPlotinfo) { From f0ae83523a1ac291c949f5602fd470cb99f39fd2 Mon Sep 17 00:00:00 2001 From: archmoj Date: Wed, 22 Apr 2020 23:46:27 -0400 Subject: [PATCH 43/71] fixup ternary clearSelect call --- src/plots/ternary/ternary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index 8d8eaab1dca..c9190c568c0 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -494,7 +494,7 @@ var SHOWZOOMOUTTIP = true; proto.clearSelect = function() { clearSelectionsCache(this.dragOptions); - clearSelect(self.dragOptions); + clearSelect(this.dragOptions.gd); }; proto.initInteractions = function() { From f7643ccb923f8efcda3a16735b59a5f3a9950dc5 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 23 Apr 2020 00:27:04 -0400 Subject: [PATCH 44/71] fixup ternary select --- src/plots/ternary/ternary.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js index c9190c568c0..ccf109c26b2 100644 --- a/src/plots/ternary/ternary.js +++ b/src/plots/ternary/ternary.js @@ -563,7 +563,7 @@ proto.initInteractions = function() { } if(clickMode.indexOf('select') > -1 && numClicks === 1) { - selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, this.dragOptions); + selectOnClick(evt, gd, [_this.xaxis], [_this.yaxis], _this.id, _this.dragOptions); } if(clickMode.indexOf('event') > -1) { From 268aaaaab992159b60972145d6485bc24e8c37c3 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 23 Apr 2020 09:43:21 -0400 Subject: [PATCH 45/71] improve read shape positions --- src/components/shapes/helpers.js | 5 +++++ src/plots/cartesian/new_shape.js | 27 +++++++++++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/components/shapes/helpers.js b/src/components/shapes/helpers.js index e69528d1f7c..fb5416a70ef 100644 --- a/src/components/shapes/helpers.js +++ b/src/components/shapes/helpers.js @@ -131,6 +131,11 @@ exports.makeOptionsAndPlotinfo = function(gd, index) { if(options.yref && options.yref !== 'paper') plotinfo.yaxis = gd._fullLayout[options.yref + 'axis']; } + plotinfo.xsizemode = options.xsizemode; + plotinfo.ysizemode = options.ysizemode; + plotinfo.xanchor = options.xanchor; + plotinfo.yanchor = options.yanchor; + return { options: options, plotinfo: plotinfo diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index b7dd9d82d67..e8772bc4b8b 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -475,6 +475,9 @@ function readPaths(str, gd, plotinfo, isActiveShape) { var domain = (plotinfo || {}).domain; var size = gd._fullLayout._size; + var xPixelSized = plotinfo && plotinfo.xsizemode === 'pixel'; + var yPixelSized = plotinfo && plotinfo.ysizemode === 'pixel'; + var noOffset = isActiveShape === false; for(var j = 0; j < newPos.length; j++) { for(k = 0; k + 2 < 7; k += 2) { @@ -488,19 +491,27 @@ function readPaths(str, gd, plotinfo, isActiveShape) { if(plotinfo) { if(plotinfo.xaxis && plotinfo.xaxis.p2r) { - if(isActiveShape === false) _x -= plotinfo.xaxis._offset; - _x = p2r(plotinfo.xaxis, _x); + if(noOffset) _x -= plotinfo.xaxis._offset; + if(xPixelSized) { + _x = r2p(plotinfo.xaxis, plotinfo.xanchor) + _x; + } else { + _x = p2r(plotinfo.xaxis, _x); + } } else { - if(isActiveShape === false) _x -= size.l; + if(noOffset) _x -= size.l; if(domain) _x = domain.x[0] + _x / size.w; else _x = _x / size.w; } if(plotinfo.yaxis && plotinfo.yaxis.p2r) { - if(isActiveShape === false) _y -= plotinfo.yaxis._offset; - _y = p2r(plotinfo.yaxis, _y); + if(noOffset) _y -= plotinfo.yaxis._offset; + if(yPixelSized) { + _y = r2p(plotinfo.yaxis, plotinfo.yanchor) - _y; + } else { + _y = p2r(plotinfo.yaxis, _y); + } } else { - if(isActiveShape === false) _y -= size.t; + if(noOffset) _y -= size.t; if(domain) _y = domain.y[1] - _y / size.h; else _y = 1 - _y / size.h; } @@ -681,8 +692,8 @@ function addNewShapes(outlines, dragOptions) { var plotinfo = dragOptions.plotinfo; var xaxis = plotinfo.xaxis; var yaxis = plotinfo.yaxis; - var xPaper = !!plotinfo.domain || !plotinfo || !plotinfo.xaxis; - var yPaper = !!plotinfo.domain || !plotinfo || !plotinfo.yaxis; + var xPaper = !!plotinfo.domain || !plotinfo.xaxis; + var yPaper = !!plotinfo.domain || !plotinfo.yaxis; var isActiveShape = dragOptions.isActiveShape; var dragmode = dragOptions.dragmode; From bea923041a658b4772947526ddb058bf5926bb88 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 23 Apr 2020 17:01:28 -0400 Subject: [PATCH 46/71] add modebar test --- test/jasmine/tests/modebar_test.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index c450a044201..66c7b7b9a9a 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -981,6 +981,29 @@ describe('ModeBar', function() { expect(function() { manageModeBar(gd); }).toThrowError(); }); + + it('add pre-defined buttons as strings for drawing shapes on cartesian subplot', function() { + var gd = setupGraphInfo(); + manageModeBar(gd); + + var initialGroupCount = countGroups(gd._fullLayout._modeBar); + var initialButtonCount = countButtons(gd._fullLayout._modeBar); + + gd._context.modeBarButtonsToAdd = [ + 'drawline', + 'drawopenpath', + 'drawclosedpath', + 'drawcircle', + 'drawrect', + 'eraseshape' + ]; + manageModeBar(gd); + + expect(countGroups(gd._fullLayout._modeBar)) + .toEqual(initialGroupCount + 0); // no new group - added inside the dragMode group + expect(countButtons(gd._fullLayout._modeBar)) + .toEqual(initialButtonCount + 6); + }); }); describe('modebar on clicks', function() { From dffe30a9043c2455b740ff8c7f4bce25c672c973 Mon Sep 17 00:00:00 2001 From: archmoj Date: Thu, 23 Apr 2020 18:15:47 -0400 Subject: [PATCH 47/71] draw shape test --- test/jasmine/tests/draw_shape_test.js | 950 ++++++++++++++++++++++++++ 1 file changed, 950 insertions(+) create mode 100644 test/jasmine/tests/draw_shape_test.js diff --git a/test/jasmine/tests/draw_shape_test.js b/test/jasmine/tests/draw_shape_test.js new file mode 100644 index 00000000000..cf08a164d73 --- /dev/null +++ b/test/jasmine/tests/draw_shape_test.js @@ -0,0 +1,950 @@ +var parseSvgPath = require('parse-svg-path'); + +var Plotly = require('@lib/index'); +var Lib = require('@src/lib'); + +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var failTest = require('../assets/fail_test'); +var mouseEvent = require('../assets/mouse_event'); +var touchEvent = require('../assets/touch_event'); + +function drag(path, options) { + var len = path.length; + + if(!options) options = { type: 'mouse' }; + + if(options.type === 'touch') { + touchEvent('touchstart', path[0][0], path[0][1], options); + + path.slice(1, len).forEach(function(pt) { + touchEvent('touchmove', pt[0], pt[1], options); + }); + + touchEvent('touchend', path[len - 1][0], path[len - 1][1], options); + return; + } + + mouseEvent('mousemove', path[0][0], path[0][1], options); + mouseEvent('mousedown', path[0][0], path[0][1], options); + + path.slice(1, len).forEach(function(pt) { + mouseEvent('mousemove', pt[0], pt[1], options); + }); + + mouseEvent('mouseup', path[len - 1][0], path[len - 1][1], options); +} + +function print(obj) { + // console.log(JSON.stringify(obj, null, 4).replace(/"/g, '\'')); + return obj; +} + +function assertPos(actual, expected) { + expect(typeof actual).toEqual(typeof expected); + + if(typeof actual === 'string') { + expect(actual).toEqual(expected); + + if(expected.indexOf('_') !== -1) { + actual = fixDates(actual); + expected = fixDates(expected); + } + + var cmd1 = parseSvgPath(actual); + var cmd2 = parseSvgPath(expected); + + expect(cmd1.length).toEqual(cmd2.length); + for(var i = 0; i < cmd1.length; i++) { + var A = cmd1[i]; + var B = cmd2[i]; + expect(A.length).toEqual(B.length); // svg letters should be identical + expect(A[0]).toEqual(B[0]); + for(var k = 1; k < A.length; k++) { + expect(A[k]).toBeCloseTo(B[k], 2); + } + } + } else { + var o1 = Object.keys(actual); + var o2 = Object.keys(expected); + expect(o1.length === o2.length); + for(var j = 0; j < o1.length; j++) { + expect(o1[j]).toEqual(o2[j]); + var key = o1[j]; + + var posA = actual[key]; + var posB = expected[key]; + + if(typeof posA === 'string') { + posA = fixDates(posA); + posB = fixDates(posB); + } + + expect(posA).toBeCloseTo(posB, 2); + } + } +} + +function fixDates(str) { + // hack to conver date axes to some numbers to parse with parse-svg-path + str = str.replace(/ /g, ''); + str = str.replace(/_/g, ''); + str = str.replace(/-/g, ''); + str = str.replace(/:/g, ''); + return str; +} + +describe('Draw new shapes to layout', function() { + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + var allMocks = [ + { + name: 'heatmap', + json: require('@mocks/13'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M3.603343465045593,16.95098039215686L5.123100303951368,18.91176470588235L6.034954407294833,18.91176470588235L3.603343465045593,16.95098039215686' + ); + }, + function(pos) { + return assertPos(pos, + 'M1.3237082066869301,17.931372549019606L4.363221884498481,17.931372549019606L4.363221884498481,14.009803921568627L1.3237082066869301,14.009803921568627Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 3.603343465045593, + 'y0': 14.990196078431373, + 'x1': 6.642857142857143, + 'y1': 11.068627450980392 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 2.0835866261398177, + 'y0': 16.95098039215686, + 'x1': 0.5638297872340426, + 'y1': 18.91176470588235 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -0.06567410694999332, + 'y0': 12.21722830907236, + 'x1': 4.232847359229629, + 'y1': 17.763163847790384 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 3.6033434650455933, + 'y0': 13.029411764705882, + 'x1': 0.5638297872340421, + 'y1': 16.950980392156865 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.5638297872340419, + 'y0': 16.950980392156865, + 'x1': 3.6033434650455938, + 'y1': 13.029411764705882 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 3.1582169926847232, + 'y0': 5.284808885674838, + 'x1': 1.0089562595949124, + 'y1': 24.695583271187907 + }); + } + ] + }, + { + name: 'log axis', + json: require('@mocks/12'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M7315.010246711367,81.03588258089053L11872.300299303395,86.01381805862732L14606.674330858614,86.01381805862732L7315.010246711367,81.03588258089053' + ); + }, + function(pos) { + return assertPos(pos, + 'M479.0751678233218,83.52485031975893L9593.655273007382,83.52485031975893L9593.655273007382,73.56897936428534L479.0751678233218,73.56897936428534Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 7315.010246711367, + 'y0': 76.05794710315374, + 'x1': 16429.590351895426, + 'y1': 66.10207614768017 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 2757.7201941193366, + 'y0': 81.03588258089053, + 'x1': -1799.5698584726929, + 'y1': 86.01381805862732 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -3687.2612059243093, + 'y0': 69.01808323792017, + 'x1': 9202.701594162983, + 'y1': 83.09781096838731 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 7315.0102467113675, + 'y0': 71.08001162541694, + 'x1': -1799.5698584726952, + 'y1': 81.03588258089053 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -1799.5698584726942, + 'y0': 81.03588258089053, + 'x1': 7315.010246711368, + 'y1': 71.08001162541694 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 5980.210894141159, + 'y0': 51.41842357483628, + 'x1': -464.77050590248655, + 'y1': 100.69747063147119 + }); + } + ] + }, + { + name: 'date axis', + json: require('@mocks/29'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M2014-04-13_03:31:06.4962,105.53339869281045L2014-04-13_08:01:18.9023,111.99745098039214L2014-04-13_10:43:26.3459,111.99745098039214L2014-04-13_03:31:06.4962,105.53339869281045' + ); + }, + function(pos) { + return assertPos(pos, + 'M2014-04-12_20:45:47.8872,108.7654248366013L2014-04-13_05:46:12.6992,108.7654248366013L2014-04-13_05:46:12.6992,95.8373202614379L2014-04-12_20:45:47.8872,95.8373202614379Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2014-04-13 03:31:06.4962', + 'y0': 99.06934640522876, + 'x1': '2014-04-13 12:31:31.3083', + 'y1': 86.14124183006535 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2014-04-12 23:00:54.0902', + 'y0': 105.53339869281045, + 'x1': '2014-04-12 18:30:41.6842', + 'y1': 111.99745098039214 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2014-04-12 16:38:46.5056', + 'y0': 89.9277959922419, + 'x1': '2014-04-13 05:23:01.6748', + 'y1': 108.21089681821562 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2014-04-13 03:31:06.4962', + 'y0': 92.60529411764705, + 'x1': '2014-04-12 18:30:41.6842', + 'y1': 105.53339869281047 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2014-04-12 18:30:41.6842', + 'y0': 105.53339869281047, + 'x1': '2014-04-13 03:31:06.4962', + 'y1': 92.60529411764705 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2014-04-13 02:11:59.5038', + 'y0': 67.07391995977471, + 'x1': '2014-04-12 19:49:48.6767', + 'y1': 131.0647728506828 + }); + } + ] + }, + { + name: 'date and log axes together', + json: require('@mocks/cliponaxis_false-dates-log'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M5290.268558951965,2017-11-19_16:54:39.7153L5608.318049490538,2017-11-20_09:59:34.3772L5799.147743813683,2017-11-20_09:59:34.3772L5290.268558951965,2017-11-19_16:54:39.7153' + ); + }, + function(pos) { + return assertPos(pos, + 'M4813.194323144105,2017-11-20_01:27:07.0463L5449.293304221252,2017-11-20_01:27:07.0463L5449.293304221252,2017-11-18_15:17:17.7224L4813.194323144105,2017-11-18_15:17:17.7224Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 5290.268558951965, + 'y0': '2017-11-18 23:49:45.0534', + 'x1': 5926.367540029112, + 'y1': '2017-11-17 13:39:55.7295' + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4972.219068413392, + 'y0': '2017-11-19 16:54:39.7153', + 'x1': 4654.169577874818, + 'y1': '2017-11-20 09:59:34.3772' + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4522.429165387887, + 'y0': '2017-11-17 23:40:19.3025', + 'x1': 5422.008971438897, + 'y1': '2017-11-19 23:59:10.8043' + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 5290.268558951966, + 'y0': '2017-11-18 06:44:50.3915', + 'x1': 4654.169577874818, + 'y1': '2017-11-19 16:54:39.7153' + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4654.169577874818, + 'y0': '2017-11-19 16:54:39.7153', + 'x1': 5290.268558951966, + 'y1': '2017-11-18 06:44:50.3915' + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 5197.114019926144, + 'y0': '2017-11-15 11:16:38.7758', + 'x1': 4747.324116900641, + 'y1': '2017-11-22 12:22:51.331' + }); + } + ] + }, + { + name: 'axes with rangebreaks', + json: require('@mocks/axes_breaks-gridlines'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M2015-06-01_11:36:29.5216,132.73855280509412L2015-07-26_21:54:15.5809,137.67764538538205L2015-08-29_04:04:55.2164,137.67764538538205L2015-06-01_11:36:29.5216,132.73855280509412' + ); + }, + function(pos) { + return assertPos(pos, + 'M2015-03-10_08:09:50.4328,135.20809909523808L2015-06-29_04:45:22.5512,135.20809909523808L2015-06-29_04:45:22.5512,125.32991393466223L2015-03-10_08:09:50.4328,125.32991393466223Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2015-06-01 11:36:29.5216', + 'y0': 127.7994602248062, + 'x1': '2015-09-20 08:12:01.6401', + 'y1': 117.92127506423034 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2015-04-07 01:18:43.4624', + 'y0': 132.73855280509412, + 'x1': '2015-02-10 15:00:57.4032', + 'y1': 137.67764538538205 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2015-01-18 16:00:26.2414', + 'y0': 120.81452851194669, + 'x1': '2015-06-24 10:37:00.6834', + 'y1': 134.7843919376657 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2015-06-01 11:36:29.5216', + 'y0': 122.86036764451828, + 'x1': '2015-02-10 15:00:57.4032', + 'y1': 132.73855280509412 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2015-02-10 15:00:57.4032', + 'y0': 132.73855280509412, + 'x1': '2015-06-01 11:36:29.5216', + 'y1': 122.86036764451825 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': '2015-05-16 06:05:50.9795', + 'y0': 103.35219922979789, + 'x1': '2015-02-26 20:31:35.9453', + 'y1': 152.2467212198145 + }); + } + ] + }, + { + name: 'subplot', + json: require('@mocks/18'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M4.933775889537972,7.614950166112958L5.2524163568773234,8.013621262458473L5.443600637280936,8.013621262458473L4.933775889537972,7.614950166112958' + ); + }, + function(pos) { + return assertPos(pos, + 'M4.455815188528943,7.814285714285716L5.093096123207648,7.814285714285716L5.093096123207648,7.016943521594685L4.455815188528943,7.016943521594685Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4.933775889537972, + 'y0': 7.216279069767443, + 'x1': 5.571056824216676, + 'y1': 6.418936877076413 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4.61513542219862, + 'y0': 7.614950166112958, + 'x1': 4.296494954859267, + 'y1': 8.013621262458473 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4.164509751766406, + 'y0': 6.652472998389465, + 'x1': 5.065761092630833, + 'y1': 7.78008514114542 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4.933775889537972, + 'y0': 6.817607973421927, + 'x1': 4.296494954859267, + 'y1': 7.614950166112958 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4.296494954859267, + 'y0': 7.614950166112958, + 'x1': 4.933775889537972, + 'y1': 6.817607973421927 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 4.840448257414727, + 'y0': 5.24295781994452, + 'x1': 4.389822586982512, + 'y1': 9.189600319590365 + }); + } + ] + }, + { + name: 'scattergl', + json: require('@mocks/gl2d_scatter2d-multiple-colors'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M-678.5714285714287,875.5760368663595L-500.00000000000006,1105.9907834101382L-392.8571428571429,1105.9907834101382L-678.5714285714287,875.5760368663595' + ); + }, + function(pos) { + return assertPos(pos, + 'M-946.4285714285716,990.7834101382489L-589.2857142857143,990.7834101382489L-589.2857142857143,529.9539170506913L-946.4285714285716,529.9539170506913Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': -678.5714285714287, + 'y0': 645.1612903225806, + 'x1': -321.42857142857144, + 'y1': 184.33179723502303 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -857.1428571428572, + 'y0': 875.5760368663595, + 'x1': -1035.7142857142858, + 'y1': 1105.9907834101382 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -1109.68099328091, + 'y0': 319.3056307896095, + 'x1': -604.6047210048046, + 'y1': 971.0169498555517 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -678.5714285714286, + 'y0': 414.74654377880177, + 'x1': -1035.7142857142858, + 'y1': 875.5760368663595 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -1035.7142857142858, + 'y0': 875.5760368663595, + 'x1': -678.5714285714286, + 'y1': 414.74654377880177 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -730.8737890738308, + 'y0': -495.3335180428189, + 'x1': -983.4119252118836, + 'y1': 1785.65609868798 + }); + } + ] + }, + { + name: 'cheater', + json: require('@mocks/cheater'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M0.08104371867979952,10.021132897603486L0.19336443753240018,10.84248366013072L0.26075686884396054,10.84248366013072L0.08104371867979952,10.021132897603486' + ); + }, + function(pos) { + return assertPos(pos, + 'M-0.08743735959910146,10.431808278867104L0.13720407810609983,10.431808278867104L0.13720407810609983,8.789106753812636L-0.08743735959910146,8.789106753812636Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.08104371867979952, + 'y0': 9.199782135076253, + 'x1': 0.3056851563850008, + 'y1': 7.557080610021787 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -0.03127700017280113, + 'y0': 10.021132897603486, + 'x1': -0.14359771902540178, + 'y1': 10.84248366013072 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -0.19012248410964433, + 'y0': 8.038216747244757, + 'x1': 0.12756848376404212, + 'y1': 10.36134752290775 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.08104371867979954, + 'y0': 8.378431372549018, + 'x1': -0.14359771902540183, + 'y1': 10.021132897603488 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': -0.1435977190254018, + 'y0': 10.021132897603488, + 'x1': 0.08104371867979956, + 'y1': 8.378431372549018 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.0481457417956205, + 'y0': 5.134303277666014, + 'x1': -0.11069974214122275, + 'y1': 13.265260992486493 + }); + } + ] + }, + { + name: 'box plot', + json: require('@mocks/1'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M492.4445277361319,7.3824607089211325L509.46101949025484,8.652380340970662L519.6709145427286,8.652380340970662L492.4445277361319,7.3824607089211325' + ); + }, + function(pos) { + return assertPos(pos, + 'M466.9197901049475,8.017420524945898L500.95277361319336,8.017420524945898L500.95277361319336,5.477581260846837L466.9197901049475,5.477581260846837Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 492.4445277361319, + 'y0': 6.112541076871603, + 'x1': 526.4775112443778, + 'y1': 3.572701812772542 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 475.42803598200896, + 'y0': 7.3824607089211325, + 'x1': 458.411544227886, + 'y1': 8.652380340970662 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 451.3630825593184, + 'y0': 4.316603510103307, + 'x1': 499.49298940469953, + 'y1': 7.908478643639898 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 492.4445277361319, + 'y0': 4.842621444822074, + 'x1': 458.41154422788605, + 'y1': 7.382460708921132 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 458.41154422788605, + 'y0': 7.382460708921133, + 'x1': 492.4445277361319, + 'y1': 4.842621444822072 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 487.4605126933543, + 'y0': -0.1732404068174329, + 'x1': 463.3955592706636, + 'y1': 12.398322560560638 + }); + } + ] + }, + { + name: 'mapbox', + json: require('@mocks/mapbox_angles'), + testPos: [ + function(pos) { + return assertPos(pos, + 'M0.2076923076923077,0.8725490196078431L0.2846153846153846,0.9705882352941176L0.33076923076923076,0.9705882352941176L0.2076923076923077,0.8725490196078431' + ); + }, + function(pos) { + return assertPos(pos, + 'M0.09230769230769231,0.9215686274509804L0.24615384615384617,0.9215686274509804L0.24615384615384617,0.7254901960784313L0.09230769230769231,0.7254901960784313Z' + ); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.2076923076923077, + 'y0': 0.7745098039215687, + 'x1': 0.36153846153846153, + 'y1': 0.5784313725490196 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.13076923076923078, + 'y0': 0.8725490196078431, + 'x1': 0.05384615384615385, + 'y1': 0.9705882352941176 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.021983572125146553, + 'y0': 0.6358614154536182, + 'x1': 0.23955488941331504, + 'y1': 0.9131581923895189 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.2076923076923077, + 'y0': 0.6764705882352943, + 'x1': 0.053846153846153794, + 'y1': 0.872549019607843 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.053846153846153835, + 'y0': 0.872549019607843, + 'x1': 0.2076923076923078, + 'y1': 0.6764705882352943 + }); + }, + function(pos) { + return assertPos(pos, { + 'x0': 0.1851620600912729, + 'y0': 0.3862943162113073, + 'x1': 0.07637640144718866, + 'y1': 1.1627252916318298 + }); + } + ] + } + ]; + + allMocks.forEach(function(mockItem) { + ['mouse', 'touch'].forEach(function(device) { + it('@flaky draw various shape types over mock ' + mockItem.name + ' using ' + device, function(done) { + var _drag = function(path) { + return drag(path, {type: device}); + }; + + var fig = Lib.extendDeep({}, mockItem.json); + fig.layout = { + width: 800, + height: 600, + margin: { + t: 60, + l: 40, + r: 20, + b: 30 + } + }; + + var n; + Plotly.newPlot(gd, { + data: fig.data, + layout: fig.layout, + config: { + mapboxAccessToken: require('@build/credentials.json').MAPBOX_ACCESS_TOKEN + } + }) + .then(function() { + n = gd._fullLayout.shapes.length; // initial number of shapes on _fullLayout + }) + + .then(function() { + var newFig = Lib.extendFlat({}, fig); + + newFig.layout.dragmode = 'drawopenpath'; + + return Plotly.react(gd, newFig); + }) + .then(function() { + return _drag([[175, 125], [225, 75], [255, 75], [175, 125]]); + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('path'); + print(obj); + mockItem.testPos[n - 1](obj.path); + }) + + .then(function() { + var newFig = Lib.extendFlat({}, fig); + + newFig.layout.dragmode = 'drawclosedpath'; + + return Plotly.react(gd, newFig); + }) + .then(function() { + return _drag([[100, 100], [200, 100], [200, 200], [100, 200]]); + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('path'); + print(obj); + mockItem.testPos[n - 1](obj.path); + }) + + .then(function() { + var newFig = Lib.extendFlat({}, fig); + + newFig.layout.dragmode = 'drawrect'; + + return Plotly.react(gd, newFig); + }) + .then(function() { + return _drag([[175, 175], [275, 275]]); + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + print(obj); + mockItem.testPos[n - 1]({ + x0: obj.x0, + y0: obj.y0, + x1: obj.x1, + y1: obj.y1 + }); + }) + + .then(function() { + var newFig = Lib.extendFlat({}, fig); + + newFig.layout.dragmode = 'drawline'; + + return Plotly.react(gd, newFig); + }) + .then(function() { + return _drag([[125, 125], [75, 75]]); + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('line'); + print(obj); + mockItem.testPos[n - 1]({ + x0: obj.x0, + y0: obj.y0, + x1: obj.x1, + y1: obj.y1 + }); + }) + + .then(function() { + var newFig = Lib.extendFlat({}, fig); + + newFig.layout.dragmode = 'drawcircle'; + + return Plotly.react(gd, newFig); + }) + .then(function() { + return _drag([[125, 175], [75, 225]]); + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('circle'); + print(obj); + mockItem.testPos[n - 1]({ + x0: obj.x0, + y0: obj.y0, + x1: obj.x1, + y1: obj.y1 + }); + }) + + .then(function() { + var newFig = Lib.extendFlat({}, fig); + + newFig.layout.dragmode = 'drawcircle'; + + return Plotly.react(gd, newFig); + }) + .then(function() { + return _drag([[125, 175], [126, 225]]); // dx close to 0 should draw a circle not an ellipse + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('circle'); + print(obj); + mockItem.testPos[n - 1]({ + x0: obj.x0, + y0: obj.y0, + x1: obj.x1, + y1: obj.y1 + }); + }) + .then(function() { + return _drag([[125, 175], [75, 176]]); // dy close to 0 should draw a circle not an ellipse + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('circle'); + print(obj); + mockItem.testPos[n - 1]({ + x0: obj.x0, + y0: obj.y0, + x1: obj.x1, + y1: obj.y1 + }); + }) + .then(function() { + return _drag([[125, 175], [150, 350]]); // ellipse + }) + .then(function() { + var shapes = gd._fullLayout.shapes; + expect(shapes.length).toEqual(++n); + var obj = shapes[n - 1]._input; + expect(obj.type).toEqual('circle'); + print(obj); + mockItem.testPos[n - 1]({ + x0: obj.x0, + y0: obj.y0, + x1: obj.x1, + y1: obj.y1 + }); + }) + + .catch(failTest) + .then(done); + }); + }); + }); +}); From ae24e27e7bff982545256c77d0033313181fd20c Mon Sep 17 00:00:00 2001 From: archmoj Date: Fri, 24 Apr 2020 19:09:50 -0400 Subject: [PATCH 48/71] active shape test --- test/jasmine/tests/draw_shape_test.js | 418 +++++++++++++++++++++++++- 1 file changed, 411 insertions(+), 7 deletions(-) diff --git a/test/jasmine/tests/draw_shape_test.js b/test/jasmine/tests/draw_shape_test.js index cf08a164d73..538e8fe1ba0 100644 --- a/test/jasmine/tests/draw_shape_test.js +++ b/test/jasmine/tests/draw_shape_test.js @@ -8,6 +8,7 @@ var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); var mouseEvent = require('../assets/mouse_event'); var touchEvent = require('../assets/touch_event'); +var click = require('../assets/click'); function drag(path, options) { var len = path.length; @@ -44,8 +45,6 @@ function assertPos(actual, expected) { expect(typeof actual).toEqual(typeof expected); if(typeof actual === 'string') { - expect(actual).toEqual(expected); - if(expected.indexOf('_') !== -1) { actual = fixDates(actual); expected = fixDates(expected); @@ -69,7 +68,6 @@ function assertPos(actual, expected) { var o2 = Object.keys(expected); expect(o1.length === o2.length); for(var j = 0; j < o1.length; j++) { - expect(o1[j]).toEqual(o2[j]); var key = o1[j]; var posA = actual[key]; @@ -748,11 +746,11 @@ describe('Draw new shapes to layout', function() { allMocks.forEach(function(mockItem) { ['mouse', 'touch'].forEach(function(device) { - it('@flaky draw various shape types over mock ' + mockItem.name + ' using ' + device, function(done) { - var _drag = function(path) { - return drag(path, {type: device}); - }; + var _drag = function(path) { + return drag(path, {type: device}); + }; + it('@flaky draw various shape types over mock ' + mockItem.name + ' using ' + device, function(done) { var fig = Lib.extendDeep({}, mockItem.json); fig.layout = { width: 800, @@ -948,3 +946,409 @@ describe('Draw new shapes to layout', function() { }); }); }); + +describe('Activate and deactivate shapes to edit', function() { + var fig = { + data: [{ x: [0, 50], y: [0, 50] }], + layout: { + width: 800, + height: 600, + margin: { + t: 100, + b: 50, + l: 100, + r: 50 + }, + + yaxis: { + autorange: 'reversed' + }, + + template: { + layout: { + shapes: [{ + name: 'myPath', + editable: true, + layer: 'below', + line: { width: 0 }, + fillcolor: 'gray', + opacity: 0.5, + xref: 'paper', + yref: 'paper', + path: 'M0.5,0.3C0.5,0.9 0.9,0.9 0.9,0.3C0.9,0.1 0.5,0.1 0.5,0.3ZM0.6,0.4C0.6,0.5 0.66,0.5 0.66,0.4ZM0.74,0.4C0.74,0.5 0.8,0.5 0.8,0.4ZM0.6,0.3C0.63,0.2 0.77,0.2 0.8,0.3Z' + }] + } + }, + shapes: [ + { + editable: true, + layer: 'below', + type: 'rect', + line: { width: 5 }, + fillcolor: 'red', + opacity: 0.5, + xref: 'xaxis', + yref: 'yaxis', + y0: 25, + y1: 75, + x0: 25, + x1: 75 + }, + { + editable: true, + layer: 'top', + type: 'circle', + line: { width: 5 }, + fillcolor: 'green', + opacity: 0.5, + xref: 'xaxis', + yref: 'yaxis', + y0: 25, + y1: 75, + x0: 125, + x1: 175 + }, + { + editable: true, + line: { width: 5 }, + fillcolor: 'blue', + path: 'M250,25L225,75L275,75Z' + }, + { + editable: true, + line: { width: 15 }, + path: 'M250,225L225,275L275,275' + }, + { + editable: true, + layer: 'below', + path: 'M320,100C390,180 290,180 360,100Z', + fillcolor: 'rgba(0,127,127,0.5)', + line: { width: 5 } + }, + { + editable: true, + line: { + width: 5, + color: 'orange' + }, + fillcolor: 'rgba(127,255,127,0.5)', + path: 'M0,100V200H50L0,300Q100,300 100,200T150,200C100,300 200,300 200,200S150,200 150,100Z' + }, + { + editable: true, + line: { width: 2 }, + fillcolor: 'yellow', + + path: 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z' + } + ] + }, + config: { + editable: false, + modeBarButtonsToAdd: [ + 'drawline', + 'drawopenpath', + 'drawclosedpath', + 'drawcircle', + 'drawrect', + 'eraseshape' + ] + } + }; + + var gd; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(destroyGraphDiv); + + ['mouse'].forEach(function(device) { + it('@flaky activate editable shapes using' + device, function(done) { + var i; + + Plotly.newPlot(gd, { + data: fig.data, + layout: fig.layout, + config: fig.config + }) + + // shape between 175, 160 and 255, 230 + .then(function() { click(200, 160); }) // activate shape + .then(function() { + i = 0; // test first shape i.e. case of rectangle + + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'activate shape by clicking border'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('rect'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'x0': 25, + 'y0': 25, + 'x1': 75, + 'y1': 75 + }); + }) + .then(function() { drag([[175, 160], [150, 100]]); }) // move vertex + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('rect'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'x0': 9.494573643410854, + 'y0': -17.732937685459945, + 'x1': 75.0015503875969, + 'y1': 74.99821958456974 + }); + }) + .then(function() { drag([[150, 100], [175, 160]]); }) // move vertex back + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('rect'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'x0': 25, + 'y0': 25, + 'x1': 75, + 'y1': 75 + }); + }) + .then(function() { drag([[215, 195], [150, 100]]); }) // move shape + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('rect'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'y0': -42.65875370919882, + 'y1': 7.342433234421367, + 'x0': -15.311627906976742, + 'x1': 34.691472868217055 + }); + }) + .then(function() { drag([[150, 100], [215, 195]]); }) // move shape back + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('rect'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'x0': 25, + 'y0': 25, + 'x1': 75, + 'y1': 75 + }); + }) + .then(function() { click(100, 100); }) + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(undefined, 'deactivate shape by clicking outside'); + }) + .then(function() { click(255, 230); }) + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'activate shape by clicking on corner'); + }) + .then(function() { click(215, 195); }) + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(undefined, 'deactivate shape by clicking inside'); + }) + + // next shape + .then(function() { click(355, 225); }) // activate shape + .then(function() { + i = 1; // test second shape i.e. case of circle + + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'activate shape by clicking border'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('circle'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'x0': 125, + 'x1': 175, + 'y0': 25, + 'y1': 75 + }); + }) + .then(function() { drag([[338, 196], [300, 175]]); }) // move vertex + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + expect(obj.type).toEqual('circle'); + print(obj); + assertPos({ + 'x0': obj.x0, + 'y0': obj.y0, + 'x1': obj.x1, + 'y1': obj.y1 + }, { + 'x0': 186.78449612403102, + 'y0': 74.99821958456971, + 'x1': 113.21550387596898, + 'y1': 10.04154302670623 + }); + }) + + // next shape + .then(function() { click(500, 225); }) // activate shape + .then(function() { + i = 2; // test third shape i.e. case of closed-path + + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'activate shape by clicking border'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M250,25L225,75L275,75Z'); + }) + .then(function() { drag([[540, 160], [500, 120]]); }) // move vertex + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M225.1968992248062,-3.4896142433234463L225,75L275,75Z'); + }) + .then(function() { drag([[500, 120], [540, 160]]); }) // move vertex back + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M250,25L225,75L275,75Z'); + }) + + // next shape + .then(function() { click(300, 266); }) // activate shape + .then(function() { + i = 5; // test case of bezier curves + + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'activate shape by clicking border'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M0,100V200H50L0,300Q100,300 100,200T150,200C100,300 200,300 200,200S150,200 150,100Z'); + }) + .then(function() { drag([[297, 407], [200, 300]]); }) // move vertex + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M0,100.00237388724034L0,199.99762611275966L50.00310077519379,199.99762611275966L0,300Q100,300,39.84496124031008,123.79584569732937T150.0031007751938,199.99762611275966C100,300,200,300,200,199.99762611275966S150.0031007751938,199.99762611275966,150.0031007751938,100.00237388724034Z'); + }) + .then(function() { drag([[200, 300], [297, 407]]); }) // move vertex back + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M0,100.00237388724034L0,199.99762611275966L50.00310077519379,199.99762611275966L0,300Q100,300,100,199.9976261127597T150.0031007751938,199.99762611275966C100,300,200,300,200,199.99762611275966S150.0031007751938,199.99762611275966,150.0031007751938,100.00237388724034Z'); + }) + + // next shape + .then(function() { click(627, 193); }) // activate shape + .then(function() { + i = 6; // test case of multi-cell path + + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'activate shape by clicking border'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z'); + }) + .then(function() { drag([[717, 225], [725, 230]]); }) // move vertex + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M300,69.99881305637984C300,9.998813056379817,380,9.998813056379817,380,69.99881305637984C380,90.00356083086054,300,90.00356083086054,300,69.99881305637984ZM320,60.00000000000001C320,50.00118694362017,332,50.00118694362017,332,60.00000000000001ZM348,60.00000000000001C348,50.00118694362017,360,50.00118694362017,360,60.00000000000001ZM320,69.99881305637984C326.0031007751938,79.99762611275966,354.0031007751938,79.99762611275966,364.9612403100775,69.99881305637984Z'); + }) + .then(function() { drag([[725, 230], [717, 225]]); }) // move vertex back + .then(function() { + var id = gd._fullLayout._activeShapeIndex; + expect(id).toEqual(i, 'keep shape active after drag corner'); + + var shapes = gd._fullLayout.shapes; + var obj = shapes[id]._input; + print(obj); + assertPos(obj.path, 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z'); + }) + + .catch(failTest) + .then(done); + }); + }); +}); From 8c8f724489f9102a9cf019d138c0fa78ae352fd8 Mon Sep 17 00:00:00 2001 From: archmoj Date: Sat, 25 Apr 2020 15:19:39 -0400 Subject: [PATCH 49/71] fix double click path vertices --- src/plots/cartesian/new_shape.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/plots/cartesian/new_shape.js b/src/plots/cartesian/new_shape.js index e8772bc4b8b..26145aeacf5 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/plots/cartesian/new_shape.js @@ -159,26 +159,36 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { function removeVertex() { if(!polygons.length) return; + var len = polygons[indexI].length; + if(len < 3) return; var newPolygon = []; - for(var j = 0; j < polygons[indexI].length; j++) { + for(var j = 0; j < len; j++) { if(j !== indexJ) { newPolygon.push( polygons[indexI][j] ); } } + + if(indexJ === 0) { + newPolygon[indexI][0] = 'M'; + } + polygons[indexI] = newPolygon; + + redraw(); } function clickVertexController(numClicks) { if(numClicks === 2) { var cell = polygons[indexI]; - if(cell.length > 4) { + if( + !pointsShapeRectangle(cell) && + !pointsShapeEllipse(cell) + ) { removeVertex(); } - - redraw(); } } From 852a7fb9605921d984ba08dba200be57ab487b5a Mon Sep 17 00:00:00 2001 From: archmoj Date: Sun, 26 Apr 2020 11:13:04 -0400 Subject: [PATCH 50/71] add test for eraseshape button --- test/jasmine/tests/draw_shape_test.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/jasmine/tests/draw_shape_test.js b/test/jasmine/tests/draw_shape_test.js index 538e8fe1ba0..9c4e9b6a5d9 100644 --- a/test/jasmine/tests/draw_shape_test.js +++ b/test/jasmine/tests/draw_shape_test.js @@ -6,6 +6,7 @@ var Lib = require('@src/lib'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var failTest = require('../assets/fail_test'); +var selectButton = require('../assets/modebar_button'); var mouseEvent = require('../assets/mouse_event'); var touchEvent = require('../assets/touch_event'); var click = require('../assets/click'); @@ -1346,6 +1347,15 @@ describe('Activate and deactivate shapes to edit', function() { print(obj); assertPos(obj.path, 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z'); }) + // erase shape + .then(function() { + expect(gd._fullLayout.shapes.length).toEqual(8); + selectButton(gd._fullLayout._modeBar, 'eraseshape').click(); + }) + .then(function() { + expect(gd._fullLayout.shapes.length).toEqual(7); + expect(gd._fullLayout._activeShapeIndex).toEqual(undefined, 'clear active shape index'); + }) .catch(failTest) .then(done); From 3eee6f112938f8dafc5953e5b851da2aad84fb37 Mon Sep 17 00:00:00 2001 From: Mojtaba Samimi <33888540+archmoj@users.noreply.github.com> Date: Mon, 27 Apr 2020 07:56:21 -0400 Subject: [PATCH 51/71] Update test/jasmine/tests/draw_shape_test.js Co-Authored-By: alexcjohnson --- test/jasmine/tests/draw_shape_test.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/jasmine/tests/draw_shape_test.js b/test/jasmine/tests/draw_shape_test.js index 9c4e9b6a5d9..2e122c75935 100644 --- a/test/jasmine/tests/draw_shape_test.js +++ b/test/jasmine/tests/draw_shape_test.js @@ -86,11 +86,7 @@ function assertPos(actual, expected) { function fixDates(str) { // hack to conver date axes to some numbers to parse with parse-svg-path - str = str.replace(/ /g, ''); - str = str.replace(/_/g, ''); - str = str.replace(/-/g, ''); - str = str.replace(/:/g, ''); - return str; + return str.replace(/[ _\-:]/g, ''); } describe('Draw new shapes to layout', function() { From 44dec73c5f682ef97454ea86a31c0ff4eb30d11a Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 08:06:06 -0400 Subject: [PATCH 52/71] update draw modebar button descriptions --- src/components/modebar/buttons.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index a30f79bcb84..069e8d0958d 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -136,7 +136,7 @@ modeBarButtons.lasso2d = { modeBarButtons.drawclosedpath = { name: 'drawclosedpath', - title: function(gd) { return _(gd, 'Draw Closed Freeform'); }, + title: function(gd) { return _(gd, 'Draw closed freeform'); }, attr: 'dragmode', val: 'drawclosedpath', icon: Icons.drawclosedpath, @@ -145,7 +145,7 @@ modeBarButtons.drawclosedpath = { modeBarButtons.drawopenpath = { name: 'drawopenpath', - title: function(gd) { return _(gd, 'Draw Open Freeform'); }, + title: function(gd) { return _(gd, 'Draw open freeform'); }, attr: 'dragmode', val: 'drawopenpath', icon: Icons.drawopenpath, @@ -154,7 +154,7 @@ modeBarButtons.drawopenpath = { modeBarButtons.drawline = { name: 'drawline', - title: function(gd) { return _(gd, 'Draw Line'); }, + title: function(gd) { return _(gd, 'Draw line'); }, attr: 'dragmode', val: 'drawline', icon: Icons.drawline, @@ -163,7 +163,7 @@ modeBarButtons.drawline = { modeBarButtons.drawrect = { name: 'drawrect', - title: function(gd) { return _(gd, 'Draw Rectangle'); }, + title: function(gd) { return _(gd, 'Draw rectangle'); }, attr: 'dragmode', val: 'drawrect', icon: Icons.drawrect, @@ -172,7 +172,7 @@ modeBarButtons.drawrect = { modeBarButtons.drawcircle = { name: 'drawcircle', - title: function(gd) { return _(gd, 'Draw Ellipse'); }, + title: function(gd) { return _(gd, 'Draw circle'); }, attr: 'dragmode', val: 'drawcircle', icon: Icons.drawcircle, @@ -181,7 +181,7 @@ modeBarButtons.drawcircle = { modeBarButtons.eraseshape = { name: 'eraseshape', - title: function(gd) { return _(gd, 'Erase Active Shape'); }, + title: function(gd) { return _(gd, 'Erase active shape'); }, icon: Icons.eraseshape, click: eraseActiveShape }; From d99b44b414b8f90feca4f285a579d0cc6306f42f Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 08:10:04 -0400 Subject: [PATCH 53/71] update shape.editable description --- src/components/shapes/attributes.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js index 0d736e9ed52..1b8b0ff2665 100644 --- a/src/components/shapes/attributes.js +++ b/src/components/shapes/attributes.js @@ -256,7 +256,8 @@ module.exports = templatedArray('shape', { editType: 'calc+arraydraw', description: [ 'Determines whether the shape could be activated for edit or not.', - 'Please note that setting to *false* has no effect in case `config.editable` is set to true' + 'Has no effect when the older editable shapes mode is enabled via', + '`config.editable` or `config.edits.shapePosition`.' ].join(' ') }, From 1f196994c9cfff253a76f5af4af9dd7b69956732 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 08:23:27 -0400 Subject: [PATCH 54/71] clear ternary and add comment about mapbox --- src/components/modebar/manage.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 12234ca8f1b..3198f0b66a2 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -187,9 +187,8 @@ function getButtonGroups(gd) { if(typeof b === 'string') { if(DRAW_MODES.indexOf(b) !== -1) { if( - // fullLayout._has('ternary') || - fullLayout._has('mapbox') || - fullLayout._has('cartesian') + fullLayout._has('mapbox') || // draw shapes in paper coordinate (could be improved in future to support data coordinate, when there is no pitch) + fullLayout._has('cartesian') // draw shapes in data coordinate ) { dragModeGroup.push(b); } From a6662067ae8bb6b62f124f5cd0ed1272e1fcc00b Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 08:28:08 -0400 Subject: [PATCH 55/71] erase getAxId helper function --- src/plots/cartesian/helpers.js | 5 ----- src/plots/cartesian/select.js | 5 ++--- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/plots/cartesian/helpers.js b/src/plots/cartesian/helpers.js index 23a1b4799c3..b07c36f0703 100644 --- a/src/plots/cartesian/helpers.js +++ b/src/plots/cartesian/helpers.js @@ -9,10 +9,6 @@ 'use strict'; -function getAxId(ax) { - return ax._id; -} - // in v2 (once log ranges are fixed), // we'll be able to p2r here for all axis types function p2r(ax, v) { @@ -49,7 +45,6 @@ function getTransform(plotinfo) { } module.exports = { - getAxId: getAxId, p2r: p2r, r2p: r2p, axValue: axValue, diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 155b83af6ba..bd9e5e51f89 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -45,7 +45,6 @@ var handleEllipse = newShape.handleEllipse; var addNewShapes = newShape.addNewShapes; var helpers = require('./helpers'); -var getAxId = helpers.getAxId; var p2r = helpers.p2r; var axValue = helpers.axValue; var getTransform = helpers.getTransform; @@ -634,8 +633,8 @@ function clearSelectionsCache(dragOptions) { function determineSearchTraces(gd, xAxes, yAxes, subplot) { var searchTraces = []; - var xAxisIds = xAxes.map(getAxId); - var yAxisIds = yAxes.map(getAxId); + var xAxisIds = xAxes.map(function(ax) { return ax._id; }); + var yAxisIds = yAxes.map(function(ax) { return ax._id; }); var cd, trace, i; for(i = 0; i < gd.calcdata.length; i++) { From 63ef7aaf311858101c7ed3075a40472eb6341cd1 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 08:36:43 -0400 Subject: [PATCH 56/71] move newshape & activeshape attributes & defaults to components/shapes/draw_newshape --- .../shapes/draw_newshape/attributes.js | 120 ++++++++++++++++++ .../shapes/draw_newshape/defaults.js | 30 +++++ src/components/shapes/index.js | 1 + src/plots/layout_attributes.js | 109 +--------------- src/plots/plots.js | 18 +-- 5 files changed, 158 insertions(+), 120 deletions(-) create mode 100644 src/components/shapes/draw_newshape/attributes.js create mode 100644 src/components/shapes/draw_newshape/defaults.js diff --git a/src/components/shapes/draw_newshape/attributes.js b/src/components/shapes/draw_newshape/attributes.js new file mode 100644 index 00000000000..90d6cb810e5 --- /dev/null +++ b/src/components/shapes/draw_newshape/attributes.js @@ -0,0 +1,120 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var dash = require('../../drawing/attributes').dash; +var extendFlat = require('../../../lib/extend').extendFlat; + +module.exports = { + newshape: { + line: { + color: { + valType: 'color', + editType: 'none', + role: 'info', + description: [ + 'Sets the line color.', + 'By default uses either dark grey or white', + 'to increase contrast with background color.' + ].join(' ') + }, + width: { + valType: 'number', + min: 0, + dflt: 4, + role: 'info', + editType: 'none', + description: 'Sets the line width (in px).' + }, + dash: extendFlat({}, dash, { + dflt: 'solid', + editType: 'none' + }), + role: 'info', + editType: 'none' + }, + fillcolor: { + valType: 'color', + dflt: 'rgba(0,0,0,0)', + role: 'info', + editType: 'none', + description: [ + 'Sets the color filling new shapes\' interior.', + 'Please note that if using a fillcolor with alpha greater than half,', + 'drag inside the active shape starts moving the shape underneath,', + 'otherwise a new shape could be started over.' + ].join(' ') + }, + fillrule: { + valType: 'enumerated', + values: ['evenodd', 'nonzero'], + dflt: 'evenodd', + role: 'info', + editType: 'none', + description: [ + 'Determines the path\'s interior.', + 'For more info please visit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule' + ].join(' ') + }, + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 1, + role: 'info', + editType: 'none', + description: 'Sets the opacity of new shapes.' + }, + layer: { + valType: 'enumerated', + values: ['below', 'above'], + dflt: 'above', + role: 'info', + editType: 'none', + description: 'Specifies whether new shapes are drawn below or above traces.' + }, + drawdirection: { + valType: 'enumerated', + role: 'info', + values: ['ortho', 'horizontal', 'vertical', 'diagonal'], + dflt: 'diagonal', + editType: 'none', + 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.' + ].join(' ') + }, + + editType: 'none' + }, + + activeshape: { + fillcolor: { + valType: 'color', + dflt: 'rgb(255,0,255)', + role: 'style', + editType: 'none', + description: 'Sets the color filling the active shape\' interior.' + }, + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.5, + role: 'info', + editType: 'none', + description: 'Sets the opacity of the active shape.' + }, + editType: 'none' + } +}; diff --git a/src/components/shapes/draw_newshape/defaults.js b/src/components/shapes/draw_newshape/defaults.js new file mode 100644 index 00000000000..06b37bd1345 --- /dev/null +++ b/src/components/shapes/draw_newshape/defaults.js @@ -0,0 +1,30 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Color = require('../../color'); + + +module.exports = function supplyDrawNewShapeDefaults(layoutIn, layoutOut, coerce) { + coerce('newshape.drawdirection'); + coerce('newshape.layer'); + coerce('newshape.fillcolor'); + coerce('newshape.fillrule'); + coerce('newshape.opacity'); + var newshapeLineWidth = coerce('newshape.line.width'); + if(newshapeLineWidth) { + var bgcolor = (layoutIn || {}).plot_bgcolor || '#FFF'; + coerce('newshape.line.color', Color.contrast(bgcolor)); + coerce('newshape.line.dash'); + } + + coerce('activeshape.fillcolor'); + coerce('activeshape.opacity'); +}; diff --git a/src/components/shapes/index.js b/src/components/shapes/index.js index 9cbdefd62f8..db2160e4cc5 100644 --- a/src/components/shapes/index.js +++ b/src/components/shapes/index.js @@ -17,6 +17,7 @@ module.exports = { layoutAttributes: require('./attributes'), supplyLayoutDefaults: require('./defaults'), + supplyDrawNewShapeDefaults: require('./draw_newshape/defaults'), includeBasePlot: require('../../plots/cartesian/include_components')('shapes'), calcAutorange: require('./calc_autorange'), diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 4c9160a160c..2a2a2f3a473 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -11,7 +11,7 @@ var fontAttrs = require('./font_attributes'); var animationAttrs = require('./animation_attributes'); var colorAttrs = require('../components/color/attributes'); -var dash = require('../components/drawing/attributes').dash; +var drawNewShapeAttrs = require('../components/shapes/draw_newshape/attributes'); var padAttrs = require('./pad_attributes'); var extendFlat = require('../lib/extend').extendFlat; @@ -445,111 +445,8 @@ module.exports = { editType: 'modebar' }, - newshape: { - line: { - color: { - valType: 'color', - editType: 'none', - role: 'info', - description: [ - 'Sets the line color.', - 'By default uses either dark grey or white', - 'to increase contrast with background color.' - ].join(' ') - }, - width: { - valType: 'number', - min: 0, - dflt: 4, - role: 'info', - editType: 'none', - description: 'Sets the line width (in px).' - }, - dash: extendFlat({}, dash, { - dflt: 'solid', - editType: 'none' - }), - role: 'info', - editType: 'none' - }, - fillcolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'info', - editType: 'none', - description: [ - 'Sets the color filling new shapes\' interior.', - 'Please note that if using a fillcolor with alpha greater than half,', - 'drag inside the active shape starts moving the shape underneath,', - 'otherwise a new shape could be started over.' - ].join(' ') - }, - fillrule: { - valType: 'enumerated', - values: ['evenodd', 'nonzero'], - dflt: 'evenodd', - role: 'info', - editType: 'none', - description: [ - 'Determines the path\'s interior.', - 'For more info please visit https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule' - ].join(' ') - }, - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - role: 'info', - editType: 'none', - description: 'Sets the opacity of new shapes.' - }, - layer: { - valType: 'enumerated', - values: ['below', 'above'], - dflt: 'above', - role: 'info', - editType: 'none', - description: 'Specifies whether new shapes are drawn below or above traces.' - }, - drawdirection: { - valType: 'enumerated', - role: 'info', - values: ['ortho', 'horizontal', 'vertical', 'diagonal'], - dflt: 'diagonal', - editType: 'none', - 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.' - ].join(' ') - }, - - editType: 'none' - }, - - activeshape: { - fillcolor: { - valType: 'color', - dflt: 'rgb(255,0,255)', - role: 'style', - editType: 'none', - description: 'Sets the color filling the active shape\' interior.' - }, - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.5, - role: 'info', - editType: 'none', - description: 'Sets the opacity of the active shape.' - }, - editType: 'none' - }, + newshape: drawNewShapeAttrs.newshape, + activeshape: drawNewShapeAttrs.activeshape, meta: { valType: 'any', diff --git a/src/plots/plots.js b/src/plots/plots.js index 8d558d01235..309e1edbe43 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1527,20 +1527,10 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { coerce('modebar.activecolor', Color.addOpacity(modebarDefaultColor, 0.7)); coerce('modebar.uirevision', uirevision); - coerce('newshape.drawdirection'); - coerce('newshape.layer'); - coerce('newshape.fillcolor'); - coerce('newshape.fillrule'); - coerce('newshape.opacity'); - var newshapeLineWidth = coerce('newshape.line.width'); - if(newshapeLineWidth) { - var bgcolor = (layoutIn || {}).plot_bgcolor || '#FFF'; - coerce('newshape.line.color', Color.contrast(bgcolor)); - coerce('newshape.line.dash'); - } - - coerce('activeshape.fillcolor'); - coerce('activeshape.opacity'); + Registry.getComponentMethod( + 'shapes', + 'supplyDrawNewShapeDefaults' + )(layoutIn, layoutOut, coerce); coerce('meta'); From d7e24b83426f276f977bfa710dae3dd457c4d94d Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 09:43:54 -0400 Subject: [PATCH 57/71] remove extra layer to activate shape on click --- src/components/shapes/draw.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 3cfe743d644..0ee044e5a33 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -171,11 +171,7 @@ function drawOne(gd, index) { ); } - path.node().addEventListener('click', function() { return clickFn(path); }); - } - - function clickFn(path) { - activateShape(gd, path); + path.node().addEventListener('click', function() { return activateShape(gd, path); }); } } From 1089cd4a26e3555efec55d636a64d80eed73df8e Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 10:46:09 -0400 Subject: [PATCH 58/71] move newshape draw functions from cartesian to components/shapes/draw_newshape --- src/components/shapes/draw.js | 12 ++++++------ .../shapes/draw_newshape/draw.js} | 16 ++++++++-------- src/plots/cartesian/select.js | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) rename src/{plots/cartesian/new_shape.js => components/shapes/draw_newshape/draw.js} (98%) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index 0ee044e5a33..a6d0722203e 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -13,9 +13,9 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); -var newShape = require('../../plots/cartesian/new_shape'); -var readPaths = newShape.readPaths; -var displayOutlines = newShape.displayOutlines; +var drawNewShape = require('./draw_newshape/draw'); +var readPaths = drawNewShape.readPaths; +var displayOutlines = drawNewShape.displayOutlines; var clearOutlineControllers = require('../../plots/cartesian/handle_outline').clearOutlineControllers; @@ -738,17 +738,17 @@ function eraseActiveShape(gd) { var id = gd._fullLayout._activeShapeIndex; var shapes = (gd.layout || {}).shapes || []; if(id < shapes.length) { - var allShapes = []; + var newShapes = []; for(var q = 0; q < shapes.length; q++) { if(q !== id) { - allShapes.push(shapes[q]); + newShapes.push(shapes[q]); } } delete gd._fullLayout._activeShapeIndex; Registry.call('_guiRelayout', gd, { - shapes: allShapes + shapes: newShapes }); } } diff --git a/src/plots/cartesian/new_shape.js b/src/components/shapes/draw_newshape/draw.js similarity index 98% rename from src/plots/cartesian/new_shape.js rename to src/components/shapes/draw_newshape/draw.js index 26145aeacf5..4189939ecac 100644 --- a/src/plots/cartesian/new_shape.js +++ b/src/components/shapes/draw_newshape/draw.js @@ -11,16 +11,16 @@ var parseSvgPath = require('parse-svg-path'); -var dragElement = require('../../components/dragelement'); -var dragHelpers = require('../../components/dragelement/helpers'); +var dragElement = require('../../dragelement'); +var dragHelpers = require('../../dragelement/helpers'); var drawMode = dragHelpers.drawMode; var openMode = dragHelpers.openMode; -var Registry = require('../../registry'); -var Lib = require('../../lib'); -var setCursor = require('../../lib/setcursor'); +var Registry = require('../../../registry'); +var Lib = require('../../../lib'); +var setCursor = require('../../../lib/setcursor'); -var constants = require('./constants'); +var constants = require('../../../plots/cartesian/constants'); var MINSELECT = constants.MINSELECT; var CIRCLE_SIDES = 32; // should be divisible by 4 var i000 = 0; @@ -31,11 +31,11 @@ var cos45 = Math.cos(Math.PI / 4); var sin45 = Math.sin(Math.PI / 4); var SQRT2 = Math.sqrt(2); -var helpers = require('./helpers'); +var helpers = require('../../../plots/cartesian/helpers'); var p2r = helpers.p2r; var r2p = helpers.r2p; -var handleOutline = require('./handle_outline'); +var handleOutline = require('../../../plots/cartesian/handle_outline'); var clearOutlineControllers = handleOutline.clearOutlineControllers; var clearSelect = handleOutline.clearSelect; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index bd9e5e51f89..0b3515cd327 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -23,6 +23,11 @@ var drawMode = dragHelpers.drawMode; var openMode = dragHelpers.openMode; var selectMode = dragHelpers.selectMode; +var drawNewShape = require('../../components/shapes/draw_newshape/draw'); +var displayOutlines = drawNewShape.displayOutlines; +var handleEllipse = drawNewShape.handleEllipse; +var addNewShapes = drawNewShape.addNewShapes; + var Lib = require('../../lib'); var polygon = require('../../lib/polygon'); var throttle = require('../../lib/throttle'); @@ -39,11 +44,6 @@ var polygonTester = polygon.tester; var clearSelect = require('./handle_outline').clearSelect; -var newShape = require('./new_shape'); -var displayOutlines = newShape.displayOutlines; -var handleEllipse = newShape.handleEllipse; -var addNewShapes = newShape.addNewShapes; - var helpers = require('./helpers'); var p2r = helpers.p2r; var axValue = helpers.axValue; From 26def59598251f6e2f999f2d16c3498661f685ce Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 10:47:47 -0400 Subject: [PATCH 59/71] rename new tests to draw_newshape --- test/jasmine/tests/{draw_shape_test.js => draw_newshape_test.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/jasmine/tests/{draw_shape_test.js => draw_newshape_test.js} (100%) diff --git a/test/jasmine/tests/draw_shape_test.js b/test/jasmine/tests/draw_newshape_test.js similarity index 100% rename from test/jasmine/tests/draw_shape_test.js rename to test/jasmine/tests/draw_newshape_test.js From 6dbbd5c6a73be9c06b6158a207cdf3363f154d36 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 11:16:05 -0400 Subject: [PATCH 60/71] fixup remove vertex --- src/components/shapes/draw_newshape/draw.js | 25 +++++++++++++-------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/components/shapes/draw_newshape/draw.js b/src/components/shapes/draw_newshape/draw.js index 4189939ecac..4291f4f8dcd 100644 --- a/src/components/shapes/draw_newshape/draw.js +++ b/src/components/shapes/draw_newshape/draw.js @@ -159,11 +159,11 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { function removeVertex() { if(!polygons.length) return; - var len = polygons[indexI].length; - if(len < 3) return; + if(!polygons[indexI]) return; + if(!polygons[indexI].length) return; var newPolygon = []; - for(var j = 0; j < len; j++) { + for(var j = 0; j < polygons[indexI].length; j++) { if(j !== indexJ) { newPolygon.push( polygons[indexI][j] @@ -171,17 +171,24 @@ function displayOutlines(polygons, outlines, dragOptions, nCalls) { } } - if(indexJ === 0) { - newPolygon[indexI][0] = 'M'; - } + if(newPolygon.length > 1 && !( + newPolygon.length === 2 && newPolygon[1][0] === 'Z') + ) { + if(indexJ === 0) { + newPolygon[0][0] = 'M'; + } - polygons[indexI] = newPolygon; + polygons[indexI] = newPolygon; - redraw(); + redraw(); + } } - function clickVertexController(numClicks) { + function clickVertexController(numClicks, evt) { if(numClicks === 2) { + indexI = +evt.srcElement.getAttribute('data-i'); + indexJ = +evt.srcElement.getAttribute('data-j'); + var cell = polygons[indexI]; if( !pointsShapeRectangle(cell) && From a824d850cd177bb2a5a4d37e456d8239c5edb6da Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 13:14:45 -0400 Subject: [PATCH 61/71] split new draw code into multiple files --- src/components/shapes/draw.js | 5 +- .../shapes/draw_newshape/constants.js | 22 + .../shapes/draw_newshape/display_outlines.js | 359 +++++++ src/components/shapes/draw_newshape/draw.js | 924 ------------------ .../shapes/draw_newshape/helpers.js | 336 +++++++ .../shapes/draw_newshape/newshapes.js | 271 +++++ src/plots/cartesian/select.js | 9 +- 7 files changed, 994 insertions(+), 932 deletions(-) create mode 100644 src/components/shapes/draw_newshape/constants.js create mode 100644 src/components/shapes/draw_newshape/display_outlines.js delete mode 100644 src/components/shapes/draw_newshape/draw.js create mode 100644 src/components/shapes/draw_newshape/helpers.js create mode 100644 src/components/shapes/draw_newshape/newshapes.js diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index a6d0722203e..fce3c864543 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -13,9 +13,8 @@ var Registry = require('../../registry'); var Lib = require('../../lib'); var Axes = require('../../plots/cartesian/axes'); -var drawNewShape = require('./draw_newshape/draw'); -var readPaths = drawNewShape.readPaths; -var displayOutlines = drawNewShape.displayOutlines; +var readPaths = require('./draw_newshape/helpers').readPaths; +var displayOutlines = require('./draw_newshape/display_outlines'); var clearOutlineControllers = require('../../plots/cartesian/handle_outline').clearOutlineControllers; diff --git a/src/components/shapes/draw_newshape/constants.js b/src/components/shapes/draw_newshape/constants.js new file mode 100644 index 00000000000..49b311ca675 --- /dev/null +++ b/src/components/shapes/draw_newshape/constants.js @@ -0,0 +1,22 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + +'use strict'; + +var CIRCLE_SIDES = 32; // should be divisible by 4 + +module.exports = { + CIRCLE_SIDES: CIRCLE_SIDES, + i000: 0, + i090: CIRCLE_SIDES / 4, + i180: CIRCLE_SIDES / 2, + i270: CIRCLE_SIDES / 4 * 3, + cos45: Math.cos(Math.PI / 4), + sin45: Math.sin(Math.PI / 4), + SQRT2: Math.sqrt(2) +}; diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js new file mode 100644 index 00000000000..e48ad54b333 --- /dev/null +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -0,0 +1,359 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var dragElement = require('../../dragelement'); +var dragHelpers = require('../../dragelement/helpers'); +var drawMode = dragHelpers.drawMode; + +var Registry = require('../../../registry'); +var Lib = require('../../../lib'); +var setCursor = require('../../../lib/setcursor'); + +var MINSELECT = require('../../../plots/cartesian/constants').MINSELECT; +var constants = require('./constants'); +var i000 = constants.i000; +var i090 = constants.i090; +var i180 = constants.i180; +var i270 = constants.i270; + +var handleOutline = require('../../../plots/cartesian/handle_outline'); +var clearOutlineControllers = handleOutline.clearOutlineControllers; + +var helpers = require('./helpers'); +var pointsShapeRectangle = helpers.pointsShapeRectangle; +var pointsShapeEllipse = helpers.pointsShapeEllipse; +var writePaths = helpers.writePaths; +var newShapes = require('./newshapes'); + +module.exports = function displayOutlines(polygons, outlines, dragOptions, nCalls) { + if(!nCalls) nCalls = 0; + + var gd = dragOptions.gd; + + function redraw() { + // recursive call + displayOutlines(polygons, outlines, dragOptions, nCalls++); + + dragOptions.isActiveShape = false; // i.e. to disable controllers + var shapes = newShapes(outlines, dragOptions); + if(shapes) { + Registry.call('_guiRelayout', gd, { + shapes: shapes // update active shape + }); + } + } + + + // remove previous controllers - only if there is an active shape + if(gd._fullLayout._activeShapeIndex >= 0) clearOutlineControllers(gd); + + 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; + + // make outline + outlines.attr('d', writePaths(polygons)); + + // add controllers + var rVertexController = MINSELECT * 1.5; // bigger vertex buttons + var vertexDragOptions; + var shapeDragOptions; + var indexI; // cell index + var indexJ; // vertex or cell-controller index + var copyPolygons; + + copyPolygons = recordPositions([], polygons); + + if(isActiveShape) { + var g = zoomLayer.append('g').attr('class', 'outline-controllers'); + addVertexControllers(g); + addShapeControllers(); + } + + function startDragVertex(evt) { + indexI = +evt.srcElement.getAttribute('data-i'); + indexJ = +evt.srcElement.getAttribute('data-j'); + + vertexDragOptions[indexI][indexJ].moveFn = moveVertexController; + } + + function moveVertexController(dx, dy) { + if(!polygons.length) return; + + var x0 = copyPolygons[indexI][indexJ][1]; + var y0 = copyPolygons[indexI][indexJ][2]; + + var cell = polygons[indexI]; + var len = cell.length; + if(pointsShapeRectangle(cell)) { + for(var q = 0; q < len; q++) { + if(q === indexJ) continue; + + // move other corners of rectangle + var pos = cell[q]; + + if(pos[1] === cell[indexJ][1]) { + pos[1] = x0 + dx; + } + + if(pos[2] === cell[indexJ][2]) { + pos[2] = y0 + dy; + } + } + // move the corner + cell[indexJ][1] = x0 + dx; + cell[indexJ][2] = y0 + dy; + + if(!pointsShapeRectangle(cell)) { + // reject result to rectangles with ensure areas + for(var j = 0; j < len; j++) { + for(var k = 0; k < cell[j].length; k++) { + cell[j][k] = copyPolygons[indexI][j][k]; + } + } + } + } else { // other polylines + cell[indexJ][1] = x0 + dx; + cell[indexJ][2] = y0 + dy; + } + + redraw(); + } + + function endDragVertexController(evt) { + Lib.noop(evt); + } + + function removeVertex() { + if(!polygons.length) return; + if(!polygons[indexI]) return; + if(!polygons[indexI].length) return; + + var newPolygon = []; + for(var j = 0; j < polygons[indexI].length; j++) { + if(j !== indexJ) { + newPolygon.push( + polygons[indexI][j] + ); + } + } + + if(newPolygon.length > 1 && !( + newPolygon.length === 2 && newPolygon[1][0] === 'Z') + ) { + if(indexJ === 0) { + newPolygon[0][0] = 'M'; + } + + polygons[indexI] = newPolygon; + + redraw(); + } + } + + function clickVertexController(numClicks, evt) { + if(numClicks === 2) { + indexI = +evt.srcElement.getAttribute('data-i'); + indexJ = +evt.srcElement.getAttribute('data-j'); + + var cell = polygons[indexI]; + if( + !pointsShapeRectangle(cell) && + !pointsShapeEllipse(cell) + ) { + removeVertex(); + } + } + } + + function addVertexControllers(g) { + vertexDragOptions = []; + + for(var i = 0; i < polygons.length; i++) { + var cell = polygons[i]; + + var onRect = pointsShapeRectangle(cell); + var onEllipse = !onRect && pointsShapeEllipse(cell); + + var minX; + var minY; + var maxX; + var maxY; + if(onRect) { + // compute bounding box + minX = calcMin(cell, 1); + minY = calcMin(cell, 2); + maxX = calcMax(cell, 1); + maxY = calcMax(cell, 2); + } + + vertexDragOptions[i] = []; + for(var j = 0; j < cell.length; j++) { + if(cell[j][0] === 'Z') continue; + + if(onEllipse && + j !== i000 && + j !== i090 && + j !== i180 && + j !== i270 + ) { + continue; + } + + var x = cell[j][1]; + var y = cell[j][2]; + + var rIcon = 3; + var button = g.append(onRect ? 'rect' : 'circle') + .style({ + 'mix-blend-mode': 'luminosity', + fill: 'black', + stroke: 'white', + 'stroke-width': 1 + }); + + if(onRect) { + button + .attr('x', x - rIcon) + .attr('y', y - rIcon) + .attr('width', 2 * rIcon) + .attr('height', 2 * rIcon); + } else { + button + .attr('cx', x) + .attr('cy', y) + .attr('r', rIcon); + } + + var vertex = g.append(onRect ? 'rect' : 'circle') + .attr('data-i', i) + .attr('data-j', j) + .style({ + opacity: 0 + }); + + if(onRect) { + var ratioX = (x - minX) / (maxX - minX); + var ratioY = (y - minY) / (maxY - minY); + if(isFinite(ratioX) && isFinite(ratioY)) { + setCursor( + vertex, + dragElement.getCursor(ratioX, 1 - ratioY) + ); + } + + vertex + .attr('x', x - rVertexController) + .attr('y', y - rVertexController) + .attr('width', 2 * rVertexController) + .attr('height', 2 * rVertexController); + } else { + vertex + .classed('cursor-grab', true) + .attr('cx', x) + .attr('cy', y) + .attr('r', rVertexController); + } + + vertexDragOptions[i][j] = { + element: vertex.node(), + gd: gd, + prepFn: startDragVertex, + doneFn: endDragVertexController, + clickFn: clickVertexController + }; + + dragElement.init(vertexDragOptions[i][j]); + } + } + } + + function moveShape(dx, dy) { + if(!polygons.length) return; + + 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) { + polygons[i][j][k + 1] = copyPolygons[i][j][k + 1] + dx; + polygons[i][j][k + 2] = copyPolygons[i][j][k + 2] + dy; + } + } + } + } + + function moveShapeController(dx, dy) { + moveShape(dx, dy); + + redraw(); + } + + function startDragShapeController(evt) { + indexI = +evt.srcElement.getAttribute('data-i'); + if(!indexI) indexI = 0; // ensure non-existing move button get zero index + + shapeDragOptions[indexI].moveFn = moveShapeController; + } + + function endDragShapeController(evt) { + Lib.noop(evt); + } + + function addShapeControllers() { + shapeDragOptions = []; + + if(!polygons.length) return; + + var i = 0; + shapeDragOptions[i] = { + element: outlines[0][0], + gd: gd, + prepFn: startDragShapeController, + doneFn: endDragShapeController + }; + + dragElement.init(shapeDragOptions[i]); + } +}; + +function calcMin(cell, dim) { + var v = Infinity; + for(var i = 0; i < cell.length; i++) { + v = Math.min(v, cell[i][dim]); + } + return v; +} + +function calcMax(cell, dim) { + var v = -Infinity; + for(var i = 0; i < cell.length; i++) { + v = Math.max(v, cell[i][dim]); + } + return v; +} + +function recordPositions(polygonsOut, polygonsIn) { + for(var i = 0; i < polygonsIn.length; i++) { + var cell = polygonsIn[i]; + polygonsOut[i] = []; + for(var j = 0; j < cell.length; j++) { + polygonsOut[i][j] = []; + for(var k = 0; k < cell[j].length; k++) { + polygonsOut[i][j][k] = cell[j][k]; + } + } + } + return polygonsOut; +} diff --git a/src/components/shapes/draw_newshape/draw.js b/src/components/shapes/draw_newshape/draw.js deleted file mode 100644 index 4291f4f8dcd..00000000000 --- a/src/components/shapes/draw_newshape/draw.js +++ /dev/null @@ -1,924 +0,0 @@ -/** -* Copyright 2012-2020, Plotly, Inc. -* All rights reserved. -* -* This source code is licensed under the MIT license found in the -* LICENSE file in the root directory of this source tree. -*/ - - -'use strict'; - -var parseSvgPath = require('parse-svg-path'); - -var dragElement = require('../../dragelement'); -var dragHelpers = require('../../dragelement/helpers'); -var drawMode = dragHelpers.drawMode; -var openMode = dragHelpers.openMode; - -var Registry = require('../../../registry'); -var Lib = require('../../../lib'); -var setCursor = require('../../../lib/setcursor'); - -var constants = require('../../../plots/cartesian/constants'); -var MINSELECT = constants.MINSELECT; -var CIRCLE_SIDES = 32; // should be divisible by 4 -var i000 = 0; -var i090 = CIRCLE_SIDES / 4; -var i180 = CIRCLE_SIDES / 2; -var i270 = CIRCLE_SIDES / 4 * 3; -var cos45 = Math.cos(Math.PI / 4); -var sin45 = Math.sin(Math.PI / 4); -var SQRT2 = Math.sqrt(2); - -var helpers = require('../../../plots/cartesian/helpers'); -var p2r = helpers.p2r; -var r2p = helpers.r2p; - -var handleOutline = require('../../../plots/cartesian/handle_outline'); -var clearOutlineControllers = handleOutline.clearOutlineControllers; -var clearSelect = handleOutline.clearSelect; - -function recordPositions(polygonsOut, polygonsIn) { - for(var i = 0; i < polygonsIn.length; i++) { - var cell = polygonsIn[i]; - polygonsOut[i] = []; - for(var j = 0; j < cell.length; j++) { - polygonsOut[i][j] = []; - for(var k = 0; k < cell[j].length; k++) { - polygonsOut[i][j][k] = cell[j][k]; - } - } - } - return polygonsOut; -} - -function displayOutlines(polygons, outlines, dragOptions, nCalls) { - if(!nCalls) nCalls = 0; - - var gd = dragOptions.gd; - - function redraw() { - // recursive call - displayOutlines(polygons, outlines, dragOptions, nCalls++); - - dragOptions.isActiveShape = false; // i.e. to disable controllers - var shapes = addNewShapes(outlines, dragOptions); - if(shapes) { - Registry.call('_guiRelayout', gd, { - shapes: shapes // update active shape - }); - } - } - - - // remove previous controllers - only if there is an active shape - if(gd._fullLayout._activeShapeIndex >= 0) clearOutlineControllers(gd); - - 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; - - // make outline - outlines.attr('d', writePaths(polygons)); - - // add controllers - var rVertexController = MINSELECT * 1.5; // bigger vertex buttons - var vertexDragOptions; - var shapeDragOptions; - var indexI; // cell index - var indexJ; // vertex or cell-controller index - var copyPolygons; - - copyPolygons = recordPositions([], polygons); - - if(isActiveShape) { - var g = zoomLayer.append('g').attr('class', 'outline-controllers'); - addVertexControllers(g); - addShapeControllers(); - } - - function startDragVertex(evt) { - indexI = +evt.srcElement.getAttribute('data-i'); - indexJ = +evt.srcElement.getAttribute('data-j'); - - vertexDragOptions[indexI][indexJ].moveFn = moveVertexController; - } - - function moveVertexController(dx, dy) { - if(!polygons.length) return; - - var x0 = copyPolygons[indexI][indexJ][1]; - var y0 = copyPolygons[indexI][indexJ][2]; - - var cell = polygons[indexI]; - var len = cell.length; - if(pointsShapeRectangle(cell)) { - for(var q = 0; q < len; q++) { - if(q === indexJ) continue; - - // move other corners of rectangle - var pos = cell[q]; - - if(pos[1] === cell[indexJ][1]) { - pos[1] = x0 + dx; - } - - if(pos[2] === cell[indexJ][2]) { - pos[2] = y0 + dy; - } - } - // move the corner - cell[indexJ][1] = x0 + dx; - cell[indexJ][2] = y0 + dy; - - if(!pointsShapeRectangle(cell)) { - // reject result to rectangles with ensure areas - for(var j = 0; j < len; j++) { - for(var k = 0; k < cell[j].length; k++) { - cell[j][k] = copyPolygons[indexI][j][k]; - } - } - } - } else { // other polylines - cell[indexJ][1] = x0 + dx; - cell[indexJ][2] = y0 + dy; - } - - redraw(); - } - - function endDragVertexController(evt) { - Lib.noop(evt); - } - - function removeVertex() { - if(!polygons.length) return; - if(!polygons[indexI]) return; - if(!polygons[indexI].length) return; - - var newPolygon = []; - for(var j = 0; j < polygons[indexI].length; j++) { - if(j !== indexJ) { - newPolygon.push( - polygons[indexI][j] - ); - } - } - - if(newPolygon.length > 1 && !( - newPolygon.length === 2 && newPolygon[1][0] === 'Z') - ) { - if(indexJ === 0) { - newPolygon[0][0] = 'M'; - } - - polygons[indexI] = newPolygon; - - redraw(); - } - } - - function clickVertexController(numClicks, evt) { - if(numClicks === 2) { - indexI = +evt.srcElement.getAttribute('data-i'); - indexJ = +evt.srcElement.getAttribute('data-j'); - - var cell = polygons[indexI]; - if( - !pointsShapeRectangle(cell) && - !pointsShapeEllipse(cell) - ) { - removeVertex(); - } - } - } - - function addVertexControllers(g) { - vertexDragOptions = []; - - for(var i = 0; i < polygons.length; i++) { - var cell = polygons[i]; - - var onRect = pointsShapeRectangle(cell); - var onEllipse = !onRect && pointsShapeEllipse(cell); - - var minX; - var minY; - var maxX; - var maxY; - if(onRect) { - // compute bounding box - minX = calcMin(cell, 1); - minY = calcMin(cell, 2); - maxX = calcMax(cell, 1); - maxY = calcMax(cell, 2); - } - - vertexDragOptions[i] = []; - for(var j = 0; j < cell.length; j++) { - if(cell[j][0] === 'Z') continue; - - if(onEllipse && - j !== i000 && - j !== i090 && - j !== i180 && - j !== i270 - ) { - continue; - } - - var x = cell[j][1]; - var y = cell[j][2]; - - var rIcon = 3; - var button = g.append(onRect ? 'rect' : 'circle') - .style({ - 'mix-blend-mode': 'luminosity', - fill: 'black', - stroke: 'white', - 'stroke-width': 1 - }); - - if(onRect) { - button - .attr('x', x - rIcon) - .attr('y', y - rIcon) - .attr('width', 2 * rIcon) - .attr('height', 2 * rIcon); - } else { - button - .attr('cx', x) - .attr('cy', y) - .attr('r', rIcon); - } - - var vertex = g.append(onRect ? 'rect' : 'circle') - .attr('data-i', i) - .attr('data-j', j) - .style({ - opacity: 0 - }); - - if(onRect) { - var ratioX = (x - minX) / (maxX - minX); - var ratioY = (y - minY) / (maxY - minY); - if(isFinite(ratioX) && isFinite(ratioY)) { - setCursor( - vertex, - dragElement.getCursor(ratioX, 1 - ratioY) - ); - } - - vertex - .attr('x', x - rVertexController) - .attr('y', y - rVertexController) - .attr('width', 2 * rVertexController) - .attr('height', 2 * rVertexController); - } else { - vertex - .classed('cursor-grab', true) - .attr('cx', x) - .attr('cy', y) - .attr('r', rVertexController); - } - - vertexDragOptions[i][j] = { - element: vertex.node(), - gd: gd, - prepFn: startDragVertex, - doneFn: endDragVertexController, - clickFn: clickVertexController - }; - - dragElement.init(vertexDragOptions[i][j]); - } - } - } - - function moveShape(dx, dy) { - if(!polygons.length) return; - - 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) { - polygons[i][j][k + 1] = copyPolygons[i][j][k + 1] + dx; - polygons[i][j][k + 2] = copyPolygons[i][j][k + 2] + dy; - } - } - } - } - - function moveShapeController(dx, dy) { - moveShape(dx, dy); - - redraw(); - } - - function startDragShapeController(evt) { - indexI = +evt.srcElement.getAttribute('data-i'); - if(!indexI) indexI = 0; // ensure non-existing move button get zero index - - shapeDragOptions[indexI].moveFn = moveShapeController; - } - - function endDragShapeController(evt) { - Lib.noop(evt); - } - - function addShapeControllers() { - shapeDragOptions = []; - - if(!polygons.length) return; - - var i = 0; - shapeDragOptions[i] = { - element: outlines[0][0], - gd: gd, - prepFn: startDragShapeController, - doneFn: endDragShapeController - }; - - dragElement.init(shapeDragOptions[i]); - } -} - -var iC = [0, 3, 4, 5, 6, 1, 2]; -var iQS = [0, 3, 4, 1, 2]; - -function writePaths(polygons) { - var nI = polygons.length; - if(!nI) return 'M0,0Z'; - - var str = ''; - for(var i = 0; i < nI; i++) { - var nJ = polygons[i].length; - for(var j = 0; j < nJ; j++) { - var w = polygons[i][j][0]; - if(w === 'Z') { - str += 'Z'; - } else { - var nK = polygons[i][j].length; - for(var k = 0; k < nK; k++) { - var realK = k; - if(w === 'Q' || w === 'S') { - realK = iQS[k]; - } else if(w === 'C') { - realK = iC[k]; - } - - str += polygons[i][j][realK]; - if(k > 0 && k < nK - 1) { - str += ','; - } - } - } - } - } - - return str; -} - -function readPaths(str, gd, plotinfo, isActiveShape) { - var cmd = parseSvgPath(str); - - var polys = []; - var n = -1; - var newPoly = function() { - n++; - polys[n] = []; - }; - - var k; - var x = 0; - var y = 0; - var initX; - var initY; - var recStart = function() { - initX = x; - initY = y; - }; - - recStart(); - for(var i = 0; i < cmd.length; i++) { - var newPos = []; - - var x1, x2, y1, y2; // i.e. extra params for curves - - var c = cmd[i][0]; - var w = c; - switch(c) { - case 'M': - newPoly(); - x = +cmd[i][1]; - y = +cmd[i][2]; - newPos.push([w, x, y]); - - recStart(); - break; - - case 'Q': - case 'S': - x1 = +cmd[i][1]; - y1 = +cmd[i][2]; - x = +cmd[i][3]; - y = +cmd[i][4]; - newPos.push([w, x, y, x1, y1]); // -> iQS order - break; - - case 'C': - x1 = +cmd[i][1]; - y1 = +cmd[i][2]; - x2 = +cmd[i][3]; - y2 = +cmd[i][4]; - x = +cmd[i][5]; - y = +cmd[i][6]; - newPos.push([w, x, y, x1, y1, x2, y2]); // -> iC order - break; - - case 'T': - case 'L': - x = +cmd[i][1]; - y = +cmd[i][2]; - newPos.push([w, x, y]); - break; - - case 'H': - w = 'L'; // convert to line (for now) - x = +cmd[i][1]; - newPos.push([w, x, y]); - break; - - case 'V': - w = 'L'; // convert to line (for now) - y = +cmd[i][1]; - newPos.push([w, x, y]); - break; - - case 'A': - w = 'L'; // convert to line to handle circle - var rx = +cmd[i][1]; - var ry = +cmd[i][2]; - if(!+cmd[i][4]) { - rx = -rx; - ry = -ry; - } - - var cenX = x - rx; - var cenY = y; - for(k = 1; k <= CIRCLE_SIDES / 2; k++) { - var t = 2 * Math.PI * k / CIRCLE_SIDES; - newPos.push([ - w, - cenX + rx * Math.cos(t), - cenY + ry * Math.sin(t) - ]); - } - break; - - case 'Z': - if(x !== initX || y !== initY) { - x = initX; - y = initY; - newPos.push([w, x, y]); - } - break; - } - - var domain = (plotinfo || {}).domain; - var size = gd._fullLayout._size; - var xPixelSized = plotinfo && plotinfo.xsizemode === 'pixel'; - var yPixelSized = plotinfo && plotinfo.ysizemode === 'pixel'; - var noOffset = isActiveShape === false; - - for(var j = 0; j < newPos.length; j++) { - for(k = 0; k + 2 < 7; k += 2) { - var _x = newPos[j][k + 1]; - var _y = newPos[j][k + 2]; - - if(_x === undefined || _y === undefined) continue; - // keep track of end point for Z - x = _x; - y = _y; - - if(plotinfo) { - if(plotinfo.xaxis && plotinfo.xaxis.p2r) { - if(noOffset) _x -= plotinfo.xaxis._offset; - if(xPixelSized) { - _x = r2p(plotinfo.xaxis, plotinfo.xanchor) + _x; - } else { - _x = p2r(plotinfo.xaxis, _x); - } - } else { - if(noOffset) _x -= size.l; - if(domain) _x = domain.x[0] + _x / size.w; - else _x = _x / size.w; - } - - if(plotinfo.yaxis && plotinfo.yaxis.p2r) { - if(noOffset) _y -= plotinfo.yaxis._offset; - if(yPixelSized) { - _y = r2p(plotinfo.yaxis, plotinfo.yanchor) - _y; - } else { - _y = p2r(plotinfo.yaxis, _y); - } - } else { - if(noOffset) _y -= size.t; - if(domain) _y = domain.y[1] - _y / size.h; - else _y = 1 - _y / size.h; - } - } - - newPos[j][k + 1] = _x; - newPos[j][k + 2] = _y; - } - polys[n].push( - newPos[j].slice() - ); - } - } - - return polys; -} - -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; -} - -function almostEq(a, b) { - return Math.abs(a - b) <= 1e-6; -} - -function dist(a, b) { - var dx = b[1] - a[1]; - var dy = b[2] - a[2]; - return Math.sqrt( - dx * dx + - dy * dy - ); -} - -function calcMin(cell, dim) { - var v = Infinity; - for(var i = 0; i < cell.length; i++) { - v = Math.min(v, cell[i][dim]); - } - return v; -} - -function calcMax(cell, dim) { - var v = -Infinity; - for(var i = 0; i < cell.length; i++) { - v = Math.max(v, cell[i][dim]); - } - return v; -} - -function pointsShapeRectangle(cell) { - var len = cell.length; - if(len !== 5) return false; - - for(var j = 1; j < 3; j++) { - var e01 = cell[0][j] - cell[1][j]; - var e32 = cell[3][j] - cell[2][j]; - - if(!almostEq(e01, e32)) return false; - - var e03 = cell[0][j] - cell[3][j]; - var e12 = cell[1][j] - cell[2][j]; - if(!almostEq(e03, e12)) return false; - } - - // N.B. rotated rectangles are not valid rects since rotation is not supported in shapes for now. - if( - !almostEq(cell[0][1], cell[1][1]) && - !almostEq(cell[0][1], cell[3][1]) - ) return false; - - // reject cases with zero area - return !!( - dist(cell[0], cell[1]) * - dist(cell[0], cell[3]) - ); -} - -function pointsShapeEllipse(cell) { - var len = cell.length; - if(len !== CIRCLE_SIDES + 1) return false; - - // opposite diagonals should be the same - len = CIRCLE_SIDES; - for(var i = 0; i < len; i++) { - var k = (len * 2 - i) % len; - - var k2 = (len / 2 + k) % len; - var i2 = (len / 2 + i) % len; - - if(!almostEq( - dist(cell[i], cell[i2]), - dist(cell[k], cell[k2]) - )) return false; - } - return true; -} - -function handleEllipse(isEllipse, start, end) { - if(!isEllipse) return [start, end]; // i.e. case of line - - var pos = ellipseOver({ - x0: start[0], - y0: start[1], - x1: end[0], - y1: end[1] - }); - - var cx = (pos.x1 + pos.x0) / 2; - var cy = (pos.y1 + pos.y0) / 2; - var rx = (pos.x1 - pos.x0) / 2; - var ry = (pos.y1 - pos.y0) / 2; - - // make a circle when one dimension is zero - if(!rx) rx = ry = ry / SQRT2; - if(!ry) ry = rx = rx / SQRT2; - - var cell = []; - for(var i = 0; i < CIRCLE_SIDES; i++) { - var t = i * 2 * Math.PI / CIRCLE_SIDES; - cell.push([ - cx + rx * Math.cos(t), - cy + ry * Math.sin(t), - ]); - } - return cell; -} - -function ellipseOver(pos) { - var x0 = pos.x0; - var y0 = pos.y0; - var x1 = pos.x1; - var y1 = pos.y1; - - var dx = x1 - x0; - var dy = y1 - y0; - - x0 -= dx; - y0 -= dy; - - var cx = (x0 + x1) / 2; - var cy = (y0 + y1) / 2; - - var scale = SQRT2; - dx *= scale; - dy *= scale; - - return { - x0: cx - dx, - y0: cy - dy, - x1: cx + dx, - y1: cy + dy - }; -} - -function addNewShapes(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 drwStyle = gd._fullLayout.newshape; - - var plotinfo = dragOptions.plotinfo; - var xaxis = plotinfo.xaxis; - var yaxis = plotinfo.yaxis; - var xPaper = !!plotinfo.domain || !plotinfo.xaxis; - var yPaper = !!plotinfo.domain || !plotinfo.yaxis; - - var isActiveShape = dragOptions.isActiveShape; - var dragmode = dragOptions.dragmode; - - var shapes = (gd.layout || {}).shapes || []; - - if(!drawMode(dragmode) && isActiveShape !== undefined) { - var id = gd._fullLayout._activeShapeIndex; - if(id < shapes.length) { - switch(gd._fullLayout.shapes[id].type) { - case 'rect': - dragmode = 'drawrect'; - break; - case 'circle': - dragmode = 'drawcircle'; - break; - case 'line': - dragmode = 'drawline'; - break; - case 'path': - var path = shapes[id].path || ''; - if(path[path.length - 1] === 'Z') { - dragmode = 'drawclosedpath'; - } else { - dragmode = 'drawopenpath'; - } - break; - } - } - } - - var isOpenMode = openMode(dragmode); - - var polygons = readPaths(d, gd, plotinfo, isActiveShape); - - var newShape = { - editable: true, - - xref: xPaper ? 'paper' : xaxis._id, - yref: yPaper ? 'paper' : yaxis._id, - - layer: drwStyle.layer, - opacity: drwStyle.opacity, - line: { - color: drwStyle.line.color, - width: drwStyle.line.width, - dash: drwStyle.line.dash - } - }; - - if(!isOpenMode) { - newShape.fillcolor = drwStyle.fillcolor; - newShape.fillrule = drwStyle.fillrule; - } - - var cell; - // line, rect and circle can be in one cell - // only define cell if there is single cell - if(polygons.length === 1) cell = polygons[0]; - - if( - cell && - dragmode === 'drawrect' - ) { - newShape.type = 'rect'; - newShape.x0 = cell[0][1]; - newShape.y0 = cell[0][2]; - newShape.x1 = cell[2][1]; - newShape.y1 = cell[2][2]; - } else if( - cell && - dragmode === 'drawline' - ) { - newShape.type = 'line'; - newShape.x0 = cell[0][1]; - newShape.y0 = cell[0][2]; - newShape.x1 = cell[1][1]; - newShape.y1 = cell[1][2]; - } else if( - cell && - dragmode === 'drawcircle' - ) { - newShape.type = 'circle'; // an ellipse! - - var xA = cell[i000][1]; - var xB = cell[i090][1]; - var xC = cell[i180][1]; - var xD = cell[i270][1]; - - var yA = cell[i000][2]; - var yB = cell[i090][2]; - var yC = cell[i180][2]; - var yD = cell[i270][2]; - - var xDateOrLog = plotinfo.xaxis && ( - plotinfo.xaxis.type === 'date' || - plotinfo.xaxis.type === 'log' - ); - - var yDateOrLog = plotinfo.yaxis && ( - plotinfo.yaxis.type === 'date' || - plotinfo.yaxis.type === 'log' - ); - - if(xDateOrLog) { - xA = r2p(plotinfo.xaxis, xA); - xB = r2p(plotinfo.xaxis, xB); - xC = r2p(plotinfo.xaxis, xC); - xD = r2p(plotinfo.xaxis, xD); - } - - if(yDateOrLog) { - yA = r2p(plotinfo.yaxis, yA); - yB = r2p(plotinfo.yaxis, yB); - yC = r2p(plotinfo.yaxis, yC); - yD = r2p(plotinfo.yaxis, yD); - } - - var x0 = (xB + xD) / 2; - var y0 = (yA + yC) / 2; - var rx = (xD - xB + xC - xA) / 2; - var ry = (yD - yB + yC - yA) / 2; - var pos = ellipseOver({ - x0: x0, - y0: y0, - x1: x0 + rx * cos45, - y1: y0 + ry * sin45 - }); - - if(xDateOrLog) { - pos.x0 = p2r(plotinfo.xaxis, pos.x0); - pos.x1 = p2r(plotinfo.xaxis, pos.x1); - } - - if(yDateOrLog) { - pos.y0 = p2r(plotinfo.yaxis, pos.y0); - pos.y1 = p2r(plotinfo.yaxis, pos.y1); - } - - newShape.x0 = pos.x0; - newShape.y0 = pos.y0; - newShape.x1 = pos.x1; - newShape.y1 = pos.y1; - } else { - newShape.type = 'path'; - if(xaxis && yaxis) fixDatesForPaths(polygons, xaxis, yaxis); - newShape.path = writePaths(polygons); - cell = null; - } - - clearSelect(gd); - - var allShapes; - var updatedActiveShape = false; - allShapes = []; - for(var q = 0; q < shapes.length; q++) { - var beforeEdit = gd._fullLayout.shapes[q]; - allShapes[q] = beforeEdit._input; - - if( - isActiveShape !== undefined && - q === gd._fullLayout._activeShapeIndex - ) { - var afterEdit = newShape; - - switch(beforeEdit.type) { - case 'line': - case 'rect': - case 'circle': - updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); - if(updatedActiveShape) { // update active shape - allShapes[q].x0 = afterEdit.x0; - allShapes[q].x1 = afterEdit.x1; - allShapes[q].y0 = afterEdit.y0; - allShapes[q].y1 = afterEdit.y1; - } - break; - - case 'path': - updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); - if(updatedActiveShape) { // update active shape - allShapes[q].path = afterEdit.path; - } - break; - } - } - } - - if(isActiveShape === undefined) { - allShapes.push(newShape); // add new shape - } - - return allShapes; -} - -function hasChanged(beforeEdit, afterEdit, keys) { - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - if(beforeEdit[k] !== afterEdit[k]) { - return true; - } - } - return false; -} - -module.exports = { - displayOutlines: displayOutlines, - handleEllipse: handleEllipse, - addNewShapes: addNewShapes, - readPaths: readPaths -}; diff --git a/src/components/shapes/draw_newshape/helpers.js b/src/components/shapes/draw_newshape/helpers.js new file mode 100644 index 00000000000..2d74f34f834 --- /dev/null +++ b/src/components/shapes/draw_newshape/helpers.js @@ -0,0 +1,336 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var parseSvgPath = require('parse-svg-path'); + +var constants = require('./constants'); +var CIRCLE_SIDES = constants.CIRCLE_SIDES; +var SQRT2 = constants.SQRT2; + +var cartesianHelpers = require('../../../plots/cartesian/helpers'); +var p2r = cartesianHelpers.p2r; +var r2p = cartesianHelpers.r2p; + +var iC = [0, 3, 4, 5, 6, 1, 2]; +var iQS = [0, 3, 4, 1, 2]; + +exports.writePaths = function(polygons) { + var nI = polygons.length; + if(!nI) return 'M0,0Z'; + + var str = ''; + for(var i = 0; i < nI; i++) { + var nJ = polygons[i].length; + for(var j = 0; j < nJ; j++) { + var w = polygons[i][j][0]; + if(w === 'Z') { + str += 'Z'; + } else { + var nK = polygons[i][j].length; + for(var k = 0; k < nK; k++) { + var realK = k; + if(w === 'Q' || w === 'S') { + realK = iQS[k]; + } else if(w === 'C') { + realK = iC[k]; + } + + str += polygons[i][j][realK]; + if(k > 0 && k < nK - 1) { + str += ','; + } + } + } + } + } + + return str; +}; + +exports.readPaths = function(str, gd, plotinfo, isActiveShape) { + var cmd = parseSvgPath(str); + + var polys = []; + var n = -1; + var newPoly = function() { + n++; + polys[n] = []; + }; + + var k; + var x = 0; + var y = 0; + var initX; + var initY; + var recStart = function() { + initX = x; + initY = y; + }; + + recStart(); + for(var i = 0; i < cmd.length; i++) { + var newPos = []; + + var x1, x2, y1, y2; // i.e. extra params for curves + + var c = cmd[i][0]; + var w = c; + switch(c) { + case 'M': + newPoly(); + x = +cmd[i][1]; + y = +cmd[i][2]; + newPos.push([w, x, y]); + + recStart(); + break; + + case 'Q': + case 'S': + x1 = +cmd[i][1]; + y1 = +cmd[i][2]; + x = +cmd[i][3]; + y = +cmd[i][4]; + newPos.push([w, x, y, x1, y1]); // -> iQS order + break; + + case 'C': + x1 = +cmd[i][1]; + y1 = +cmd[i][2]; + x2 = +cmd[i][3]; + y2 = +cmd[i][4]; + x = +cmd[i][5]; + y = +cmd[i][6]; + newPos.push([w, x, y, x1, y1, x2, y2]); // -> iC order + break; + + case 'T': + case 'L': + x = +cmd[i][1]; + y = +cmd[i][2]; + newPos.push([w, x, y]); + break; + + case 'H': + w = 'L'; // convert to line (for now) + x = +cmd[i][1]; + newPos.push([w, x, y]); + break; + + case 'V': + w = 'L'; // convert to line (for now) + y = +cmd[i][1]; + newPos.push([w, x, y]); + break; + + case 'A': + w = 'L'; // convert to line to handle circle + var rx = +cmd[i][1]; + var ry = +cmd[i][2]; + if(!+cmd[i][4]) { + rx = -rx; + ry = -ry; + } + + var cenX = x - rx; + var cenY = y; + for(k = 1; k <= CIRCLE_SIDES / 2; k++) { + var t = 2 * Math.PI * k / CIRCLE_SIDES; + newPos.push([ + w, + cenX + rx * Math.cos(t), + cenY + ry * Math.sin(t) + ]); + } + break; + + case 'Z': + if(x !== initX || y !== initY) { + x = initX; + y = initY; + newPos.push([w, x, y]); + } + break; + } + + var domain = (plotinfo || {}).domain; + var size = gd._fullLayout._size; + var xPixelSized = plotinfo && plotinfo.xsizemode === 'pixel'; + var yPixelSized = plotinfo && plotinfo.ysizemode === 'pixel'; + var noOffset = isActiveShape === false; + + for(var j = 0; j < newPos.length; j++) { + for(k = 0; k + 2 < 7; k += 2) { + var _x = newPos[j][k + 1]; + var _y = newPos[j][k + 2]; + + if(_x === undefined || _y === undefined) continue; + // keep track of end point for Z + x = _x; + y = _y; + + if(plotinfo) { + if(plotinfo.xaxis && plotinfo.xaxis.p2r) { + if(noOffset) _x -= plotinfo.xaxis._offset; + if(xPixelSized) { + _x = r2p(plotinfo.xaxis, plotinfo.xanchor) + _x; + } else { + _x = p2r(plotinfo.xaxis, _x); + } + } else { + if(noOffset) _x -= size.l; + if(domain) _x = domain.x[0] + _x / size.w; + else _x = _x / size.w; + } + + if(plotinfo.yaxis && plotinfo.yaxis.p2r) { + if(noOffset) _y -= plotinfo.yaxis._offset; + if(yPixelSized) { + _y = r2p(plotinfo.yaxis, plotinfo.yanchor) - _y; + } else { + _y = p2r(plotinfo.yaxis, _y); + } + } else { + if(noOffset) _y -= size.t; + if(domain) _y = domain.y[1] - _y / size.h; + else _y = 1 - _y / size.h; + } + } + + newPos[j][k + 1] = _x; + newPos[j][k + 2] = _y; + } + polys[n].push( + newPos[j].slice() + ); + } + } + + return polys; +}; + +function almostEq(a, b) { + return Math.abs(a - b) <= 1e-6; +} + +function dist(a, b) { + var dx = b[1] - a[1]; + var dy = b[2] - a[2]; + return Math.sqrt( + dx * dx + + dy * dy + ); +} + +exports.pointsShapeRectangle = function(cell) { + var len = cell.length; + if(len !== 5) return false; + + for(var j = 1; j < 3; j++) { + var e01 = cell[0][j] - cell[1][j]; + var e32 = cell[3][j] - cell[2][j]; + + if(!almostEq(e01, e32)) return false; + + var e03 = cell[0][j] - cell[3][j]; + var e12 = cell[1][j] - cell[2][j]; + if(!almostEq(e03, e12)) return false; + } + + // N.B. rotated rectangles are not valid rects since rotation is not supported in shapes for now. + if( + !almostEq(cell[0][1], cell[1][1]) && + !almostEq(cell[0][1], cell[3][1]) + ) return false; + + // reject cases with zero area + return !!( + dist(cell[0], cell[1]) * + dist(cell[0], cell[3]) + ); +}; + +exports.pointsShapeEllipse = function(cell) { + var len = cell.length; + if(len !== CIRCLE_SIDES + 1) return false; + + // opposite diagonals should be the same + len = CIRCLE_SIDES; + for(var i = 0; i < len; i++) { + var k = (len * 2 - i) % len; + + var k2 = (len / 2 + k) % len; + var i2 = (len / 2 + i) % len; + + if(!almostEq( + dist(cell[i], cell[i2]), + dist(cell[k], cell[k2]) + )) return false; + } + return true; +}; + +exports.handleEllipse = function(isEllipse, start, end) { + if(!isEllipse) return [start, end]; // i.e. case of line + + var pos = exports.ellipseOver({ + x0: start[0], + y0: start[1], + x1: end[0], + y1: end[1] + }); + + var cx = (pos.x1 + pos.x0) / 2; + var cy = (pos.y1 + pos.y0) / 2; + var rx = (pos.x1 - pos.x0) / 2; + var ry = (pos.y1 - pos.y0) / 2; + + // make a circle when one dimension is zero + if(!rx) rx = ry = ry / SQRT2; + if(!ry) ry = rx = rx / SQRT2; + + var cell = []; + for(var i = 0; i < CIRCLE_SIDES; i++) { + var t = i * 2 * Math.PI / CIRCLE_SIDES; + cell.push([ + cx + rx * Math.cos(t), + cy + ry * Math.sin(t), + ]); + } + return cell; +}; + +exports.ellipseOver = function(pos) { + var x0 = pos.x0; + var y0 = pos.y0; + var x1 = pos.x1; + var y1 = pos.y1; + + var dx = x1 - x0; + var dy = y1 - y0; + + x0 -= dx; + y0 -= dy; + + var cx = (x0 + x1) / 2; + var cy = (y0 + y1) / 2; + + var scale = SQRT2; + dx *= scale; + dy *= scale; + + return { + x0: cx - dx, + y0: cy - dy, + x1: cx + dx, + y1: cy + dy + }; +}; diff --git a/src/components/shapes/draw_newshape/newshapes.js b/src/components/shapes/draw_newshape/newshapes.js new file mode 100644 index 00000000000..68a98dcb0a8 --- /dev/null +++ b/src/components/shapes/draw_newshape/newshapes.js @@ -0,0 +1,271 @@ +/** +* Copyright 2012-2020, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var dragHelpers = require('../../dragelement/helpers'); +var drawMode = dragHelpers.drawMode; +var openMode = dragHelpers.openMode; + +var constants = require('./constants'); +var i000 = constants.i000; +var i090 = constants.i090; +var i180 = constants.i180; +var i270 = constants.i270; +var cos45 = constants.cos45; +var sin45 = constants.sin45; + +var cartesianHelpers = require('../../../plots/cartesian/helpers'); +var p2r = cartesianHelpers.p2r; +var r2p = cartesianHelpers.r2p; + +var handleOutline = require('../../../plots/cartesian/handle_outline'); +var clearSelect = handleOutline.clearSelect; + +var helpers = require('./helpers'); +var readPaths = helpers.readPaths; +var writePaths = helpers.writePaths; +var ellipseOver = helpers.ellipseOver; + + +module.exports = function newShapes(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 drwStyle = gd._fullLayout.newshape; + + var plotinfo = dragOptions.plotinfo; + var xaxis = plotinfo.xaxis; + var yaxis = plotinfo.yaxis; + var xPaper = !!plotinfo.domain || !plotinfo.xaxis; + var yPaper = !!plotinfo.domain || !plotinfo.yaxis; + + var isActiveShape = dragOptions.isActiveShape; + var dragmode = dragOptions.dragmode; + + var shapes = (gd.layout || {}).shapes || []; + + if(!drawMode(dragmode) && isActiveShape !== undefined) { + var id = gd._fullLayout._activeShapeIndex; + if(id < shapes.length) { + switch(gd._fullLayout.shapes[id].type) { + case 'rect': + dragmode = 'drawrect'; + break; + case 'circle': + dragmode = 'drawcircle'; + break; + case 'line': + dragmode = 'drawline'; + break; + case 'path': + var path = shapes[id].path || ''; + if(path[path.length - 1] === 'Z') { + dragmode = 'drawclosedpath'; + } else { + dragmode = 'drawopenpath'; + } + break; + } + } + } + + var isOpenMode = openMode(dragmode); + + var polygons = readPaths(d, gd, plotinfo, isActiveShape); + + var newShape = { + editable: true, + + xref: xPaper ? 'paper' : xaxis._id, + yref: yPaper ? 'paper' : yaxis._id, + + layer: drwStyle.layer, + opacity: drwStyle.opacity, + line: { + color: drwStyle.line.color, + width: drwStyle.line.width, + dash: drwStyle.line.dash + } + }; + + if(!isOpenMode) { + newShape.fillcolor = drwStyle.fillcolor; + newShape.fillrule = drwStyle.fillrule; + } + + var cell; + // line, rect and circle can be in one cell + // only define cell if there is single cell + if(polygons.length === 1) cell = polygons[0]; + + if( + cell && + dragmode === 'drawrect' + ) { + newShape.type = 'rect'; + newShape.x0 = cell[0][1]; + newShape.y0 = cell[0][2]; + newShape.x1 = cell[2][1]; + newShape.y1 = cell[2][2]; + } else if( + cell && + dragmode === 'drawline' + ) { + newShape.type = 'line'; + newShape.x0 = cell[0][1]; + newShape.y0 = cell[0][2]; + newShape.x1 = cell[1][1]; + newShape.y1 = cell[1][2]; + } else if( + cell && + dragmode === 'drawcircle' + ) { + newShape.type = 'circle'; // an ellipse! + + var xA = cell[i000][1]; + var xB = cell[i090][1]; + var xC = cell[i180][1]; + var xD = cell[i270][1]; + + var yA = cell[i000][2]; + var yB = cell[i090][2]; + var yC = cell[i180][2]; + var yD = cell[i270][2]; + + var xDateOrLog = plotinfo.xaxis && ( + plotinfo.xaxis.type === 'date' || + plotinfo.xaxis.type === 'log' + ); + + var yDateOrLog = plotinfo.yaxis && ( + plotinfo.yaxis.type === 'date' || + plotinfo.yaxis.type === 'log' + ); + + if(xDateOrLog) { + xA = r2p(plotinfo.xaxis, xA); + xB = r2p(plotinfo.xaxis, xB); + xC = r2p(plotinfo.xaxis, xC); + xD = r2p(plotinfo.xaxis, xD); + } + + if(yDateOrLog) { + yA = r2p(plotinfo.yaxis, yA); + yB = r2p(plotinfo.yaxis, yB); + yC = r2p(plotinfo.yaxis, yC); + yD = r2p(plotinfo.yaxis, yD); + } + + var x0 = (xB + xD) / 2; + var y0 = (yA + yC) / 2; + var rx = (xD - xB + xC - xA) / 2; + var ry = (yD - yB + yC - yA) / 2; + var pos = ellipseOver({ + x0: x0, + y0: y0, + x1: x0 + rx * cos45, + y1: y0 + ry * sin45 + }); + + if(xDateOrLog) { + pos.x0 = p2r(plotinfo.xaxis, pos.x0); + pos.x1 = p2r(plotinfo.xaxis, pos.x1); + } + + if(yDateOrLog) { + pos.y0 = p2r(plotinfo.yaxis, pos.y0); + pos.y1 = p2r(plotinfo.yaxis, pos.y1); + } + + newShape.x0 = pos.x0; + newShape.y0 = pos.y0; + newShape.x1 = pos.x1; + newShape.y1 = pos.y1; + } else { + newShape.type = 'path'; + if(xaxis && yaxis) fixDatesForPaths(polygons, xaxis, yaxis); + newShape.path = writePaths(polygons); + cell = null; + } + + clearSelect(gd); + + var allShapes; + var updatedActiveShape = false; + allShapes = []; + for(var q = 0; q < shapes.length; q++) { + var beforeEdit = gd._fullLayout.shapes[q]; + allShapes[q] = beforeEdit._input; + + if( + isActiveShape !== undefined && + q === gd._fullLayout._activeShapeIndex + ) { + var afterEdit = newShape; + + switch(beforeEdit.type) { + case 'line': + case 'rect': + case 'circle': + updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); + if(updatedActiveShape) { // update active shape + allShapes[q].x0 = afterEdit.x0; + allShapes[q].x1 = afterEdit.x1; + allShapes[q].y0 = afterEdit.y0; + allShapes[q].y1 = afterEdit.y1; + } + break; + + case 'path': + updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); + if(updatedActiveShape) { // update active shape + allShapes[q].path = afterEdit.path; + } + break; + } + } + } + + if(isActiveShape === undefined) { + allShapes.push(newShape); // add new shape + } + + return allShapes; +}; + +function hasChanged(beforeEdit, afterEdit, keys) { + for(var i = 0; i < keys.length; i++) { + var k = keys[i]; + if(beforeEdit[k] !== afterEdit[k]) { + return true; + } + } + return false; +} + +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/plots/cartesian/select.js b/src/plots/cartesian/select.js index 0b3515cd327..a61bbbcb23c 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -23,10 +23,9 @@ var drawMode = dragHelpers.drawMode; var openMode = dragHelpers.openMode; var selectMode = dragHelpers.selectMode; -var drawNewShape = require('../../components/shapes/draw_newshape/draw'); -var displayOutlines = drawNewShape.displayOutlines; -var handleEllipse = drawNewShape.handleEllipse; -var addNewShapes = drawNewShape.addNewShapes; +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 Lib = require('../../lib'); var polygon = require('../../lib/polygon'); @@ -615,7 +614,7 @@ function clearSelectionsCache(dragOptions) { var outlines = zoomLayer.selectAll('.select-outline-' + plotinfo.id); if(outlines && gd._fullLayout._drawing) { // add shape - var shapes = addNewShapes(outlines, dragOptions); + var shapes = newShapes(outlines, dragOptions); if(shapes) { Registry.call('_guiRelayout', gd, { shapes: shapes From c5ff5dfeec859a1c46cfc1494155bab8a1270bd0 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 13:38:57 -0400 Subject: [PATCH 62/71] revise outline updates in shape eidts - record outline positions only on activeshapes and at init time - call update on end drags --- .../shapes/draw_newshape/display_outlines.js | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index e48ad54b333..0067a22fa61 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -14,7 +14,6 @@ var dragHelpers = require('../../dragelement/helpers'); var drawMode = dragHelpers.drawMode; var Registry = require('../../../registry'); -var Lib = require('../../../lib'); var setCursor = require('../../../lib/setcursor'); var MINSELECT = require('../../../plots/cartesian/constants').MINSELECT; @@ -42,10 +41,17 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall // recursive call displayOutlines(polygons, outlines, dragOptions, nCalls++); + if(pointsShapeEllipse(polygons[0])) { + update({redrawing: true}); + } + } + + function update(opts) { dragOptions.isActiveShape = false; // i.e. to disable controllers + var shapes = newShapes(outlines, dragOptions); if(shapes) { - Registry.call('_guiRelayout', gd, { + Registry.call(opts && opts.redrawing ? 'relayout' : '_guiRelayout', gd, { shapes: shapes // update active shape }); } @@ -75,9 +81,9 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var indexJ; // vertex or cell-controller index var copyPolygons; - copyPolygons = recordPositions([], polygons); - if(isActiveShape) { + if(!nCalls) copyPolygons = recordPositions([], polygons); + var g = zoomLayer.append('g').attr('class', 'outline-controllers'); addVertexControllers(g); addShapeControllers(); @@ -133,8 +139,8 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall redraw(); } - function endDragVertexController(evt) { - Lib.noop(evt); + function endDragVertexController() { + update(); } function removeVertex() { @@ -307,8 +313,8 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall shapeDragOptions[indexI].moveFn = moveShapeController; } - function endDragShapeController(evt) { - Lib.noop(evt); + function endDragShapeController() { + update(); } function addShapeControllers() { From 4039257533a3053fa340b5427c2d91de1a01a430 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 15:26:55 -0400 Subject: [PATCH 63/71] correct call to update vertices on click --- src/components/shapes/draw_newshape/display_outlines.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 0067a22fa61..b765dea3601 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -166,7 +166,7 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall polygons[indexI] = newPolygon; - redraw(); + update(); } } From 0662ae79f58cfc7def507416000b26e46b873575 Mon Sep 17 00:00:00 2001 From: archmoj Date: Mon, 27 Apr 2020 16:08:54 -0400 Subject: [PATCH 64/71] apply arrayEditor to produce the updates for shapes in layout --- src/components/shapes/draw.js | 9 +++-- .../shapes/draw_newshape/display_outlines.js | 9 +++-- .../shapes/draw_newshape/newshapes.js | 36 ++++++------------- test/jasmine/tests/draw_newshape_test.js | 34 +++++++++--------- 4 files changed, 39 insertions(+), 49 deletions(-) diff --git a/src/components/shapes/draw.js b/src/components/shapes/draw.js index fce3c864543..28e7f849f74 100644 --- a/src/components/shapes/draw.js +++ b/src/components/shapes/draw.js @@ -141,6 +141,9 @@ function drawOne(gd, index) { setClipPath(path, gd, options); + var editHelpers; + if(isActiveShape || gd._context.edits.shapePosition) editHelpers = arrayEditor(gd.layout, 'shapes', options); + if(isActiveShape) { path.style({ 'cursor': 'move', @@ -150,6 +153,7 @@ function drawOne(gd, index) { element: path.node(), plotinfo: plotinfo, gd: gd, + editHelpers: editHelpers, isActiveShape: true // i.e. to enable controllers }; @@ -158,7 +162,7 @@ function drawOne(gd, index) { displayOutlines(polygons, path, dragOptions); } else { if(gd._context.edits.shapePosition) { - setupDragElement(gd, path, options, index, shapeLayer); + setupDragElement(gd, path, options, index, shapeLayer, editHelpers); } path.style('pointer-events', @@ -187,7 +191,7 @@ function setClipPath(shapePath, gd, shapeOptions) { ); } -function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { +function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer, editHelpers) { var MINWIDTH = 10; var MINHEIGHT = 10; @@ -196,7 +200,6 @@ function setupDragElement(gd, shapePath, shapeOptions, index, shapeLayer) { var isLine = shapeOptions.type === 'line'; var isPath = shapeOptions.type === 'path'; - var editHelpers = arrayEditor(gd.layout, 'shapes', shapeOptions); var modifyItem = editHelpers.modifyItem; var x0, y0, x1, y1, xAnchor, yAnchor; diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index b765dea3601..9c659c5ef32 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -49,11 +49,9 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall function update(opts) { dragOptions.isActiveShape = false; // i.e. to disable controllers - var shapes = newShapes(outlines, dragOptions); - if(shapes) { - Registry.call(opts && opts.redrawing ? 'relayout' : '_guiRelayout', gd, { - shapes: shapes // update active shape - }); + var updateObject = newShapes(outlines, dragOptions); + if(Object.keys(updateObject).length) { + Registry.call(opts && opts.redrawing ? 'relayout' : '_guiRelayout', gd, updateObject); } } @@ -166,6 +164,7 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall polygons[indexI] = newPolygon; + redraw(); update(); } } diff --git a/src/components/shapes/draw_newshape/newshapes.js b/src/components/shapes/draw_newshape/newshapes.js index 68a98dcb0a8..5138ecc410a 100644 --- a/src/components/shapes/draw_newshape/newshapes.js +++ b/src/components/shapes/draw_newshape/newshapes.js @@ -200,9 +200,10 @@ module.exports = function newShapes(outlines, dragOptions) { clearSelect(gd); - var allShapes; - var updatedActiveShape = false; - allShapes = []; + var editHelpers = dragOptions.editHelpers; + var modifyItem = (editHelpers || {}).modifyItem; + + var allShapes = []; for(var q = 0; q < shapes.length; q++) { var beforeEdit = gd._fullLayout.shapes[q]; allShapes[q] = beforeEdit._input; @@ -217,20 +218,14 @@ module.exports = function newShapes(outlines, dragOptions) { case 'line': case 'rect': case 'circle': - updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['x0', 'x1', 'y0', 'y1']); - if(updatedActiveShape) { // update active shape - allShapes[q].x0 = afterEdit.x0; - allShapes[q].x1 = afterEdit.x1; - allShapes[q].y0 = afterEdit.y0; - allShapes[q].y1 = afterEdit.y1; - } + modifyItem('x0', afterEdit.x0); + modifyItem('x1', afterEdit.x1); + modifyItem('y0', afterEdit.y0); + modifyItem('y1', afterEdit.y1); break; case 'path': - updatedActiveShape = hasChanged(beforeEdit, afterEdit, ['path']); - if(updatedActiveShape) { // update active shape - allShapes[q].path = afterEdit.path; - } + modifyItem('path', afterEdit.path); break; } } @@ -238,21 +233,12 @@ module.exports = function newShapes(outlines, dragOptions) { if(isActiveShape === undefined) { allShapes.push(newShape); // add new shape + return allShapes; } - return allShapes; + return editHelpers ? editHelpers.getUpdateObj() : {}; }; -function hasChanged(beforeEdit, afterEdit, keys) { - for(var i = 0; i < keys.length; i++) { - var k = keys[i]; - if(beforeEdit[k] !== afterEdit[k]) { - return true; - } - } - return false; -} - function fixDatesForPaths(polygons, xaxis, yaxis) { var xIsDate = xaxis.type === 'date'; var yIsDate = yaxis.type === 'date'; diff --git a/test/jasmine/tests/draw_newshape_test.js b/test/jasmine/tests/draw_newshape_test.js index 2e122c75935..604bbeaf479 100644 --- a/test/jasmine/tests/draw_newshape_test.js +++ b/test/jasmine/tests/draw_newshape_test.js @@ -42,7 +42,9 @@ function print(obj) { return obj; } -function assertPos(actual, expected) { +function assertPos(actual, expected, tolerance) { + if(tolerance === undefined) tolerance = 2; + expect(typeof actual).toEqual(typeof expected); if(typeof actual === 'string') { @@ -61,7 +63,7 @@ function assertPos(actual, expected) { expect(A.length).toEqual(B.length); // svg letters should be identical expect(A[0]).toEqual(B[0]); for(var k = 1; k < A.length; k++) { - expect(A[k]).toBeCloseTo(B[k], 2); + expect(A[k]).toBeCloseTo(B[k], tolerance); } } } else { @@ -79,7 +81,7 @@ function assertPos(actual, expected) { posB = fixDates(posB); } - expect(posA).toBeCloseTo(posB, 2); + expect(posA).toBeCloseTo(posB, tolerance); } } } @@ -1063,7 +1065,7 @@ describe('Activate and deactivate shapes to edit', function() { afterEach(destroyGraphDiv); ['mouse'].forEach(function(device) { - it('@flaky activate editable shapes using' + device, function(done) { + it('@flaky activate and edit editable shapes using' + device, function(done) { var i; Plotly.newPlot(gd, { @@ -1096,7 +1098,7 @@ describe('Activate and deactivate shapes to edit', function() { 'y1': 75 }); }) - .then(function() { drag([[175, 160], [150, 100]]); }) // move vertex + .then(function() { drag([[255, 230], [300, 200]]); }) // move vertex .then(function() { var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'keep shape active after drag corner'); @@ -1111,13 +1113,13 @@ describe('Activate and deactivate shapes to edit', function() { 'x1': obj.x1, 'y1': obj.y1 }, { - 'x0': 9.494573643410854, - 'y0': -17.732937685459945, - 'x1': 75.0015503875969, - 'y1': 74.99821958456974 + 'x0': 24.998449612403103, + 'y0': 24.997032640949552, + 'x1': 102.90852713178295, + 'y1': 53.63323442136499 }); }) - .then(function() { drag([[150, 100], [175, 160]]); }) // move vertex back + .then(function() { drag([[300, 200], [255, 230]]); }) // move vertex back .then(function() { var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'keep shape active after drag corner'); @@ -1138,7 +1140,7 @@ describe('Activate and deactivate shapes to edit', function() { 'y1': 75 }); }) - .then(function() { drag([[215, 195], [150, 100]]); }) // move shape + .then(function() { drag([[215, 195], [300, 200]]); }) // move shape .then(function() { var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'keep shape active after drag corner'); @@ -1153,13 +1155,13 @@ describe('Activate and deactivate shapes to edit', function() { 'x1': obj.x1, 'y1': obj.y1 }, { - 'y0': -42.65875370919882, - 'y1': 7.342433234421367, - 'x0': -15.311627906976742, - 'x1': 34.691472868217055 + 'x0': 77.71162790697674, + 'y0': 24.997032640949552, + 'x1': 127.71472868217053, + 'y1': 74.99821958456974 }); }) - .then(function() { drag([[150, 100], [215, 195]]); }) // move shape back + .then(function() { drag([[300, 200], [215, 195]]); }) // move shape back .then(function() { var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'keep shape active after drag corner'); From d017bb73703f9a84d8a2b047203b4625316a7d63 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 09:28:03 -0400 Subject: [PATCH 65/71] simplify ellipse edit updates --- src/components/shapes/draw_newshape/display_outlines.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 9c659c5ef32..31eee7b5d22 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -42,16 +42,16 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall displayOutlines(polygons, outlines, dragOptions, nCalls++); if(pointsShapeEllipse(polygons[0])) { - update({redrawing: true}); + Registry.getComponentMethod('shapes', 'drawOne')(gd, gd._fullLayout._activeShapeIndex); } } - function update(opts) { + function update() { dragOptions.isActiveShape = false; // i.e. to disable controllers var updateObject = newShapes(outlines, dragOptions); if(Object.keys(updateObject).length) { - Registry.call(opts && opts.redrawing ? 'relayout' : '_guiRelayout', gd, updateObject); + Registry.call('_guiRelayout', gd, updateObject); } } From 9cd1ba1c69d9a23edeaec7752111cd92a7880569 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 09:44:27 -0400 Subject: [PATCH 66/71] simplify vertex buttons --- .../shapes/draw_newshape/display_outlines.js | 75 +++---------------- 1 file changed, 9 insertions(+), 66 deletions(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 31eee7b5d22..f2aff901235 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -193,18 +193,6 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var onRect = pointsShapeRectangle(cell); var onEllipse = !onRect && pointsShapeEllipse(cell); - var minX; - var minY; - var maxX; - var maxY; - if(onRect) { - // compute bounding box - minX = calcMin(cell, 1); - minY = calcMin(cell, 2); - maxX = calcMax(cell, 1); - maxY = calcMax(cell, 2); - } - vertexDragOptions[i] = []; for(var j = 0; j < cell.length; j++) { if(cell[j][0] === 'Z') continue; @@ -222,7 +210,10 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var y = cell[j][2]; var rIcon = 3; - var button = g.append(onRect ? 'rect' : 'circle') + g.append('circle') + .attr('cx', x) + .attr('cy', y) + .attr('r', rIcon) .style({ 'mix-blend-mode': 'luminosity', fill: 'black', @@ -230,49 +221,17 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall 'stroke-width': 1 }); - if(onRect) { - button - .attr('x', x - rIcon) - .attr('y', y - rIcon) - .attr('width', 2 * rIcon) - .attr('height', 2 * rIcon); - } else { - button + var vertex = g.append('circle') + .classed('cursor-grab', true) + .attr('data-i', i) + .attr('data-j', j) .attr('cx', x) .attr('cy', y) - .attr('r', rIcon); - } - - var vertex = g.append(onRect ? 'rect' : 'circle') - .attr('data-i', i) - .attr('data-j', j) + .attr('r', rVertexController) .style({ opacity: 0 }); - if(onRect) { - var ratioX = (x - minX) / (maxX - minX); - var ratioY = (y - minY) / (maxY - minY); - if(isFinite(ratioX) && isFinite(ratioY)) { - setCursor( - vertex, - dragElement.getCursor(ratioX, 1 - ratioY) - ); - } - - vertex - .attr('x', x - rVertexController) - .attr('y', y - rVertexController) - .attr('width', 2 * rVertexController) - .attr('height', 2 * rVertexController); - } else { - vertex - .classed('cursor-grab', true) - .attr('cx', x) - .attr('cy', y) - .attr('r', rVertexController); - } - vertexDragOptions[i][j] = { element: vertex.node(), gd: gd, @@ -333,22 +292,6 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall } }; -function calcMin(cell, dim) { - var v = Infinity; - for(var i = 0; i < cell.length; i++) { - v = Math.min(v, cell[i][dim]); - } - return v; -} - -function calcMax(cell, dim) { - var v = -Infinity; - for(var i = 0; i < cell.length; i++) { - v = Math.max(v, cell[i][dim]); - } - return v; -} - function recordPositions(polygonsOut, polygonsIn) { for(var i = 0; i < polygonsIn.length; i++) { var cell = polygonsIn[i]; From 00b6ae272046373b369f8fbcf3f1ba7203f29977 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 09:54:33 -0400 Subject: [PATCH 67/71] no need to display vertex controllers while dragging --- src/components/shapes/draw_newshape/display_outlines.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index f2aff901235..73a2b0971a5 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -56,9 +56,6 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall } - // remove previous controllers - only if there is an active shape - if(gd._fullLayout._activeShapeIndex >= 0) clearOutlineControllers(gd); - var isActiveShape = dragOptions.isActiveShape; var fullLayout = gd._fullLayout; var zoomLayer = fullLayout._zoomlayer; @@ -67,6 +64,7 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var isDrawMode = drawMode(dragmode); if(isDrawMode) gd._fullLayout._drawing = true; + else if(gd._fullLayout._activeShapeIndex >= 0) clearOutlineControllers(gd); // make outline outlines.attr('d', writePaths(polygons)); @@ -79,8 +77,8 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var indexJ; // vertex or cell-controller index var copyPolygons; - if(isActiveShape) { - if(!nCalls) copyPolygons = recordPositions([], polygons); + if(isActiveShape && !nCalls) { + copyPolygons = recordPositions([], polygons); var g = zoomLayer.append('g').attr('class', 'outline-controllers'); addVertexControllers(g); From efc85d66fc60598cb30274a07e9f7ac9dc0da14c Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 10:04:53 -0400 Subject: [PATCH 68/71] simplify - no need for special cursor handling for rects --- src/components/shapes/draw_newshape/display_outlines.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 73a2b0971a5..43432e0add8 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -14,7 +14,6 @@ var dragHelpers = require('../../dragelement/helpers'); var drawMode = dragHelpers.drawMode; var Registry = require('../../../registry'); -var setCursor = require('../../../lib/setcursor'); var MINSELECT = require('../../../plots/cartesian/constants').MINSELECT; var constants = require('./constants'); From c69244f921b95782cba4960ded8fa04f9522b5e0 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 15:17:27 -0400 Subject: [PATCH 69/71] simplify controllers - no need for invisible draggers --- .../shapes/draw_newshape/display_outlines.js | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 43432e0add8..796ed981fab 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -15,7 +15,6 @@ var drawMode = dragHelpers.drawMode; var Registry = require('../../../registry'); -var MINSELECT = require('../../../plots/cartesian/constants').MINSELECT; var constants = require('./constants'); var i000 = constants.i000; var i090 = constants.i090; @@ -69,7 +68,6 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall outlines.attr('d', writePaths(polygons)); // add controllers - var rVertexController = MINSELECT * 1.5; // bigger vertex buttons var vertexDragOptions; var shapeDragOptions; var indexI; // cell index @@ -206,28 +204,19 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall var x = cell[j][1]; var y = cell[j][2]; - var rIcon = 3; - g.append('circle') - .attr('cx', x) - .attr('cy', y) - .attr('r', rIcon) - .style({ - 'mix-blend-mode': 'luminosity', - fill: 'black', - stroke: 'white', - 'stroke-width': 1 - }); - var vertex = g.append('circle') .classed('cursor-grab', true) .attr('data-i', i) .attr('data-j', j) - .attr('cx', x) - .attr('cy', y) - .attr('r', rVertexController) - .style({ - opacity: 0 - }); + .attr('cx', x) + .attr('cy', y) + .attr('r', 4) + .style({ + 'mix-blend-mode': 'luminosity', + fill: 'black', + stroke: 'white', + 'stroke-width': 1 + }); vertexDragOptions[i][j] = { element: vertex.node(), From 0ad14d1fb37d5de4c5fdde62aec39ab295c473b1 Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 15:23:18 -0400 Subject: [PATCH 70/71] split one big test into smaller tests to avoid side effects and future expansion --- test/jasmine/tests/draw_newshape_test.js | 254 ++++++++++++++--------- 1 file changed, 160 insertions(+), 94 deletions(-) diff --git a/test/jasmine/tests/draw_newshape_test.js b/test/jasmine/tests/draw_newshape_test.js index 604bbeaf479..83c10f8b505 100644 --- a/test/jasmine/tests/draw_newshape_test.js +++ b/test/jasmine/tests/draw_newshape_test.js @@ -946,106 +946,130 @@ describe('Draw new shapes to layout', function() { }); }); -describe('Activate and deactivate shapes to edit', function() { +describe('Activate and edit editable shapes', function() { var fig = { - data: [{ x: [0, 50], y: [0, 50] }], - layout: { - width: 800, - height: 600, - margin: { - t: 100, - b: 50, - l: 100, - r: 50 + 'data': [ + { + 'x': [ + 0, + 50 + ], + 'y': [ + 0, + 50 + ] + } + ], + 'layout': { + 'width': 800, + 'height': 600, + 'margin': { + 't': 100, + 'b': 50, + 'l': 100, + 'r': 50 }, - - yaxis: { - autorange: 'reversed' + 'yaxis': { + 'autorange': 'reversed' }, - - template: { - layout: { - shapes: [{ - name: 'myPath', - editable: true, - layer: 'below', - line: { width: 0 }, - fillcolor: 'gray', - opacity: 0.5, - xref: 'paper', - yref: 'paper', - path: 'M0.5,0.3C0.5,0.9 0.9,0.9 0.9,0.3C0.9,0.1 0.5,0.1 0.5,0.3ZM0.6,0.4C0.6,0.5 0.66,0.5 0.66,0.4ZM0.74,0.4C0.74,0.5 0.8,0.5 0.8,0.4ZM0.6,0.3C0.63,0.2 0.77,0.2 0.8,0.3Z' - }] + 'template': { + 'layout': { + 'shapes': [ + { + 'name': 'myPath', + 'editable': true, + 'layer': 'below', + 'line': { + 'width': 0 + }, + 'fillcolor': 'gray', + 'opacity': 0.5, + 'xref': 'paper', + 'yref': 'paper', + 'path': 'M0.5,0.3C0.5,0.9 0.9,0.9 0.9,0.3C0.9,0.1 0.5,0.1 0.5,0.3ZM0.6,0.4C0.6,0.5 0.66,0.5 0.66,0.4ZM0.74,0.4C0.74,0.5 0.8,0.5 0.8,0.4ZM0.6,0.3C0.63,0.2 0.77,0.2 0.8,0.3Z' + } + ] } }, - shapes: [ + 'shapes': [ { - editable: true, - layer: 'below', - type: 'rect', - line: { width: 5 }, - fillcolor: 'red', - opacity: 0.5, - xref: 'xaxis', - yref: 'yaxis', - y0: 25, - y1: 75, - x0: 25, - x1: 75 + 'editable': true, + 'layer': 'below', + 'type': 'rect', + 'line': { + 'width': 5 + }, + 'fillcolor': 'red', + 'opacity': 0.5, + 'xref': 'xaxis', + 'yref': 'yaxis', + 'x0': 25, + 'y0': 25, + 'x1': 75, + 'y1': 75 }, { - editable: true, - layer: 'top', - type: 'circle', - line: { width: 5 }, - fillcolor: 'green', - opacity: 0.5, - xref: 'xaxis', - yref: 'yaxis', - y0: 25, - y1: 75, - x0: 125, - x1: 175 + 'editable': true, + 'layer': 'top', + 'type': 'circle', + 'line': { + 'width': 5 + }, + 'fillcolor': 'green', + 'opacity': 0.5, + 'xref': 'xaxis', + 'yref': 'yaxis', + 'x0': 125, + 'y0': 25, + 'x1': 175, + 'y1': 75 }, { - editable: true, - line: { width: 5 }, - fillcolor: 'blue', - path: 'M250,25L225,75L275,75Z' + 'editable': true, + 'line': { + 'width': 5 + }, + 'fillcolor': 'blue', + 'path': 'M250,25L225,75L275,75Z' }, { - editable: true, - line: { width: 15 }, - path: 'M250,225L225,275L275,275' + 'editable': true, + 'line': { + 'width': 15 + }, + 'path': 'M250,225L225,275L275,275' }, { - editable: true, - layer: 'below', - path: 'M320,100C390,180 290,180 360,100Z', - fillcolor: 'rgba(0,127,127,0.5)', - line: { width: 5 } + 'editable': true, + 'layer': 'below', + 'path': 'M320,100C390,180 290,180 360,100Z', + 'fillcolor': 'rgba(0,127,127,0.5)', + 'line': { + 'width': 5 + } }, { - editable: true, - line: { - width: 5, - color: 'orange' + 'editable': true, + 'line': { + 'width': 5, + 'color': 'orange' }, - fillcolor: 'rgba(127,255,127,0.5)', - path: 'M0,100V200H50L0,300Q100,300 100,200T150,200C100,300 200,300 200,200S150,200 150,100Z' + 'fillcolor': 'rgba(127,255,127,0.5)', + 'path': 'M0,100V200H50L0,300Q100,300 100,200T150,200C100,300 200,300 200,200S150,200 150,100Z' }, { - editable: true, - line: { width: 2 }, - fillcolor: 'yellow', - - path: 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z' + 'editable': true, + 'line': { + 'width': 2 + }, + 'fillcolor': 'yellow', + 'path': 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z' } ] }, - config: { - editable: false, - modeBarButtonsToAdd: [ + 'config': { + 'editable': false, + 'modeBarButtonsToAdd': [ 'drawline', 'drawopenpath', 'drawclosedpath', @@ -1065,8 +1089,8 @@ describe('Activate and deactivate shapes to edit', function() { afterEach(destroyGraphDiv); ['mouse'].forEach(function(device) { - it('@flaky activate and edit editable shapes using' + device, function(done) { - var i; + it('@flaky reactangle using' + device, function(done) { + var i = 0; // shape index Plotly.newPlot(gd, { data: fig.data, @@ -1077,8 +1101,6 @@ describe('Activate and deactivate shapes to edit', function() { // shape between 175, 160 and 255, 230 .then(function() { click(200, 160); }) // activate shape .then(function() { - i = 0; // test first shape i.e. case of rectangle - var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'activate shape by clicking border'); @@ -1198,11 +1220,22 @@ describe('Activate and deactivate shapes to edit', function() { expect(id).toEqual(undefined, 'deactivate shape by clicking inside'); }) + .catch(failTest) + .then(done); + }); + + it('@flaky circle using' + device, function(done) { + var i = 1; // shape index + + Plotly.newPlot(gd, { + data: fig.data, + layout: fig.layout, + config: fig.config + }) + // next shape .then(function() { click(355, 225); }) // activate shape .then(function() { - i = 1; // test second shape i.e. case of circle - var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'activate shape by clicking border'); @@ -1244,11 +1277,22 @@ describe('Activate and deactivate shapes to edit', function() { }); }) + .catch(failTest) + .then(done); + }); + + it('@flaky closed-path using' + device, function(done) { + var i = 2; // shape index + + Plotly.newPlot(gd, { + data: fig.data, + layout: fig.layout, + config: fig.config + }) + // next shape .then(function() { click(500, 225); }) // activate shape .then(function() { - i = 2; // test third shape i.e. case of closed-path - var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'activate shape by clicking border'); @@ -1278,11 +1322,22 @@ describe('Activate and deactivate shapes to edit', function() { assertPos(obj.path, 'M250,25L225,75L275,75Z'); }) + .catch(failTest) + .then(done); + }); + + it('@flaky bezier curves using' + device, function(done) { + var i = 5; // shape index + + Plotly.newPlot(gd, { + data: fig.data, + layout: fig.layout, + config: fig.config + }) + // next shape .then(function() { click(300, 266); }) // activate shape .then(function() { - i = 5; // test case of bezier curves - var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'activate shape by clicking border'); @@ -1312,11 +1367,21 @@ describe('Activate and deactivate shapes to edit', function() { assertPos(obj.path, 'M0,100.00237388724034L0,199.99762611275966L50.00310077519379,199.99762611275966L0,300Q100,300,100,199.9976261127597T150.0031007751938,199.99762611275966C100,300,200,300,200,199.99762611275966S150.0031007751938,199.99762611275966,150.0031007751938,100.00237388724034Z'); }) - // next shape + .catch(failTest) + .then(done); + }); + + it('@flaky multi-cell path using' + device, function(done) { + var i = 6; // shape index + + Plotly.newPlot(gd, { + data: fig.data, + layout: fig.layout, + config: fig.config + }) + .then(function() { click(627, 193); }) // activate shape .then(function() { - i = 6; // test case of multi-cell path - var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'activate shape by clicking border'); @@ -1325,7 +1390,7 @@ describe('Activate and deactivate shapes to edit', function() { print(obj); assertPos(obj.path, 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z'); }) - .then(function() { drag([[717, 225], [725, 230]]); }) // move vertex + .then(function() { drag([[717, 225], [700, 250]]); }) // move vertex .then(function() { var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'keep shape active after drag corner'); @@ -1333,9 +1398,9 @@ describe('Activate and deactivate shapes to edit', function() { var shapes = gd._fullLayout.shapes; var obj = shapes[id]._input; print(obj); - assertPos(obj.path, 'M300,69.99881305637984C300,9.998813056379817,380,9.998813056379817,380,69.99881305637984C380,90.00356083086054,300,90.00356083086054,300,69.99881305637984ZM320,60.00000000000001C320,50.00118694362017,332,50.00118694362017,332,60.00000000000001ZM348,60.00000000000001C348,50.00118694362017,360,50.00118694362017,360,60.00000000000001ZM320,69.99881305637984C326.0031007751938,79.99762611275966,354.0031007751938,79.99762611275966,364.9612403100775,69.99881305637984Z'); + assertPos(obj.path, 'M300,69.99881305637984C300,9.998813056379817,380,9.998813056379817,380,69.99881305637984C380,90.00356083086054,300,90.00356083086054,300,69.99881305637984ZM320,60.00000000000001C320,50.00118694362017,332,50.00118694362017,332,60.00000000000001ZM348,60.00000000000001C348,50.00118694362017,360,50.00118694362017,360,60.00000000000001ZM320,69.99881305637984C326.0031007751938,79.99762611275966,354.0031007751938,79.99762611275966,349.4573643410853,87.80296735905047Z'); }) - .then(function() { drag([[725, 230], [717, 225]]); }) // move vertex back + .then(function() { drag([[700, 250], [717, 225]]); }) // move vertex back .then(function() { var id = gd._fullLayout._activeShapeIndex; expect(id).toEqual(i, 'keep shape active after drag corner'); @@ -1345,6 +1410,7 @@ describe('Activate and deactivate shapes to edit', function() { print(obj); assertPos(obj.path, 'M300,70C300,10 380,10 380,70C380,90 300,90 300,70ZM320,60C320,50 332,50 332,60ZM348,60C348,50 360,50 360,60ZM320,70C326,80 354,80 360,70Z'); }) + // erase shape .then(function() { expect(gd._fullLayout.shapes.length).toEqual(8); From 2911e4f48e374fba2f43f70013950ec47665896d Mon Sep 17 00:00:00 2001 From: archmoj Date: Tue, 28 Apr 2020 15:33:11 -0400 Subject: [PATCH 71/71] speed up interactive circle edits --- src/components/shapes/draw_newshape/display_outlines.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/shapes/draw_newshape/display_outlines.js b/src/components/shapes/draw_newshape/display_outlines.js index 796ed981fab..7f5704438d0 100644 --- a/src/components/shapes/draw_newshape/display_outlines.js +++ b/src/components/shapes/draw_newshape/display_outlines.js @@ -40,16 +40,16 @@ module.exports = function displayOutlines(polygons, outlines, dragOptions, nCall displayOutlines(polygons, outlines, dragOptions, nCalls++); if(pointsShapeEllipse(polygons[0])) { - Registry.getComponentMethod('shapes', 'drawOne')(gd, gd._fullLayout._activeShapeIndex); + update({redrawing: true}); } } - function update() { + function update(opts) { dragOptions.isActiveShape = false; // i.e. to disable controllers var updateObject = newShapes(outlines, dragOptions); if(Object.keys(updateObject).length) { - Registry.call('_guiRelayout', gd, updateObject); + Registry.call((opts || {}).redrawing ? 'relayout' : '_guiRelayout', gd, updateObject); } }