From a40fc83fe61edbbd57ee0d69213a7989a3e7ae4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:52:48 -0400 Subject: [PATCH 01/22] factor out legend helper into separate file + add tests --- src/components/legend/helpers.js | 25 +++++++++++++++++++++ src/components/legend/index.js | 16 -------------- test/jasmine/tests/legend_test.js | 36 +++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 src/components/legend/helpers.js diff --git a/src/components/legend/helpers.js b/src/components/legend/helpers.js new file mode 100644 index 00000000000..cf300a4a9da --- /dev/null +++ b/src/components/legend/helpers.js @@ -0,0 +1,25 @@ +/** +* Copyright 2012-2016, 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 Plots = require('../../plots/plots'); + + +exports.legendGetsTrace = function legendGetsTrace(trace) { + return trace.visible && Plots.traceIs(trace, 'showLegend'); +}; + +exports.isGrouped = function isGrouped(legendLayout) { + return (legendLayout.traceorder || '').indexOf('grouped') !== -1; +}; + +exports.isReversed = function isReversed(legendLayout) { + return (legendLayout.traceorder || '').indexOf('reversed') !== -1; +}; diff --git a/src/components/legend/index.js b/src/components/legend/index.js index 80257f445ef..46ab1255ee4 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -329,22 +329,6 @@ legend.texts = function(context, td, d, i, traces) { else text.call(textLayout); }; -// ----------------------------------------------------- -// legend drawing -// ----------------------------------------------------- - -function legendGetsTrace(trace) { - return trace.visible && Plots.traceIs(trace, 'showLegend'); -} - -function isGrouped(legendLayout) { - return (legendLayout.traceorder || '').indexOf('grouped') !== -1; -} - -function isReversed(legendLayout) { - return (legendLayout.traceorder || '').indexOf('reversed') !== -1; -} - legend.getLegendData = function(calcdata, opts) { // build an { legendgroup: [cd0, cd0], ... } object diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 8de626b6c83..c05a18bad35 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1,6 +1,7 @@ var Legend = require('@src/components/legend'); var Plots = require('@src/plots/plots'); +var helpers = require('@src/components/legend/helpers'); describe('Test legend:', function() { 'use strict'; @@ -325,4 +326,39 @@ describe('Test legend:', function() { }); }); + describe('legendGetsTraces helper', function() { + var legendGetsTrace = helpers.legendGetsTrace; + + it('should return true when trace is visible and supports legend', function() { + expect(legendGetsTrace({ visible: true, type: 'bar' })).toBe(true); + expect(legendGetsTrace({ visible: false, type: 'bar' })).toBe(false); + expect(legendGetsTrace({ visible: true, type: 'contour' })).toBe(false); + expect(legendGetsTrace({ visible: false, type: 'contour' })).toBe(false); + }); + }); + + describe('isGrouped helper', function() { + var isGrouped = helpers.isGrouped; + + it('should return true when trace is visible and supports legend', function() { + expect(isGrouped({ traceorder: 'normal' })).toBe(false); + expect(isGrouped({ traceorder: 'grouped' })).toBe(true); + expect(isGrouped({ traceorder: 'reversed+grouped' })).toBe(true); + expect(isGrouped({ traceorder: 'grouped+reversed' })).toBe(true); + expect(isGrouped({ traceorder: 'reversed' })).toBe(false); + }); + }); + + describe('isReversed helper', function() { + var isReversed = helpers.isReversed; + + it('should return true when trace is visible and supports legend', function() { + expect(isReversed({ traceorder: 'normal' })).toBe(false); + expect(isReversed({ traceorder: 'grouped' })).toBe(false); + expect(isReversed({ traceorder: 'reversed+grouped' })).toBe(true); + expect(isReversed({ traceorder: 'grouped+reversed' })).toBe(true); + expect(isReversed({ traceorder: 'reversed' })).toBe(true); + }); + }); + }); From b3af44533c503b84b503dbf5e207824ba29121ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:53:59 -0400 Subject: [PATCH 02/22] move auto-anchor logic to own module + add tests --- src/components/legend/anchor_utils.js | 47 +++++++++++++++ test/jasmine/tests/legend_test.js | 87 +++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) create mode 100644 src/components/legend/anchor_utils.js diff --git a/src/components/legend/anchor_utils.js b/src/components/legend/anchor_utils.js new file mode 100644 index 00000000000..a4b19262239 --- /dev/null +++ b/src/components/legend/anchor_utils.js @@ -0,0 +1,47 @@ +/** +* Copyright 2012-2016, 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'; + + +/** + * Determine the position anchor property of x/y xanchor/yanchor components. + * + * - values < 1/3 align the low side at that fraction, + * - values [1/3, 2/3] align the center at that fraction, + * - values > 2/3 align the right at that fraction. + */ + +exports.isRightAnchor = function isRightAnchor(opts) { + return ( + opts.xanchor === 'right' || + (opts.xanchor === 'auto' && opts.x >= 2 / 3) + ); +}; + +exports.isCenterAnchor = function isCenterAnchor(opts) { + return ( + opts.xanchor === 'center' || + (opts.xanchor === 'auto' && opts.x > 1 / 3 && opts.x < 2 / 3) + ); +}; + +exports.isBottomAnchor = function isTopAnchor(opts) { + return ( + opts.yanchor === 'bottom' || + (opts.yanchor === 'auto' && opts.y <= 1 / 3) + ); +}; + +exports.isMiddleAnchor = function isMiddleAnchor(opts) { + return ( + opts.yanchor === 'middle' || + (opts.yanchor === 'auto' && opts.y > 1 / 3 && opts.y < 2 / 3) + ); +}; diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index c05a18bad35..b3a474b1625 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -2,6 +2,7 @@ var Legend = require('@src/components/legend'); var Plots = require('@src/plots/plots'); var helpers = require('@src/components/legend/helpers'); +var anchorUtils = require('@src/components/legend/anchor_utils'); describe('Test legend:', function() { 'use strict'; @@ -361,4 +362,90 @@ describe('Test legend:', function() { }); }); + describe('isRightAnchor anchor util', function() { + var isRightAnchor = anchorUtils.isRightAnchor; + var threshold = 2/3; + + it('should return true when \'xanchor\' is set to \'right\'', function() { + expect(isRightAnchor({ xanchor: 'left' })).toBe(false); + expect(isRightAnchor({ xanchor: 'center' })).toBe(false); + expect(isRightAnchor({ xanchor: 'right' })).toBe(true); + }); + + it('should return true when \'xanchor\' is set to \'auto\' and \'x\' >= 2/3', function() { + var opts = { xanchor: 'auto' }; + + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.x = v; + expect(isRightAnchor(opts)) + .toBe(v > threshold, 'case ' + v); + }); + }); + }); + + describe('isCenterAnchor anchor util', function() { + var isCenterAnchor = anchorUtils.isCenterAnchor; + var threshold0 = 1/3; + var threshold1 = 2/3; + + it('should return true when \'xanchor\' is set to \'center\'', function() { + expect(isCenterAnchor({ xanchor: 'left' })).toBe(false); + expect(isCenterAnchor({ xanchor: 'center' })).toBe(true); + expect(isCenterAnchor({ xanchor: 'right' })).toBe(false); + }); + + it('should return true when \'xanchor\' is set to \'auto\' and 1/3 < \'x\' < 2/3', function() { + var opts = { xanchor: 'auto' }; + + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.x = v; + expect(isCenterAnchor(opts)) + .toBe(v > threshold0 && v < threshold1, 'case ' + v); + }); + }); + }); + + describe('isBottomAnchor anchor util', function() { + var isBottomAnchor = anchorUtils.isBottomAnchor; + var threshold = 1/3; + + it('should return true when \'yanchor\' is set to \'right\'', function() { + expect(isBottomAnchor({ yanchor: 'top' })).toBe(false); + expect(isBottomAnchor({ yanchor: 'middle' })).toBe(false); + expect(isBottomAnchor({ yanchor: 'bottom' })).toBe(true); + }); + + it('should return true when \'yanchor\' is set to \'auto\' and \'y\' <= 1/3', function() { + var opts = { yanchor: 'auto' }; + + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.y = v; + expect(isBottomAnchor(opts)) + .toBe(v < threshold, 'case ' + v); + }); + }); + }); + + describe('isMiddleAnchor anchor util', function() { + var isMiddleAnchor = anchorUtils.isMiddleAnchor; + var threshold0 = 1/3; + var threshold1 = 2/3; + + it('should return true when \'yanchor\' is set to \'center\'', function() { + expect(isMiddleAnchor({ yanchor: 'top' })).toBe(false); + expect(isMiddleAnchor({ yanchor: 'middle' })).toBe(true); + expect(isMiddleAnchor({ yanchor: 'bottom' })).toBe(false); + }); + + it('should return true when \'yanchor\' is set to \'auto\' and 1/3 < \'y\' < 2/3', function() { + var opts = { yanchor: 'auto' }; + + [0, 0.4, 0.7, 1].forEach(function(v) { + opts.y = v; + expect(isMiddleAnchor(opts)) + .toBe(v > threshold0 && v < threshold1, 'case ' + v); + }); + }); + }); + }); From 67eda7598526f1ddc04912723ebddb43f23f4167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:54:41 -0400 Subject: [PATCH 03/22] move legend defaults to own module --- src/components/legend/defaults.js | 70 +++++++++++++++++++++++++++++++ src/components/legend/index.js | 55 +----------------------- 2 files changed, 71 insertions(+), 54 deletions(-) create mode 100644 src/components/legend/defaults.js diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js new file mode 100644 index 00000000000..f5f8a7ea3be --- /dev/null +++ b/src/components/legend/defaults.js @@ -0,0 +1,70 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var Lib = require('../../lib'); +var Plots = require('../../plots/plots'); + +var attributes = require('./attributes'); +var helpers = require('./helpers'); + + +module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { + var containerIn = layoutIn.legend || {}, + containerOut = layoutOut.legend = {}; + + var visibleTraces = 0, + defaultOrder = 'normal', + trace; + + for(var i = 0; i < fullData.length; i++) { + trace = fullData[i]; + + if(helpers.legendGetsTrace(trace)) { + visibleTraces++; + // always show the legend by default if there's a pie + if(Plots.traceIs(trace, 'pie')) visibleTraces++; + } + + if((Plots.traceIs(trace, 'bar') && layoutOut.barmode==='stack') || + ['tonextx','tonexty'].indexOf(trace.fill)!==-1) { + defaultOrder = helpers.isGrouped({traceorder: defaultOrder}) ? + 'grouped+reversed' : 'reversed'; + } + + if(trace.legendgroup !== undefined && trace.legendgroup !== '') { + defaultOrder = helpers.isReversed({traceorder: defaultOrder}) ? + 'reversed+grouped' : 'grouped'; + } + } + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, attributes, attr, dflt); + } + + var showLegend = Lib.coerce(layoutIn, layoutOut, + Plots.layoutAttributes, 'showlegend', visibleTraces > 1); + + if(showLegend === false) return; + + coerce('bgcolor', layoutOut.paper_bgcolor); + coerce('bordercolor'); + coerce('borderwidth'); + Lib.coerceFont(coerce, 'font', layoutOut.font); + + coerce('traceorder', defaultOrder); + if(helpers.isGrouped(layoutOut.legend)) coerce('tracegroupgap'); + + coerce('x'); + coerce('xanchor'); + coerce('y'); + coerce('yanchor'); + Lib.noneOrAll(containerIn, containerOut, ['x', 'y']); +}; diff --git a/src/components/legend/index.js b/src/components/legend/index.js index 46ab1255ee4..ef9022da0b3 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -28,60 +28,7 @@ var legend = module.exports = {}; var constants = require('./constants'); legend.layoutAttributes = require('./attributes'); -legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { - var containerIn = layoutIn.legend || {}, - containerOut = layoutOut.legend = {}; - - var visibleTraces = 0, - defaultOrder = 'normal', - trace; - - for(var i = 0; i < fullData.length; i++) { - trace = fullData[i]; - - if(legendGetsTrace(trace)) { - visibleTraces++; - // always show the legend by default if there's a pie - if(Plots.traceIs(trace, 'pie')) visibleTraces++; - } - - if((Plots.traceIs(trace, 'bar') && layoutOut.barmode==='stack') || - ['tonextx','tonexty'].indexOf(trace.fill)!==-1) { - defaultOrder = isGrouped({traceorder: defaultOrder}) ? - 'grouped+reversed' : 'reversed'; - } - - if(trace.legendgroup !== undefined && trace.legendgroup !== '') { - defaultOrder = isReversed({traceorder: defaultOrder}) ? - 'reversed+grouped' : 'grouped'; - } - } - - function coerce(attr, dflt) { - return Lib.coerce(containerIn, containerOut, - legend.layoutAttributes, attr, dflt); - } - - var showLegend = Lib.coerce(layoutIn, layoutOut, - Plots.layoutAttributes, 'showlegend', visibleTraces > 1); - - if(showLegend === false) return; - - coerce('bgcolor', layoutOut.paper_bgcolor); - coerce('bordercolor'); - coerce('borderwidth'); - Lib.coerceFont(coerce, 'font', layoutOut.font); - - coerce('traceorder', defaultOrder); - if(isGrouped(layoutOut.legend)) coerce('tracegroupgap'); - - coerce('x'); - coerce('xanchor'); - coerce('y'); - coerce('yanchor'); - Lib.noneOrAll(containerIn, containerOut, ['x', 'y']); -}; - +legend.supplyLayoutDefaults = require('./defaults'); // ----------------------------------------------------- // styling functions for traces in legends. // same functions for styling traces in the popovers From 09354bf97df5b8c6eaee1e516ec17b43fa331673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:56:07 -0400 Subject: [PATCH 04/22] move legend style step to own module: - un-exposed legend.lines, legend.bars, legend.points, legend.boxes and legend.pie --- src/components/legend/index.js | 202 +----------------------------- src/components/legend/style.js | 216 +++++++++++++++++++++++++++++++++ 2 files changed, 217 insertions(+), 201 deletions(-) create mode 100644 src/components/legend/style.js diff --git a/src/components/legend/index.js b/src/components/legend/index.js index ef9022da0b3..d9a39e8fbee 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -29,207 +29,6 @@ var constants = require('./constants'); legend.layoutAttributes = require('./attributes'); legend.supplyLayoutDefaults = require('./defaults'); -// ----------------------------------------------------- -// styling functions for traces in legends. -// same functions for styling traces in the popovers -// ----------------------------------------------------- - -legend.lines = function(d) { - var trace = d[0].trace, - showFill = trace.visible && trace.fill && trace.fill!=='none', - showLine = subTypes.hasLines(trace); - - var fill = d3.select(this).select('.legendfill').selectAll('path') - .data(showFill ? [d] : []); - fill.enter().append('path').classed('js-fill',true); - fill.exit().remove(); - fill.attr('d', 'M5,0h30v6h-30z') - .call(Drawing.fillGroupStyle); - - var line = d3.select(this).select('.legendlines').selectAll('path') - .data(showLine ? [d] : []); - line.enter().append('path').classed('js-line',true) - .attr('d', 'M5,0h30'); - line.exit().remove(); - line.call(Drawing.lineGroupStyle); -}; - -legend.points = function(d) { - var d0 = d[0], - trace = d0.trace, - showMarkers = subTypes.hasMarkers(trace), - showText = subTypes.hasText(trace), - showLines = subTypes.hasLines(trace); - - var dMod, tMod; - - // 'scatter3d' and 'scattergeo' don't use gd.calcdata yet; - // use d0.trace to infer arrayOk attributes - - function boundVal(attrIn, arrayToValFn, bounds) { - var valIn = Lib.nestedProperty(trace, attrIn).get(), - valToBound = (Array.isArray(valIn) && arrayToValFn) ? - arrayToValFn(valIn) : valIn; - - if(bounds) { - if(valToBound < bounds[0]) return bounds[0]; - else if(valToBound > bounds[1]) return bounds[1]; - } - return valToBound; - } - - function pickFirst(array) { return array[0]; } - - // constrain text, markers, etc so they'll fit on the legend - if(showMarkers || showText || showLines) { - var dEdit = {}, - tEdit = {}; - - if(showMarkers) { - dEdit.mc = boundVal('marker.color', pickFirst); - dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]); - dEdit.ms = boundVal('marker.size', Lib.mean, [2, 16]); - dEdit.mlc = boundVal('marker.line.color', pickFirst); - dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]); - tEdit.marker = { - sizeref: 1, - sizemin: 1, - sizemode: 'diameter' - }; - } - - if(showLines) { - tEdit.line = { - width: boundVal('line.width', pickFirst, [0, 10]) - }; - } - - if(showText) { - dEdit.tx = 'Aa'; - dEdit.tp = boundVal('textposition', pickFirst); - dEdit.ts = 10; - dEdit.tc = boundVal('textfont.color', pickFirst); - dEdit.tf = boundVal('textfont.family', pickFirst); - } - - dMod = [Lib.minExtend(d0, dEdit)]; - tMod = Lib.minExtend(trace, tEdit); - } - - var ptgroup = d3.select(this).select('g.legendpoints'); - - var pts = ptgroup.selectAll('path.scatterpts') - .data(showMarkers ? dMod : []); - pts.enter().append('path').classed('scatterpts', true) - .attr('transform', 'translate(20,0)'); - pts.exit().remove(); - pts.call(Drawing.pointStyle, tMod); - - // 'mrc' is set in pointStyle and used in textPointStyle: - // constrain it here - if(showMarkers) dMod[0].mrc = 3; - - var txt = ptgroup.selectAll('g.pointtext') - .data(showText ? dMod : []); - txt.enter() - .append('g').classed('pointtext',true) - .append('text').attr('transform', 'translate(20,0)'); - txt.exit().remove(); - txt.selectAll('text').call(Drawing.textPointStyle, tMod); - -}; - -legend.bars = function(d) { - var trace = d[0].trace, - marker = trace.marker||{}, - markerLine = marker.line||{}, - barpath = d3.select(this).select('g.legendpoints') - .selectAll('path.legendbar') - .data(Plots.traceIs(trace, 'bar') ? [d] : []); - barpath.enter().append('path').classed('legendbar',true) - .attr('d','M6,6H-6V-6H6Z') - .attr('transform','translate(20,0)'); - barpath.exit().remove(); - barpath.each(function(d) { - var w = (d.mlw+1 || markerLine.width+1) - 1, - p = d3.select(this); - p.style('stroke-width',w+'px') - .call(Color.fill, d.mc || marker.color); - if(w) { - p.call(Color.stroke, d.mlc || markerLine.color); - } - }); -}; - -legend.boxes = function(d) { - var trace = d[0].trace, - pts = d3.select(this).select('g.legendpoints') - .selectAll('path.legendbox') - .data(Plots.traceIs(trace, 'box') && trace.visible ? [d] : []); - pts.enter().append('path').classed('legendbox', true) - // if we want the median bar, prepend M6,0H-6 - .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); - pts.exit().remove(); - pts.each(function(d) { - var w = (d.lw+1 || trace.line.width+1) - 1, - p = d3.select(this); - p.style('stroke-width', w+'px') - .call(Color.fill, d.fc || trace.fillcolor); - if(w) { - p.call(Color.stroke, d.lc || trace.line.color); - } - }); -}; - -legend.pie = function(d) { - var trace = d[0].trace, - pts = d3.select(this).select('g.legendpoints') - .selectAll('path.legendpie') - .data(Plots.traceIs(trace, 'pie') && trace.visible ? [d] : []); - pts.enter().append('path').classed('legendpie', true) - .attr('d', 'M6,6H-6V-6H6Z') - .attr('transform', 'translate(20,0)'); - pts.exit().remove(); - - if(pts.size()) pts.call(styleOne, d[0], trace); -}; - -legend.style = function(s) { - s.each(function(d) { - var traceGroup = d3.select(this); - - var fill = traceGroup - .selectAll('g.legendfill') - .data([d]); - fill.enter().append('g') - .classed('legendfill',true); - - var line = traceGroup - .selectAll('g.legendlines') - .data([d]); - line.enter().append('g') - .classed('legendlines',true); - - var symbol = traceGroup - .selectAll('g.legendsymbols') - .data([d]); - symbol.enter().append('g') - .classed('legendsymbols',true); - symbol.style('opacity', d[0].trace.opacity); - - symbol.selectAll('g.legendpoints') - .data([d]) - .enter().append('g') - .classed('legendpoints',true); - }) - .each(legend.bars) - .each(legend.boxes) - .each(legend.pie) - .each(legend.lines) - .each(legend.points); -}; - legend.texts = function(context, td, d, i, traces) { var fullLayout = td._fullLayout, trace = d[0].trace, @@ -724,6 +523,7 @@ legend.repositionLegend = function(td, traces) { lx = Math.round(lx); ly = Math.round(ly); +legend.style = require('./style'); // lastly check if the margin auto-expand has changed Plots.autoMargin(td,'legend',{ x: opts.x, diff --git a/src/components/legend/style.js b/src/components/legend/style.js new file mode 100644 index 00000000000..a7e9f6a8fc9 --- /dev/null +++ b/src/components/legend/style.js @@ -0,0 +1,216 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var d3 = require('d3'); + +var Lib = require('../../lib'); +var Plots = require('../../plots/plots'); +var Drawing = require('../drawing'); +var Color = require('../color'); + +var subTypes = require('../../traces/scatter/subtypes'); +var stylePie = require('../../traces/pie/style_one'); + + +module.exports = function style(s) { + s.each(function(d) { + var traceGroup = d3.select(this); + + var fill = traceGroup + .selectAll('g.legendfill') + .data([d]); + fill.enter().append('g') + .classed('legendfill',true); + + var line = traceGroup + .selectAll('g.legendlines') + .data([d]); + line.enter().append('g') + .classed('legendlines',true); + + var symbol = traceGroup + .selectAll('g.legendsymbols') + .data([d]); + symbol.enter().append('g') + .classed('legendsymbols',true); + symbol.style('opacity', d[0].trace.opacity); + + symbol.selectAll('g.legendpoints') + .data([d]) + .enter().append('g') + .classed('legendpoints',true); + }) + .each(styleBars) + .each(styleBoxes) + .each(stylePies) + .each(styleLines) + .each(stylePoints); +}; + +function styleLines(d) { + var trace = d[0].trace, + showFill = trace.visible && trace.fill && trace.fill!=='none', + showLine = subTypes.hasLines(trace); + + var fill = d3.select(this).select('.legendfill').selectAll('path') + .data(showFill ? [d] : []); + fill.enter().append('path').classed('js-fill',true); + fill.exit().remove(); + fill.attr('d', 'M5,0h30v6h-30z') + .call(Drawing.fillGroupStyle); + + var line = d3.select(this).select('.legendlines').selectAll('path') + .data(showLine ? [d] : []); + line.enter().append('path').classed('js-line',true) + .attr('d', 'M5,0h30'); + line.exit().remove(); + line.call(Drawing.lineGroupStyle); +} + +function stylePoints(d) { + var d0 = d[0], + trace = d0.trace, + showMarkers = subTypes.hasMarkers(trace), + showText = subTypes.hasText(trace), + showLines = subTypes.hasLines(trace); + + var dMod, tMod; + + // 'scatter3d' and 'scattergeo' don't use gd.calcdata yet; + // use d0.trace to infer arrayOk attributes + + function boundVal(attrIn, arrayToValFn, bounds) { + var valIn = Lib.nestedProperty(trace, attrIn).get(), + valToBound = (Array.isArray(valIn) && arrayToValFn) ? + arrayToValFn(valIn) : valIn; + + if(bounds) { + if(valToBound < bounds[0]) return bounds[0]; + else if(valToBound > bounds[1]) return bounds[1]; + } + return valToBound; + } + + function pickFirst(array) { return array[0]; } + + // constrain text, markers, etc so they'll fit on the legend + if(showMarkers || showText || showLines) { + var dEdit = {}, + tEdit = {}; + + if(showMarkers) { + dEdit.mc = boundVal('marker.color', pickFirst); + dEdit.mo = boundVal('marker.opacity', Lib.mean, [0.2, 1]); + dEdit.ms = boundVal('marker.size', Lib.mean, [2, 16]); + dEdit.mlc = boundVal('marker.line.color', pickFirst); + dEdit.mlw = boundVal('marker.line.width', Lib.mean, [0, 5]); + tEdit.marker = { + sizeref: 1, + sizemin: 1, + sizemode: 'diameter' + }; + } + + if(showLines) { + tEdit.line = { + width: boundVal('line.width', pickFirst, [0, 10]) + }; + } + + if(showText) { + dEdit.tx = 'Aa'; + dEdit.tp = boundVal('textposition', pickFirst); + dEdit.ts = 10; + dEdit.tc = boundVal('textfont.color', pickFirst); + dEdit.tf = boundVal('textfont.family', pickFirst); + } + + dMod = [Lib.minExtend(d0, dEdit)]; + tMod = Lib.minExtend(trace, tEdit); + } + + var ptgroup = d3.select(this).select('g.legendpoints'); + + var pts = ptgroup.selectAll('path.scatterpts') + .data(showMarkers ? dMod : []); + pts.enter().append('path').classed('scatterpts', true) + .attr('transform', 'translate(20,0)'); + pts.exit().remove(); + pts.call(Drawing.pointStyle, tMod); + + // 'mrc' is set in pointStyle and used in textPointStyle: + // constrain it here + if(showMarkers) dMod[0].mrc = 3; + + var txt = ptgroup.selectAll('g.pointtext') + .data(showText ? dMod : []); + txt.enter() + .append('g').classed('pointtext',true) + .append('text').attr('transform', 'translate(20,0)'); + txt.exit().remove(); + txt.selectAll('text').call(Drawing.textPointStyle, tMod); +} + +function styleBars(d) { + var trace = d[0].trace, + marker = trace.marker||{}, + markerLine = marker.line||{}, + barpath = d3.select(this).select('g.legendpoints') + .selectAll('path.legendbar') + .data(Plots.traceIs(trace, 'bar') ? [d] : []); + barpath.enter().append('path').classed('legendbar',true) + .attr('d','M6,6H-6V-6H6Z') + .attr('transform','translate(20,0)'); + barpath.exit().remove(); + barpath.each(function(d) { + var w = (d.mlw+1 || markerLine.width+1) - 1, + p = d3.select(this); + p.style('stroke-width',w+'px') + .call(Color.fill, d.mc || marker.color); + if(w) { + p.call(Color.stroke, d.mlc || markerLine.color); + } + }); +} + +function styleBoxes(d) { + var trace = d[0].trace, + pts = d3.select(this).select('g.legendpoints') + .selectAll('path.legendbox') + .data(Plots.traceIs(trace, 'box') && trace.visible ? [d] : []); + pts.enter().append('path').classed('legendbox', true) + // if we want the median bar, prepend M6,0H-6 + .attr('d', 'M6,6H-6V-6H6Z') + .attr('transform', 'translate(20,0)'); + pts.exit().remove(); + pts.each(function(d) { + var w = (d.lw+1 || trace.line.width+1) - 1, + p = d3.select(this); + p.style('stroke-width', w+'px') + .call(Color.fill, d.fc || trace.fillcolor); + if(w) { + p.call(Color.stroke, d.lc || trace.line.color); + } + }); +} + +function stylePies(d) { + var trace = d[0].trace, + pts = d3.select(this).select('g.legendpoints') + .selectAll('path.legendpie') + .data(Plots.traceIs(trace, 'pie') && trace.visible ? [d] : []); + pts.enter().append('path').classed('legendpie', true) + .attr('d', 'M6,6H-6V-6H6Z') + .attr('transform', 'translate(20,0)'); + pts.exit().remove(); + + if(pts.size()) pts.call(stylePie, d[0], trace); +} From 1b69395916e8434d69e9005b338888ace37ee609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:56:46 -0400 Subject: [PATCH 05/22] move legend-data logic to own module --- src/components/legend/get_legend_data.js | 104 +++++++++++++++++++++++ src/components/legend/index.js | 85 ------------------ test/jasmine/tests/legend_test.js | 3 +- 3 files changed, 105 insertions(+), 87 deletions(-) create mode 100644 src/components/legend/get_legend_data.js diff --git a/src/components/legend/get_legend_data.js b/src/components/legend/get_legend_data.js new file mode 100644 index 00000000000..a8698f140fe --- /dev/null +++ b/src/components/legend/get_legend_data.js @@ -0,0 +1,104 @@ +/** +* Copyright 2012-2016, 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 Plots = require('../../plots/plots'); + +var helpers = require('./helpers'); + + +module.exports = function getLegendData(calcdata, opts) { + var lgroupToTraces = {}, + lgroups = [], + hasOneNonBlankGroup = false, + slicesShown = {}, + lgroupi = 0; + + var i, j; + + function addOneItem(legendGroup, legendItem) { + // each '' legend group is treated as a separate group + if(legendGroup === '' || !helpers.isGrouped(opts)) { + var uniqueGroup = '~~i' + lgroupi; // TODO: check this against fullData legendgroups? + + lgroups.push(uniqueGroup); + lgroupToTraces[uniqueGroup] = [[legendItem]]; + lgroupi++; + } + else if(lgroups.indexOf(legendGroup) === -1) { + lgroups.push(legendGroup); + hasOneNonBlankGroup = true; + lgroupToTraces[legendGroup] = [[legendItem]]; + } + else lgroupToTraces[legendGroup].push([legendItem]); + } + + // build an { legendgroup: [cd0, cd0], ... } object + for(i = 0; i < calcdata.length; i++) { + var cd = calcdata[i], + cd0 = cd[0], + trace = cd0.trace, + lgroup = trace.legendgroup; + + if(!helpers.legendGetsTrace(trace) || !trace.showlegend) continue; + + if(Plots.traceIs(trace, 'pie')) { + if(!slicesShown[lgroup]) slicesShown[lgroup] = {}; + + for(j = 0; j < cd.length; j++) { + var labelj = cd[j].label; + + if(!slicesShown[lgroup][labelj]) { + addOneItem(lgroup, { + label: labelj, + color: cd[j].color, + i: cd[j].i, + trace: trace + }); + + slicesShown[lgroup][labelj] = true; + } + } + } + + else addOneItem(lgroup, cd0); + } + + // won't draw a legend in this case + if(!lgroups.length) return []; + + // rearrange lgroupToTraces into a d3-friendly array of arrays + var lgroupsLength = lgroups.length, + ltraces, + legendData; + + if(hasOneNonBlankGroup && helpers.isGrouped(opts)) { + legendData = new Array(lgroupsLength); + + for(i = 0; i < lgroupsLength; i++) { + ltraces = lgroupToTraces[lgroups[i]]; + legendData[i] = helpers.isReversed(opts) ? ltraces.reverse() : ltraces; + } + } + else { + // collapse all groups into one if all groups are blank + legendData = [new Array(lgroupsLength)]; + + for(i = 0; i < lgroupsLength; i++) { + ltraces = lgroupToTraces[lgroups[i]][0]; + legendData[0][helpers.isReversed(opts) ? lgroupsLength-i-1 : i] = ltraces; + } + lgroupsLength = 1; + } + + // needed in repositionLegend + opts._lgroupsLength = lgroupsLength; + return legendData; +}; diff --git a/src/components/legend/index.js b/src/components/legend/index.js index d9a39e8fbee..de18db52c5d 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -75,91 +75,6 @@ legend.texts = function(context, td, d, i, traces) { else text.call(textLayout); }; -legend.getLegendData = function(calcdata, opts) { - - // build an { legendgroup: [cd0, cd0], ... } object - var lgroupToTraces = {}, - lgroups = [], - hasOneNonBlankGroup = false, - slicesShown = {}, - lgroupi = 0; - - var cd, cd0, trace, lgroup, i, j, labelj; - - function addOneItem(legendGroup, legendItem) { - // each '' legend group is treated as a separate group - if(legendGroup==='' || !isGrouped(opts)) { - var uniqueGroup = '~~i' + lgroupi; // TODO: check this against fullData legendgroups? - lgroups.push(uniqueGroup); - lgroupToTraces[uniqueGroup] = [[legendItem]]; - lgroupi++; - } - else if(lgroups.indexOf(legendGroup) === -1) { - lgroups.push(legendGroup); - hasOneNonBlankGroup = true; - lgroupToTraces[legendGroup] = [[legendItem]]; - } - else lgroupToTraces[legendGroup].push([legendItem]); - } - - for(i = 0; i < calcdata.length; i++) { - cd = calcdata[i]; - cd0 = cd[0]; - trace = cd0.trace; - lgroup = trace.legendgroup; - - if(!legendGetsTrace(trace) || !trace.showlegend) continue; - - if(Plots.traceIs(trace, 'pie')) { - if(!slicesShown[lgroup]) slicesShown[lgroup] = {}; - for(j = 0; j < cd.length; j++) { - labelj = cd[j].label; - if(!slicesShown[lgroup][labelj]) { - addOneItem(lgroup, { - label: labelj, - color: cd[j].color, - i: cd[j].i, - trace: trace - }); - - slicesShown[lgroup][labelj] = true; - } - } - } - - else addOneItem(lgroup, cd0); - } - - // won't draw a legend in this case - if(!lgroups.length) return []; - - // rearrange lgroupToTraces into a d3-friendly array of arrays - var lgroupsLength = lgroups.length, - ltraces, - legendData; - - if(hasOneNonBlankGroup && isGrouped(opts)) { - legendData = new Array(lgroupsLength); - for(i = 0; i < lgroupsLength; i++) { - ltraces = lgroupToTraces[lgroups[i]]; - legendData[i] = isReversed(opts) ? ltraces.reverse() : ltraces; - } - } - else { - // collapse all groups into one if all groups are blank - legendData = [new Array(lgroupsLength)]; - for(i = 0; i < lgroupsLength; i++) { - ltraces = lgroupToTraces[lgroups[i]][0]; - legendData[0][isReversed(opts) ? lgroupsLength-i-1 : i] = ltraces; - } - lgroupsLength = 1; - } - - // needed in repositionLegend - opts._lgroupsLength = lgroupsLength; - return legendData; -}; - legend.draw = function(td) { var fullLayout = td._fullLayout; diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index b3a474b1625..58f10776d7e 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1,6 +1,7 @@ var Legend = require('@src/components/legend'); var Plots = require('@src/plots/plots'); +var getLegendData = require('@src/components/legend/get_legend_data'); var helpers = require('@src/components/legend/helpers'); var anchorUtils = require('@src/components/legend/anchor_utils'); describe('Test legend:', function() { @@ -65,8 +66,6 @@ describe('Test legend:', function() { }); describe('getLegendData', function() { - var getLegendData = Legend.getLegendData; - var calcdata, opts, legendData, expected; it('should group legendgroup traces', function() { From 0b6dfcf962c7d8f231b12a928238e5ea237bc39a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:59:23 -0400 Subject: [PATCH 06/22] move legend draw step to own module: - un-expose legend.repositionText and legend.texts - use anchor utils --- src/components/legend/draw.js | 448 +++++++++++++++++++++++++++++++++ src/components/legend/index.js | 418 +----------------------------- 2 files changed, 449 insertions(+), 417 deletions(-) create mode 100644 src/components/legend/draw.js diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js new file mode 100644 index 00000000000..b6db5c8188f --- /dev/null +++ b/src/components/legend/draw.js @@ -0,0 +1,448 @@ +/** +* Copyright 2012-2016, Plotly, Inc. +* All rights reserved. +* +* This source code is licensed under the MIT license found in the +* LICENSE file in the root directory of this source tree. +*/ + + +'use strict'; + +var d3 = require('d3'); + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); +var Plots = require('../../plots/plots'); +var Fx = require('../../plots/cartesian/graph_interact'); +var Drawing = require('../drawing'); +var Color = require('../color'); + +var constants = require('./constants'); +var getLegendData = require('./get_legend_data'); +var style = require('./style'); +var helpers = require('./helpers'); +var anchorUtils = require('./anchor_utils'); + + +module.exports = function draw(gd) { + var fullLayout = gd._fullLayout; + + if(!fullLayout._infolayer || !gd.calcdata) return; + + var opts = fullLayout.legend, + legendData = fullLayout.showlegend && getLegendData(gd.calcdata, opts), + hiddenSlices = fullLayout.hiddenlabels || []; + + if(!fullLayout.showlegend || !legendData.length) { + fullLayout._infolayer.selectAll('.legend').remove(); + Plots.autoMargin(gd, 'legend'); + return; + } + + if(typeof gd.firstRender === 'undefined') gd.firstRender = true; + else if(gd.firstRender) gd.firstRender = false; + + var legendsvg = fullLayout._infolayer.selectAll('svg.legend') + .data([0]); + legendsvg.enter().append('svg') + .attr({ + 'class': 'legend', + 'pointer-events': 'all' + }); + + var bg = legendsvg.selectAll('rect.bg') + .data([0]); + bg.enter().append('rect') + .attr({ + 'class': 'bg', + 'shape-rendering': 'crispEdges' + }) + .call(Color.stroke, opts.bordercolor) + .call(Color.fill, opts.bgcolor) + .style('stroke-width', opts.borderwidth + 'px'); + + var scrollBox = legendsvg.selectAll('g.scrollbox') + .data([0]); + scrollBox.enter().append('g') + .attr('class', 'scrollbox'); + scrollBox.exit().remove(); + + var scrollBar = legendsvg.selectAll('rect.scrollbar') + .data([0]); + scrollBar.enter().append('rect') + .attr({ + 'class': 'scrollbar', + 'rx': 20, + 'ry': 2, + 'width': 0, + 'height': 0 + }) + .call(Color.fill, '#808BA4'); + + var groups = scrollBox.selectAll('g.groups') + .data(legendData); + groups.enter().append('g') + .attr('class', 'groups'); + groups.exit().remove(); + + if(helpers.isGrouped(opts)) { + groups.attr('transform', function(d, i) { + return 'translate(0,' + i * opts.tracegroupgap + ')'; + }); + } + + var traces = groups.selectAll('g.traces') + .data(Lib.identity); + + traces.enter().append('g').attr('class', 'traces'); + traces.exit().remove(); + + traces.call(style) + .style('opacity', function(d) { + var trace = d[0].trace; + if(Plots.traceIs(trace, 'pie')) { + return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1; + } else { + return trace.visible === 'legendonly' ? 0.5 : 1; + } + }) + .each(function(d, i) { + drawTexts(this, gd, d, i, traces); + + var traceToggle = d3.select(this).selectAll('rect') + .data([0]); + traceToggle.enter().append('rect') + .classed('legendtoggle', true) + .style('cursor', 'pointer') + .attr('pointer-events', 'all') + .call(Color.fill, 'rgba(0,0,0,0)'); + traceToggle.on('click', function() { + if(gd._dragged) return; + + var fullData = gd._fullData, + trace = d[0].trace, + legendgroup = trace.legendgroup, + traceIndicesInGroup = [], + tracei, + newVisible; + + if(Plots.traceIs(trace, 'pie')) { + var thisLabel = d[0].label, + newHiddenSlices = hiddenSlices.slice(), + thisLabelIndex = newHiddenSlices.indexOf(thisLabel); + + if(thisLabelIndex === -1) newHiddenSlices.push(thisLabel); + else newHiddenSlices.splice(thisLabelIndex, 1); + + Plotly.relayout(gd, 'hiddenlabels', newHiddenSlices); + } else { + if(legendgroup === '') { + traceIndicesInGroup = [trace.index]; + } else { + for(var i = 0; i < fullData.length; i++) { + tracei = fullData[i]; + if(tracei.legendgroup === legendgroup) { + traceIndicesInGroup.push(tracei.index); + } + } + } + + newVisible = trace.visible === true ? 'legendonly' : true; + Plotly.restyle(gd, 'visible', newVisible, traceIndicesInGroup); + } + }); + }); + + // Position and size the legend + repositionLegend(gd, traces); + + // Scroll section must be executed after repositionLegend. + // It requires the legend width, height, x and y to position the scrollbox + // and these values are mutated in repositionLegend. + var gs = fullLayout._size, + lx = gs.l + gs.w * opts.x, + ly = gs.t + gs.h * (1-opts.y); + + if(anchorUtils.isRightAnchor(opts)) { + lx -= opts.width; + } + if(anchorUtils.isCenterAnchor(opts)) { + lx -= opts.width / 2; + } + + if(anchorUtils.isBottomAnchor(opts)) { + ly -= opts.height; + } + if(anchorUtils.isMiddleAnchor(opts)) { + ly -= opts.height / 2; + } + + // Deal with scrolling + var plotHeight = fullLayout.height - fullLayout.margin.b, + scrollheight = Math.min(plotHeight - ly, opts.height), + scrollPosition = scrollBox.attr('data-scroll') ? scrollBox.attr('data-scroll') : 0; + + scrollBox.attr('transform', 'translate(0, ' + scrollPosition + ')'); + bg.attr({ + width: opts.width - 2 * opts.borderwidth, + height: scrollheight - 2 * opts.borderwidth, + x: opts.borderwidth, + y: opts.borderwidth + }); + + legendsvg.call(Drawing.setRect, lx, ly, opts.width, scrollheight); + + // If scrollbar should be shown. + if(gd.firstRender && opts.height - scrollheight > 0 && !gd._context.staticPlot) { + + bg.attr({ width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth }); + + legendsvg.node().addEventListener('wheel', function(e) { + e.preventDefault(); + scrollHandler(e.deltaY / 20); + }); + + scrollBar.node().addEventListener('mousedown', function(e) { + e.preventDefault(); + + function mMove(e) { + if(e.buttons === 1) { + scrollHandler(e.movementY); + } + } + + function mUp() { + scrollBar.node().removeEventListener('mousemove', mMove); + window.removeEventListener('mouseup', mUp); + } + + window.addEventListener('mousemove', mMove); + window.addEventListener('mouseup', mUp); + }); + + // Move scrollbar to starting position on the first render + scrollBar.call( + Drawing.setRect, + opts.width - (constants.scrollBarWidth + constants.scrollBarMargin), + constants.scrollBarMargin, + constants.scrollBarWidth, + constants.scrollBarHeight + ); + } + + function scrollHandler(delta) { + + var scrollBarTrack = scrollheight - constants.scrollBarHeight - 2 * constants.scrollBarMargin, + translateY = scrollBox.attr('data-scroll'), + scrollBoxY = Lib.constrain(translateY - delta, Math.min(scrollheight - opts.height, 0), 0), + scrollBarY = -scrollBoxY / (opts.height - scrollheight) * scrollBarTrack + constants.scrollBarMargin; + + scrollBox.attr('data-scroll', scrollBoxY); + scrollBox.attr('transform', 'translate(0, ' + scrollBoxY + ')'); + scrollBar.call( + Drawing.setRect, + opts.width - (constants.scrollBarWidth + constants.scrollBarMargin), + scrollBarY, + constants.scrollBarWidth, + constants.scrollBarHeight + ); + } + + if(gd._context.editable) { + var xf, + yf, + x0, + y0, + lw, + lh; + + Fx.dragElement({ + element: legendsvg.node(), + prepFn: function() { + x0 = Number(legendsvg.attr('x')); + y0 = Number(legendsvg.attr('y')); + lw = Number(legendsvg.attr('width')); + lh = Number(legendsvg.attr('height')); + Fx.setCursor(legendsvg); + }, + moveFn: function(dx, dy) { + var gs = gd._fullLayout._size; + + legendsvg.call(Drawing.setPosition, x0+dx, y0+dy); + + xf = Fx.dragAlign(x0+dx, lw, gs.l, gs.l+gs.w, + opts.xanchor); + yf = Fx.dragAlign(y0+dy+lh, -lh, gs.t+gs.h, gs.t, + opts.yanchor); + + var csr = Fx.dragCursors(xf, yf, + opts.xanchor, opts.yanchor); + Fx.setCursor(legendsvg, csr); + }, + doneFn: function(dragged) { + Fx.setCursor(legendsvg); + if(dragged && xf !== undefined && yf !== undefined) { + Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); + } + } + }); + } +}; + +function drawTexts(context, gd, d, i, traces) { + var fullLayout = gd._fullLayout, + trace = d[0].trace, + isPie = Plots.traceIs(trace, 'pie'), + traceIndex = trace.index, + name = isPie ? d[0].label : trace.name; + + var text = d3.select(context).selectAll('text.legendtext') + .data([0]); + text.enter().append('text').classed('legendtext', true); + text.attr({ + x: 40, + y: 0, + 'data-unformatted': name + }) + .style({ + 'text-anchor': 'start', + '-webkit-user-select': 'none', + '-moz-user-select': 'none', + '-ms-user-select': 'none', + 'user-select': 'none' + }) + .call(Drawing.font, fullLayout.legend.font) + .text(name); + + function textLayout(s) { + Plotly.util.convertToTspans(s, function() { + if(gd.firstRender) repositionLegend(gd, traces); + }); + s.selectAll('tspan.line').attr({x: s.attr('x')}); + } + + if(gd._context.editable && !isPie) { + text.call(Plotly.util.makeEditable) + .call(textLayout) + .on('edit', function(text) { + this.attr({'data-unformatted': text}); + this.text(text) + .call(textLayout); + if(!this.text()) text = ' \u0020\u0020 '; + Plotly.restyle(gd, 'name', text, traceIndex); + }); + } + else text.call(textLayout); +} + +function repositionLegend(gd, traces) { + var fullLayout = gd._fullLayout, + gs = fullLayout._size, + opts = fullLayout.legend, + borderwidth = opts.borderwidth; + + opts.width = 0; + opts.height = 0; + + traces.each(function(d) { + var trace = d[0].trace, + g = d3.select(this), + bg = g.selectAll('.legendtoggle'), + text = g.selectAll('.legendtext'), + tspans = g.selectAll('.legendtext>tspan'), + tHeight = opts.font.size * 1.3, + tLines = tspans[0].length || 1, + tWidth = text.node() && Drawing.bBox(text.node()).width, + mathjaxGroup = g.select('g[class*=math-group]'), + textY, + tHeightFull; + + if(!trace.showlegend) { + g.remove(); + return; + } + + if(mathjaxGroup.node()) { + var mathjaxBB = Drawing.bBox(mathjaxGroup.node()); + tHeight = mathjaxBB.height; + tWidth = mathjaxBB.width; + mathjaxGroup.attr('transform','translate(0,' + (tHeight / 4) + ')'); + } + else { + // approximation to height offset to center the font + // to avoid getBoundingClientRect + textY = tHeight * (0.3 + (1-tLines) / 2); + text.attr('y',textY); + tspans.attr('y',textY); + } + + tHeightFull = Math.max(tHeight*tLines, 16) + 3; + + g.attr('transform', + 'translate(' + borderwidth + ',' + + (5 + borderwidth + opts.height + tHeightFull / 2) + + ')' + ); + bg.attr({x: 0, y: -tHeightFull / 2, height: tHeightFull}); + + opts.height += tHeightFull; + opts.width = Math.max(opts.width, tWidth || 0); + }); + + + opts.width += 45 + borderwidth * 2; + opts.height += 10 + borderwidth * 2; + + if(helpers.isGrouped(opts)) { + opts.height += (opts._lgroupsLength-1) * opts.tracegroupgap; + } + + traces.selectAll('.legendtoggle') + .attr('width', (gd._context.editable ? 0 : opts.width) + 40); + + // now position the legend. for both x,y the positions are recorded as + // fractions of the plot area (left, bottom = 0,0). Outside the plot + // area is allowed but position will be clipped to the page. + // values <1/3 align the low side at that fraction, 1/3-2/3 align the + // center at that fraction, >2/3 align the right at that fraction + + var lx = gs.l + gs.w * opts.x, + ly = gs.t + gs.h * (1-opts.y); + + var xanchor = 'left'; + if(anchorUtils.isRightAnchor(opts)) { + lx -= opts.width; + xanchor = 'right'; + } + if(anchorUtils.isCenterAnchor(opts)) { + lx -= opts.width / 2; + xanchor = 'center'; + } + + var yanchor = 'top'; + if(anchorUtils.isBottomAnchor(opts)) { + ly -= opts.height; + yanchor = 'bottom'; + } + if(anchorUtils.isMiddleAnchor(opts)) { + ly -= opts.height / 2; + yanchor = 'middle'; + } + + // make sure we're only getting full pixels + opts.width = Math.ceil(opts.width); + opts.height = Math.ceil(opts.height); + lx = Math.round(lx); + ly = Math.round(ly); + + // lastly check if the margin auto-expand has changed + Plots.autoMargin(gd, 'legend', { + x: opts.x, + y: opts.y, + l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), + r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), + b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0), + t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) + }); +} diff --git a/src/components/legend/index.js b/src/components/legend/index.js index de18db52c5d..91f19c4086b 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -29,423 +29,7 @@ var constants = require('./constants'); legend.layoutAttributes = require('./attributes'); legend.supplyLayoutDefaults = require('./defaults'); -legend.texts = function(context, td, d, i, traces) { - var fullLayout = td._fullLayout, - trace = d[0].trace, - isPie = Plots.traceIs(trace, 'pie'), - traceIndex = trace.index, - name = isPie ? d[0].label : trace.name; - var text = d3.select(context).selectAll('text.legendtext') - .data([0]); - text.enter().append('text').classed('legendtext', true); - text.attr({ - x: 40, - y: 0, - 'data-unformatted': name - }) - .style({ - 'text-anchor': 'start', - '-webkit-user-select': 'none', - '-moz-user-select': 'none', - '-ms-user-select': 'none', - 'user-select': 'none' - }) - .call(Drawing.font, fullLayout.legend.font) - .text(name); - - function textLayout(s) { - Plotly.util.convertToTspans(s, function() { - if(td.firstRender) legend.repositionLegend(td, traces); - }); - s.selectAll('tspan.line').attr({x: s.attr('x')}); - } - - if(td._context.editable && !isPie) { - text.call(Plotly.util.makeEditable) - .call(textLayout) - .on('edit', function(text) { - this.attr({'data-unformatted': text}); - this.text(text) - .call(textLayout); - if(!this.text()) text = ' \u0020\u0020 '; - Plotly.restyle(td, 'name', text, traceIndex); - }); - } - else text.call(textLayout); -}; - -legend.draw = function(td) { - var fullLayout = td._fullLayout; - - if(!fullLayout._infolayer || !td.calcdata) return; - - var opts = fullLayout.legend, - legendData = fullLayout.showlegend && legend.getLegendData(td.calcdata, opts), - hiddenSlices = fullLayout.hiddenlabels || []; - - if(!fullLayout.showlegend || !legendData.length) { - fullLayout._infolayer.selectAll('.legend').remove(); - Plots.autoMargin(td, 'legend'); - return; - } - - if(typeof td.firstRender === 'undefined') td.firstRender = true; - else if(td.firstRender) td.firstRender = false; - - var legendsvg = fullLayout._infolayer.selectAll('svg.legend') - .data([0]); - legendsvg.enter().append('svg') - .attr({ - 'class': 'legend', - 'pointer-events': 'all' - }); - - var bg = legendsvg.selectAll('rect.bg') - .data([0]); - bg.enter().append('rect') - .attr({ - 'class': 'bg', - 'shape-rendering': 'crispEdges' - }) - .call(Color.stroke, opts.bordercolor) - .call(Color.fill, opts.bgcolor) - .style('stroke-width', opts.borderwidth + 'px'); - - var scrollBox = legendsvg.selectAll('g.scrollbox') - .data([0]); - scrollBox.enter().append('g') - .attr('class', 'scrollbox'); - scrollBox.exit().remove(); - - var scrollBar = legendsvg.selectAll('rect.scrollbar') - .data([0]); - scrollBar.enter().append('rect') - .attr({ - 'class': 'scrollbar', - 'rx': 20, - 'ry': 2, - 'width': 0, - 'height': 0 - }) - .call(Color.fill, '#808BA4'); - - var groups = scrollBox.selectAll('g.groups') - .data(legendData); - groups.enter().append('g') - .attr('class', 'groups'); - groups.exit().remove(); - - if(isGrouped(opts)) { - groups.attr('transform', function(d, i) { - return 'translate(0,' + i * opts.tracegroupgap + ')'; - }); - } - - var traces = groups.selectAll('g.traces') - .data(Lib.identity); - - traces.enter().append('g').attr('class', 'traces'); - traces.exit().remove(); - - traces.call(legend.style) - .style('opacity', function(d) { - var trace = d[0].trace; - if(Plots.traceIs(trace, 'pie')) { - return hiddenSlices.indexOf(d[0].label) !== -1 ? 0.5 : 1; - } else { - return trace.visible === 'legendonly' ? 0.5 : 1; - } - }) - .each(function(d, i) { - legend.texts(this, td, d, i, traces); - - var traceToggle = d3.select(this).selectAll('rect') - .data([0]); - traceToggle.enter().append('rect') - .classed('legendtoggle', true) - .style('cursor', 'pointer') - .attr('pointer-events', 'all') - .call(Color.fill, 'rgba(0,0,0,0)'); - traceToggle.on('click', function() { - if(td._dragged) return; - - var fullData = td._fullData, - trace = d[0].trace, - legendgroup = trace.legendgroup, - traceIndicesInGroup = [], - tracei, - newVisible; - - if(Plots.traceIs(trace, 'pie')) { - var thisLabel = d[0].label, - newHiddenSlices = hiddenSlices.slice(), - thisLabelIndex = newHiddenSlices.indexOf(thisLabel); - - if(thisLabelIndex === -1) newHiddenSlices.push(thisLabel); - else newHiddenSlices.splice(thisLabelIndex, 1); - - Plotly.relayout(td, 'hiddenlabels', newHiddenSlices); - } else { - if(legendgroup === '') { - traceIndicesInGroup = [trace.index]; - } else { - for(var i = 0; i < fullData.length; i++) { - tracei = fullData[i]; - if(tracei.legendgroup === legendgroup) { - traceIndicesInGroup.push(tracei.index); - } - } - } - - newVisible = trace.visible === true ? 'legendonly' : true; - Plotly.restyle(td, 'visible', newVisible, traceIndicesInGroup); - } - }); - }); - - // Position and size the legend - legend.repositionLegend(td, traces); - - // Scroll section must be executed after repositionLegend. - // It requires the legend width, height, x and y to position the scrollbox - // and these values are mutated in repositionLegend. - var gs = fullLayout._size, - lx = gs.l + gs.w * opts.x, - ly = gs.t + gs.h * (1-opts.y); - - if(opts.xanchor === 'right' || (opts.xanchor === 'auto' && opts.x >= 2 / 3)) { - lx -= opts.width; - } - else if(opts.xanchor === 'center' || (opts.xanchor === 'auto' && opts.x > 1 / 3)) { - lx -= opts.width / 2; - } - - if(opts.yanchor === 'bottom' || (opts.yanchor === 'auto' && opts.y <= 1 / 3)) { - ly -= opts.height; - } - else if(opts.yanchor === 'middle' || (opts.yanchor === 'auto' && opts.y < 2 / 3)) { - ly -= opts.height / 2; - } - - // Deal with scrolling - var plotHeight = fullLayout.height - fullLayout.margin.b, - scrollheight = Math.min(plotHeight - ly, opts.height), - scrollPosition = scrollBox.attr('data-scroll') ? scrollBox.attr('data-scroll') : 0; - - scrollBox.attr('transform', 'translate(0, ' + scrollPosition + ')'); - bg.attr({ - width: opts.width - 2 * opts.borderwidth, - height: scrollheight - 2 * opts.borderwidth, - x: opts.borderwidth, - y: opts.borderwidth - }); - - legendsvg.call(Drawing.setRect, lx, ly, opts.width, scrollheight); - - // If scrollbar should be shown. - if(td.firstRender && opts.height - scrollheight > 0 && !td._context.staticPlot) { - - bg.attr({ width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth }); - - legendsvg.node().addEventListener('wheel', function(e) { - e.preventDefault(); - scrollHandler(e.deltaY / 20); - }); - - scrollBar.node().addEventListener('mousedown', function(e) { - e.preventDefault(); - - function mMove(e) { - if(e.buttons === 1) { - scrollHandler(e.movementY); - } - } - - function mUp() { - scrollBar.node().removeEventListener('mousemove', mMove); - window.removeEventListener('mouseup', mUp); - } - - window.addEventListener('mousemove', mMove); - window.addEventListener('mouseup', mUp); - }); - - // Move scrollbar to starting position on the first render - scrollBar.call( - Drawing.setRect, - opts.width - (constants.scrollBarWidth + constants.scrollBarMargin), - constants.scrollBarMargin, - constants.scrollBarWidth, - constants.scrollBarHeight - ); - } - - function scrollHandler(delta) { - - var scrollBarTrack = scrollheight - constants.scrollBarHeight - 2 * constants.scrollBarMargin, - translateY = scrollBox.attr('data-scroll'), - scrollBoxY = Lib.constrain(translateY - delta, Math.min(scrollheight - opts.height, 0), 0), - scrollBarY = -scrollBoxY / (opts.height - scrollheight) * scrollBarTrack + constants.scrollBarMargin; - - scrollBox.attr('data-scroll', scrollBoxY); - scrollBox.attr('transform', 'translate(0, ' + scrollBoxY + ')'); - scrollBar.call( - Drawing.setRect, - opts.width - (constants.scrollBarWidth + constants.scrollBarMargin), - scrollBarY, - constants.scrollBarWidth, - constants.scrollBarHeight - ); - } - - if(td._context.editable) { - var xf, - yf, - x0, - y0, - lw, - lh; - - Fx.dragElement({ - element: legendsvg.node(), - prepFn: function() { - x0 = Number(legendsvg.attr('x')); - y0 = Number(legendsvg.attr('y')); - lw = Number(legendsvg.attr('width')); - lh = Number(legendsvg.attr('height')); - Fx.setCursor(legendsvg); - }, - moveFn: function(dx, dy) { - var gs = td._fullLayout._size; - - legendsvg.call(Drawing.setPosition, x0+dx, y0+dy); - - xf = Fx.dragAlign(x0+dx, lw, gs.l, gs.l+gs.w, - opts.xanchor); - yf = Fx.dragAlign(y0+dy+lh, -lh, gs.t+gs.h, gs.t, - opts.yanchor); - - var csr = Fx.dragCursors(xf, yf, - opts.xanchor, opts.yanchor); - Fx.setCursor(legendsvg, csr); - }, - doneFn: function(dragged) { - Fx.setCursor(legendsvg); - if(dragged && xf !== undefined && yf !== undefined) { - Plotly.relayout(td, {'legend.x': xf, 'legend.y': yf}); - } - } - }); - } -}; - -legend.repositionLegend = function(td, traces) { - var fullLayout = td._fullLayout, - gs = fullLayout._size, - opts = fullLayout.legend, - borderwidth = opts.borderwidth; - - opts.width = 0, - opts.height = 0, - - traces.each(function(d) { - var trace = d[0].trace, - g = d3.select(this), - bg = g.selectAll('.legendtoggle'), - text = g.selectAll('.legendtext'), - tspans = g.selectAll('.legendtext>tspan'), - tHeight = opts.font.size * 1.3, - tLines = tspans[0].length || 1, - tWidth = text.node() && Drawing.bBox(text.node()).width, - mathjaxGroup = g.select('g[class*=math-group]'), - textY, - tHeightFull; - - if(!trace.showlegend) { - g.remove(); - return; - } - - if(mathjaxGroup.node()) { - var mathjaxBB = Drawing.bBox(mathjaxGroup.node()); - tHeight = mathjaxBB.height; - tWidth = mathjaxBB.width; - mathjaxGroup.attr('transform','translate(0,' + (tHeight / 4) + ')'); - } - else { - // approximation to height offset to center the font - // to avoid getBoundingClientRect - textY = tHeight * (0.3 + (1-tLines) / 2); - text.attr('y',textY); - tspans.attr('y',textY); - } - - tHeightFull = Math.max(tHeight*tLines, 16) + 3; - - g.attr('transform', - 'translate(' + borderwidth + ',' + - (5 + borderwidth + opts.height + tHeightFull / 2) + - ')' - ); - bg.attr({x: 0, y: -tHeightFull / 2, height: tHeightFull}); - - opts.height += tHeightFull; - opts.width = Math.max(opts.width, tWidth || 0); - }); - - - opts.width += 45 + borderwidth * 2; - opts.height += 10 + borderwidth * 2; - - if(isGrouped(opts)) opts.height += (opts._lgroupsLength-1) * opts.tracegroupgap; - - traces.selectAll('.legendtoggle') - .attr('width', (td._context.editable ? 0 : opts.width) + 40); - - // now position the legend. for both x,y the positions are recorded as - // fractions of the plot area (left, bottom = 0,0). Outside the plot - // area is allowed but position will be clipped to the page. - // values <1/3 align the low side at that fraction, 1/3-2/3 align the - // center at that fraction, >2/3 align the right at that fraction - - var lx = gs.l + gs.w * opts.x, - ly = gs.t + gs.h * (1-opts.y); - - var xanchor = 'left'; - if(opts.xanchor === 'right' || (opts.xanchor === 'auto' && opts.x >= 2 / 3)) { - lx -= opts.width; - xanchor = 'right'; - } - else if(opts.xanchor === 'center' || (opts.xanchor === 'auto' && opts.x > 1 / 3)) { - lx -= opts.width / 2; - xanchor = 'center'; - } - - var yanchor = 'top'; - if(opts.yanchor === 'bottom' || (opts.yanchor === 'auto' && opts.y <= 1 / 3)) { - ly -= opts.height; - yanchor = 'bottom'; - } - else if(opts.yanchor === 'middle' || (opts.yanchor === 'auto' && opts.y < 2 / 3)) { - ly -= opts.height / 2; - yanchor = 'middle'; - } - - // make sure we're only getting full pixels - opts.width = Math.ceil(opts.width); - opts.height = Math.ceil(opts.height); - lx = Math.round(lx); - ly = Math.round(ly); +legend.draw = require('./draw'); legend.style = require('./style'); - // lastly check if the margin auto-expand has changed - Plots.autoMargin(td,'legend',{ - x: opts.x, - y: opts.y, - l: opts.width * ({right: 1, center: 0.5}[xanchor] || 0), - r: opts.width * ({left: 1, center: 0.5}[xanchor] || 0), - b: opts.height * ({top: 1, middle: 0.5}[yanchor] || 0), - t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) - }); -}; From aa13bfb5ed0417a2dd791c2548a0032275431cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Mon, 14 Mar 2016 14:59:28 -0400 Subject: [PATCH 07/22] lint --- src/components/legend/index.js | 14 -------------- src/plots/plots.js | 11 +++++++---- test/jasmine/tests/legend_test.js | 4 +++- 3 files changed, 10 insertions(+), 19 deletions(-) diff --git a/src/components/legend/index.js b/src/components/legend/index.js index 91f19c4086b..4b033d1bad9 100644 --- a/src/components/legend/index.js +++ b/src/components/legend/index.js @@ -9,23 +9,9 @@ 'use strict'; -var Plotly = require('../../plotly'); -var d3 = require('d3'); - -var Lib = require('../../lib'); - -var Plots = require('../../plots/plots'); -var Fx = require('../../plots/cartesian/graph_interact'); - -var Color = require('../color'); -var Drawing = require('../drawing'); - -var subTypes = require('../../traces/scatter/subtypes'); -var styleOne = require('../../traces/pie/style_one'); var legend = module.exports = {}; -var constants = require('./constants'); legend.layoutAttributes = require('./attributes'); legend.supplyLayoutDefaults = require('./defaults'); diff --git a/src/plots/plots.js b/src/plots/plots.js index 6d3693b754c..990b715edc6 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -840,6 +840,7 @@ plots.sanitizeMargins = function(fullLayout) { margin.b = Math.floor(correction * margin.b); } }; + // called by legend and colorbar routines to see if we need to // expand the margins to show them // o is {x,l,r,y,t,b} where x and y are plot fractions, @@ -874,17 +875,19 @@ plots.doAutoMargin = function(gd) { var fullLayout = gd._fullLayout; if(!fullLayout._size) fullLayout._size = {}; if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; + var gs = fullLayout._size, oldmargins = JSON.stringify(gs); // adjust margins for outside legends and colorbars // fullLayout.margin is the requested margin, // fullLayout._size has margins and plotsize after adjustment - var ml = Math.max(fullLayout.margin.l||0,0), - mr = Math.max(fullLayout.margin.r||0,0), - mt = Math.max(fullLayout.margin.t||0,0), - mb = Math.max(fullLayout.margin.b||0,0), + var ml = Math.max(fullLayout.margin.l || 0, 0), + mr = Math.max(fullLayout.margin.r || 0, 0), + mt = Math.max(fullLayout.margin.t || 0, 0), + mb = Math.max(fullLayout.margin.b || 0, 0), pm = fullLayout._pushmargin; + if(fullLayout.margin.autoexpand!==false) { // fill in the requested margins pm.base = { diff --git a/test/jasmine/tests/legend_test.js b/test/jasmine/tests/legend_test.js index 58f10776d7e..34ef1e618a2 100644 --- a/test/jasmine/tests/legend_test.js +++ b/test/jasmine/tests/legend_test.js @@ -1,9 +1,11 @@ -var Legend = require('@src/components/legend'); var Plots = require('@src/plots/plots'); +var Legend = require('@src/components/legend'); var getLegendData = require('@src/components/legend/get_legend_data'); var helpers = require('@src/components/legend/helpers'); var anchorUtils = require('@src/components/legend/anchor_utils'); + + describe('Test legend:', function() { 'use strict'; From dcea0dd9a8ed4c1b616ecec0a5921db723a21d69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 12:41:26 -0400 Subject: [PATCH 08/22] fix typo (function name) --- src/components/legend/anchor_utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/legend/anchor_utils.js b/src/components/legend/anchor_utils.js index a4b19262239..7b5bde268f7 100644 --- a/src/components/legend/anchor_utils.js +++ b/src/components/legend/anchor_utils.js @@ -32,7 +32,7 @@ exports.isCenterAnchor = function isCenterAnchor(opts) { ); }; -exports.isBottomAnchor = function isTopAnchor(opts) { +exports.isBottomAnchor = function isBottomAnchor(opts) { return ( opts.yanchor === 'bottom' || (opts.yanchor === 'auto' && opts.y <= 1 / 3) From a11e69890ed6e4688ba1cc70bd7ee401b4b7023a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 12:42:07 -0400 Subject: [PATCH 09/22] lint (mostly infix spacing) --- src/components/legend/style.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/legend/style.js b/src/components/legend/style.js index a7e9f6a8fc9..8ff88fe3e2a 100644 --- a/src/components/legend/style.js +++ b/src/components/legend/style.js @@ -28,25 +28,25 @@ module.exports = function style(s) { .selectAll('g.legendfill') .data([d]); fill.enter().append('g') - .classed('legendfill',true); + .classed('legendfill', true); var line = traceGroup .selectAll('g.legendlines') .data([d]); line.enter().append('g') - .classed('legendlines',true); + .classed('legendlines', true); var symbol = traceGroup .selectAll('g.legendsymbols') .data([d]); symbol.enter().append('g') - .classed('legendsymbols',true); + .classed('legendsymbols', true); symbol.style('opacity', d[0].trace.opacity); symbol.selectAll('g.legendpoints') .data([d]) .enter().append('g') - .classed('legendpoints',true); + .classed('legendpoints', true); }) .each(styleBars) .each(styleBoxes) @@ -62,14 +62,14 @@ function styleLines(d) { var fill = d3.select(this).select('.legendfill').selectAll('path') .data(showFill ? [d] : []); - fill.enter().append('path').classed('js-fill',true); + fill.enter().append('path').classed('js-fill', true); fill.exit().remove(); fill.attr('d', 'M5,0h30v6h-30z') .call(Drawing.fillGroupStyle); var line = d3.select(this).select('.legendlines').selectAll('path') .data(showLine ? [d] : []); - line.enter().append('path').classed('js-line',true) + line.enter().append('path').classed('js-line', true) .attr('d', 'M5,0h30'); line.exit().remove(); line.call(Drawing.lineGroupStyle); @@ -161,20 +161,22 @@ function stylePoints(d) { function styleBars(d) { var trace = d[0].trace, - marker = trace.marker||{}, - markerLine = marker.line||{}, + marker = trace.marker || {}, + markerLine = marker.line || {}, barpath = d3.select(this).select('g.legendpoints') .selectAll('path.legendbar') .data(Plots.traceIs(trace, 'bar') ? [d] : []); - barpath.enter().append('path').classed('legendbar',true) + barpath.enter().append('path').classed('legendbar', true) .attr('d','M6,6H-6V-6H6Z') - .attr('transform','translate(20,0)'); + .attr('transform', 'translate(20,0)'); barpath.exit().remove(); barpath.each(function(d) { - var w = (d.mlw+1 || markerLine.width+1) - 1, + var w = (d.mlw + 1 || markerLine.width + 1) - 1, p = d3.select(this); + p.style('stroke-width',w+'px') .call(Color.fill, d.mc || marker.color); + if(w) { p.call(Color.stroke, d.mlc || markerLine.color); } @@ -192,10 +194,12 @@ function styleBoxes(d) { .attr('transform', 'translate(20,0)'); pts.exit().remove(); pts.each(function(d) { - var w = (d.lw+1 || trace.line.width+1) - 1, + var w = (d.lw + 1 || trace.line.width + 1) - 1, p = d3.select(this); + p.style('stroke-width', w+'px') .call(Color.fill, d.fc || trace.fillcolor); + if(w) { p.call(Color.stroke, d.lc || trace.line.color); } From cdd7240ba26c372edc4f09349101786a39808d00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 12:43:38 -0400 Subject: [PATCH 10/22] make legend element a instead of nestead --- src/components/legend/draw.js | 36 +++++++++++++++++++---------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index b6db5c8188f..4ddd714a216 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -43,16 +43,18 @@ module.exports = function draw(gd) { if(typeof gd.firstRender === 'undefined') gd.firstRender = true; else if(gd.firstRender) gd.firstRender = false; - var legendsvg = fullLayout._infolayer.selectAll('svg.legend') + var legend = fullLayout._infolayer.selectAll('g.legend') .data([0]); - legendsvg.enter().append('svg') + + legend.enter().append('g') .attr({ 'class': 'legend', 'pointer-events': 'all' }); - var bg = legendsvg.selectAll('rect.bg') + var bg = legend.selectAll('rect.bg') .data([0]); + bg.enter().append('rect') .attr({ 'class': 'bg', @@ -62,14 +64,16 @@ module.exports = function draw(gd) { .call(Color.fill, opts.bgcolor) .style('stroke-width', opts.borderwidth + 'px'); - var scrollBox = legendsvg.selectAll('g.scrollbox') + var scrollBox = legend.selectAll('g.scrollbox') .data([0]); + scrollBox.enter().append('g') .attr('class', 'scrollbox'); scrollBox.exit().remove(); - var scrollBar = legendsvg.selectAll('rect.scrollbar') + var scrollBar = legend.selectAll('rect.scrollbar') .data([0]); + scrollBar.enter().append('rect') .attr({ 'class': 'scrollbar', @@ -191,14 +195,14 @@ module.exports = function draw(gd) { y: opts.borderwidth }); - legendsvg.call(Drawing.setRect, lx, ly, opts.width, scrollheight); + legend.attr('transform', 'translate(' + lx + ',' + ly + ')'); // If scrollbar should be shown. if(gd.firstRender && opts.height - scrollheight > 0 && !gd._context.staticPlot) { bg.attr({ width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth }); - legendsvg.node().addEventListener('wheel', function(e) { + legend.node().addEventListener('wheel', function(e) { e.preventDefault(); scrollHandler(e.deltaY / 20); }); @@ -258,18 +262,18 @@ module.exports = function draw(gd) { lh; Fx.dragElement({ - element: legendsvg.node(), + element: legend.node(), prepFn: function() { - x0 = Number(legendsvg.attr('x')); - y0 = Number(legendsvg.attr('y')); - lw = Number(legendsvg.attr('width')); - lh = Number(legendsvg.attr('height')); - Fx.setCursor(legendsvg); + x0 = Number(legend.attr('x')); + y0 = Number(legend.attr('y')); + lw = Number(legend.attr('width')); + lh = Number(legend.attr('height')); + Fx.setCursor(legend); }, moveFn: function(dx, dy) { var gs = gd._fullLayout._size; - legendsvg.call(Drawing.setPosition, x0+dx, y0+dy); + legend.call(Drawing.setPosition, x0+dx, y0+dy); xf = Fx.dragAlign(x0+dx, lw, gs.l, gs.l+gs.w, opts.xanchor); @@ -278,10 +282,10 @@ module.exports = function draw(gd) { var csr = Fx.dragCursors(xf, yf, opts.xanchor, opts.yanchor); - Fx.setCursor(legendsvg, csr); + Fx.setCursor(legend, csr); }, doneFn: function(dragged) { - Fx.setCursor(legendsvg); + Fx.setCursor(legend); if(dragged && xf !== undefined && yf !== undefined) { Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); } From 7b393d43987dab29556ac204ffd2a670bea02f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 12:46:05 -0400 Subject: [PATCH 11/22] cropped legend element using clip-path on legend --- src/components/legend/constants.js | 4 +++- src/components/legend/draw.js | 36 +++++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index 346c5f34433..7aeca1e5099 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -12,5 +12,7 @@ module.exports = { scrollBarWidth: 4, scrollBarHeight: 20, scrollBarColor: '#808BA4', - scrollBarMargin: 4 + scrollBarMargin: 4, + + clipId: 'legend' }; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 4ddd714a216..c021fcb21f5 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -197,10 +197,26 @@ module.exports = function draw(gd) { legend.attr('transform', 'translate(' + lx + ',' + ly + ')'); + var clipPath = selectClipPath(gd); + + clipPath.attr({ + width: opts.width, + height: scrollheight, + x: 0, + y: 0 + }); + + legend.call(Drawing.setClipUrl, constants.clipId); + // If scrollbar should be shown. if(gd.firstRender && opts.height - scrollheight > 0 && !gd._context.staticPlot) { + bg.attr({ + width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth + }); - bg.attr({ width: opts.width - 2 * opts.borderwidth + constants.scrollBarWidth }); + clipPath.attr({ + width: opts.width + constants.scrollBarWidth + }); legend.node().addEventListener('wheel', function(e) { e.preventDefault(); @@ -450,3 +466,21 @@ function repositionLegend(gd, traces) { t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) }); } + +function selectClipPath(gd) { + var container = gd._fullLayout._infolayer.node().parentNode; + + var defs = d3.select(container).selectAll('defs') + .data([0]); + + defs.enter().append('defs'); + + var clipPath = defs.selectAll('#' + constants.clipId) + .data([0]); + + var path = clipPath.enter().append('clipPath') + .attr('id', constants.clipId) + .append('rect'); + + return path; +} From a7829cf902fad66431f29cddba1a4103928338df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 12:46:59 -0400 Subject: [PATCH 12/22] rm useless .exit().remove() : - the exit selection will always be empty as the joined data is always [0]. --- src/components/legend/draw.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index c021fcb21f5..eff48cb9605 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -69,7 +69,6 @@ module.exports = function draw(gd) { scrollBox.enter().append('g') .attr('class', 'scrollbox'); - scrollBox.exit().remove(); var scrollBar = legend.selectAll('rect.scrollbar') .data([0]); @@ -86,8 +85,10 @@ module.exports = function draw(gd) { var groups = scrollBox.selectAll('g.groups') .data(legendData); + groups.enter().append('g') .attr('class', 'groups'); + groups.exit().remove(); if(helpers.isGrouped(opts)) { @@ -116,11 +117,13 @@ module.exports = function draw(gd) { var traceToggle = d3.select(this).selectAll('rect') .data([0]); + traceToggle.enter().append('rect') .classed('legendtoggle', true) .style('cursor', 'pointer') .attr('pointer-events', 'all') .call(Color.fill, 'rgba(0,0,0,0)'); + traceToggle.on('click', function() { if(gd._dragged) return; @@ -188,6 +191,7 @@ module.exports = function draw(gd) { scrollPosition = scrollBox.attr('data-scroll') ? scrollBox.attr('data-scroll') : 0; scrollBox.attr('transform', 'translate(0, ' + scrollPosition + ')'); + bg.attr({ width: opts.width - 2 * opts.borderwidth, height: scrollheight - 2 * opts.borderwidth, From 59b0c52972324fb76be44336f6d615138b18e1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 16:31:33 -0400 Subject: [PATCH 13/22] robustify top-paper : - append to toppaper in make-framework step - use fullLayout._uid in legend id to avoid conflict on DOM queries. --- src/components/legend/constants.js | 4 +--- src/components/legend/draw.js | 31 ++++++++++-------------------- src/plot_api/plot_api.js | 3 +++ 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/components/legend/constants.js b/src/components/legend/constants.js index 7aeca1e5099..346c5f34433 100644 --- a/src/components/legend/constants.js +++ b/src/components/legend/constants.js @@ -12,7 +12,5 @@ module.exports = { scrollBarWidth: 4, scrollBarHeight: 20, scrollBarColor: '#808BA4', - scrollBarMargin: 4, - - clipId: 'legend' + scrollBarMargin: 4 }; diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index eff48cb9605..8849829f9e9 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -27,6 +27,7 @@ var anchorUtils = require('./anchor_utils'); module.exports = function draw(gd) { var fullLayout = gd._fullLayout; + var clipId = 'legend' + fullLayout._uid; if(!fullLayout._infolayer || !gd.calcdata) return; @@ -36,6 +37,8 @@ module.exports = function draw(gd) { if(!fullLayout.showlegend || !legendData.length) { fullLayout._infolayer.selectAll('.legend').remove(); + fullLayout._topdefs.select('#' + clipId).remove(); + Plots.autoMargin(gd, 'legend'); return; } @@ -52,6 +55,12 @@ module.exports = function draw(gd) { 'pointer-events': 'all' }); + var clipPath = fullLayout._topdefs.selectAll('#' + clipId) + .data([0]) + .enter().append('clipPath') + .attr('id', clipId) + .append('rect'); + var bg = legend.selectAll('rect.bg') .data([0]); @@ -201,8 +210,6 @@ module.exports = function draw(gd) { legend.attr('transform', 'translate(' + lx + ',' + ly + ')'); - var clipPath = selectClipPath(gd); - clipPath.attr({ width: opts.width, height: scrollheight, @@ -210,7 +217,7 @@ module.exports = function draw(gd) { y: 0 }); - legend.call(Drawing.setClipUrl, constants.clipId); + legend.call(Drawing.setClipUrl, clipId); // If scrollbar should be shown. if(gd.firstRender && opts.height - scrollheight > 0 && !gd._context.staticPlot) { @@ -470,21 +477,3 @@ function repositionLegend(gd, traces) { t: opts.height * ({bottom: 1, middle: 0.5}[yanchor] || 0) }); } - -function selectClipPath(gd) { - var container = gd._fullLayout._infolayer.node().parentNode; - - var defs = d3.select(container).selectAll('defs') - .data([0]); - - defs.enter().append('defs'); - - var clipPath = defs.selectAll('#' + constants.clipId) - .data([0]); - - var path = clipPath.enter().append('clipPath') - .attr('id', constants.clipId) - .append('rect'); - - return path; -} diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 1753264abf2..38ee77db540 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2585,6 +2585,9 @@ function makePlotFramework(gd) { fullLayout._defs = fullLayout._paper.append('defs') .attr('id', 'defs-' + fullLayout._uid); + fullLayout._topdefs = fullLayout._toppaper.append('defs') + .attr('id', 'defs-' + fullLayout._uid); + fullLayout._draggers = fullLayout._paper.append('g') .classed('draglayer', true); From 00078079be6c7f6a8cdcdd96722c2c9d63e84ad9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 16:33:00 -0400 Subject: [PATCH 14/22] merge top-paper into main svg on to-svg step --- src/snapshot/tosvg.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index 8f312bdf870..cf6fe17b1c7 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -89,10 +89,21 @@ module.exports = function toSVG(gd, format) { // assumes everything in toppaper is a group, and if it's empty (like hoverlayer) // we can ignore it if(fullLayout._toppaper) { - var topGroups = fullLayout._toppaper.node().childNodes, - topGroup; + var topDefs = fullLayout._topdefs.node().childNodes; + + for(i = 0; i < topDefs.length; i++) { + var topDef = topDefs[i]; + + fullLayout._defs.node().appendChild(topDef); + } + + fullLayout._topdefs.remove(); + + var topGroups = fullLayout._toppaper.node().childNodes; + for(i = 0; i < topGroups.length; i++) { - topGroup = topGroups[i]; + var topGroup = topGroups[i]; + if(topGroup.childNodes.length) svg.node().appendChild(topGroup); } } From 335bb4021339a56f03c84fd50911765f03c3b162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 16:34:28 -0400 Subject: [PATCH 15/22] add in-house getBBox test asset: - needed to determine bounding box of clipped element, as the stock SVG getBBox method does not take clip path into consideration - use it legend scroll test to determine the height of the legend --- test/jasmine/assets/get_bbox.js | 35 ++++++++++++++++++++++++ test/jasmine/tests/legend_scroll_test.js | 11 +++++--- 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 test/jasmine/assets/get_bbox.js diff --git a/test/jasmine/assets/get_bbox.js b/test/jasmine/assets/get_bbox.js new file mode 100644 index 00000000000..f1df4dd660d --- /dev/null +++ b/test/jasmine/assets/get_bbox.js @@ -0,0 +1,35 @@ +'use strict'; + +var d3 = require('d3'); + + +// In-house implementation of SVG getBBox that takes clip paths into account +module.exports = function getBBox(element) { + var elementBBox = element.getBBox(); + + var s = d3.select(element); + var clipPathAttr = s.attr('clip-path'); + + if(!clipPathAttr) return elementBBox; + + // only supports 'url(#)' at the moment + var clipPathId = clipPathAttr.substring(5, clipPathAttr.length-1); + var clipPath = d3.select('#' + clipPathId).node(); + + return minBBox(elementBBox, clipPath.getBBox()); +}; + +function minBBox(bbox1, bbox2) { + var keys = ['x', 'y', 'width', 'height']; + var out = {}; + + function min(attr) { + return Math.min(bbox1[attr], bbox2[attr]); + } + + keys.forEach(function(key) { + out[key] = min(key); + }); + + return out; +} diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js index e8f02a1661e..ccc818bf0b7 100644 --- a/test/jasmine/tests/legend_scroll_test.js +++ b/test/jasmine/tests/legend_scroll_test.js @@ -1,11 +1,14 @@ var Plotly = require('@lib/index'); var createGraph = require('../assets/create_graph_div'); var destroyGraph = require('../assets/destroy_graph_div'); +var getBBox = require('../assets/get_bbox'); var mock = require('../../image/mocks/legend_scroll.json'); + describe('The legend', function() { - var gd, - legend; + 'use strict'; + + var gd, legend; describe('when plotted with many traces', function() { beforeEach(function() { @@ -17,7 +20,7 @@ describe('The legend', function() { afterEach(destroyGraph); it('should not exceed plot height', function() { - var legendHeight = legend.getAttribute('height'), + var legendHeight = getBBox(legend).height, plotHeight = gd._fullLayout.height - gd._fullLayout.margin.t - gd._fullLayout.margin.b; expect(+legendHeight).toBe(plotHeight); @@ -53,7 +56,7 @@ describe('The legend', function() { it('should scale the scrollbar movement from top to bottom', function() { var scrollBar = legend.getElementsByClassName('scrollbar')[0], - legendHeight = legend.getAttribute('height'); + legendHeight = getBBox(legend).height; // The scrollbar is 20px tall and has 4px margins From 857878b78794e6ef5015eaed2644769ace0eceb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 16:35:21 -0400 Subject: [PATCH 16/22] add legend and clip path count test, - to ensure that they are properly removed when 'showlegend' is relayout'ed to false. --- test/jasmine/tests/legend_scroll_test.js | 43 ++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/test/jasmine/tests/legend_scroll_test.js b/test/jasmine/tests/legend_scroll_test.js index ccc818bf0b7..5bdf05d8e62 100644 --- a/test/jasmine/tests/legend_scroll_test.js +++ b/test/jasmine/tests/legend_scroll_test.js @@ -10,6 +10,16 @@ describe('The legend', function() { var gd, legend; + function countLegendGroups(gd) { + return gd._fullLayout._toppaper.selectAll('g.legend').size(); + } + + function countLegendClipPaths(gd) { + var uid = gd._fullLayout._uid; + + return gd._fullLayout._topdefs.selectAll('#legend' + uid).size(); + } + describe('when plotted with many traces', function() { beforeEach(function() { gd = createGraph(); @@ -66,6 +76,18 @@ describe('The legend', function() { legend.dispatchEvent(scrollTo(10000)); expect(+scrollBar.getAttribute('y')).toBe(legendHeight - 4 - 20); }); + + it('should be removed from DOM when \'showlegend\' is relayout\'ed to false', function(done) { + expect(countLegendGroups(gd)).toBe(1); + expect(countLegendClipPaths(gd)).toBe(1); + + Plotly.relayout(gd, 'showlegend', false).then(function() { + expect(countLegendGroups(gd)).toBe(0); + expect(countLegendClipPaths(gd)).toBe(0); + + done(); + }); + }); }); describe('when plotted with few traces', function() { @@ -73,7 +95,11 @@ describe('The legend', function() { beforeEach(function() { gd = createGraph(); - Plotly.plot(gd, [{ x: [1,2,3], y: [2,3,4], name: 'Test' }], {}); + + var data = [{ x: [1,2,3], y: [2,3,4], name: 'Test' }]; + var layout = { showlegend: true }; + + Plotly.plot(gd, data, layout); }); afterEach(destroyGraph); @@ -81,7 +107,20 @@ describe('The legend', function() { it('should not display the scrollbar', function() { var scrollBar = document.getElementsByClassName('scrollbar')[0]; - expect(scrollBar).toBeUndefined(); + expect(+scrollBar.getAttribute('width')).toBe(0); + expect(+scrollBar.getAttribute('height')).toBe(0); + }); + + it('should be removed from DOM when \'showlegend\' is relayout\'ed to false', function(done) { + expect(countLegendGroups(gd)).toBe(1); + expect(countLegendClipPaths(gd)).toBe(1); + + Plotly.relayout(gd, 'showlegend', false).then(function() { + expect(countLegendGroups(gd)).toBe(0); + expect(countLegendClipPaths(gd)).toBe(0); + + done(); + }); }); }); }); From dcd0cb578a109d4975fdf21a2acba61abe4af028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 16:36:32 -0400 Subject: [PATCH 17/22] update baseline: - sub-pixel diff due to legend clipping --- .../baselines/gl3d_projection-traces.png | Bin 40573 -> 40578 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test/image/baselines/gl3d_projection-traces.png b/test/image/baselines/gl3d_projection-traces.png index 49470a45b2599b5eacd71705f4a54ee11c24d5bc..1de40e957a82af8abb9bc455c894763eda160937 100644 GIT binary patch literal 40578 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 PJm6=GIc8O9;T`)AC!FJ2 literal 40573 zcmeFZWl&sO*ELEAB#pZScXto&5-f(hyIVse!5tDv&_IF(r*UoEk^sRS8h2~l!`-~! z_nh-o-5>Y&t$M4vQmNGL-D|Hk=a^%TF?XV0Ybar%lc6IaAz`T~%fCfJLWUtBJvjqC z1O8=6@9z>45)G1yyv%#B@qQLsnpSW69|N=(=(CMnk2a~5p=%5CqZUNi;A(FkbfGj;ThJWV#p&+_&P;MIusc_mu7Kj%C&u~ zeB7($u*vIBvFO0!)wiwVH@6eTetSBFD?v-QTvo;J*%TV3kx>5qe3wW2EQ3hd{$*}J zgM|FgCj<$F+ZY8G>B)aS!AVR=od#q5u|vT~pb!7~NF!D$|Gnl3E{h#9E{rh%Hbe36 zH6MUm!T-4r@R$F+)&K0q|L)aeD#VyQuZ_A*|~n;LXE6f)5Z`^tQUL?ZZ))E0;M?Ae_2OIwsaK;esklOA?!lc zdNJqlUPni%pVEiIDCqwDtbjYhO#U5LK&ft)vQJ?l(qnja@Ksio>`Z!b;6c-LsUbO$ZvFhWE(~POd zRk>T``ECA8IRCxBTq=yD4os_eS<=|p*boT31Ct3krjhQEjJZx~>5S4Rip%y>K^!?= z(}FEJqg4KVdKzQoUK;T8fHOWUGJ&Xy#?_dzA?lazVa{#W*0k=CIMf={{)k}2g5=qV z+(fy7aQU)?=k`aClWvtc>SEBtebtlj<#;ScCggvPI`|4i7`iM`wb|ggQ;<4F(uN2x zi}$eYihIGoP11g)a04&Xt0}S>&i35+qGOE)MSHcK1XfC#^X*T&RIX*Wf9Qkf#k&wRz!Dn8Z)BRU^BtFLamG{>L zHC))Qs@}o>`pt|FgfKHEmLvXD{m(7}ex65jJBn)yp}F9`LUjLEd zF$1?{5qb33W)lNSsmk<02yT$|7S4G#+Z56hF?5z9j>s!;|1 z6^JSTUB`u~w3(!l|9kK!8b~1|A;L%8w3z>*PA~(SG(r}^bRB5s{O_~<6ng|2IIKO4 z=f9eYB!<^&Av)grpr8x%wO;MVc$ny;@h1n92A0(!o1JfIe+=by{#AAB>-@g2{(M>e_^&%u) z1_|Xvb9r1p*C$A6qS3@*z(vPjn%ciP@om;puAGx(fzEtgfqJ=qLBBSoxID@Jypq#f z*5ewfdhSB4hpXr-mr?L)o=^1DUrET}yLL6tu932DQV(~hXJe||ql4-Ebr-OQzqe4n)nE>h8S$1#_rkc?ZZG=Mr|vW-vTZ~cvab9rFRRZt4-VJatP^8hxD2fGZ2 z-qw_2@;eXSzaDOnwaw?(Xbn8eqaOV(6S^J4Uh%WpXZTaVgqd#+-+nty`7$vmY6hk{ll9}#pt{X_Y()=>DR@Ni`0h764} za!QDg##(`A&{~}xJ5!y1$`!TmeY>3SXh=6bMdvM)1o!^DP&>ruWc}B_XqMG>35N(+ z^kLrgatD5D6Ly&5IF6tEqGMc41QYW)WWcrouSPQ$Sq-FAjfVb%j=`27<<5a$G?2@~ z0`B|7AiAhjA`0ZuQXOtLa`=+vAM~0K=~a}l<$L4gpsAobdQVJ==@ro%^+?{G;D=ft z$$)?(WVSrQ<9j8sB@OOW6S_mGfYWp;DmY*6DYKmf?BRs_K^BO$f&s1;j)AlS)zmTS zzli5I$B98-+b++>HOGNC3Y*p;8AA-1{7ssxwUe{x6PTxX{tg5eVKK*k%p~O~q}Tg zem(835D)*w#Tr>|5Qlg6mdNruMSQPv`|Q2@0(pan?3eA%hT{b7*F!Gb|8K}$Y!8x( zrxeqUle(C(2)(r7MGl!pCXRTl_tIP-Wlb(b0O?JOeD@IKM1cK)s*7ZaHIgv!SO#DM;?<(5xT#t$w6>*Z7GTuni zkE)*4M$LE@Y!Ub^upwd2(|}|5Z`Z9u9OYBsJLqvpC@#U2pus1A#$k-i6f*sO{S|u2 z%*MANy_b>J?GgqH_3=ws1UIgxTgtTHN!7l;bjRi3B`sZSWF>L{wDS&ns!V`xw+949 zGdB2CO17Q-jtDjeaj2$U)J-0Fp0(@+eSQS}g=UHgY`-LcwVTP3Qviaqup*a;#M8N} zqeAy;5wr`O-}JVTMD(LtdBdnl0Y0a^E1{0TAa zB#E&n*LiNZo1DRXvd*cmdV3=VXK#0Uv}B0cI$QvJpwjRR*8XM*NgM~62KPCz&i@mX z!Feekc7JW0m}zOfk{BQ@T9-pq&%T9KM>aGM-|l2xX$5?wn{}AC^`+>>2xbN9i7eiS zWvp;48~}eQhEQPQO-w2d^$V-`Fd6d+r&KoZWWL~w6H@}IvU&atbF{0})Vy*bPZBk!fDznVx&<;h zsyHlolRTv+M{Q{v#s*&W3c|aEyuXhzuc7$}0`^P3()=xOtg?*X31DyJomtW0Jm#q{ z!CD~*jDXh`;n8n{>By_v!*4QCTcp2N0>?^xaor&bd?O7_xsyEgCns^ILx$y)?o#U8 zVm`8o)-PVVAv0gB%X0TRV^0@w7@KtXq-VujAC9wDjB z3clIqMd_F+|2I2IaE1Xy(TcK=y}%$}oS)gV&_oEcX9+EqPJ#IN*zyOgSr(w}tb7p+3I+C^?fv(i z@EulD3eaas;JL@7EcS6x5=4;$SI||W#@^z>_&&>MTk+t1-i@^)<68-Q0UUW-p&!^Q z2gl6B8XVM})a^m@nhe-y)isJL3|x^Cw4%j7q+uqFvQ-#xa+BsyDFKafj;PijKVW4R zF`0-PBv$%slwU#+YoD@dvG|VT`#p*jbQ7x$N#IL)H;I^ze_0riIw6EqqV<@_;fgeB z{2=mArH2hOs7K5F&tnh5h|-#Cd>}sEFlw2QITwxgBunGrs$~I^?5D=u!!W+1S|1E}g3r&hyoA78{8XX1Wi8Ztr){~9J{0h!O6Hg?>F=(O2Yfi&!3R7Z2prujB42^d7biS>Mw5gK zJFI&yA(Vz4C?3*gtfxLaRgp`)L0)WitNw$@I(|HQkw%8pk9RthNuAko+C5ToJG!e(P=%z*FEIG2q4Jf9a$ihe>w(yy{ z%cs`%AE`Pla%ry7Jan`5J7Ei{>UR|;;~4KW+$n(c!|w*;vQPlbFWiH;5(Oo}9$8RO zFjtbx#H^c04T*{N)kd?*;6e7gKEx|-##obc^CKuM|8iP2PH5_qfNHh6dhlfz2+lM7 zQ_Cq+BMfgEMjUJMi_>udx=X1zj*+}1689vyiw01ONw<*LrI(8saD|RJ(90O)?Icm4 zBu}-FRF9y9cJ^1HDhr3{w-@6v-cIqZ^I>R>%(X-?#b>^lN!}6XcYe`M;Gn#(=-H^j z>%g`$4U@71`N@I;>mo`b%-wy$(4>Ly0r3T^8?&(ndadB}*UrmVZ&o;0xCAywG9AL@ z2IlH>GW{-i^#1t8nP3_3{i2h^wV8Y_gbH7ZdLGg*Lt_nW0q@gZPaywiQPj-clW}94 zYn0uy7&a5=z#D%Hmc1gS8s%QRHzvqHIoj8twEN?hMc`XLTT!^Qs9N5_McYmWJM8U_ z5I~4C4gV%JThz_GGPCpXiY@m1yDZUTylf^B!PteL3{=#s$K)kPMt%ehf={JfU&D06 zmc2k>=)%ecQ3~7;aB6D9X@F8!l=Mrpv?R+ntKm+1jdIj#C^h6M3wd|f-CGhI_8?7C zKH5)95^CtcebfPt?t%)`if2!VOZ>yj-PoCH)Wt)WC)=ZvVjWZc`%8VIg3XdELX3S9 z=CdwWu$e!vTpj*#mC~uqnVxW1S#9w`ujbpANYTE4*a!`C(%`hDeasG>_a+(+F)T&oa(IU&=;&omT zD^o$th4F})^$%S6d zjv8_+UREk{p(WXk04mxBp0f-wHRa&1?7wToBTYoY$Zk3;HlE{Mwt0FC-u@N`gY#f& zK%Kz^L>4mXE?iuZInZ{P0X`iG72F!xez4|-wu{|PeTx;-z+Uw-5IR@OIEs$-_Ud4X zH|h89w{&hp@+R|r%bR;X(~Am!h7%^2bhEhmtGGZEf&iW+X5{rL%5;4FAm;q}Tiif$ zY0YY9@6l*xruV3wk-<1l1*yHWZPX_>Uo$V&`9DrcGh2+{P}rBZxS0`>MS?y}46{xx zmmk%2<)ZR%6W6Psxh&9PF+n!(wJv>4!2Z{RSMHEiTClpZIVMP+9;uT&o;v$ka1wn7 za%{Vz&YEKK-1=Djw~;!JrSDTlB=2HR)IGJ9?03iPG1bX@?cb^$LD35TcZkLWJq0#j%llUv+RZ38#jnqJAZW4W9fU$W}E(Fx5%5&umo^ajX)< zc5m5wT_O{ge)v)!)s6l4o(Mwv-T)hCbcQ8HBsIgX>zhYEytfwa9cDrF7S z^~c?0MdW4(9^+RDc=!Yo#Zc)t1U%~Ev2SCipW#Bls&5bx=bO7TzG%kepI!)fOZz^Y zjXh<1HrC#HU3nFR2miSIWq=H3hUzeJAcs(7jM|c+p#o6o*tLg)=5-~CnnDHSsfw@6 z;@*1=-)vFLuNi-NF~Tx_;V}H>FK(Tjn1}+_4>u(5B!5n$@jkN=g0nbLqvR)g0sC8| zg23kD>CEIF{q5T27p2v?SN|B!c5XF?dFqhm*FXKg>GPdcSHoc~r}Wn4la(!$G1@L3`{ zmhr|=7uaY~&>m;R>jX{F*5);w>zl${pqm>CO!Qi%>&R9Q$^Az z$S)a9*)r7Z8x?sMCK6a25H%&sUij&|=-P;9y_*H@6Tci8txC0t9&WiC~aT;7TAjHz}6hBD+ zK?9=|<||Y*k2dyZlE`Zzs#)~(=3Bm_K(S66)0?(1=)q2M*p7XtGu736uL|B~p9kCr z`c7PFCr@>~3AdRsS!Xd%pAK8bQl%OWmi7mLWX&QvU7AJ+xn!gY&#>x+^k>`9XOkLQ zh?C*#y@P(mp}dv*gjf%;7ar-LcbS{G?(WIHPC`VH%ahUa?e(2FRQs&rpMEg@S`VD~ z<7|A~whJfUyB%AJNFHb0QA2UjT}kK8;kX>2+J85oI4ZTBXomu_sk~P2(8Y}c1Rr5h zxj`i4DuUPS>^6t3y5A0~Wdz3kzbi`Y8D;;e=bW?LP@lVgxu!wNh+6(bFeT*YL*hce=z) zmED|RJqk~!`}iIpNdH`iqMDe&nyztSHA~rY9DeDe9+^4?22(d#E-60adFY{O-ACE| zbV4c)C0CQt22!2m3}yGE^1eb`IR|iZ{Hdqjw5w+x}P35X$&n*Xs&CcE>oWlcRrCI&f0reHTx$0$~~0`!JTpP3^6rFRME2 z{!cU1W`HKr0GQ@gnq8WOJ47>eTM+c03ek`UgPHSLH$wNA!y zbA{QO(Xum#Iq;0+P{Ya%w$}7j?$GI=XKBk_uM8ELMp_<-Evy7TWHb4g-CyzS-Ae(u zX8SR>JRWAD28iGI8qcMuSnKKkXGI1qO#{YNGXbl=sc(cTlE(!JLU`zM`qfO*T2zyY%?aNGVR2&hkrB=YwIt4 zoHd_rWrm6v`%b|sGWItw;Yw(BQM;7ZEfX)VU>8nJ;X-!^1Gk3~b4j2JM|w;H1z@Q^ zR+(8fE0&(5&84iW-@Hl=rJycVDEOlA64 zKwoFp+fHcR@5TMEr87T^e1^DT7;}Hh*@O2|$r0mr-+I;AD}0^E2E#q5Zp5@O(~^$A zNMMXLdyN-+-!N(`*#go9)!XZbQ%O%Y{`Ym%N}O}T%vC#G&&n`sC(Lm$IpEY-wORi z5x~|%#O}oH4ow~i7E7pf&YLZNF0;BkB2{S!aL)abI(=^pKi;L(o5b)R=KF$==IpRH zoxFR!G3I30rxw41^)6{p!0>}qT9H#&C|v6A2iz%)a;BE_xh*SA!XuvHVuD>|T@2BH zh39~x3p9h>L^Z@Of-^G6R#xK0jyc#O^25^OjojvzE7I8sMm*h`zWS%=jj?_eZg`Et z4dI+6m#|we?z>u~ZUbBc-JY%cTk{X;hB@qOsE<-`52byeEac_sOv}x zLwYn#Em6yuX*(0)52T}Fm=eQ^0xP?9bIOhg$oSn`(&xy>fTt3l%52R}hg_0oT9f=r zGB$)kqw2*}f`~bI(%syCXyH4GK*ErfW|g`Aa-sV}gg4)@hp_ z;`ab|gqEK}Kjo6qyk2>e=d3cGHBE+gzVZa8agnDa#_Egr1)ZGU>L}G3h&l1XnwQhR zoX4IuEJiJ1`Q-uXMH0{wWKtLm8k(Z$ivLV$b=Isfp>UCGcmv{$=#G)&cHugmVEleO zJeg5CTvm)xR*!q(*mcNi+gcQ&gKp%LhuKuY2G%lIFr)sOB?OSKT)t47d+yQYZdN=d z=!1bbbtf)7hL4jkCq>r2lfr2X7RnZusJ62c*{Z!fWD#>;3Hw5% zVUI*+BYGA+4GF{8$UH;fB`Y2wkxJV^wUJbru+BzLSCMLImA1*lyleep^3H-8D)g?t zQ+2x;6`D#qpbTH_;enbrs1v+;bA4r_ssQS_XATO^7B^%+{EcsI)&tGRCpHaXo?~%? zxsTC|W>{8dIy;5&r~PFDw7?!sfX4*>NUKstV_*!)UH#c$#I!f>=TX3{6T#7_pfwhf zIvq>@zB{14{_)|Vu4GU)EaU7kKrvvZY{=X=pM*udeP031WX{kl;tcmXh*9r#nQ1g? zh~5vbI6Mv9CN(45%@WJo%Tr&SRC;Ze(?i?X2;QW~bmeM7J2eTsDdhK_}!Ck=?T-gdidm zcBThgB}ZGS3;Os5vaVjWx8gVGzMqD;l&Xg#FqWom^7n zB4K7z1~tX%!py|)V~iU52ca*+-o^T8D=x649{U{~I?50|f0s>o}TP&s@dRbA^v3`NF2W#W7c3JfMdR0mH?bL#!mwWhrkW8uZw3oNN zxzWh0;ydos%jSgVewH_j)h{9Qb)FuPD&EPbji}tKK291q7BVz}kDU;IZyYg-c7>rz zsrE}1zRAf+0n4Abv4v~Y8DXFpdn?)*LI$u+`vC}E;;r?*2HMyBHLc|ug`Xh1|5pJc zxcf8XyVedqJ9UyVo-#A)p<7#KtO{}7`{Hi)v(9_7^^OEQf&7!F`qP#0D=(t+fV~WI zWf7}CLTD@dL=6rE<}_FlwztVc+vu{h zR&8$P&?bkVM{T-_ve2)%peV&??Jmsa^T%hIx)#YIc5r?+fthUazR-UX9TQFY=n$AQ z2up3i?RqhD=8rKTO9FelUY)h8bFiO72~=@qX$*+g|7mF1sBAu+p0#)>03m+AAS|?> z`%Cq&+5!8PwOOT|I4!mIk`j04z`1w9vJt$ZTrhApKR2j1o{pmzniDSC6wmUR+MHNV zpbjw~WpH((>$A8_b5y@{XVzPtx<#^rIR#~;LJFP%`is5;S zE#*$eldeMgKmpOzdi?Drl_mZsXo&oE5e7%bL- z^>nj>_G@7cBq_4f3}5MWLUe@EO`RtLG*85LX>eGFyE(&~^c0SgEAS?9VKCUCf@8SL z`{ClD+d&Hq)`cf%4)ukR$27SAGkKJ5t|*6W7<4JNbe+JWdeK9mIieYOLhzaS)QqHI zS>RZ=WV5HW4N~e{_0zoEVb5h<9We)5G=zYg___g!4d_EgT192tYW5keJ0WYwi^V?Q z9CmhmE@KjWg>pJ%7@PBrWL6Dqf^FlS80p8Wt0DYm@ltSxQ!#L$xE|+wwli(_3?+P5 zMdLJ`14LxM8w*@FqCI^R+2yU`zJGiMFH8lWpWZ~EX@cmJ?)S%i1J=q8ze5UX!0HcS zog%o(DF6+u_UDCs!dKQ`2=B!2Wy01mO=L7NOCHWGbE!7*;WQ51e3ZA;5Y3lsU!ttd zTPTt-I&bG}`p0}JrM%i|a9n$z0RunZ_KANCq;xUy!o($QjmO8Vg&fzY*n3^R4sm!= z*@f}<1h}W+pmaU@c>P$KUqt@r8U5>pR6b?CDi~wz?J8scR%IkLsBg~+Hv}0c3|MKr zkpvaps_7%Bu1^Ktc%)i5yX?-=W3CR`7#@tOp+bkcIFPM;xiYu966&x*Uw2b#u#z*v zwwhlNFwsyQ#0>Bkaf`Co`6%wR2*=U1ne|DEE^p3DaB^K<#R`{t%I8R( zDdG#&F^rm2Knd7yupZsEV2gHuadEql^vN&T0M-hO1~A#D%84AHF1jw34dt$1k|VG} zaj`u#yx-}QMiRA+rPL%y#Zuj$45-k`rX9Xw1g>^t#B75-3C2m~3bwLt ztSt3#h;%iOk;xmev$1K?>grfpT3SRC*sxU%b~ZNo8QC$Nv0R<0)r|ndseAWk|Lu*c ziIvITEvBl}9u${@Qi0opSpl12dpxQRDtJ+!lz+IS4`%neA7ihq#se?Fva0Vi)fO3o8&oB`;RO2bUg&T5QT? zejJ9|DDIPsgP)&bd#l zkH?kGEs4hGN_1g_oBi7EA>os!?X+@oG2gz2x$4X2i3j^PZJ(~F6C|(*1B(gOuRIfr zMrd05euPX<4LGjhrF03WMmwubPV`wrp&osMEr6@0KP-kn6#~}7!1 zrWyV-fvL+pxfaE!d!tF8D1yB`pQcxt>FCA=^G|yltYvq+gotwMg_`>3W3IltKA%{M z_oD+-p{xQ56FIl$ptd=Rqk@j%{Fc}3yO}DB{xa?M>+0yslz}?+>9O9e=HXxq2b)$w zvBmeDKa$gS(uEw`6hKB>Yy_*#uwldTev23Vba4s}dU`PxFIz(km$eMgq05^@>N;;j zgm*X9x+%LrdqwpsD+3|ElbZxRp8gk0qArB$fL*kE1}sCGh6$inyum#J1u7lyJ$tJl z57wZB@Sh806_r-nl9Ig5GXS?+X>-C4?+m$G25f{`z$b-UGRbKaVL$@@0^vD$6G zFL%8|xI36vY>Bma6vm~Q5_D!_B$iH-$vDM1wbruri}Y0Qo@YIrQv+h@E;mW`^I+1) zN(>@+_k}7rA5ewe%|%@*5^T8?Y$Q>+p$G6%9e?MM%+#>ubNpt&zGIO^*RNk9od9wE z+<$#puzc=-H%LVG2OjjH?|eJu&G585Vh+8V2YJ>K>m3xz+9T4-hq2`2tn1V)@Hl3; z{sF*BY#NK`Ri=+&9cQt^Sh-)qBxD?Z-!>T=RFH&|%u^LEJC(U4jC<)5iKMc5UOyy* zGI?Gy6Lb!6hBF^t$34@@X2GmuRVAL#o^i^H275_a&`WBqkXv&S{HV7WaqUVW1&Ghd zV~X3v5ygC!Hl!QAZ~${Vy3G0%a|9Ibi)1KAQAZ2h+-{x~XxGr5sIAwZu#%J8w z**&31cJ`oGu}gm1*L(Dfm z+<1D*!fx#6JO079s%ghq6<7=Yf)Fp}cu%tAqRS?M^yx3@GgIRB;C3znn2C;l^RdON z$-iGBsMiEiuJrc!w-Y>+q8~Q<2=*1)N?g25&rv5^98V^tFnWEwDZaliQ8SPe*&bYN zqh`XCtqVrw5C&|OHnYo7!zm{eK=hRD@u2zmsK$rmKm zZlBd~7uR`FAsPXrTc~pRV2sTGQG%TAaM8?7-0b{m(BkWfjb>5{#fv42#KVs%OL>KT zI3%>hhrX1f^Wl;Y?H0CIybg8-le*2W2C6`x^1SCHl!~0=Z-7GLz*AJ{;ax}=cRxeJ zVKakE4ykizGcXNNYC@2ujQz?z8AG0EMm%x7-}ttU;J~qAj^$Y>5-5xgNK|xkD$zMm z8aj8oySW=5e;xB5!41Q}NrJAjt<)&uOG#h^N8{rOT=!$O>3NHYr|i~-m5jh~-29kq zdhapdzx~W8@k&j#dplrb2YGNA8F$z%wyvfJKbJZL8IP0dbL060=uz(?ez5^sHC8yQ ziuLVz`@t2@5v~*gq38;F1BdTS!-aTa2W*=Ej4 z9AX_WaSG(WJg>;R1=x2E&}@&aqhr||NJ(E_wtPrwaLD{Sx0Ej%8o!)^q=xE8Gn4l0 zCl0b3*lU(yvu}&BTN$^Vd|bwi3^ytr$g)9cUL!kv1Y2Tn%f{M$SvAy&#{R(l? zJu54wQ0U{px%yfd(B{ko0S_Q`2AUJrY@(S{TuZol%m?=ZaBV#PQmE=4s%qXbQGK`gwMs{0p{6%d`Lj;NSk=AIEmr28fs;9yv8B=& z(Rf*e4&Us<8`2_43)a~Fon{P77mql-X_HxAg;qfiuNrY-`&EIq$B?s*RWb)0sO9jU z_}yHrr{y=tH~R}_nx`#J&QU1{yMXpI(XYs4mRI@iauzt>#{Qc&ZDOjTK3^(83?+|&)t z={Glr39x#;R7`&OM1o;NtQe1VZ~Gfn1IESt{g|}kivvNoY8_N`TsvEHe}WyE&z6h_SA^wcq*JhTXK=&q)1}zJGSE;Lnj&UMfPXRBszLiVOQ6 zw*QeFs8&rYaau89A51)=8hdr#GhEm9J2+)t87ANNH}^xtsx(vBE~QximK$rvOEJ^a zsxy09y-i{Z>-#1edMI)^&=YM>^ps-7puxF0YdK&aA3yi9tN8u(>hhVmxK8)xaa9b! zh!1n3TP{cT|76+zsT$0?KEX)+2&s?v!6L}URb+_t?OVTJp_lzDlZ=KdKKr^i&}Ubrk`$hq_5h^9wvgf*urc<^>_3yCB5C>>7eCTMXJ#&2O6t~ z&Y5NKd^4Mu6FJGK;eY(`XZu<8hLba|KP9S)t=P?C{ z;Zgna?bFQ(1FgTaZ06k@CpOL`sI|=1PI4PEyJQV__SisGVyX6N&`RyyS?9*4G|lPK zBAFOL%~G1?^xR07$A^idLBQ<8!}zd$uMJU{hxVVG2g8}PjRPwP;JBOrC1)}~$G}WC zcFqhFNfy!Z$U6}po8aGf{!J^#vam|Af_W{bT84=%17zU;46U&Md<)7z6S0Ft5=rUS zyOlAAAJJs87DWWfCXS~J4E(yLC-Ht-6QuGYuauzKaLCNIztpaCd#}zq5+4Rt^Vw{! zY_f*7L_pcV06j8bD5sngd|TBSy=OJA#Q zwa$-sF%+8IK4iB6=V1NDZo8*@%$`RgKA&C2qhE*U^c}(xA&A414PqEprVE}IxL@+} z=xhpJ82#{&=A9`&X5=B8SwlTJ??8{VF#`ZcK!caD$@~ujA@BZU2+>(*2XwJ|zD4Y2 zebjUK`m)E2c=K`k_mQ!k?2?D`9-xqyjBHT(soO!?vRk>Y%IHF;8zS z@)N%|J&IjrE-_7}TBONg-$G{uU$6Xl{cKVEu4WuZoG!!66?V81(f0Y(mjXH7nZEm@ zQo*pyQjzVj9~BHQpgHWSo9Bn=pfPiFA5aWQ{Wb1~E2CtoDxe8F0md1GgZ8VcQG>Ng zwMPjw#bwxaDhM@!@!*ucxEB_@a{hAaD7yZo*TaN@IRg$C-{or!Bi>yd{{hI9_xj<- zzU-w);^~ne^%^?0vst2JdbgM3N0AG!95OSWxsE?E$QkfBUZ8Lusc(5#KPYOa{t~4j zvZ3nly64am;87HrcTChWXLP&G!Us>R5*jYQMIL|mU5}xc{b=qsJd5;kiX{rNPMVE`fB?XWN~pG8TIISaugf) z*O9!;pwFOtJ++3E!I^Am5=2@XJ9^K8bhmQKTBl1EB$U-hknAr=#+QUOaXThbK+ z6QS)m&&$P-G5M=!(IY(GU$Y5wrXb#sH}#vTIz82j(~+dh?ch|0 zYN}R`o?_4GUJZ$b?|*E?Bf=|Sfpz!vqL7CtxftzAo@R|<^bVc#CwilnvP>n#PO(jf z`g(e=-W$4N$V+t&+7 zCUE*B89l4g9d)#kJxm1g+Zm~Q8R#*&}4g!D& zMC(m(L|@;<&muWJAv*7+zU7s8d%1L<6?naQQPw{xV$>dir*%+LR92=2GD`4unr*vw zfH?Pb7>}Y#UHR&shW*UGzu5QW(=%0;8yBM|IGEudkky&PcZ0|p_ z00E(!ppywd32KJVp`ydZ)L#K8)j+$Hp?Lrz=``qBB@IfjVWdb=xy439XSJd|=+q*h z=RNpUjsyNT2lMYc%}gFUeLqEvE{XIa(jm6YlzhQr|KPqBefzM-K;Ox(!2Mw0 z2hrO{1t*?Zu>!40XZ2Qq6&2PVFUQn5 z?+ZYt{EsK(E>ANZlDfvOBLr6h<_My7U;iC=NEbZM#n5hp(yY8-lAN~Gl-bJ|Tw(Ui z6w4P(^s~QvNjEiBi(2{4Yvmizl(c6gjH=(OGCS8R0vaMdfzti5`PvHQjS4rwsmpN} zfV)PWUk4dT&URr~8+#Xr4sag^wc~O_BiUlN-9-kcX+uO{j!uXbVT890o0dps-k$Vt35YowoY39 z>bD`~%T~}z!avogsS8UhH-WWB_XZjDbk&zVK@DO%czCbO=)j>OjYspJc|4>X$aa?* zyC|y+n8GAvQN3;q*-pUPx2or*j{>R@!o3LDM|$mbIPmTUI_+;OnBVy>5qAB`yWQr9L@+kS zD`h}SH}I3vJk@hI z6dv~kd(P$UrI)eJyOq+p{1~G^B}+4Os_x^_5(E}KcySnj++tzm(|;YvS8384pSSqY zA5OLN0x79TDge8f7Ob~@((d7+WnZduzSRBo-W;hhvFF0) z-0qIz7a^%~M10}r$}2>d_S&D`v<#9GpMtXk|ii`Rejy6FGSO(P8Q*=1(Pk(qQxHVRbESz`yVKcO|M~*8A;{CEf@k z*bmCF^|@V9AF03SPHYc)`U1dMLLF2|SoQ4AS9J72gW#7zwR|crjF#Ow4P<<=>MjUk z_V%IQal9aag;G=R&vwRNZdG9B1Wpc9PCctV(mW-1*g3yZ4a0gnu>)%@Db%$hVNjl| zlHsj>^VjxuIMn{#$C;s8mk&`yo378XkCuZ}hy7l6qxq1cuKMJCWdcUD|B=|%QNH%n z*}bqTKOyShOBlvt7tf=LK4=c49J7?o_`{?%vveLgbapwMO_z{$hGB%5n;(!A8-H`t%kldi=npp^ecO^27;ZZLVxR!Y1p`BX69wQ zbsL7;Km;rKMIgB-N8o*ghy0M!2TfLUUh}ymykTo?J;eC!svcSYqeY0fNoZ9h3OXG? z+*a*Lp14A4o1o%-K7q(GCOPa_opzwg`WU&41$lg{$z2sFDGjhHeLRAXCgG}@xGv!j z$d1=LmH-+dbiMmRJ|@?D!WG+Znt(b&Y+;o1OvWRFA>TLJC_%lV#63e5(fif1QEsI9 z@YWFDajeuPni}U2@>Wq0ie;j@@hGjzp6Wme~P9Gs{$_QSg-uBRfEG<7w#`*Y)UB-cXc!JLovMNDJ$EkjQFNF7p*oY`h&8cP}Ib?rvDvL^w!y)tWSct*8F1mM^u;B zvKb^Ibr=+}0STH3epQ?@P=d|IoUKf0Wu`D z-VY^~4*SV+zDxzmh;C4I)Tq^)>+O0RRxRxm=d+HG-Q|9gLW06G$QyR-(J#YUvmTE#vw6R6#tUB zOWoQ{#2&NAqtN_1#Ms)j@3!7LcN=xn^xx>;F`m*#Y8tMWJoU36dCQem+ z!5Q6a_+clTk(pir=pL=VDy%hFYxqPLUZ+>scZ6I{4>V)ArMBtPur0-V5IxlsPDRD* zZ*%t%V0FHJnJ~W5WW|WY72xffN}rwI@xEPFI6&vh=;KjX`?8m4`SFcCGFd<3gmLYQ z7h}Vf4aII{MfO0RoAR^%`jamZ7bq*qudhKa6Fd<-UnnO88L5pLyC(~^Msl-a$GS&d zyEbcih*3@+qqzO9%I_22MSq?B@kkpj)v$sFn(lFQ`)vD>VB0$#o>T5HgGI5dL zWX^g`(wzKB3XuMV12!rUv25tw6yeV5^I-x_6()Urb9+XRbem4Cw&ICasSDjE5ZccKhg}_5?!-he`(wlC@53OGo$zyLzqS{rQKKiHwoO3>;8$)l& zD94@OY0G9{LR`Nk6ZQ^CC2n$h&F=>?=aX)>B6=ZKw=VOoktua{>cSVIlJ`-N_QQuQ zpNwvPZKuPmjRQee6JkOkTrHZK883*zec-iPYg-`Q+zxVsF$aCP%g{Q4vp5!!D3ykZ ztN>^)4MmAKeOnnHxZL6&+)ec64Ggk5#2j<w=^>ZSS~ON)K8dremgsZim)z zpR)>VEbeDC`1j+r)gM=xp&V641b9yUeQN$~U|_x=e$$%hYg#Y$!kq#fsnhyA1^zoC zYsUDU3-s4fQM|O!@xEXAFSt?rRBs^V)O+&THOTxH(^wcSfm(eeFW^1_|E)tv0}}QV zCPJJiMyq6yK=`a%6czcot>|%dFLmPm{Dl=AntI<>Nma!D=tilv3075yyM0ZdR?pV% z)Thtrzq$uhOmW%_#~D^KhL8W?6^|oRb|v| ztwk8v*G)bf<9WI&^n;$M^8Q_uetS|BQ!ycCEGMnse^4 z(=T?~Yahl3E~m$qL6huUBzXC#)l6mETW8=DA|%TKx?p>9MSLK&M%f_#A%Tf$M^`>j z!G}QNC5{M^7d1SaDu!SIYC|y()6YUlHUEqW3f}Rv=7yOr0@5t!;cwyq%X|7XAkY4u zCeJ;=tmbUD=_rQ5Eh7&MO)!TP0&uXzcg<@WL%Xev0>vN}na@G3?_)j{Jy+<|3YqmjDRC;bK)qhg`vGu<8nqa_C zf5o-_^a{J>*qcJ1naLSIi0>$XQ$cpD0S-lKz=pap%0t2N6EI(F6v8Pug_2Irpw)*I z>%Wwn7$aA=+;fq;vkl{>(H1u}c1z0qrXQ4o$~`kHmo}5>^d9x|?tV!b&oyt~YCn&4 z=^55kZdG(PCLWb7G>-|BJ+{#mWLMlb%2&j1ZsSfzyZufo#qW&byj?%VEuUFUs-3y+ zX(W(S3JAI_tWLS?pCXYAQ1|&_I1X91Xg@dRLS=BXF=lQ6@bJRA|kseS)?)Pc@fn%=WUD6lGBsTUf6yIGC&^F_DzuqbQEXrUMNen#fwjsFU{- zpM{m(dU4iczj%%ms^vzm%Y<9F^6<6NY&1XUdH@bV?^_!xz9purM?0)!& zePSPgaXrvr5ybGJQ8X}8*@Z0j(j?U@sv`XRURiXBR|<;{RH{YBIBfajS@XGUYf$y( zkMDDDQR%`@(QT5wrU1B5a9{83n@XB&OSZ#j8SN*TNh$FMy13Bot#RiC#g75digTEg zpZr~oKg||JfgV>XqD}&vXWog`9TVz`K+vg<$47bQ>kTk3q4mkW`sE%K43?aUpq^sx`U)sL=A4gp|jL70<`6fijeqR5Ns z8x#PFl@-^X`zbrR7M z3~%pl@p+-y+T4pf$xf;Uj|~sxo?Upy{Be|5GsrhaSsO&z^&Lr;s%Q1l;LuyGDY0Se z-Cvw)u&LPmjB6>64=KuA*tR2hO2DESpI7H|PFYe1@Rwi+>Lq z0SK`5ePV7>H}m2!gY00^SxeKWK=MvYfbn3XPR;3aV*qp1v3 z)!eDQZWnPboQ}MJTwwF|pFZU+VTJ z6!%HITD=>I7f%>+^yh9JT|9IJ>=yUw11xe7kiGd6!w!8!gW+y-qOuR+Xj?pMWb)#7 zIsCSvkfDDx?tCIt3LN(at#{l3m919T69CMcb~{hroY!G|QdWeZ9vpt_uCR2#cg;^( zU^)uQwGWP?6FMRg%GwRa3l-B*J9Ov!LzKy{9|>0yiHbJg4t>g|=5SqDpH8d$AILnq zu)ftu#_);E2df+ck?rdN7>g`!#8Ucm40}v&+5n`*v z_b`%c9MG7HG%2}yGD&ZGeeL_kz3IbO!8loUWq!_VO?8&Viw-n)Td6h=Vsp!z1SXx@ zcNc=x3-|Y}dm;gx$17MsPPbz$qrY&s1e2O|6Rwcx`1R`g9trfkv|brqi9`4(N@z70 zN0HunE!PD|UEc%s`cHeb%G_^=OfP1ZiJ_;*x_SZsi~HJ#iG(5)Yokc?YJhfS zr^k|nH@XFV6xHp_^CF13VZbi907N@HLzH3YXk0m10IRV@CK(Ra;kDTU?WqEdV>dhx z!1Wl1H56Ee_$$|3J5D0~WOZv@cQW3BQ_f-Rzx$~)tERZh>r8>XP-puWBjX4c!@a=yZ9(ZQaR zp?)}G-X#$3Jn+FqrOR!T0QdmsV!%Lz7pp9gFvg}gT5AvTFVn!ND(rXu-}q$*yDj5m zO_V{hSHI?wbtZw?P_T*}!$F{4w{MqJanB6x2e*MzV*!bVXmXUq)iAU6Z9(?0kMqAKI$&v><}+jB zJ|?T;@h%4@eTBA8Er+mVqfP7KT)CB1^t0sT6A2d}5Vp(PlJX8;*t<@{w>!@}J;Z|v ze9-OO1^$W#&26dt=F#MDC_SPr-YsE`+U9PaTG0_=2J3-oBuw8wqJYin{VlFJT{|zN zwb&Q@mp-Q1!DkWxYCl#$?Uw@>axwpo_al*z`cl?DNc02!qy~I4X8312#rV919}=0g}_tDQRkDOS^Cson!9KDQQHMOf*TK65L?O7mLB05iv0AHp8qo&2*x{jPcx^)W&tCSGI%x5bA7 z6`1$)?F~(gkFTo3UPrac+BZnMU4*x+SA}7gN^mER6>VYwO?)!(>J@q;Fj!Qe4lc^Z zNa~m-saG$pR2NMhU^Jx63*3%7c^fn7EFLd>T$J0 z^l(TXp!cL0b8}}fdqU;^%q-<#vL*QO;3zO|0u78wMC5wF#aF`&q&bt#;9NkWIE1mZzs_{r*cm*$r5*_F z>jrWLqJ4Z6n4c0rB3e1}Zl*{4$8@pkWyXnDNMAIGjA377nEA}1xsyIBG3zY&ikni^ zB9zkYZZ0HoDv#ers$>btoG*t+auqoG*VI1d-?Dl%E_!V2Ek67u`k}O+u%~i8$dQZk zH@p}{pT*?)u01km4Nq6zTaUQZ`t9Lyt;0V`Xwr!H6G2Fb& ztDl}X0kSqZF^2l@49?VlGT9>hLvlK5oZ)4noW2?;&TppwVgIBuG4ceW$Wc`E`hRb`VJi_4MKkae8y@^&0`lt?g_ zWNK5jb-UxneAth!I`}k%eB9-}wmtL|h7>8!}ts`M~=}fENL28QOluJHD=k7kE@o z#@w1(ptn=AYy98@!@=~4Ui=Abo@Z_}qsO(doQ!1NIS#ScY_5&V?yhp}JpaqC7T>^L-IKSS6e6Acb2KXm;7mk!}&ibhG#3wa-w=xSyx9)DQ zVi;nS!GIY?ile^Kn>)7?}}^`1GG*XRay0#M*R2hqj#OVkesYTy6x1n)>tgkzqy(vZFD-9|GrI` zAJOQ0LpFzZXpdGsQ1HyM?iy&eE`<1j>-Dz@k!hGp`L#B)n+#znY)PN)cM+!ZELVjg zf}UmOW$gY1RfXX4ldin6W)x?C=}zJo@5J&p2vA4u{3fjuQZ|(>lh|ys?>m9#F_f1@ zwm|5<#s!zI-TP5%f2`_H-CSRc-aWJ6)mpnr&*|VTNcFa<`>h)~ z@%-3v-d8I<^pZ2wwEoFBZ~41wHC-Au$YQSq8KhiMUYi0X zx;%L)i9+3GBkpDtY;m%UA8T_(Z#+_g<69Wa`BZ|Y01>4sVQwIjIwhU$-p9>Wnmx!= zW#s*RlX9>iuR%8@cesX5tP@bC+ADcvSFb4fSdOInYM=oWK-vwa?;uP_-(t$Wn34k2 zgE{|2%PDQR&878&b(~3=qV; zf1W5I&)UJTT$U03AkvRT0rH2z4}!GK{I+AHT5~)ZgM*iEk(gl9qgA~#vKL`leyh`d@Y#lnV8kb+GHZI}XdO zoTwvhNq4@K{_m`rU7dPpe~H0NY%q@4!GE1)`sP~ni`H>Tv0Rm~zc-+#v4zSm5NkAdzLTx% zQQ_Xwp&O|i`Fq@@suD#{dBvxtwxxRP^qdsU?z);)?>&>Rr^~0JecpTa*VQpAVO66~ zp~Vkg0`-0GtM?1fD5}~{*qacRArH&CI)V5- z^$PdTOBR~AQ;U?`=AD)c$UFcEX}%@-rXx7=(;J^Y1^jdGIf3|CWfW*JY zz;)dQ81z(42u6#1LhxMA*`Io~;eXN}F11HxP8|eJd)RFpx7QVe=hcgBW{R%kpNqNec!rCN|CkN;BS=_-CLtwhX~*u9!B|6M)`1V2Xw1I?pt?W16i%B2 zy8)GepgF*n`M+I^9P6j|S@Ufw(Li3L_{Z)PS-7IIu>?rkh^?88N3!QK`9tbTAOPLA zFo=DS?u5+gmRPRf`FP0xh+li718=4KX^AV%y38>z2`#!YvJJ zW}nt}TFrFK{>%b0w1&=vwPTx{Wuwzc7Iga%B6bx$3v_XL*C4E39R-mDA>Iu#aYZhW zecVU(xj4CHS16L2^>;(Bl;+5KD4YEW+*xy7yV9%W4#0`1KOKR^#Q}(npqFkWtUS>A z;N?H;vI)R~P3J5dg28a$>96+5;kFBqr{@J)j{t zY&v)G3|ZW!X0pXB)Ef)fPa^5g!m87oPa8`G&n7yic|p1s@47>pNvo5*&3N=39bWSv z0P?N38--8AAqO_Q-D^=ZeAy5v4&D>k=fON^dNj?|7uyGTPz9Kgi{r;T2XvErOInse z<&*SBr5mHTlrD22w&8o|-01+5(q)b?SB0SLqix1GMWkR@v|Tx-KHAEmaG;SX|BcWi zV1^psABtuTTqku*$?p~nf6W#I!xn}a)LHKLIXdFV&rkj2ZV={iE5lRqO%go%+$fjP z{X~p4v(BRgxKZ4_vi@J#{oo|T{oqalBcMc@LvR;z0`dYW=8L(|t|qrWa;l7S&1F?m z+NeX@rN}O3$v~{O0IR8`812&n{swE$=-mW!O_D*Q?}S-#{9gV?hA~v0Z&RSBKK)4X zcLZ&>FUrK~{G)pT*W27&VK;xDPo;~QZI2uJ0+fPzPrY>7HAiR&VBOtL1h%LcHo&58 zuFjb^**#p*#yhT|jo;#S={`S+=Rc%B6^rDFNp`ge$c;Ai0>%U-lfX#&WbKd|K=pTb4IGgdE+Wcpt&Kt;JyN>xEqLo`CdZVIQf z=xrS!Gwy?4e;!&}%0MtT>$fxmulOJ{kLyct zW_;i*PHA*AAjGb%wof&EzY#G0C(5Gnyo;8rC#=4pK=L=9eXN5U<C1Yp3zbvET=_(Z7Q z&`b3qe6fLU@kkNCx|0?xyG9hUdUei~MqSFQPkSKoU4M8J;ZIBXe{ABuyhinZA{~(v z)E%P!th!eDSqLgFHW%Ugr5VqhhCl2P#p8SPKCV|uh3RJU5Bjtum%z9IOpx)#n`2y> z9+isn%TmYNF6E6;HWA7`BX`BQ_|hN}yW4l3o-faHlLX81dq>7=&iDR}J*Wx|izyEV z_8-NoE>D-6>Bj0RmtB_}DW4m~;DFw(0+2Unl<#H}QnZ#0Rr=n&xkq?nBRQtBlrHJw zM(aF)>#QJ7@d`em$@bxTR zCXu@H)S+C!KEr+2Fo_53z?zL4>7QF!yxo>{eqPJx*vyLNK1YLM4ZGqJ-`vplWC}U!?l;%yHBoq>(C${ajs?E zY8j9Js<+IYOg%owZaKv9sdJ&hBoSkVa8LA%%H<)6(Iy)JYNa>+Ft3k9bcDQ{ln(U! z;Kv05^T64DhnZreV2&gQ^O`T*{Y03kj38PV_qiM0P2}bqKd-ZvD0~~jir>4(kansQ z-^&76O$rE9eID`;xwIAbvw8Mn9nUyQ4|yhiHJADXn-Sub9!SY6@3MLc*Jis}|$-NqO+doPCdNOqBb^-RK9POEs zmQA9BP8FOXyjDw132l#!ZRUOcY%M~h%RDVjR)6I=AX$P><=AcANrVeST{mk`&Wn!HZtu0#UZYRR0+?+%~y`!kbos zs0jz8r0J z6^8mo9pKdJ6Zqw=pz{6gosXPdRdjJDj$Y9@c2$+D1JK-`$BKOas!nw;ZzO8% z`nw_%7AX)#0-#}z%bRx$?qJ^Q@RhABM=E46F6;h@#7|DKXoCOyUt828gfOq z*Qq>_{h@?}Rf*$g@98E7tw%yR<)@vz>LLXbl97BMvc_&BKRiR^Ue*4Ejv`Z$CpamX zZy42>&*tfA>$ZG39%;YWl#G3J?ZO+lAG}0+JQpg({AAO40DU9oP=^~VgkV&ozTn6OX0NrILWyQrzzdK{g#_OeCmGrgU0BC zEAwdcTd>jkseYv_yI;Ls^3{cp`F|3Kmky&18ytwtL62lUUySv!|j>iC{S;vyRHl0 z#u$Nai+EYhOdw0sS8`H(@EMwPEb~o&O*)2|ta?j^g#wo0c%gu;4!ld8;phRj`ZGDn zcE?#OJ1!@ebEbAW__rPj2)8ZCwLLOZbL00nWx=*@fQ1Dophuf{y(Q;&B9?G4{K?}A zQd3bnA=j|ZUMLZxCKV!<4g8I8VK)ueM)AK*C|Tk$ep99g_TCk=V_mh9Gj1nq+EW^9 zYNnVb2bvpbjqwgVG}R>OZfd!DrvON&vIhg5R1}b+hnNBB0l=8!ZL3GI56GOf&}4NR zCB82-rQ^&LnA}LivfPKQc<+d>!S)7dgCax%ZP2{e*|}W(;llCCrTj}F7OV*Pa@U42 zW!7Q(OCc{etUEo`+0;O@-JNtNc*bR{&53R@Ww8eq>kh=r!tik~zA03EMc^Ve5J-Z2 zVil?jGxsqyHXv$;vg^J4+5Pc15l4B$lb1B9Fw(fh_FG_ia7A-V%7?axx?15w39vcJ8?`T>#7AveUzG2KRoe2iuNszbcs^kDr<4QoZ$hHm}QBt+idy9s0^eyE^V008mceel8=31E~Wcb=a0u`a3^EG5mpwpNdh zMs+)#Mi$;vSmT)~k#*vk{%vFe6yqILcwJF@Gqg~27@<7Cts+KSnvjrRBHgg#Wj5e> zS$_oCEpD53>U9sSPJBm8k2mb~L^Ho~&ueg@8b?U&X8Bi-vcSdFO4;(+ z;oM-jjgU<~tCH(`4;F;5t=e+ofAq2`#Jqj4h#e-7@>23KB@?2*rFV0$(*ejfskEY> zN>}B$*$W-!hniU9!5cZmEeb?A#(Xm>J+yJ9TX-e>9-Y49#eL>e?N4v~Z!$;_s#96N z9eb+3a67wQXJz!1dAf!Op`j7rKP<+WecMu_s6~g~{5ey7Nf|#C$yikB={rNKvIa7( zuYpbVIV$_1GgOc|cUALMuEBW8%v7$G5<{X8UJkCyKhDkvMXEoQQ8Yl`VK0&azpP2! z1q)hX{8V02Sillm=e42b$t9sr(BDyQrowuiyv-m^&On{g!v*ZdLS+JBTS|N@xQadX z9a>8wTk0R#dc+k)4WE~ivsT=-Yx_!_Dcr4Bobj4msgaEz^eLqy4I|p5_3Nr#AH?S(8B7oc}-c9oe0t= zSa}#hEeWjidjLoz5`cAMJtBHptlW0825w6u(+$qguT_8zv`Df{XNfjMoP6+t1E<%= zKn16D=3)TNEcLT~-C{+?gQT+V9F4hS#y*VkC16vK3xCv}F37}R6f5DnbvJ+CWI&k| z#YAh`_Dl(wf9a~QYOGYUw1T>~wL|$s1v#J7JM;+Z8^9(zRb5vK&sui39$P=OY8{nk zKGvN98c3v*GAkt}1p6J%wrKPEICy7Un%OG9r=1PokJrv;u|n%YA-siWtrk3ivQ`_{ zeMKsoiBP<=X8#I!c)u^>fb4{M{tu%-H^7@tkg+XdhSN+C5mFzbKqao32R!Is@D=a? zUmdSrr7p(MRt7igmOw7UK&x0%UEH>r1XuyAuC8!lT~w`W7KiGYC-4hJOKObX*^^U$ zm6Pr=%HRFg*2ATQ{GsC|D%f>a3)+ZJ{?1pH>|>R&I-fk8E*3>rQ;!$Cz96Ok>OJnP zLot7))Iv3dx?!AF63;HDZXLl~(q{|TJzvGs4EDe#E{iG~b8;nnjwBTY{3B1&lKot` zZ58FmzcC3rb6uymmvf!jT#c1~a>Lq=(Z6EDtgnTmU~@oq`i6n#Y1`vD1kN~V1Yh-X zoAHwJ-z4FOiPQUfA-W!$WJa^2PmcSUaR?iO6&xE$Vq>{iCbFa#k z$7=Ya6=BbC_^eA2zM{dBjpW_jK2p%qG__k&rW4ArmEdo*+Pe(1nQR~H6%9QpAZqN> zQ$Ja$-ktd#5c(xoZf@2B5L|PiH-ENZKZMIOrRgQ^>TK&D$b91Tr8*GkHWGKJaqPQa@23z@ab+$8=nDeVihWiJt5&GpYHUyNa*y> zPnF(>({)z@7wMzV&Z^k$G+i4Ib;ozebX=*eU{IK~MOW$~zpoLn_gt76hr-7g+n2L!M8DeQL!X6##+a6xlo7mSN|0Ex zN4h(2{N4K@X>_F>%j;{QjN?Zdgh93DY3714lGgdjQ)b672G(yOT>mt5(J;qUc`L)9 z(_y;pr|6T4gaigTGV<{T0z=JAEA*^B)%!3Zd=2q1?iD)YYk&qsJUMVa#rksK&tB%m zNfobrBph&QQ398igp-CCw;bVvz6v@ zGnj5t7*O^NFGN<6E53SgMw|>G4;svMbXu#l85p7enoys^4T2XyN9P5c)5 z@bRs1VXPIrEt!zpJH{LA>F3^h4$aso+G}_4+*TvP@0-7@KBom5+rvWj6Dv1DV!&$- zzgcuSlK?na=G9)d0q^(imZ_LjwiG7L{sPl)@N;H)oZI73WA(M>7Xutbaq?7_`8~WmY#|1tFfOrLLgkhmB>h-gqq z$P`WCQyEh(ZCzCgF`d9eHKee~lPIpC(9?8oBjVUX>4l>MsOe^b(V{$f{m@0iR)n)n z4$A&$mh0i3-#^UZ=-V5PBl_Vhj+(=${l)s-?1t-him8dgRmm?5yEpo+1)yb6x%lay z_TdjDYU-KEpjLf|+s1uU@c=DIIn8xwSTU+9!>!NSAIYr=Y_%W(;oY3yLA{r)Bv?M- zv4_ycO_-bjz$m%m?duR;B7f+IVa4V~0Zy7291=JZfISSaYVy3m4mhSkBK3nvb*4SV z+`r7hgs92gOPjLE{61Q*>a@C>7kFkXi}?d}X6}2lKS!a7fD98=OD~ zHG)+8iIDlWAJ$Y_E<#^bz~0J?IP>0ehXv=zRpDShoa^_KQgeoCT3u+?%jzNj1S(#@ z{`GbQRY-X9l)?5BwXq@bj=?!d|Mh)}wb#hDSmSR@Bl(36);h>EokzA+TV!1oJisL) zBjf5Wh;HX$4#3|I>vzWXmb52rm))fvZ|{}2W3}d`qrv){rmRq%Hk;=P z^KV-SQ@LJLt<4wYBsRNgEa2WS^K`%7n`4W!(qxNk`~L8AW8meAh@SHMdF@@OVL4Sf zv=n4N_i?9I?u*elGo?~IMbiMja;Bxt-#uD)JsEzw*84F?P< zMSGts7XAa(n>)W5Zk{q%UXPrrHZqbqXcFMJZzn9iPWM2_ec$!cl#$a;I)fNz!u~69f&3@x(;8qi-Z^r(F}n9RAKLi zrfP4{+%|B4DVc5G1sKGV(_`^TH-r0h&uz07)$*O!anRNG*`2a0STqOT>Svb*% zTm=#aSts~h$x^IBtU_@aJFPFIw6t8{#PycX1S=RyU@R&=M~<8&MIXv*_g(;d&N zwnq|nKFHfz|2C_20@Xkf@Vv&n4hLdVo|OM4bCX;kG#25vafSHEb4mw+pWaSzE$56! zpG@|+A=WCq>0A(UR14~0g30+A#0Y9GSGf06gPkF@LZMK?6`@8=3_;`AoRmqny|qK(Y>>eXRu<$CSBg${Y5F5fW9 z8lx`2=I_-L=S)%-^MuVV)tbl8wRtzEq^Fk>jvQe4ed&kAXd&VE+oBlDU&XmEr<>)3 zl~_)v49??E5jXL!P+Wpe9JHOTM{I9j`I~XcJDTZgnv_A$B2wP<|ZnxBv zIA}0gwiC-X>mMWyWm26aGA$9P zb@0?}ym#SsOsW63BUh{wbf3Lx&`v^S;0j#KCVLb4h#)e%$e&-nzK+n}R1-0e%1rK$ zb{mJa3Uuq>S#f-#HcHho{1%f)7os_^C=_R*4!X`>XbICx;s9NwRos626%gA7zlRu; zCW0Lr))j_*F!W37K+j3R4aCdm!=UUEdd1XYN>|@(MbkaIA&k9nBRV0y1YU{%c;)RPZJ~I zUDeLu2|Ta1E*H!%WwZ3m!LC9^ZSGytHC`KP>^nhGH4Y||0S*{Pt!CS?DS5<;l~>P; zf#&CaQpka|?X8Ymjmx9j;|L+S*FEp}5fDz)GO8HDq)jV|(W#atJ!YWJSAT;uoUemd z%fzBQ$;X7=!rDFzX|fr>aIxB+t@ldVWa?mJWAEcyqAXL|E7q1rj9vcx&JCk&BD2I> zk{f&Op;_Et%78W*pTP%p5hs$#Eu{Ro^-D*0U_dU3IKI7JX=z#p*S#J-@j(|hYc?h7 zIy|HO@lI+x!IM>p+PTYEUauWG{FbFA|2Vy^t~v7SX_KPCd)OvAzty8<1Pu&Ke=8%` zqj|7YW~NK6Isg+&K&t=Fa|VecVkTrz^`yJU=oXD7;dY|{O$oNXO;!8Yc43VHeMLj7 zaV3=aJ`B&S=1=B?6w@EP&G}YlO;r1zPHGx6$q;wEj{XJq$u{-CH z(|(7`Q3*V4f!2Gn5Aob7-8;wWa{+m1?;Si{hlpk40}ZkE5{}JHNoy8MLDhC@)=R}t zkb^dokqJlXT6LDNk1`wY0SBAw-W!%hNFO=S0N(ULtQ7+`1DdJY9>v?R>vGMS0xHUEz4BEb{q z5tNjQh0U`3fhq8PNXZjrc3F(*=#hokG7qdRw#Ooh|1~1H+k)Y+v>?@(niJH(oWZSR z0&a6h;<5;K*zmTT?X5>OjW&w<^TVot9f`^4G0pZF z*mtMtD8-)QPrl3dC=N{9f@tdM=b6H=wp&Z3fqLy>NP1?Y}M*? zL9(sQ>NTQMz3NnBe3Mwb#*Q$r!&htWP$rFZs;j+0o>GCDa7yszEn+0cTfh;eIcwMy zDReeLX7JaX3Pq|FrSg+Vc-q~P&b}sUi&x`$QyO)K zsfVgf1+9;l5AoKcylZ3ctW``GJGtfX%blstp~`oW!&}qZZ2PR8R$G<<&iqZw-q*7Bu<~ut<0#qV5W}%L ziu&AQZBT9x&AML4N#;AR2xBq*1(!LLX;g99UlNLLCiJq)Q8jH!@}K`G0sKd;XOsFk z?2p(OZ3h21y>pPIt`97t3T-~V&B&l@D4RrpE_=^UXd+Ao>1%ra@GJ!S`|ZCLa*l*u zUOpEsf3jaY`OVc*c&@s9kdxcH=Pr83Xjvg?Ax>hl`0g#@dop0sPVat`TqS?WTrJ}> zN~9wUkl^@BR}b@bP*iox_kJCn=HJ9YFxG>MJZ355@qMVK_iQ`0?^Tid%YLbKQ!PRw z@9RA&HF6Ty@9n(p!#hShzRcWw!s+R%XJ*rS+r50{ev$m(^TpvzdnNN-k{}yA30ncRUYfG&lQfyWGN{oDM=K4P(`uo`EK! zg*#Z!*A7_CR*JNyN`$_Wo3f%Ti#i-v`v0!mdLF=PE{REhWLm@ikt>#|0902?*^QR z$@JGkF)AtlalX`*@0ND$@l{leossAnoAQb7{%TrndPdc5H)^}?f&P-jnqzps@p7f+ z_|WFdHSxvZup-Nq6QhNX?adp~4^rYHD)uHinYXz1t^X6mej>~RfoV|3ad`n5T0*t2 z8|jLZ-*)g2?OFvsPA=cKfr66&T+QDDnE0y(dpy_x%zu(}CGy1~Lb1l&-@gW_%rQ7Z zQ=o7*8Sc8`C-l&z*jhyS2;G6{1T=GK`i)D?eGWJJQ1gt!soEUl7UUeQ)_UcR&vBa# zG7Kbo1HIolD^MUUY%FcV%R60wA))h)qH|VRvmhIr66bFy1u*T{z_c&=C$HGToI`|9 zRuw`mP|J-{3nYFcvEaSkv@rG1iFb$OCVoEoGkVPFTWt)HP3_EF!lR|>R%6wi;`y1g zAFF=!+H>N{q)CjfvBM6_W^w?E31OYj6|M_4g4M(5{t!m{ywii zZNXPVxCgdW;}>(VP;dDaXMG+8eW3%bh8=;DcjTv?3|`&w%mUh*&~!lXxRK%o8~f#Y3_x9eNP+eu|o%72=rJ z7;x7%*bDM(J%r{d^Xi}@(^?*SnVp(;-USNtCgB>Uy$}}n*=Ze0i*wb6CfEi2@=q(b~qg5)CWkEx4ds)S0qyd77=WKTmaJGJg%$@VxwySpk= z733E_^sw;ZaOXJVm^LoxT9+UO;Lx)V+#xeLeS%bS9f=%RoeS3@c$gU{tuL+HzvAV& zZv>iqzljBDvoske?oNlsN_)@x`fhLMVb8j{wv8-$oQ1R&veB_uTrh&~hzxM!{SQT@b(wAYYY!(|I5rZjbQ|Gxj%pca+Osl1yj!;?=XBO1nuEJAF_Sw^izPYc7?2yL;5R-SVshOXrT^)p&(Zc@d8I;!p+ z&bUa-$A>v0RW$Vabvq$}(x`9YY~usN+#@Ff^ce36_r)Yg$Q;pO$=|#sg8>Q=t;nAp z9R4h@6->Y_$USuJ?Q4ma)y!UHG_DjeROG0@sqJFR_Mpet`#33)C3WtRrsMlEO*cf> z_B24Pev@f>lEOJXrL4}mFJMCVYFjbE%v?L`ESRho5)*0`mYwB1qv>qJ+WJ){b}+X~ zYyY5WTc_DZJ|0|x8X5TiPr$D9X*d$f%u|~Cg=Usq=^PuF>du_1m8&&J-u07|UW5L@ zlgSBVJ+(PDe*tOQT-7FW`{Sjw!yan}6r9U5jFYC=WYjoP3LCt@#s(5Pt3Q}g2(L6^@qu<_?<{8p`#xJyux z-rgBAB0x7|LYL}bD-fCQJhXDFl5Cc(qWR7yYeMhCm*q0JZpl11Z5<|dULS6Z1MRtS`phB>A0_GXT{LmTXxKx)4)4)D&PGO& zB;!mch$t&O#Wmh9x+bGD>1q(kFTImvxhr+vahZ!$FFzOwiF7{~F_gczzKEw%D6+Cz zbw7U$L61qa-!+2H^n5{^+JmL2dVDc~l1 zcUZwT)=Nj~lH|X|eZ(&@^Y8PAzu^ATH8$-rPIYpqW2y#$nSK?ze=6G(DJ>@ynhKjK zm`g%B;J-zLq7GkTA)qtrxVeMBCIXZZ_Rfw z-LJI-v5yfR30A1cX<`^?@1M(w;|*;-1o$QeT7h|JlUS-?%Qd*Obx|Wv<^oB|x?#Po za}6B1JH2W^VYc~6+w+LzTina^ zFH8aTvX|0E(3wMU!_DEd?rRPr7awRI1!ws4L}E&+J7^4*q4ZNiR+M;#xEoo+b8NI_ zmodo)_I&`iv3# zFJ7nW@jM~5DeG~BZs;~lSpIMjSy$#7FtDMCqTUCk<}yAxH}$C>bXwtcHeYo5ugXcd zmBpeZIQ;OQ>O*+H=SN2d!$JAaIg9i7^}-nY4J<0X$Z<#q$J+&@;zNw6P2vJp`jQXl zjB_II!i2g1lH4$wSPcPS2;VF-^FiUHUe(B={axoiYI*b_Wfs@(F(5i)mI%b*OIL=Z z{{45I+eaQnxmu2nvjNt+-YvlYX(==19Q*8xp2bEj9`h39$UbOu} z{v8O@-$(*ApD4*2e_|Dt@o|E#JVJFNc3aP%8BGi3ZzfSB4Th3C3u`(B9QK8EaVPD+| z!rQ}*Q7#~-UK5+gAPe#`a}2M2=hb6I(m##qRDNTI(v-(?>bSyoQMY8)_uaikbzCdV zs9Agjz`tf6F=q39p!zloW9l#n~Wf3 z$vb$Wx1LkAR*6eqFqPk@>P@;-^^XYO#1X~gb`Nxd_dHru(LJU^LW`j#+Z7(I;kwgL zEdZMTCEFIrp}jBguO;QOrB6+wZb(*)E-3wyY!@xI(<{e`g>DAEj{^!1(Xeq!ez9Ra z>RM9?vwXA@r=Re8FCR!gI7YpBC{M!OCzdP`i+L?b+Fq;sX~$U-nzwpy5BP<83X*Ie zgXh{#JRE106h_27@kTVw`uh5YqKo>6CAP_i^mNZn67H)A8$0G{h9-tMg%I;3Dhd<< z$I^m;Ah(fZAi47UA`6U?&HbIWhXOg{4{O(t+8(mcD>DZPm+(@S$;ruP-Yf6Mo0oyn zer30mq2p@IBU)JR7Bb@URI#<2^9$CHttesMmhLqx=&eMFDUDqQTP9U;+IgS25>%w1 zA65$)zHT`uDc~ZZ@tz^&gK-Z~2LxP&a!D-lQ6c4>2Pl)09yG0;b!0-$$3@#6{_mvn zp!G(YbNAL^N@Xg3gZ~b|6@*qV^E=*4v61x`mh&eL-^vruqPcr#vU|s^fS0*p#6*E> zg*w`Z2%}JJwW+y2#kTd7_d@KVH)SCowMvIVyuyXXujNiG+|u+x_(9%~I_W0-q2=8D zPgVO!_lA9p_29%+hyZ##0Wwf#698c(miPD3Wkkn3*b+N1Upz%TfZJ+zOe?~GNVR1@ z`YXDWEh6S#S!6hulj__f#msfF5xD`B6yzw-p_aJ526lP-_2>f@+0B3>%V>iR1wYFD zY=;KommjsSp=06Yu+0!=FCCOL(~~oV!}eP!KTF=W(o$pmVd<|=cPXD?f@bUHl|00f zSQf%^NNJW9+83*|E?`!EEdx5~`iYX!@E3t|uKC6Qhx=Q3Q>gBM`q$Io0*Duo_%Q;B zA0vIANH!ddu8MW#!l0V%?9If0$%!LgssgjNiI44Bn0zxdnt|^xzO1%(Kj~4oE{g#- z>H?G$!03s!9b<>JlZaik15*)g<&qcS-y(8Q0%1HTCXE6k`pf%(4&Zyj&cApsl5dg$ zurWpb#B-#`8i_G z3fk5kW)3fb>HlAQ*ZtH~zQrj~15yGCglecN0d`S|YlIjLp&EJ(MM9S(1_4n}e6%%y zAgEL$Eg;fCL=Xt52nblH8VCr2^nk<_QPE}h8)kOry+7g2m>)Cu-kf{#`Fu{hcaC^S z@XO4(AtEC}6=2F2xHCcv(XStq-El@fiN)$ioB%l;?1a8iR9@?ob+2u-5<6V?C63{M zn`V4VB#SB@)Pjj^L|WJCK@bQ4d&3>Dg*5D`j%vJbC*#MX;bf}9IbEn`h*+R{V?PkGwMB7_ddBocY-lqTc`r6OFtnldMa>aph}?*{ zn^=pX=;)zDm(U9y>+NuUjY9<$Z3F(+ImA2c{tn=qxSCD1sCEASQhY6YnY;mBr+y!-eAbTl z^*&f)E6u9z@WA^;wxO6AZI04QBgMMLIDCOeRL^gGqo0}q8Yg@DS>td8I$*J`WJBqr zAIr_zk5U(^)ODhAiOvblNi(SUKt-wGQ`O1*;-cRafC>{S&M=MTvz1`$Tu%9&PGF#N zn3pC|B*(-F6vREjz$akJ4Aj~I9@~=SBphxqo zaM-I@w4MAk%r!5Udg*h%%&-;N!s4kgTQT$r{q~?j{nxKwQ5T^Hx+HBd-$^=rBXk5I zzw|*szLSi-eqpEX66fpEqreo40WNqhH?)k~y?l5lbfQ@U^dMjf8K8IN)aNA!F%&Vo zR^zVbL?KTnCW`dAE=kr%7piu6Bph)MU#OGI37B9-d>b;qv)w!Zq<8JJ-sK&L*u%fj z#(#HA_R;Xxb#6wTo0#kjJc#z7B0$J)LIVa_4d$D{83kv? zO!YBHf_!%#g&HK)LVi=JfAi)|!rj@+0Kj*_xkl%d{BZ?I*h;mMYYSB;)k|Zic#+C2 zOXEXbo=Ocz$q7{wYLjB!`S4rWUq7fG#ps4`(>KV?m@ha1p|gNSU9*yvF<5mWl$^b> zCB?6IP%%^{iNGwyAQ&d89!2fd?=MlbqETD;0r0MTKNzpP z4tOC73rxWhdA&^izi!k>w>s9)V->WgiJJZuo{|spVV}}p$4Th_$1T+533AWomHnno zx|b`3V@N=Jm9=Pfa?oPv+VT3`iJXQIcU;ZQy?JBOA5`~jk`j2F^zG(-X+PeQ%!Z(g zK69~;OM%7LZao`a?S>^(y$zQC8n$+@9{SSF`JkhkTbzpm?YF~~3k!?8W01BsO&Z(0 z;2orfgN4h;Ihr9l#OV16FJI6dG3|Oy2^$gdC6`&K(DE-o8GA+HOxLC)RLBeu=2q2) zjn*DZQ~qE*v2%2OGINDHbdiaR*Ab5?R0sB=v+4?MHPcoyU*ElrN5VSK-->NqoI1n~ zc%F2A<lG%w(?s{=<#{U7V3)r*gMAqZs6YnUI0{}l213rE00YTVzyf8WzrXXmG%$qZ79 z9Fs1jCN33Twi)G%x1b+rAox8#DH)RuP0!H2YReOCK{Fdu2K~L=+?eQ+0I1uRj$8*i zlIr=?A3BUc40xJT_XQofUEQf&#*4IbSXZr zVbPB%;KuTxP%(YM#Nm?AvqO#wl%4~N-A*Vy6GNv94Kh@!y~DF^MaF;2C_IhHibb**NpT|5>N#1<(}n-~92dyeh$Om0Dui(W}t%zo#X`W5Vhkzw_W3aFgXJ z?-ua;{UDi@s3MQTAy-ndp7oBFYhC-oshTMxy}nWNL>>73lf6 z_dhrXd(|m>RO?NWJm#X;ma6?Dt*u%%4nyHCorw+?(s0y!ujdHIj0t#Jvfr~_&halB z;_)B(fs6>0pDKm26yGB#)u@P~Wbgk|bK2OL>bR6QNV`z05jSxf%(H#@awBu4-zTkPyCXqgs9cfXAKG`X3%+RB01|=Xg zA#4}`e<<{s_{Y)FE1v!lX7hEVCSTfMWqUZ8%jnnMRj;pN_59+Qx~6G-)BC4?ytpUH z54Z}zbqWqSy8s@*ML?tgJ83~Y4RnuD{r8)nNB!fQpPu;XiJ$EFsT2Rtin+PRSZ1-| TEg>sd0DR2xR>svQykh?bkeNsa From 8440ef1135862fc3e15128d293697771a1a3a3ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Tue, 15 Mar 2016 17:22:08 -0400 Subject: [PATCH 18/22] add fallback for FF in getBBox test asset: - getBBox fails when call on a clipPath node in FF - use DOM attributes as fallback to grab the bounding box --- test/jasmine/assets/get_bbox.js | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/test/jasmine/assets/get_bbox.js b/test/jasmine/assets/get_bbox.js index f1df4dd660d..e651f414dd9 100644 --- a/test/jasmine/assets/get_bbox.js +++ b/test/jasmine/assets/get_bbox.js @@ -2,6 +2,8 @@ var d3 = require('d3'); +var ATTRS = ['x', 'y', 'width', 'height']; + // In-house implementation of SVG getBBox that takes clip paths into account module.exports = function getBBox(element) { @@ -14,21 +16,42 @@ module.exports = function getBBox(element) { // only supports 'url(#)' at the moment var clipPathId = clipPathAttr.substring(5, clipPathAttr.length-1); - var clipPath = d3.select('#' + clipPathId).node(); + var clipBBox = getClipBBox(clipPathId); - return minBBox(elementBBox, clipPath.getBBox()); + return minBBox(elementBBox, clipBBox); }; +function getClipBBox(clipPathId) { + var clipPath = d3.select('#' + clipPathId); + var clipBBox; + + try { + // this line throws an error in FF (38 and 45 at least) + clipBBox = clipPath.node().getBBox(); + } + catch(e) { + // use DOM attributes as fallback + var path = d3.select(clipPath.node().firstChild); + + clipBBox = {}; + + ATTRS.forEach(function(attr) { + clipBBox[attr] = path.attr(attr); + }); + } + + return clipBBox; +} + function minBBox(bbox1, bbox2) { - var keys = ['x', 'y', 'width', 'height']; var out = {}; function min(attr) { return Math.min(bbox1[attr], bbox2[attr]); } - keys.forEach(function(key) { - out[key] = min(key); + ATTRS.forEach(function(attr) { + out[attr] = min(attr); }); return out; From 757ffdf95040bd2611acf9c068efdd7a2e59644c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 16 Mar 2016 13:41:20 -0400 Subject: [PATCH 19/22] lint (define variable in block) --- src/components/legend/defaults.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/legend/defaults.js b/src/components/legend/defaults.js index f5f8a7ea3be..eb95148ce13 100644 --- a/src/components/legend/defaults.js +++ b/src/components/legend/defaults.js @@ -21,11 +21,10 @@ module.exports = function legendDefaults(layoutIn, layoutOut, fullData) { containerOut = layoutOut.legend = {}; var visibleTraces = 0, - defaultOrder = 'normal', - trace; + defaultOrder = 'normal'; for(var i = 0; i < fullData.length; i++) { - trace = fullData[i]; + var trace = fullData[i]; if(helpers.legendGetsTrace(trace)) { visibleTraces++; From 196d7dc27543b64d716f196d646a7296cf71471b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 16 Mar 2016 13:43:24 -0400 Subject: [PATCH 20/22] change top-paper id to 'topdefs' + uid: - that way ids remain unique on page - rm special merge block in to-svg step --- src/plot_api/plot_api.js | 2 +- src/snapshot/tosvg.js | 8 -------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index 38ee77db540..eeea7c0c1f5 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -2586,7 +2586,7 @@ function makePlotFramework(gd) { .attr('id', 'defs-' + fullLayout._uid); fullLayout._topdefs = fullLayout._toppaper.append('defs') - .attr('id', 'defs-' + fullLayout._uid); + .attr('id', 'topdefs-' + fullLayout._uid); fullLayout._draggers = fullLayout._paper.append('g') .classed('draglayer', true); diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index cf6fe17b1c7..7cf715abc18 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -89,15 +89,7 @@ module.exports = function toSVG(gd, format) { // assumes everything in toppaper is a group, and if it's empty (like hoverlayer) // we can ignore it if(fullLayout._toppaper) { - var topDefs = fullLayout._topdefs.node().childNodes; - for(i = 0; i < topDefs.length; i++) { - var topDef = topDefs[i]; - - fullLayout._defs.node().appendChild(topDef); - } - - fullLayout._topdefs.remove(); var topGroups = fullLayout._toppaper.node().childNodes; From 860e6471f223a9611e28575a8854d334f7613393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 16 Mar 2016 13:45:00 -0400 Subject: [PATCH 21/22] make copy of top-paper childNodes array-like: - as childNodes prop gets mutated in top-group loop --- src/snapshot/tosvg.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index 7cf715abc18..ec8801fc1d7 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -85,13 +85,14 @@ module.exports = function toSVG(gd, format) { .appendChild(geoFramework.node()); } - // now that we've got the 3d images in the right layer, add top items above them - // assumes everything in toppaper is a group, and if it's empty (like hoverlayer) - // we can ignore it + // now that we've got the 3d images in the right layer, + // add top items above them assumes everything in toppaper is either + // a group or a defs, and if it's empty (like hoverlayer) we can ignore it. if(fullLayout._toppaper) { + var nodes = fullLayout._toppaper.node().childNodes; - - var topGroups = fullLayout._toppaper.node().childNodes; + // make copy of nodes as childNodes prop gets mutated in loop below + var topGroups = Array.prototype.slice.call(nodes); for(i = 0; i < topGroups.length; i++) { var topGroup = topGroups[i]; From dbaf34076f3bd23e7ba0ed4a73ff8d92501b5a09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20T=C3=A9treault-Pinard?= Date: Wed, 16 Mar 2016 14:31:06 -0400 Subject: [PATCH 22/22] make user-select-none CSS class: - use it in legend text - rm now useless user-select purge step in to-svg --- build/plotcss.js | 1 + src/components/legend/draw.js | 9 ++------- src/css/_base.scss | 4 ++++ src/snapshot/tosvg.js | 7 +------ 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/build/plotcss.js b/build/plotcss.js index 4b412c9eee6..169edfce295 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -8,6 +8,7 @@ var rules = { "X a": "text-decoration:none;", "X a:hover": "text-decoration:none;", "X .crisp": "shape-rendering:crispEdges;", + "X .user-select-none": "-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;-o-user-select:none;user-select:none;", "X svg": "overflow:hidden;", "X svg a": "fill:#447adb;", "X svg a:hover": "fill:#3c6dc5;", diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 8849829f9e9..8fe9f62d35f 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -336,13 +336,8 @@ function drawTexts(context, gd, d, i, traces) { y: 0, 'data-unformatted': name }) - .style({ - 'text-anchor': 'start', - '-webkit-user-select': 'none', - '-moz-user-select': 'none', - '-ms-user-select': 'none', - 'user-select': 'none' - }) + .style('text-anchor', 'start') + .classed('user-select-none', true) .call(Drawing.font, fullLayout.legend.font) .text(name); diff --git a/src/css/_base.scss b/src/css/_base.scss index b7c2b66aeaf..3551948be0a 100644 --- a/src/css/_base.scss +++ b/src/css/_base.scss @@ -22,6 +22,10 @@ a { .crisp { shape-rendering: crispEdges; } +.user-select-none { + @include vendor('user-select', none); +} + //Required for IE11. Other browsers set this by default. svg { overflow: hidden; } diff --git a/src/snapshot/tosvg.js b/src/snapshot/tosvg.js index ec8801fc1d7..14c36df1ce3 100644 --- a/src/snapshot/tosvg.js +++ b/src/snapshot/tosvg.js @@ -107,12 +107,7 @@ module.exports = function toSVG(gd, format) { svg.node().style.background = ''; svg.selectAll('text') - .attr({'data-unformatted': null}) - .style({ - '-webkit-user-select': null, - '-moz-user-select': null, - '-ms-user-select': null - }) + .attr('data-unformatted', null) .each(function() { // hidden text is pre-formatting mathjax, the browser ignores it but it can still confuse batik var txt = d3.select(this);