diff --git a/.eslintrc b/.eslintrc index f39c183260f..5f0ec7491e4 100644 --- a/.eslintrc +++ b/.eslintrc @@ -8,7 +8,7 @@ }, "rules": { "no-trailing-spaces": [2], - "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 1}], + "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 0}], "eol-last": [2], "linebreak-style": [2, "unix"], "indent": [2, 4, {"SwitchCase": 1}], diff --git a/lib/index.js b/lib/index.js index 8a7548e57a7..631fed56656 100644 --- a/lib/index.js +++ b/lib/index.js @@ -29,7 +29,8 @@ Core.register([ require('./mesh3d'), require('./scattergeo'), require('./choropleth'), - require('./scattergl') + require('./scattergl'), + require('./scatterternary') ]); module.exports = Core; diff --git a/lib/scatterternary.js b/lib/scatterternary.js new file mode 100644 index 00000000000..e6b5bb4a4b3 --- /dev/null +++ b/lib/scatterternary.js @@ -0,0 +1,9 @@ +/** +* 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. +*/ + +module.exports = require('../src/traces/scatterternary'); diff --git a/src/components/annotations/index.js b/src/components/annotations/index.js index f033d3265f7..520b7b78083 100644 --- a/src/components/annotations/index.js +++ b/src/components/annotations/index.js @@ -12,6 +12,8 @@ var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); +var setCursor = require('../../lib/setcursor'); +var dragElement = require('../dragelement'); var annotations = module.exports = {}; @@ -583,7 +585,7 @@ annotations.draw = function(gd, index, opt, value) { annx0, anny0; - Plotly.Fx.dragElement({ + dragElement.init({ element: arrowdrag.node(), prepFn: function() { annx0 = Number(ann.attr('x')); @@ -641,7 +643,7 @@ annotations.draw = function(gd, index, opt, value) { y0, update; - Plotly.Fx.dragElement({ + dragElement.init({ element: ann.node(), prepFn: function() { x0 = Number(ann.attr('x')); @@ -662,7 +664,7 @@ annotations.draw = function(gd, index, opt, value) { var widthFraction = options._xsize / gs.w, xLeft = options.x + options._xshift / gs.w - widthFraction / 2; - update[annbase + '.x'] = Plotly.Fx.dragAlign(xLeft + dx / gs.w, + update[annbase + '.x'] = dragElement.align(xLeft + dx / gs.w, widthFraction, 0, 1, options.xanchor); } @@ -671,11 +673,11 @@ annotations.draw = function(gd, index, opt, value) { var heightFraction = options._ysize / gs.h, yBottom = options.y - options._yshift / gs.h - heightFraction / 2; - update[annbase + '.y'] = Plotly.Fx.dragAlign(yBottom - dy / gs.h, + update[annbase + '.y'] = dragElement.align(yBottom - dy / gs.h, heightFraction, 0, 1, options.yanchor); } if(!xa || !ya) { - csr = Plotly.Fx.dragCursors( + csr = dragElement.cursor( xa ? 0.5 : update[annbase + '.x'], ya ? 0.5 : update[annbase + '.y'], options.xanchor, options.yanchor @@ -694,10 +696,10 @@ annotations.draw = function(gd, index, opt, value) { x1 + ',' + y1 + ')' }); - Plotly.Fx.setCursor(ann, csr); + setCursor(ann, csr); }, doneFn: function(dragged) { - Plotly.Fx.setCursor(ann); + setCursor(ann); if(dragged) { Plotly.relayout(gd, update); var notesBox = document.querySelector('.js-notes-box-panel'); diff --git a/src/components/color/attributes.js b/src/components/color/attributes.js index 6febca3091c..fe9b763e80f 100644 --- a/src/components/color/attributes.js +++ b/src/components/color/attributes.js @@ -28,3 +28,9 @@ exports.defaultLine = '#444'; exports.lightLine = '#eee'; exports.background = '#fff'; + +// with axis.color and Color.interp we aren't using lightLine +// itself anymore, instead interpolating between axis.color +// and the background color using tinycolor.mix. lightFraction +// gives back exactly lightLine if the other colors are defaults. +exports.lightFraction = 100 * (0xe - 0x4) / (0xf - 0x4); diff --git a/src/components/colorbar/defaults.js b/src/components/colorbar/defaults.js index feba990b80f..117e8457272 100644 --- a/src/components/colorbar/defaults.js +++ b/src/components/colorbar/defaults.js @@ -11,7 +11,8 @@ var Lib = require('../../lib'); var handleTickValueDefaults = require('../../plots/cartesian/tick_value_defaults'); -var handleTickDefaults = require('../../plots/cartesian/tick_defaults'); +var handleTickMarkDefaults = require('../../plots/cartesian/tick_mark_defaults'); +var handleTickLabelDefaults = require('../../plots/cartesian/tick_label_defaults'); var attributes = require('./attributes'); @@ -52,7 +53,10 @@ module.exports = function colorbarDefaults(containerIn, containerOut, layout) { handleTickValueDefaults(colorbarIn, colorbarOut, coerce, 'linear'); - handleTickDefaults(colorbarIn, colorbarOut, coerce, 'linear', + handleTickLabelDefaults(colorbarIn, colorbarOut, coerce, 'linear', + {outerTicks: false, font: layout.font, noHover: true}); + + handleTickMarkDefaults(colorbarIn, colorbarOut, coerce, 'linear', {outerTicks: false, font: layout.font, noHover: true}); coerce('title'); diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 8dc4545accf..6479c5f347e 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -14,8 +14,10 @@ var d3 = require('d3'); var Plotly = require('../../plotly'); var Plots = require('../../plots/plots'); var Axes = require('../../plots/cartesian/axes'); -var Fx = require('../../plots/cartesian/graph_interact'); +var dragElement = require('../dragelement'); var Lib = require('../../lib'); +var extendFlat = require('../../lib/extend').extendFlat; +var setCursor = require('../../lib/setcursor'); var Drawing = require('../drawing'); var Color = require('../color'); var Titles = require('../titles'); @@ -51,7 +53,8 @@ module.exports = function draw(gd, id) { opts.filllevels = null; function component() { - var fullLayout = gd._fullLayout; + var fullLayout = gd._fullLayout, + gs = fullLayout._size; if((typeof opts.fillcolor !== 'function') && (typeof opts.line.color !== 'function')) { fullLayout._infolayer.selectAll('g.'+id).remove(); @@ -115,17 +118,17 @@ module.exports = function draw(gd, id) { originalPlotWidth = fullLayout.width - fullLayout.margin.l - fullLayout.margin.r, thickPx = Math.round(opts.thickness * (opts.thicknessmode==='fraction' ? originalPlotWidth : 1)), - thickFrac = thickPx / fullLayout._size.w, + thickFrac = thickPx / gs.w, lenPx = Math.round(opts.len * (opts.lenmode==='fraction' ? originalPlotHeight : 1)), - lenFrac = lenPx / fullLayout._size.h, - xpadFrac = opts.xpad/fullLayout._size.w, + lenFrac = lenPx / gs.h, + xpadFrac = opts.xpad/gs.w, yExtraPx = (opts.borderwidth + opts.outlinewidth)/2, - ypadFrac = opts.ypad / fullLayout._size.h, + ypadFrac = opts.ypad / gs.h, // x positioning: do it initially just for left anchor, // then fix at the end (since we don't know the width yet) - xLeft = Math.round(opts.x*fullLayout._size.w + opts.xpad), + xLeft = Math.round(opts.x*gs.w + opts.xpad), // for dragging... this is getting a little muddled... xLeftFrac = opts.x - thickFrac * ({middle: 0.5, right: 1}[opts.xanchor]||0), @@ -133,7 +136,7 @@ module.exports = function draw(gd, id) { // y positioning we can do correctly from the start yBottomFrac = opts.y + lenFrac * (({top: -0.5, bottom: 0.5}[opts.yanchor] || 0) - 0.5), - yBottomPx = Math.round(fullLayout._size.h * (1-yBottomFrac)), + yBottomPx = Math.round(gs.h * (1-yBottomFrac)), yTopPx = yBottomPx-lenPx, titleEl, cbAxisIn = { @@ -182,7 +185,7 @@ module.exports = function draw(gd, id) { handleAxisPositionDefaults(cbAxisIn, cbAxisOut, coerce, axisOptions); cbAxisOut._id = 'y' + id; - cbAxisOut._td = gd; + cbAxisOut._gd = gd; // position can't go in through supplyDefaults // because that restricts it to [0,1] @@ -242,22 +245,38 @@ module.exports = function draw(gd, id) { s.append('g').classed('cbtitleunshift',true) .append('g').classed('cbtitle',true); s.append('rect').classed('cboutline',true); + s.select('.cbtitle').datum(0); }); - container.attr('transform','translate('+Math.round(fullLayout._size.l)+ - ','+Math.round(fullLayout._size.t)+')'); + container.attr('transform','translate('+Math.round(gs.l)+ + ','+Math.round(gs.t)+')'); // TODO: this opposite transform is a hack until we make it // more rational which items get this offset var titleCont = container.select('.cbtitleunshift') .attr('transform', 'translate(-'+ - Math.round(fullLayout._size.l) + ',-' + - Math.round(fullLayout._size.t) + ')'); + Math.round(gs.l) + ',-' + + Math.round(gs.t) + ')'); cbAxisOut._axislayer = container.select('.cbaxis'); var titleHeight = 0; if(['top', 'bottom'].indexOf(opts.titleside) !== -1) { // draw the title so we know how much room it needs - // when we squish the axis - Titles.draw(gd, cbAxisOut._id + 'title'); + // when we squish the axis. This one only applies to + // top or bottom titles, not right side. + var x = gs.l + (opts.x + xpadFrac) * gs.w, + fontSize = cbAxisOut.titlefont.size, + y; + + if(opts.titleside === 'top') { + y = (1 - (yBottomFrac + lenFrac - ypadFrac)) * gs.h + + gs.t + 3 + fontSize * 0.75; + } + else { + y = (1 - (yBottomFrac + ypadFrac)) * gs.h + + gs.t - 3 - fontSize * 0.25; + } + drawTitle(cbAxisOut._id + 'title', { + attributes: {x: x, y: y, 'text-anchor': 'start'} + }); } function drawAxis() { @@ -294,11 +313,11 @@ module.exports = function draw(gd, id) { titleHeight += 5; if(opts.titleside==='top') { - cbAxisOut.domain[1] -= titleHeight/fullLayout._size.h; + cbAxisOut.domain[1] -= titleHeight/gs.h; titleTrans[1] *= -1; } else { - cbAxisOut.domain[0] += titleHeight/fullLayout._size.h; + cbAxisOut.domain[0] += titleHeight/gs.h; var nlines = Math.max(1, titleText.selectAll('tspan.line').size()); titleTrans[1] += (1-nlines)*lineSize; @@ -313,7 +332,7 @@ module.exports = function draw(gd, id) { container.selectAll('.cbfills,.cblines,.cbaxis') .attr('transform','translate(0,'+ - Math.round(fullLayout._size.h*(1-cbAxisOut.domain[1]))+')'); + Math.round(gs.h*(1-cbAxisOut.domain[1]))+')'); var fills = container.select('.cbfills') .selectAll('rect.cbfill') @@ -370,7 +389,67 @@ module.exports = function draw(gd, id) { (opts.outlinewidth||0)/2 - (opts.ticks==='outside' ? 1 : 0); cbAxisOut.side = 'right'; - return Axes.doTicks(gd, cbAxisOut); + // separate out axis and title drawing, + // so we don't need such complicated logic in Titles.draw + // if title is on the top or bottom, we've already drawn it + // this title call only handles side=right + return Lib.syncOrAsync([ + function() { + return Axes.doTicks(gd, cbAxisOut, true); + }, + function() { + if(['top','bottom'].indexOf(opts.titleside)===-1) { + var fontSize = cbAxisOut.titlefont.size, + y = cbAxisOut._offset + cbAxisOut._length / 2, + x = gs.l + (cbAxisOut.position || 0) * gs.w + ((cbAxisOut.side === 'right') ? + 10 + fontSize*((cbAxisOut.showticklabels ? 1 : 0.5)) : + -10 - fontSize*((cbAxisOut.showticklabels ? 0.5 : 0))); + + // the 'h' + is a hack to get around the fact that + // convertToTspans rotates any 'y...' class by 90 degrees. + // TODO: find a better way to control this. + drawTitle('h' + cbAxisOut._id + 'title', { + avoid: { + selection: d3.select(gd).selectAll('g.' + cbAxisOut._id + 'tick'), + side: opts.titleside, + offsetLeft: gs.l, + offsetTop: gs.t, + maxShift: fullLayout.width + }, + attributes: {x: x, y: y, 'text-anchor': 'middle'}, + transform: {rotate: '-90', offset: 0} + }); + } + }]); + } + + function drawTitle(titleClass, titleOpts) { + var trace = getTrace(), + propName; + if(Plots.traceIs(trace, 'markerColorscale')) { + propName = 'marker.colorbar.title'; + } + else propName = 'colorbar.title'; + + var dfltTitleOpts = { + propContainer: cbAxisOut, + propName: propName, + traceIndex: trace.index, + dfltName: 'colorscale', + containerGroup: container.select('.cbtitle') + }; + + // this class-to-rotate thing with convertToTspans is + // getting hackier and hackier... delete groups with the + // wrong class (in case earlier the colorbar was drawn on + // a different side, I think?) + var otherClass = titleClass.charAt(0) === 'h' ? + titleClass.substr(1) : ('h' + titleClass); + container.selectAll('.' + otherClass + ',.' + otherClass + '-math-group') + .remove(); + + Titles.draw(gd, titleClass, + extendFlat(dfltTitleOpts, titleOpts || {})); } function positionCB() { @@ -393,11 +472,11 @@ module.exports = function draw(gd, id) { else { // note: the formula below works for all titlesides, // (except for top/bottom mathjax, above) - // but the weird fullLayout._size.l is because the titleunshift + // but the weird gs.l is because the titleunshift // transform gets removed by Drawing.bBox titleWidth = Drawing.bBox(titleCont.node()).right - - xLeft - fullLayout._size.l; + xLeft - gs.l; } innerWidth = Math.max(innerWidth,titleWidth); } @@ -434,7 +513,7 @@ module.exports = function draw(gd, id) { var xoffset = ({center: 0.5, right: 1}[opts.xanchor] || 0) * outerwidth; container.attr('transform', - 'translate('+(fullLayout._size.l-xoffset)+','+fullLayout._size.t+')'); + 'translate('+(gs.l-xoffset)+','+gs.t+')'); //auto margin adjustment Plots.autoMargin(gd, id,{ @@ -462,43 +541,32 @@ module.exports = function draw(gd, id) { xf, yf; - Fx.dragElement({ + dragElement.init({ element: container.node(), prepFn: function() { t0 = container.attr('transform'); - Fx.setCursor(container); + setCursor(container); }, moveFn: function(dx, dy) { - var gs = gd._fullLayout._size; - container.attr('transform', t0+' ' + 'translate('+dx+','+dy+')'); - xf = Fx.dragAlign(xLeftFrac + (dx/gs.w), thickFrac, + xf = dragElement.align(xLeftFrac + (dx/gs.w), thickFrac, 0, 1, opts.xanchor); - yf = Fx.dragAlign(yBottomFrac - (dy/gs.h), lenFrac, + yf = dragElement.align(yBottomFrac - (dy/gs.h), lenFrac, 0, 1, opts.yanchor); - var csr = Fx.dragCursors(xf, yf, + var csr = dragElement.getCursor(xf, yf, opts.xanchor, opts.yanchor); - Fx.setCursor(container, csr); + setCursor(container, csr); }, doneFn: function(dragged) { - Fx.setCursor(container); + setCursor(container); if(dragged && xf!==undefined && yf!==undefined) { - var idNum = id.substr(2), - traceNum; - gd._fullData.some(function(trace) { - if(trace.uid===idNum) { - traceNum = trace.index; - return true; - } - }); - Plotly.restyle(gd, {'colorbar.x': xf, 'colorbar.y': yf}, - traceNum); + getTrace().index); } } }); @@ -506,6 +574,16 @@ module.exports = function draw(gd, id) { return cbDone; } + function getTrace() { + var idNum = id.substr(2), + i, + trace; + for(i = 0; i < gd._fullData.length; i++) { + trace = gd._fullData[i]; + if(trace.uid === idNum) return trace; + } + } + // setter/getters for every item defined in opts Object.keys(opts).forEach(function(name) { component[name] = function(v) { diff --git a/src/components/dragelement/align.js b/src/components/dragelement/align.js new file mode 100644 index 00000000000..66e5c8f681f --- /dev/null +++ b/src/components/dragelement/align.js @@ -0,0 +1,31 @@ +/** +* 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'; + + +// for automatic alignment on dragging, <1/3 means left align, +// >2/3 means right, and between is center. Pick the right fraction +// based on where you are, and return the fraction corresponding to +// that position on the object +module.exports = function align(v, dv, v0, v1, anchor) { + var vmin = (v - v0) / (v1 - v0), + vmax = vmin + dv / (v1 - v0), + vc = (vmin + vmax) / 2; + + // explicitly specified anchor + if(anchor === 'left' || anchor === 'bottom') return vmin; + if(anchor === 'center' || anchor === 'middle') return vc; + if(anchor === 'right' || anchor === 'top') return vmax; + + // automatic based on position + if(vmin < (2/3) - vc) return vmin; + if(vmax > (4/3) - vc) return vmax; + return vc; +}; diff --git a/src/components/dragelement/cursor.js b/src/components/dragelement/cursor.js new file mode 100644 index 00000000000..8bffe085592 --- /dev/null +++ b/src/components/dragelement/cursor.js @@ -0,0 +1,36 @@ +/** +* 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'); + + +// set cursors pointing toward the closest corner/side, +// to indicate alignment +// x and y are 0-1, fractions of the plot area +var cursorset = [ + ['sw-resize', 's-resize', 'se-resize'], + ['w-resize', 'move', 'e-resize'], + ['nw-resize', 'n-resize', 'ne-resize'] +]; + +module.exports = function getCursor(x, y, xanchor, yanchor) { + if(xanchor === 'left') x=0; + else if(xanchor === 'center') x=1; + else if(xanchor === 'right') x=2; + else x = Lib.constrain(Math.floor(x * 3), 0, 2); + + if(yanchor === 'bottom') y = 0; + else if(yanchor === 'middle') y = 1; + else if(yanchor === 'top') y = 2; + else y = Lib.constrain(Math.floor(y * 3), 0, 2); + + return cursorset[y][x]; +}; diff --git a/src/components/dragelement/index.js b/src/components/dragelement/index.js new file mode 100644 index 00000000000..e9d1ce56b9c --- /dev/null +++ b/src/components/dragelement/index.js @@ -0,0 +1,167 @@ +/** +* 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 Plotly = require('../../plotly'); +var Lib = require('../../lib'); + +var constants = require('../../plots/cartesian/constants'); + + +var dragElement = module.exports = {}; + +dragElement.align = require('./align'); +dragElement.getCursor = require('./cursor'); + +var unhover = require('./unhover'); +dragElement.unhover = unhover.wrapped; +dragElement.unhoverRaw = unhover.raw; + +/** + * Abstracts click & drag interactions + * @param {object} options with keys: + * element (required) the DOM element to drag + * prepFn (optional) function(event, startX, startY) + * executed on mousedown + * startX and startY are the clientX and clientY pixel position + * of the mousedown event + * moveFn (optional) function(dx, dy, dragged) + * executed on move + * dx and dy are the net pixel offset of the drag, + * dragged is true/false, has the mouse moved enough to + * constitute a drag + * doneFn (optional) function(dragged, numClicks) + * executed on mouseup, or mouseout of window since + * we don't get events after that + * dragged is as in moveFn + * numClicks is how many clicks we've registered within + * a doubleclick time + */ +dragElement.init = function init(options) { + var gd = Lib.getPlotDiv(options.element) || {}, + numClicks = 1, + DBLCLICKDELAY = constants.DBLCLICKDELAY, + startX, + startY, + newMouseDownTime, + dragCover, + initialTarget; + + if(!gd._mouseDownTime) gd._mouseDownTime = 0; + + function onStart(e) { + // make dragging and dragged into properties of gd + // so that others can look at and modify them + gd._dragged = false; + gd._dragging = true; + startX = e.clientX; + startY = e.clientY; + initialTarget = e.target; + + newMouseDownTime = (new Date()).getTime(); + if(newMouseDownTime - gd._mouseDownTime < DBLCLICKDELAY) { + // in a click train + numClicks += 1; + } + else { + // new click train + numClicks = 1; + gd._mouseDownTime = newMouseDownTime; + } + + if(options.prepFn) options.prepFn(e, startX, startY); + + dragCover = coverSlip(); + + dragCover.onmousemove = onMove; + dragCover.onmouseup = onDone; + dragCover.onmouseout = onDone; + + dragCover.style.cursor = window.getComputedStyle(options.element).cursor; + + return Lib.pauseEvent(e); + } + + function onMove(e) { + var dx = e.clientX - startX, + dy = e.clientY - startY, + minDrag = options.minDrag || constants.MINDRAG; + + if(Math.abs(dx) < minDrag) dx = 0; + if(Math.abs(dy) < minDrag) dy = 0; + if(dx||dy) { + gd._dragged = true; + dragElement.unhover(gd); + } + + if(options.moveFn) options.moveFn(dx, dy, gd._dragged); + + return Lib.pauseEvent(e); + } + + function onDone(e) { + dragCover.onmousemove = null; + dragCover.onmouseup = null; + dragCover.onmouseout = null; + Lib.removeElement(dragCover); + + if(!gd._dragging) { + gd._dragged = false; + return; + } + gd._dragging = false; + + // don't count as a dblClick unless the mouseUp is also within + // the dblclick delay + if((new Date()).getTime() - gd._mouseDownTime > DBLCLICKDELAY) { + numClicks = Math.max(numClicks - 1, 1); + } + + if(options.doneFn) options.doneFn(gd._dragged, numClicks); + + if(!gd._dragged) { + var e2 = document.createEvent('MouseEvents'); + e2.initEvent('click', true, true); + initialTarget.dispatchEvent(e2); + } + + finishDrag(gd); + + gd._dragged = false; + + return Lib.pauseEvent(e); + } + + options.element.onmousedown = onStart; + options.element.style.pointerEvents = 'all'; +}; + +function coverSlip() { + var cover = document.createElement('div'); + + cover.className = 'dragcover'; + var cStyle = cover.style; + cStyle.position = 'fixed'; + cStyle.left = 0; + cStyle.right = 0; + cStyle.top = 0; + cStyle.bottom = 0; + cStyle.zIndex = 999999999; + cStyle.background = 'none'; + + document.body.appendChild(cover); + + return cover; +} + +function finishDrag(gd) { + gd._dragging = false; + if(gd._replotPending) Plotly.plot(gd); +} diff --git a/src/components/dragelement/unhover.js b/src/components/dragelement/unhover.js new file mode 100644 index 00000000000..a477501fe50 --- /dev/null +++ b/src/components/dragelement/unhover.js @@ -0,0 +1,49 @@ +/** +* 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 Events = require('../../lib/events'); + + +var unhover = module.exports = {}; + + +unhover.wrapped = function(gd, evt, subplot) { + if(typeof gd === 'string') gd = document.getElementById(gd); + + // Important, clear any queued hovers + if(gd._hoverTimer) { + clearTimeout(gd._hoverTimer); + gd._hoverTimer = undefined; + } + + unhover.raw(gd, evt, subplot); +}; + + +// remove hover effects on mouse out, and emit unhover event +unhover.raw = function unhoverRaw(gd, evt) { + var fullLayout = gd._fullLayout; + + if(!evt) evt = {}; + if(evt.target && + Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { + return; + } + + fullLayout._hoverlayer.selectAll('g').remove(); + + if(evt.target && gd._hoverdata) { + gd.emit('plotly_unhover', {points: gd._hoverdata}); + } + + gd._hoverdata = undefined; +}; diff --git a/src/components/errorbars/index.js b/src/components/errorbars/index.js index 4986e60c6b1..8e7dd771ffc 100644 --- a/src/components/errorbars/index.js +++ b/src/components/errorbars/index.js @@ -46,11 +46,11 @@ errorBars.plot = require('./plot'); errorBars.style = require('./style'); errorBars.hoverInfo = function(calcPoint, trace, hoverPoint) { - if(trace.error_y.visible) { + if((trace.error_y || {}).visible) { hoverPoint.yerr = calcPoint.yh - calcPoint.y; if(!trace.error_y.symmetric) hoverPoint.yerrneg = calcPoint.y - calcPoint.ys; } - if(trace.error_x.visible) { + if((trace.error_x || {}).visible) { hoverPoint.xerr = calcPoint.xh - calcPoint.x; if(!trace.error_x.symmetric) hoverPoint.xerrneg = calcPoint.x - calcPoint.xs; } diff --git a/src/components/errorbars/plot.js b/src/components/errorbars/plot.js index 0df1b4060b8..dd05c65e7df 100644 --- a/src/components/errorbars/plot.js +++ b/src/components/errorbars/plot.js @@ -22,8 +22,12 @@ module.exports = function plot(traces, plotinfo) { traces.each(function(d) { var trace = d[0].trace, - xObj = trace.error_x, - yObj = trace.error_y; + // || {} is in case the trace (specifically scatterternary) + // doesn't support error bars at all, but does go through + // the scatter.plot mechanics, which calls ErrorBars.plot + // internally + xObj = trace.error_x || {}, + yObj = trace.error_y || {}; var sparse = ( subTypes.hasMarkers(trace) && diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index c7b47a2ce1f..bdafb3441f4 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -13,8 +13,9 @@ var d3 = require('d3'); var Plotly = require('../../plotly'); var Lib = require('../../lib'); +var setCursor = require('../../lib/setcursor'); var Plots = require('../../plots/plots'); -var Fx = require('../../plots/cartesian/graph_interact'); +var dragElement = require('../dragelement'); var Drawing = require('../drawing'); var Color = require('../color'); @@ -292,31 +293,31 @@ module.exports = function draw(gd) { lw, lh; - Fx.dragElement({ + dragElement.init({ element: legend.node(), prepFn: function() { x0 = Number(legend.attr('x')); y0 = Number(legend.attr('y')); lw = Number(legend.attr('width')); lh = Number(legend.attr('height')); - Fx.setCursor(legend); + setCursor(legend); }, moveFn: function(dx, dy) { var gs = gd._fullLayout._size; legend.call(Drawing.setPosition, x0+dx, y0+dy); - xf = Fx.dragAlign(x0+dx, lw, gs.l, gs.l+gs.w, + xf = dragElement.align(x0+dx, lw, gs.l, gs.l+gs.w, opts.xanchor); - yf = Fx.dragAlign(y0+dy+lh, -lh, gs.t+gs.h, gs.t, + yf = dragElement.align(y0+dy+lh, -lh, gs.t+gs.h, gs.t, opts.yanchor); - var csr = Fx.dragCursors(xf, yf, + var csr = dragElement.getCursor(xf, yf, opts.xanchor, opts.yanchor); - Fx.setCursor(legend, csr); + setCursor(legend, csr); }, doneFn: function(dragged) { - Fx.setCursor(legend); + setCursor(legend); if(dragged && xf !== undefined && yf !== undefined) { Plotly.relayout(gd, {'legend.x': xf, 'legend.y': yf}); } diff --git a/src/components/modebar/buttons.js b/src/components/modebar/buttons.js index 67d118fc3b9..e3e2a6c03c7 100644 --- a/src/components/modebar/buttons.js +++ b/src/components/modebar/buttons.js @@ -11,6 +11,7 @@ var Plotly = require('../../plotly'); var Lib = require('../../lib'); +var setCursor = require('../../lib/setcursor'); var Snapshot = require('../../snapshot'); var Icons = require('../../../build/ploticon'); @@ -253,7 +254,7 @@ function handleCartesian(gd, ev) { Plotly.relayout(gd, aobj).then(function() { if(astr === 'dragmode') { if(fullLayout._hasCartesian) { - Plotly.Fx.setCursor( + setCursor( fullLayout._paper.select('.nsewdrag'), DRAGCURSORS[val] ); diff --git a/src/components/modebar/manage.js b/src/components/modebar/manage.js index 65ea0c16ff3..e0c6ba3f6bf 100644 --- a/src/components/modebar/manage.js +++ b/src/components/modebar/manage.js @@ -73,11 +73,12 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { var fullLayout = gd._fullLayout, fullData = gd._fullData; - var hasCartesian = fullLayout._hasCartesian, - hasGL3D = fullLayout._hasGL3D, - hasGeo = fullLayout._hasGeo, - hasPie = fullLayout._hasPie, - hasGL2D = fullLayout._hasGL2D; + var hasCartesian = !!fullLayout._hasCartesian, + hasGL3D = !!fullLayout._hasGL3D, + hasGeo = !!fullLayout._hasGeo, + hasPie = !!fullLayout._hasPie, + hasGL2D = !!fullLayout._hasGL2D, + hasTernary = !!fullLayout._hasTernary; var groups = []; @@ -98,7 +99,7 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { // graphs with more than one plot types get 'union buttons' // which reset the view or toggle hover labels across all subplots. - if((hasCartesian || hasGL2D || hasPie) + hasGeo + hasGL3D > 1) { + if((hasCartesian || hasGL2D || hasPie || hasTernary) + hasGeo + hasGL3D > 1) { addGroup(['resetViews', 'toggleHover']); return appendButtonsToGroups(groups, buttonsToAdd); } @@ -117,16 +118,16 @@ function getButtonGroups(gd, buttonsToRemove, buttonsToAdd) { var allAxesFixed = areAllAxesFixed(fullLayout), dragModeGroup = []; - if((hasCartesian || hasGL2D) && !allAxesFixed) { + if(((hasCartesian || hasGL2D) && !allAxesFixed) || hasTernary) { dragModeGroup = ['zoom2d', 'pan2d']; } - if(hasCartesian && isSelectable(fullData)) { + if((hasCartesian || hasTernary) && isSelectable(fullData)) { dragModeGroup.push('select2d'); dragModeGroup.push('lasso2d'); } if(dragModeGroup.length) addGroup(dragModeGroup); - if((hasCartesian || hasGL2D) && !allAxesFixed) { + if((hasCartesian || hasGL2D) && !allAxesFixed && !hasTernary) { addGroup(['zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']); } @@ -172,7 +173,7 @@ function isSelectable(fullData) { if(!trace._module || !trace._module.selectPoints) continue; - if(trace.type === 'scatter') { + if(trace.type === 'scatter' || trace.type === 'scatterternary') { if(scatterSubTypes.hasMarkers(trace) || scatterSubTypes.hasText(trace)) { selectable = true; } diff --git a/src/components/titles/index.js b/src/components/titles/index.js index 112f3a96606..9fedc319856 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -18,130 +18,54 @@ var Lib = require('../../lib'); var Drawing = require('../drawing'); var Color = require('../color'); var svgTextUtils = require('../../lib/svg_text_utils'); -var axisIds = require('../../plots/cartesian/axis_ids'); var Titles = module.exports = {}; /** * Titles - (re)draw titles on the axes and plot: - * title can be 'xtitle', 'ytitle', 'gtitle' + * @param {DOM element} gd - the graphDiv + * @param {string} titleClass - the css class of this title + * @param {object} options - how and what to draw + * propContainer - the layout object containing `title` and `titlefont` + * attributes that apply to this title + * propName - the full name of the title property (for Plotly.relayout) + * [traceIndex] - include only if this property applies to one trace + * (such as a colorbar title) - then editing pipes to Plotly.restyle + * instead of Plotly.relayout + * dfltName - the name of the title in placeholder text + * [avoid] {object} - include if this title should move to avoid other elements + * selection - d3 selection of elements to avoid + * side - which direction to move if there is a conflict + * [offsetLeft] - if these elements are subject to a translation + * wrt the title element + * [offsetTop] + * attributes {object} - position and alignment attributes + * x - pixels + * y - pixels + * text-anchor - start|middle|end + * transform {object} - how to transform the title after positioning + * rotate - degrees + * offset - shift up/down in the rotated frame (unused?) + * containerGroup - if an svg element already exists to hold this + * title, include here. Otherwise it will go in fullLayout._infolayer */ -Titles.draw = function(gd, title) { - var fullLayout = gd._fullLayout, - gs = fullLayout._size, - axletter = title.charAt(0), - colorbar = (title.substr(1, 2) === 'cb'); - - var cbnum, cont, options; - - if(colorbar) { - var uid = title.substr(3).replace('title', ''); - gd._fullData.some(function(trace, i) { - if(trace.uid === uid) { - cbnum = i; - cont = gd.calcdata[i][0].t.cb.axis; - return true; - } - }); - } - else cont = fullLayout[axisIds.id2name(title.replace('title', ''))] || fullLayout; - - var prop = (cont === fullLayout) ? 'title' : cont._name+'.title', - name = colorbar ? 'colorscale' : - ((cont._id || axletter).toUpperCase()+' axis'), +Titles.draw = function(gd, titleClass, options) { + var cont = options.propContainer, + prop = options.propName, + traceIndex = options.traceIndex, + name = options.dfltName, + avoid = options.avoid || {}, + attributes = options.attributes, + transform = options.transform, + group = options.containerGroup, + + fullLayout = gd._fullLayout, font = cont.titlefont.family, fontSize = cont.titlefont.size, fontColor = cont.titlefont.color, - x, - y, - transform = '', - xa, - ya, - avoid = { - selection: d3.select(gd).selectAll('g.'+cont._id+'tick'), - side: cont.side - }, - // multiples of fontsize to offset label from axis - offsetBase = colorbar ? 0 : 1.5, - avoidTransform; - - // find the transform applied to the parents of the avoid selection - // which doesn't get picked up by Drawing.bBox - if(colorbar) { - avoid.offsetLeft = gs.l; - avoid.offsetTop = gs.t; - } - else if(avoid.selection.size()) { - avoidTransform = d3.select(avoid.selection.node().parentNode) - .attr('transform') - .match(/translate\(([-\.\d]+),([-\.\d]+)\)/); - if(avoidTransform) { - avoid.offsetLeft = +avoidTransform[1]; - avoid.offsetTop = +avoidTransform[2]; - } - } - - if(colorbar && cont.titleside) { - // argh, we only make it here if the title is on top or bottom, - // not right - x = gs.l + cont.titlex * gs.w; - y = gs.t + (1 - cont.titley) * gs.h + ((cont.titleside === 'top') ? - 3 + fontSize * 0.75 : - 3 - fontSize * 0.25); - options = {x: x, y: y, 'text-anchor': 'start'}; - avoid = {}; - - // convertToTspans rotates any 'y...' by 90 degrees... - // TODO: need a better solution than this hack - title = 'h' + title; - } - else if(axletter === 'x') { - xa = cont; - ya = (xa.anchor === 'free') ? - {_offset: gs.t + (1 - (xa.position || 0)) * gs.h, _length: 0} : - axisIds.getFromId(gd, xa.anchor); - x = xa._offset + xa._length / 2; - y = ya._offset + ((xa.side === 'top') ? - -10 - fontSize*(offsetBase + (xa.showticklabels ? 1 : 0)) : - ya._length + 10 + - fontSize*(offsetBase + (xa.showticklabels ? 1.5 : 0.5))); - - if(xa.rangeslider && xa.rangeslider.visible && xa._boundingBox) { - y += (fullLayout.height - fullLayout.margin.b - fullLayout.margin.t) * xa.rangeslider.thickness + - xa._boundingBox.height; - } - - options = {x: x, y: y, 'text-anchor': 'middle'}; - if(!avoid.side) avoid.side = 'bottom'; - } - else if(axletter === 'y') { - ya = cont; - xa = (ya.anchor === 'free') ? - {_offset: gs.l + (ya.position || 0) * gs.w, _length: 0} : - axisIds.getFromId(gd, ya.anchor); - - y = ya._offset + ya._length / 2; - x = xa._offset + ((ya.side === 'right') ? - xa._length + 10 + - fontSize*(offsetBase + (ya.showticklabels ? 1 : 0.5)) : - -10 - fontSize*(offsetBase + (ya.showticklabels ? 0.5 : 0))); - - options = {x: x, y: y, 'text-anchor': 'middle'}; - transform = {rotate: '-90', offset: 0}; - if(!avoid.side) avoid.side = 'left'; - } - else { - // plot title - name = 'Plot'; - fontSize = fullLayout.titlefont.size; - x = fullLayout.width / 2; - y = fullLayout._size.t / 2; - options = {x: x, y: y, 'text-anchor': 'middle'}; - avoid = {}; - } - - var opacity = 1, + opacity = 1, isplaceholder = false, txt = cont.title.trim(); if(txt === '') opacity = 0; @@ -150,23 +74,11 @@ Titles.draw = function(gd, title) { isplaceholder = true; } - var group; - if(colorbar) { - group = d3.select(gd) - .selectAll('.' + cont._id.substr(1) + ' .cbtitle'); - // this class-to-rotate thing with convertToTspans is - // getting hackier and hackier... delete groups with the - // wrong class - var otherClass = title.charAt(0) === 'h' ? - title.substr(1) : ('h' + title); - group.selectAll('.' + otherClass + ',.' + otherClass + '-math-group') - .remove(); - } - else { - group = fullLayout._infolayer.selectAll('.g-' + title) + if(!group) { + group = fullLayout._infolayer.selectAll('.g-' + titleClass) .data([0]); group.enter().append('g') - .classed('g-' + title, true); + .classed('g-' + titleClass, true); } var el = group.selectAll('text') @@ -178,7 +90,7 @@ Titles.draw = function(gd, title) { // so we need to clear out any old class and put the // correct one (only relevant for colorbars, at least // for now) - ie don't use .classed - .attr('class', title); + .attr('class', titleClass); function titleLayout(titleEl) { Lib.syncOrAsync([drawTitle,scootTitle], titleEl); @@ -186,7 +98,7 @@ Titles.draw = function(gd, title) { function drawTitle(titleEl) { titleEl.attr('transform', transform ? - 'rotate(' + [transform.rotate, options.x, options.y] + + 'rotate(' + [transform.rotate, attributes.x, attributes.y] + ') translate(0, ' + transform.offset + ')' : null); @@ -197,12 +109,12 @@ Titles.draw = function(gd, title) { opacity: opacity * Color.opacity(fontColor), 'font-weight': Plots.fontWeight }) - .attr(options) + .attr(attributes) .call(svgTextUtils.convertToTspans) - .attr(options); + .attr(attributes); titleEl.selectAll('tspan.line') - .attr(options); + .attr(attributes); return Plots.previousPromises(gd); } @@ -231,9 +143,9 @@ Titles.draw = function(gd, title) { right: fullLayout.width, bottom: fullLayout.height }, - maxshift = colorbar ? fullLayout.width: + maxshift = avoid.maxShift || ( (paperbb[avoid.side]-titlebb[avoid.side]) * - ((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1); + ((avoid.side === 'left' || avoid.side === 'top') ? -1 : 1)); // Prevent the title going off the paper if(maxshift < 0) shift = maxshift; else { @@ -272,13 +184,13 @@ Titles.draw = function(gd, title) { el.attr({'data-unformatted': txt}) .call(titleLayout); - var placeholderText = 'Click to enter ' + name.replace(/\d+/, '') + ' title'; + var placeholderText = 'Click to enter ' + name + ' title'; function setPlaceholder() { opacity = 0; isplaceholder = true; txt = placeholderText; - fullLayout._infolayer.select('.' + title) + fullLayout._infolayer.select('.' + titleClass) .attr({'data-unformatted': txt}) .text(txt) .on('mouseover.opacity', function() { @@ -296,23 +208,17 @@ Titles.draw = function(gd, title) { el.call(svgTextUtils.makeEditable) .on('edit', function(text) { - if(colorbar) { - var trace = gd._fullData[cbnum]; - if(Plots.traceIs(trace, 'markerColorscale')) { - Plotly.restyle(gd, 'marker.colorbar.title', text, cbnum); - } - else Plotly.restyle(gd, 'colorbar.title', text, cbnum); - } - else Plotly.relayout(gd,prop,text); + if(traceIndex !== undefined) Plotly.restyle(gd, prop, text, traceIndex); + else Plotly.relayout(gd, prop, text); }) .on('cancel', function() { this.text(this.attr('data-unformatted')) .call(titleLayout); }) .on('input', function(d) { - this.text(d || ' ').attr(options) + this.text(d || ' ').attr(attributes) .selectAll('tspan.line') - .attr(options); + .attr(attributes); }); } else if(!txt || txt.match(/Click to enter .+ title/)) { diff --git a/src/lib/coerce.js b/src/lib/coerce.js index 44d2e8c49e9..c00c510295c 100644 --- a/src/lib/coerce.js +++ b/src/lib/coerce.js @@ -16,6 +16,7 @@ var nestedProperty = require('./nested_property'); var getColorscale = require('../components/colorscale/get_scale'); var colorscaleNames = Object.keys(require('../components/colorscale/scales')); +var idRegex = /^([2-9]|[1-9][0-9]+)$/; exports.valObjects = { data_array: { @@ -158,53 +159,20 @@ exports.valObjects = { } } }, - axisid: { + subplotid: { description: [ - 'An axis id string (e.g. \'x\', \'x2\', \'x3\', ...).' + 'An id string of a subplot type (given by dflt), optionally', + 'followed by an integer >1. e.g. if dflt=\'geo\', we can have', + '\'geo\', \'geo2\', \'geo3\', ...' ].join(' '), requiredOpts: [], otherOpts: ['dflt'], coerceFunction: function(v, propOut, dflt) { - if(typeof v === 'string' && v.charAt(0)===dflt) { - var axnum = Number(v.substr(1)); - if(axnum%1 === 0 && axnum>1) { - propOut.set(v); - return; - } - } - propOut.set(dflt); - } - }, - sceneid: { - description: [ - 'A scene id string (e.g. \'scene\', \'scene2\', \'scene3\', ...).' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(typeof v === 'string' && v.substr(0,5)===dflt) { - var scenenum = Number(v.substr(5)); - if(scenenum%1 === 0 && scenenum>1) { - propOut.set(v); - return; - } - } - propOut.set(dflt); - } - }, - geoid: { - description: [ - 'A geo id string (e.g. \'geo\', \'geo2\', \'geo3\', ...).' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(typeof v === 'string' && v.substr(0,3)===dflt) { - var geonum = Number(v.substr(3)); - if(geonum%1 === 0 && geonum>1) { - propOut.set(v); - return; - } + var dlen = dflt.length; + if(typeof v === 'string' && v.substr(0, dlen) === dflt && + idRegex.test(v.substr(dlen))) { + propOut.set(v); + return; } propOut.set(dflt); } diff --git a/src/lib/filter_visible.js b/src/lib/filter_visible.js new file mode 100644 index 00000000000..cfea8b73941 --- /dev/null +++ b/src/lib/filter_visible.js @@ -0,0 +1,22 @@ +/** +* 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'; + +module.exports = function filterVisible(dataIn) { + var dataOut = []; + + for(var i = 0; i < dataIn.length; i++) { + var trace = dataIn[i]; + + if(trace.visible === true) dataOut.push(trace); + } + + return dataOut; +}; diff --git a/src/lib/setcursor.js b/src/lib/setcursor.js new file mode 100644 index 00000000000..cab1f189b73 --- /dev/null +++ b/src/lib/setcursor.js @@ -0,0 +1,21 @@ +/** +* 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'; + +// works with our CSS cursor classes (see css/_cursor.scss) +// to apply cursors to d3 single-element selections. +// omit cursor to revert to the default. +module.exports = function setCursor(el3, csr) { + (el3.attr('class') || '').split(' ').forEach(function(cls) { + if(cls.indexOf('cursor-') === 0) el3.classed(cls, false); + }); + + if(csr) el3.classed('cursor-' + csr, true); +}; diff --git a/src/plot_api/plot_api.js b/src/plot_api/plot_api.js index ef17d83e1c0..f94b03c959a 100644 --- a/src/plot_api/plot_api.js +++ b/src/plot_api/plot_api.js @@ -281,6 +281,7 @@ Plotly.plot = function(gd, data, layout, config) { if(fullLayout._hasCartesian || fullLayout._hasPie) { plotRegistry.cartesian.plot(gd); } + if(fullLayout._hasTernary) plotRegistry.ternary.plot(gd); // clean up old scenes that no longer have associated data // will this be a performance hit? @@ -2275,6 +2276,7 @@ Plotly.relayout = function relayout(gd, astr, val) { // 3d or geo at this point just needs to redraw. if(p.parts[0].indexOf('scene') === 0) doplot = true; else if(p.parts[0].indexOf('geo') === 0) doplot = true; + else if(p.parts[0].indexOf('ternary') === 0) doplot = true; else if(fullLayout._hasGL2D && (ai.indexOf('axis') !== -1 || p.parts[0] === 'plot_bgcolor') ) doplot = true; @@ -2368,7 +2370,7 @@ Plotly.relayout = function relayout(gd, astr, val) { if(doticks) { seq.push(function() { Plotly.Axes.doTicks(gd,'redraw'); - Titles.draw(gd, 'gtitle'); + drawMainTitle(gd); return Plots.previousPromises(gd); }); } @@ -2620,7 +2622,8 @@ function makePlotFramework(gd) { if(fullLayout._hasCartesian) makeCartesianPlotFramwork(gd, subplots); - // single shape and pie layers for the whole plot + // single ternary, shape and pie layers for the whole plot + fullLayout._ternarylayer = fullLayout._paper.append('g').classed('ternarylayer', true); fullLayout._shapelayer = fullLayout._paper.append('g').classed('shapelayer', true); fullLayout._pielayer = fullLayout._paper.append('g').classed('pielayer', true); @@ -2967,9 +2970,24 @@ function lsInner(gd) { Plotly.Axes.makeClipPaths(gd); - Titles.draw(gd, 'gtitle'); + drawMainTitle(gd); manageModeBar(gd); return gd._promises.length && Promise.all(gd._promises); } + +function drawMainTitle(gd) { + var fullLayout = gd._fullLayout; + + Titles.draw(gd, 'gtitle', { + propContainer: fullLayout, + propName: 'title', + dfltName: 'Plot', + attributes: { + x: fullLayout.width / 2, + y: fullLayout._size.t / 2, + 'text-anchor': 'middle' + } + }); +} diff --git a/src/plots/cartesian/attributes.js b/src/plots/cartesian/attributes.js index 56d6cca9450..7e44198831d 100644 --- a/src/plots/cartesian/attributes.js +++ b/src/plots/cartesian/attributes.js @@ -11,7 +11,7 @@ module.exports = { xaxis: { - valType: 'axisid', + valType: 'subplotid', role: 'info', dflt: 'x', description: [ @@ -23,7 +23,7 @@ module.exports = { ].join(' ') }, yaxis: { - valType: 'axisid', + valType: 'subplotid', role: 'info', dflt: 'y', description: [ diff --git a/src/plots/cartesian/axes.js b/src/plots/cartesian/axes.js index a28a531132d..b7059abe40c 100644 --- a/src/plots/cartesian/axes.js +++ b/src/plots/cartesian/axes.js @@ -33,8 +33,8 @@ axes.getFromTrace = axisIds.getFromTrace; // find the list of possible axes to reference with an xref or yref attribute // and coerce it to that list -axes.coerceRef = function(containerIn, containerOut, td, axLetter) { - var axlist = td._fullLayout._hasGL2D ? [] : axes.listIds(td, axLetter), +axes.coerceRef = function(containerIn, containerOut, gd, axLetter) { + var axlist = gd._fullLayout._hasGL2D ? [] : axes.listIds(gd, axLetter), refAttr = axLetter + 'ref', attrDef = {}; @@ -187,8 +187,8 @@ axes.doAutoRange = function(ax) { // doAutoRange will get called on fullLayout, // but we want to report its results back to layout - var axIn = ax._td.layout[ax._name]; - if(!axIn) ax._td.layout[ax._name] = axIn = {}; + var axIn = ax._gd.layout[ax._name]; + if(!axIn) ax._gd.layout[ax._name] = axIn = {}; if(axIn!==ax) { axIn.range = ax.range.slice(); axIn.autorange = ax.autorange; @@ -832,7 +832,7 @@ function modDateFormat(fmt,x) { } // draw the text for one tick. -// px,py are the location on td.paper +// px,py are the location on gd.paper // prefix is there so the x axis ticks can be dropped a line // ax is the axis layout, x is the tick value // hover is a (truthy) flag for whether to show numbers with a bit @@ -884,7 +884,7 @@ axes.tickText = function(ax, x, hover) { }; function tickTextObj(ax, x, text) { - var tf = ax.tickfont || ax._td._fullLayout.font; + var tf = ax.tickfont || ax._gd._fullLayout.font; return { x: x, @@ -1085,7 +1085,7 @@ function numFormat(v, ax, fmtoverride, hover) { if(dp) v = v.substr(0, dp + tickRound).replace(/\.?0+$/, ''); } // insert appropriate decimal point and thousands separator - v = numSeparate(v, ax._td._fullLayout.separators); + v = numSeparate(v, ax._gd._fullLayout.separators); } // add exponent @@ -1251,13 +1251,13 @@ axes.findSubplotsWithAxis = function(subplots, ax) { }; // makeClipPaths: prepare clipPaths for all single axes and all possible xy pairings -axes.makeClipPaths = function(td) { - var layout = td._fullLayout, +axes.makeClipPaths = function(gd) { + var layout = gd._fullLayout, defs = layout._defs, fullWidth = {_offset: 0, _length: layout.width, _id: ''}, fullHeight = {_offset: 0, _length: layout.height, _id: ''}, - xaList = axes.list(td, 'x', true), - yaList = axes.list(td, 'y', true), + xaList = axes.list(gd, 'x', true), + yaList = axes.list(gd, 'y', true), clipList = [], i, j; @@ -1322,11 +1322,11 @@ axes.doTicks = function(gd, axid, skipTitle) { xa = plotinfo.x(), ya = plotinfo.y(); plotinfo.plot.attr('viewBox', - '0 0 '+xa._length+' '+ya._length); + '0 0 '+xa._length + ' ' + ya._length); plotinfo.xaxislayer - .selectAll('.'+xa._id+'tick').remove(); + .selectAll('.' + xa._id + 'tick').remove(); plotinfo.yaxislayer - .selectAll('.'+ya._id+'tick').remove(); + .selectAll('.' + ya._id + 'tick').remove(); plotinfo.gridlayer .selectAll('path').remove(); plotinfo.zerolinelayer @@ -1338,7 +1338,7 @@ axes.doTicks = function(gd, axid, skipTitle) { return Plotly.Lib.syncOrAsync(axes.list(gd, '', true).map(function(ax) { return function() { if(!ax._id) return; - var axDone = axes.doTicks(gd,ax._id); + var axDone = axes.doTicks(gd, ax._id); if(axid === 'redraw') ax._r = ax.range.slice(); return axDone; }; @@ -1349,10 +1349,10 @@ axes.doTicks = function(gd, axid, skipTitle) { // make sure we only have allowed options for exponents // (others can make confusing errors) if(!ax.tickformat) { - if(['none','e','E','power','SI','B'].indexOf(ax.exponentformat)===-1) { + if(['none', 'e', 'E', 'power', 'SI', 'B'].indexOf(ax.exponentformat) === -1) { ax.exponentformat = 'e'; } - if(['all','first','last','none'].indexOf(ax.showexponent)===-1) { + if(['all', 'first', 'last', 'none'].indexOf(ax.showexponent) === -1) { ax.showexponent = 'all'; } } @@ -1367,36 +1367,51 @@ axes.doTicks = function(gd, axid, skipTitle) { counterLetter = axes.counterLetter(axid), vals = axes.calcTicks(ax), datafn = function(d) { return d.text + d.x + ax.mirror; }, - tcls = axid+'tick', - gcls = axid+'grid', - zcls = axid+'zl', - pad = (ax.linewidth||1) / 2, + tcls = axid + 'tick', + gcls = axid + 'grid', + zcls = axid + 'zl', + pad = (ax.linewidth || 1) / 2, labelStandoff = - (ax.ticks==='outside' ? ax.ticklen : 1) + (ax.linewidth||0), + (ax.ticks === 'outside' ? ax.ticklen : 1) + (ax.linewidth || 0), + labelShift = 0, gridWidth = Plotly.Drawing.crispRound(gd, ax.gridwidth, 1), zeroLineWidth = Plotly.Drawing.crispRound(gd, ax.zerolinewidth, gridWidth), tickWidth = Plotly.Drawing.crispRound(gd, ax.tickwidth, 1), - sides, transfn, tickprefix, tickmid, + sides, transfn, tickpathfn, i; + if(ax._counterangle && ax.ticks === 'outside') { + var caRad = ax._counterangle * Math.PI / 180; + labelStandoff = ax.ticklen * Math.cos(caRad) + (ax.linewidth || 0); + labelShift = ax.ticklen * Math.sin(caRad); + } + // positioning arguments for x vs y axes if(axletter === 'x') { sides = ['bottom', 'top']; transfn = function(d) { return 'translate(' + ax.l2p(d.x) + ',0)'; }; - // dumb templating with string concat - // would be better to use an actual template - tickprefix = 'M0,'; - tickmid = 'v'; + tickpathfn = function(shift, len) { + if(ax._counterangle) { + var caRad = ax._counterangle * Math.PI / 180; + return 'M0,' + shift + 'l' + (Math.sin(caRad) * len) + ',' + (Math.cos(caRad) * len); + } + else return 'M0,' + shift + 'v' + len; + }; } else if(axletter === 'y') { sides = ['left', 'right']; transfn = function(d) { return 'translate(0,' + ax.l2p(d.x) + ')'; }; - tickprefix = 'M'; - tickmid = ',0h'; + tickpathfn = function(shift, len) { + if(ax._counterangle) { + var caRad = ax._counterangle * Math.PI / 180; + return 'M' + shift + ',0l' + (Math.cos(caRad) * len) + ',' + (-Math.sin(caRad) * len); + } + else return 'M' + shift + ',0h' + len; + }; } else { console.log('unrecognized doTicks axis', axid); @@ -1415,12 +1430,12 @@ axes.doTicks = function(gd, axid, skipTitle) { // The key case here is removing zero lines when the axis bound is zero. function clipEnds(d) { var p = ax.l2p(d.x); - return (p>1 && p 1 && p < ax._length - 1); } var valsClipped = vals.filter(clipEnds); - function drawTicks(container,tickpath) { - var ticks = container.selectAll('path.'+tcls) + function drawTicks(container, tickpath) { + var ticks = container.selectAll('path.' + tcls) .data(ax.ticks==='inside' ? valsClipped : vals, datafn); if(tickpath && ax.ticks) { ticks.enter().append('path').classed(tcls, 1).classed('ticks', 1) @@ -1440,35 +1455,35 @@ axes.doTicks = function(gd, axid, skipTitle) { var tickLabels = container.selectAll('g.' + tcls).data(vals, datafn); if(!ax.showticklabels || !isNumeric(position)) { tickLabels.remove(); - Titles.draw(gd, axid + 'title'); + drawAxTitle(axid); return; } - var labelx, labely, labelanchor, labelpos0; - if(axletter==='x') { - var flipit = axside==='bottom' ? 1 : -1; - labelx = function(d) { return d.dx; }; - labelpos0 = position + (labelStandoff+pad)*flipit; + var labelx, labely, labelanchor, labelpos0, flipit; + if(axletter === 'x') { + flipit = (axside === 'bottom') ? 1 : -1; + labelx = function(d) { return d.dx + labelShift * flipit; }; + labelpos0 = position + (labelStandoff + pad) * flipit; labely = function(d) { - return d.dy+labelpos0+d.fontSize * - (axside==='bottom' ? 1 : -0.5); + return d.dy + labelpos0 + d.fontSize * + ((axside === 'bottom') ? 1 : -0.5); }; labelanchor = function(angle) { - if(!isNumeric(angle) || angle===0 || angle===180) { + if(!isNumeric(angle) || angle === 0 || angle === 180) { return 'middle'; } - return angle*flipit<0 ? 'end' : 'start'; + return (angle * flipit < 0) ? 'end' : 'start'; }; } else { - labely = function(d) { return d.dy+d.fontSize/2; }; + flipit = (axside === 'right') ? 1 : -1; + labely = function(d) { return d.dy + d.fontSize / 2 - labelShift * flipit; }; labelx = function(d) { return d.dx + position + (labelStandoff + pad + - (Math.abs(ax.tickangle)===90 ? d.fontSize/2 : 0)) * - (axside==='right' ? 1 : -1); + ((Math.abs(ax.tickangle) === 90) ? d.fontSize / 2 : 0)) * flipit; }; labelanchor = function(angle) { - if(isNumeric(angle) && Math.abs(angle)===90) { + if(isNumeric(angle) && Math.abs(angle) === 90) { return 'middle'; } return axside==='right' ? 'start' : 'end'; @@ -1477,7 +1492,7 @@ axes.doTicks = function(gd, axid, skipTitle) { var maxFontSize = 0, autoangle = 0, labelsReady = []; - tickLabels.enter().append('g').classed(tcls,1) + tickLabels.enter().append('g').classed(tcls, 1) .append('text') // only so tex has predictable alignment that we can // alter later @@ -1519,9 +1534,9 @@ axes.doTicks = function(gd, axid, skipTitle) { var thisLabel = d3.select(this), mathjaxGroup = thisLabel.select('.text-math-group'), transform = transfn(d) + - ((isNumeric(angle) && +angle!==0) ? - (' rotate('+angle+','+labelx(d)+','+ - (labely(d)-d.fontSize/2)+')') : + ((isNumeric(angle) && +angle !== 0) ? + (' rotate(' + angle + ',' + labelx(d) + ',' + + (labely(d) - d.fontSize / 2) + ')') : ''); if(mathjaxGroup.empty()) { var txt = thisLabel.select('text').attr({ @@ -1558,19 +1573,20 @@ axes.doTicks = function(gd, axid, skipTitle) { } function fixLabelOverlaps() { - positionLabels(tickLabels,ax.tickangle); + positionLabels(tickLabels, ax.tickangle); // check for auto-angling if x labels overlap // don't auto-angle at all for log axes with // base and digit format if(axletter==='x' && !isNumeric(ax.tickangle) && - (ax.type!=='log' || String(ax.dtick).charAt(0)!=='D')) { + (ax.type !== 'log' || String(ax.dtick).charAt(0) !== 'D')) { var lbbArray = []; tickLabels.each(function(d) { var s = d3.select(this), thisLabel = s.select('.text-math-group'), x = ax.l2p(d.x); - if(thisLabel.empty()) { thisLabel = s.select('text'); } + if(thisLabel.empty()) thisLabel = s.select('text'); + var bb = Plotly.Drawing.bBox(thisLabel.node()); lbbArray.push({ @@ -1578,15 +1594,15 @@ axes.doTicks = function(gd, axid, skipTitle) { top: 0, bottom: 10, height: 10, - left: x-bb.width/2, + left: x - bb.width / 2, // impose a 2px gap - right: x+bb.width/2 + 2, + right: x + bb.width / 2 + 2, width: bb.width + 2 }); }); - for(i=0; i0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)', + 'stroke-width': 0 + }) + .attr('d', path0 + 'Z'); + + corners = plotinfo.plot.append('path') + .attr('class', 'zoombox-corners') + .style({ + fill: Plotly.Color.background, + stroke: Plotly.Color.defaultLine, + 'stroke-width': 1, + opacity: 0 + }) + .attr('d','M0,0Z'); + + clearSelect(); + for(i = 0; i < allaxes.length; i++) forceNumbers(allaxes[i].range); + } + + function clearSelect() { + // until we get around to persistent selections, remove the outline + // here. The selection itself will be removed when the plot redraws + // at the end. + plotinfo.plot.selectAll('.select-outline').remove(); + } + + function zoomMove(dx0, dy0) { + var x1 = Math.max(0, Math.min(pw, dx0 + x0)), + y1 = Math.max(0, Math.min(ph, dy0 + y0)), + dx = Math.abs(x1 - x0), + dy = Math.abs(y1 - y0), + clen = Math.floor(Math.min(dy, dx, MINZOOM) / 2); + + box.l = Math.min(x0, x1); + box.r = Math.max(x0, x1); + box.t = Math.min(y0, y1); + box.b = Math.max(y0, y1); + + // look for small drags in one direction or the other, + // and only drag the other axis + if(!yActive || dy < Math.min(Math.max(dx * 0.6, MINDRAG), MINZOOM)) { + if(dx < MINDRAG) { + zoomMode = ''; + box.r = box.l; + box.t = box.b; + corners.attr('d', 'M0,0Z'); + } + else { + box.t = 0; + box.b = ph; + zoomMode = 'x'; + corners.attr('d', + 'M' + (box.l - 0.5) + ',' + (y0 - MINZOOM - 0.5) + + 'h-3v' + (2 * MINZOOM + 1) + 'h3ZM' + + (box.r + 0.5) + ',' + (y0 - MINZOOM - 0.5) + + 'h3v' + (2 * MINZOOM + 1) + 'h-3Z'); + } + } + else if(!xActive || dx < Math.min(dy * 0.6, MINZOOM)) { + box.l = 0; + box.r = pw; + zoomMode = 'y'; + corners.attr('d', + 'M' + (x0 - MINZOOM - 0.5) + ',' + (box.t - 0.5) + + 'v-3h' + (2 * MINZOOM + 1) + 'v3ZM' + + (x0 - MINZOOM - 0.5) + ',' + (box.b + 0.5) + + 'v3h' + (2 * MINZOOM + 1) + 'v-3Z'); + } + else { + zoomMode = 'xy'; + corners.attr('d', + 'M'+(box.l-3.5)+','+(box.t-0.5+clen)+'h3v'+(-clen)+ + 'h'+clen+'v-3h-'+(clen+3)+'ZM'+ + (box.r+3.5)+','+(box.t-0.5+clen)+'h-3v'+(-clen)+ + 'h'+(-clen)+'v-3h'+(clen+3)+'ZM'+ + (box.r+3.5)+','+(box.b+0.5-clen)+'h-3v'+clen+ + 'h'+(-clen)+'v3h'+(clen+3)+'ZM'+ + (box.l-3.5)+','+(box.b+0.5-clen)+'h3v'+clen+ + 'h'+clen+'v3h-'+(clen+3)+'Z'); + } + box.w = box.r - box.l; + box.h = box.b - box.t; + + // Not sure about the addition of window.scrollX/Y... + // seems to work but doesn't seem robust. + zb.attr('d', + path0+'M'+(box.l)+','+(box.t)+'v'+(box.h)+ + 'h'+(box.w)+'v-'+(box.h)+'h-'+(box.w)+'Z'); + if(!dimmed) { + zb.transition() + .style('fill', lum>0.2 ? 'rgba(0,0,0,0.4)' : + 'rgba(255,255,255,0.3)') + .duration(200); + corners.transition() + .style('opacity',1) + .duration(200); + dimmed = true; + } + } + + function zoomAxRanges(axList, r0Fraction, r1Fraction) { + var i, + axi, + axRange; + + for(i = 0; i < axList.length; i++) { + axi = axList[i]; + if(axi.fixedrange) continue; + + axRange = axi.range; + axi.range = [ + axRange[0] + (axRange[1] - axRange[0]) * r0Fraction, + axRange[0] + (axRange[1] - axRange[0]) * r1Fraction + ]; + } + } + + function zoomDone(dragged, numClicks) { + if(Math.min(box.h, box.w) < MINDRAG * 2) { + if(numClicks === 2) doubleClick(); + + return removeZoombox(gd); + } + + if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw); + if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph); + + removeZoombox(gd); + dragTail(zoomMode); + + if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { + Plotly.Lib.notifier('Double-click to
zoom back out','long'); + SHOWZOOMOUTTIP = false; + } + } + + function dragDone(dragged, numClicks) { + var singleEnd = (ns + ew).length === 1; + if(dragged) dragTail(); + else if(numClicks === 2 && !singleEnd) doubleClick(); + else if(numClicks === 1 && singleEnd) { + var ax = ns ? ya[0] : xa[0], + end = (ns==='s' || ew==='w') ? 0 : 1, + attrStr = ax._name + '.range[' + end + ']', + initialText = getEndText(ax, end), + hAlign = 'left', + vAlign = 'middle'; + + if(ax.fixedrange) return; + + if(ns) { + vAlign = (ns === 'n') ? 'top' : 'bottom'; + if(ax.side === 'right') hAlign = 'right'; + } + else if(ew === 'e') hAlign = 'right'; + + dragger3 + .call(Plotly.util.makeEditable, null, { + immediate: true, + background: fullLayout.paper_bgcolor, + text: String(initialText), + fill: ax.tickfont ? ax.tickfont.color : '#444', + horizontalAlign: hAlign, + verticalAlign: vAlign + }) + .on('edit', function(text) { + var v = ax.type==='category' ? ax.c2l(text) : ax.d2l(text); + if(v !== undefined) { + Plotly.relayout(gd, attrStr, v); + } + }); + } + } + + // scroll zoom, on all draggers except corners + var scrollViewBox = [0,0,pw,ph], + // wait a little after scrolling before redrawing + redrawTimer = null, + REDRAWDELAY = 300, + mainplot = plotinfo.mainplot ? + fullLayout._plots[plotinfo.mainplot] : plotinfo; + + function zoomWheel(e) { + // deactivate mousewheel scrolling on embedded graphs + // devs can override this with layout._enablescrollzoom, + // but _ ensures this setting won't leave their page + if(!gd._context.scrollZoom && !fullLayout._enablescrollzoom) { + return; + } + var pc = gd.querySelector('.plotly'); + + // if the plot has scrollbars (more than a tiny excess) + // disable scrollzoom too. + if(pc.scrollHeight-pc.clientHeight>10 || + pc.scrollWidth-pc.clientWidth>10) { + return; + } + + clearTimeout(redrawTimer); + + var wheelDelta = -e.deltaY; + if(!isFinite(wheelDelta)) wheelDelta = e.wheelDelta / 10; + if(!isFinite(wheelDelta)) { + console.log('did not find wheel motion attributes', e); + return; + } + + var zoom = Math.exp(-Math.min(Math.max(wheelDelta, -20), 20) / 100), + gbb = mainplot.draglayer.select('.nsewdrag') + .node().getBoundingClientRect(), + xfrac = (e.clientX - gbb.left) / gbb.width, + vbx0 = scrollViewBox[0] + scrollViewBox[2]*xfrac, + yfrac = (gbb.bottom - e.clientY)/gbb.height, + vby0 = scrollViewBox[1]+scrollViewBox[3]*(1-yfrac), + i; + + function zoomWheelOneAxis(ax, centerFraction, zoom) { + if(ax.fixedrange) return; + forceNumbers(ax.range); + var axRange = ax.range, + v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; + ax.range = [v0 + (axRange[0] - v0) * zoom, v0 + (axRange[1] - v0) * zoom]; + } + + if(ew) { + for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom); + scrollViewBox[2] *= zoom; + scrollViewBox[0] = vbx0 - scrollViewBox[2] * xfrac; + } + if(ns) { + for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom); + scrollViewBox[3] *= zoom; + scrollViewBox[1] = vby0 - scrollViewBox[3] * (1 - yfrac); + } + + // viewbox redraw at first + updateViewBoxes(scrollViewBox); + ticksAndAnnotations(ns,ew); + + // then replot after a delay to make sure + // no more scrolling is coming + redrawTimer = setTimeout(function() { + scrollViewBox = [0,0,pw,ph]; + dragTail(); + }, REDRAWDELAY); + + return Plotly.Lib.pauseEvent(e); + } + + // everything but the corners gets wheel zoom + if(ns.length*ew.length!==1) { + // still seems to be some confusion about onwheel vs onmousewheel... + if(dragger.onwheel!==undefined) dragger.onwheel = zoomWheel; + else if(dragger.onmousewheel!==undefined) dragger.onmousewheel = zoomWheel; + } + + // plotDrag: move the plot in response to a drag + function plotDrag(dx,dy) { + function dragAxList(axList, pix) { + for(var i = 0; i < axList.length; i++) { + var axi = axList[i]; + if(!axi.fixedrange) { + axi.range = [axi._r[0] - pix / axi._m, axi._r[1] - pix / axi._m]; + } + } + } + + if(xActive === 'ew' || yActive === 'ns') { + if(xActive) dragAxList(xa, dx); + if(yActive) dragAxList(ya, dy); + updateViewBoxes([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); + ticksAndAnnotations(yActive, xActive); + return; + } + + // common transform for dragging one end of an axis + // d>0 is compressing scale (cursor is over the plot, + // the axis end should move with the cursor) + // d<0 is expanding (cursor is off the plot, axis end moves + // nonlinearly so you can expand far) + function dZoom(d) { + return 1-((d>=0) ? Math.min(d,0.9) : + 1/(1/Math.max(d,-0.3)+3.222)); + } + + // dz: set a new value for one end (0 or 1) of an axis array ax, + // and return a pixel shift for that end for the viewbox + // based on pixel drag distance d + // TODO: this makes (generally non-fatal) errors when you get + // near floating point limits + function dz(ax, end, d) { + var otherEnd = 1 - end, + movedi = 0; + for(var i = 0; i < ax.length; i++) { + var axi = ax[i]; + if(axi.fixedrange) continue; + movedi = i; + axi.range[end] = axi._r[otherEnd] + + (axi._r[end] - axi._r[otherEnd]) / dZoom(d / axi._length); + } + return ax[movedi]._length * (ax[movedi]._r[end] - ax[movedi].range[end]) / + (ax[movedi]._r[end] - ax[movedi]._r[otherEnd]); + } + + if(xActive === 'w') dx = dz(xa, 0, dx); + else if(xActive === 'e') dx = dz(xa, 1, -dx); + else if(!xActive) dx = 0; + + if(yActive === 'n') dy = dz(ya, 1, dy); + else if(yActive === 's') dy = dz(ya, 0, -dy); + else if(!yActive) dy = 0; + + updateViewBoxes([ + (xActive === 'w') ? dx : 0, + (yActive === 'n') ? dy : 0, + pw - dx, + ph - dy + ]); + ticksAndAnnotations(yActive, xActive); + } + + function ticksAndAnnotations(ns, ew) { + var activeAxIds = [], + i; + + function pushActiveAxIds(axList) { + for(i = 0; i < axList.length; i++) { + if(!axList[i].fixedrange) activeAxIds.push(axList[i]._id); + } + } + + if(ew) pushActiveAxIds(xa); + if(ns) pushActiveAxIds(ya); + + for(i = 0; i < activeAxIds.length; i++) { + Plotly.Axes.doTicks(gd, activeAxIds[i], true); + } + + function redrawObjs(objArray, module) { + var obji; + for(i = 0; i < objArray.length; i++) { + obji = objArray[i]; + if((ew && activeAxIds.indexOf(obji.xref) !== -1) || + (ns && activeAxIds.indexOf(obji.yref) !== -1)) { + module.draw(gd, i); + } + } + } + + redrawObjs(fullLayout.annotations || [], Plotly.Annotations); + redrawObjs(fullLayout.shapes || [], Plotly.Shapes); + } + + function doubleClick() { + var doubleClickConfig = gd._context.doubleClick, + axList = (xActive ? xa : []).concat(yActive ? ya : []), + attrs = {}; + + var ax, i; + + if(doubleClickConfig === 'autosize') { + for(i = 0; i < axList.length; i++) { + ax = axList[i]; + if(!ax.fixedrange) attrs[ax._name + '.autorange'] = true; + } + } + else if(doubleClickConfig === 'reset') { + for(i = 0; i < axList.length; i++) { + ax = axList[i]; + + if(!ax._rangeInitial) { + attrs[ax._name + '.autorange'] = true; + } + else { + attrs[ax._name + '.range'] = ax._rangeInitial.slice(); + } + } + } + else if(doubleClickConfig === 'reset+autosize') { + for(i = 0; i < axList.length; i++) { + ax = axList[i]; + + if(ax.fixedrange) continue; + if(ax._rangeInitial === undefined || + ax.range[0] === ax._rangeInitial[0] && + ax.range[1] === ax._rangeInitial[1] + ) { + attrs[ax._name + '.autorange'] = true; + } + else attrs[ax._name + '.range'] = ax._rangeInitial.slice(); + } + } + + gd.emit('plotly_doubleclick', null); + Plotly.relayout(gd, attrs); + } + + // dragTail - finish a drag event with a redraw + function dragTail(zoommode) { + var attrs = {}; + // revert to the previous axis settings, then apply the new ones + // through relayout - this lets relayout manage undo/redo + for(var i = 0; i < allaxes.length; i++) { + var axi = allaxes[i]; + if(zoommode && zoommode.indexOf(axi._id.charAt(0))===-1) { + continue; + } + if(axi._r[0] !== axi.range[0]) attrs[axi._name+'.range[0]'] = axi.range[0]; + if(axi._r[1] !== axi.range[1]) attrs[axi._name+'.range[1]'] = axi.range[1]; + + axi.range=axi._r.slice(); + } + + updateViewBoxes([0,0,pw,ph]); + Plotly.relayout(gd,attrs); + } + + // updateViewBoxes - find all plot viewboxes that should be + // affected by this drag, and update them. look for all plots + // sharing an affected axis (including the one being dragged) + function updateViewBoxes(viewBox) { + var plotinfos = fullLayout._plots, + subplots = Object.keys(plotinfos), + i, + plotinfo2, + xa2, + ya2, + editX, + editY; + + for(i = 0; i < subplots.length; i++) { + plotinfo2 = plotinfos[subplots[i]]; + xa2 = plotinfo2.x(); + ya2 = plotinfo2.y(); + editX = ew && xa.indexOf(xa2)!==-1 && !xa2.fixedrange; + editY = ns && ya.indexOf(ya2)!==-1 && !ya2.fixedrange; + + if(editX || editY) { + var newVB = [0,0,xa2._length,ya2._length]; + if(editX) { + newVB[0] = viewBox[0]; + newVB[2] = viewBox[2]; + } + if(editY) { + newVB[1] = viewBox[1]; + newVB[3] = viewBox[3]; + } + plotinfo2.plot.attr('viewBox',newVB.join(' ')); + } + } + } + + return dragger; +}; + +function getEndText(ax, end) { + var initialVal = ax.range[end], + diff = Math.abs(initialVal - ax.range[1 - end]), + dig; + + if(ax.type === 'date') { + return Plotly.Lib.ms2DateTime(initialVal, diff); + } + else if(ax.type==='log') { + dig = Math.ceil(Math.max(0, -Math.log(diff) / Math.LN10)) + 3; + return d3.format('.' + dig + 'g')(Math.pow(10, initialVal)); + } + else { // linear numeric (or category... but just show numbers here) + dig = Math.floor(Math.log(Math.abs(initialVal)) / Math.LN10) - + Math.floor(Math.log(diff) / Math.LN10) + 4; + return d3.format('.'+String(dig)+'g')(initialVal); + } +} + +function getDragCursor(nsew, dragmode) { + if(!nsew) return 'pointer'; + if(nsew === 'nsew') { + if(dragmode === 'pan') return 'move'; + return 'crosshair'; + } + return nsew.toLowerCase() + '-resize'; +} + +function removeZoombox(gd) { + d3.select(gd) + .selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners') + .remove(); +} diff --git a/src/plots/cartesian/graph_interact.js b/src/plots/cartesian/graph_interact.js index 2fe124521e6..8ff557f7674 100644 --- a/src/plots/cartesian/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -17,11 +17,16 @@ var Plotly = require('../../plotly'); var Lib = require('../../lib'); var Events = require('../../lib/events'); -var prepSelect = require('./select'); var constants = require('./constants'); +var dragBox = require('./dragbox'); +var dragElement = require('../../components/dragelement'); var fx = module.exports = {}; +// TODO remove this in version 2.0 +// copy on Fx for backward compatible +fx.unhover = dragElement.unhover; + fx.layoutAttributes = { dragmode: { valType: 'enumerated', @@ -137,7 +142,7 @@ fx.init = function(gd) { maindrag.onmouseout = function(evt) { if(gd._dragging) return; - fx.unhover(gd, evt); + dragElement.unhover(gd, evt); }; maindrag.onclick = function(evt) { @@ -289,16 +294,6 @@ fx.hover = function(gd, evt, subplot) { }, constants.HOVERMINTIME); }; -fx.unhover = function(gd, evt, subplot) { - if(typeof gd === 'string') gd = document.getElementById(gd); - // Important, clear any queued hovers - if(gd._hoverTimer) { - clearTimeout(gd._hoverTimer); - gd._hoverTimer = undefined; - } - unhover(gd,evt,subplot); -}; - // The actual implementation is here: function hover(gd, evt, subplot) { @@ -322,16 +317,20 @@ function hover(gd, evt, subplot) { .map(function(pi) { return pi.id; })), xaArray = subplots.map(function(spId) { + var ternary = (gd._fullLayout[spId] || {})._ternary; + if(ternary) return ternary.xaxis; return Plotly.Axes.getFromId(gd, spId, 'x'); }), yaArray = subplots.map(function(spId) { + var ternary = (gd._fullLayout[spId] || {})._ternary; + if(ternary) return ternary.yaxis; return Plotly.Axes.getFromId(gd, spId, 'y'); }), hovermode = evt.hovermode || fullLayout.hovermode; if(['x','y','closest'].indexOf(hovermode)===-1 || !gd.calcdata || gd.querySelector('.zoombox') || gd._dragging) { - return unhover(gd, evt); + return dragElement.unhoverRaw(gd, evt); } // hoverData: the set of candidate points we've found to highlight @@ -375,7 +374,7 @@ function hover(gd, evt, subplot) { for(curvenum = 0; curvenumdbb.width || ypx<0 || ypx>dbb.height) { - return unhover(gd,evt); + return dragElement.unhoverRaw(gd,evt); } } else { @@ -422,7 +421,7 @@ function hover(gd, evt, subplot) { if(!isNumeric(xvalArray[0]) || !isNumeric(yvalArray[0])) { console.log('Plotly.Fx.hover failed', evt, gd); - return unhover(gd, evt); + return dragElement.unhoverRaw(gd, evt); } } @@ -440,7 +439,7 @@ function hover(gd, evt, subplot) { if(!cd || !cd[0] || !cd[0].trace || cd[0].trace.visible !== true) continue; trace = cd[0].trace; - subploti = subplots.indexOf(trace.xaxis + trace.yaxis); + subploti = subplots.indexOf(getSubplot(trace)); // within one trace mode can sometimes be overridden mode = hovermode; @@ -521,7 +520,7 @@ function hover(gd, evt, subplot) { } // nothing left: remove all labels and quit - if(hoverData.length===0) return unhover(gd,evt); + if(hoverData.length===0) return dragElement.unhoverRaw(gd,evt); // if there's more than one horz bar trace, // rotate the labels so they don't overlap @@ -529,10 +528,15 @@ function hover(gd, evt, subplot) { hoverData.sort(function(d1, d2) { return d1.distance - d2.distance; }); + var bgColor = Plotly.Color.combine( + fullLayout.plot_bgcolor || Plotly.Color.background, + fullLayout.paper_bgcolor + ); + var labelOpts = { hovermode: hovermode, rotateLabels: rotateLabels, - bgColor: Plotly.Color.combine(fullLayout.plot_bgcolor, fullLayout.paper_bgcolor), + bgColor: bgColor, container: fullLayout._hoverlayer, outerContainer: fullLayout._paperdiv }; @@ -584,6 +588,12 @@ function hover(gd, evt, subplot) { }); } +// look for either .subplot (currently just ternary) +// or xaxis and yaxis attributes +function getSubplot(trace) { + return trace.subplot || (trace.xaxis + trace.yaxis); +} + fx.getDistanceFunction = function(mode, dx, dy, dxy) { if(mode==='closest') return dxy || quadrature(dx, dy); return mode==='x' ? dx : dy; @@ -924,6 +934,13 @@ function createHoverText(hoverData, opts) { if(name.length>15) name = name.substr(0,12)+'...'; } + // used by other modules (initially just ternary) that + // manage their own hoverinfo independent of cleanPoint + // the rest of this will still apply, so such modules + // can still put things in (x|y|z)Label, text, and name + // and hoverinfo will still determine their visibility + if(d.extraText!==undefined) text += d.extraText; + if(d.zLabel!==undefined) { if(d.xLabel!==undefined) text += 'x: ' + d.xLabel + '
'; if(d.yLabel!==undefined) text += 'y: ' + d.yLabel + '
'; @@ -1276,28 +1293,6 @@ function hoverChanged(gd, evt, oldhoverdata) { return false; } -// remove hover effects on mouse out, and emit unhover event -function unhover(gd, evt, subplot) { - if(subplot === 'pie') { - gd.emit('plotly_unhover', { - points: [evt] - }); - return; - } - - var fullLayout = gd._fullLayout; - if(!evt) evt = {}; - if(evt.target && - Events.triggerHandler(gd, 'plotly_beforehover', evt) === false) { - return; - } - fullLayout._hoverlayer.selectAll('g').remove(); - if(evt.target && gd._hoverdata) { - gd.emit('plotly_unhover', {points: gd._hoverdata}); - } - gd._hoverdata = undefined; -} - // on click fx.click = function(gd,evt) { if(gd._hoverdata && evt && evt.target) { @@ -1308,831 +1303,6 @@ fx.click = function(gd,evt) { }; -// ---------------------------------------------------- -// Axis dragging functions -// ---------------------------------------------------- - -function getDragCursor(nsew, dragmode) { - if(!nsew) return 'pointer'; - if(nsew === 'nsew') { - if(dragmode === 'pan') return 'move'; - return 'crosshair'; - } - return nsew.toLowerCase() + '-resize'; -} - -// flag for showing "doubleclick to zoom out" only at the beginning -var SHOWZOOMOUTTIP = true; - -// dragBox: create an element to drag one or more axis ends -// inputs: -// plotinfo - which subplot are we making dragboxes on? -// x,y,w,h - left, top, width, height of the box -// ns - how does this drag the vertical axis? -// 'n' - top only -// 's' - bottom only -// 'ns' - top and bottom together, difference unchanged -// ew - same for horizontal axis -function dragBox(gd, plotinfo, x, y, w, h, ns, ew) { - // mouseDown stores ms of first mousedown event in the last - // DBLCLICKDELAY ms on the drag bars - // numClicks stores how many mousedowns have been seen - // within DBLCLICKDELAY so we can check for click or doubleclick events - // dragged stores whether a drag has occurred, so we don't have to - // redraw unnecessarily, ie if no move bigger than MINDRAG or MINZOOM px - var fullLayout = gd._fullLayout, - // if we're dragging two axes at once, also drag overlays - subplots = [plotinfo].concat((ns && ew) ? plotinfo.overlays : []), - xa = [plotinfo.x()], - ya = [plotinfo.y()], - pw = xa[0]._length, - ph = ya[0]._length, - MINDRAG = constants.MINDRAG, - MINZOOM = constants.MINZOOM, - i, - subplotXa, - subplotYa; - - for(i = 1; i < subplots.length; i++) { - subplotXa = subplots[i].x(); - subplotYa = subplots[i].y(); - if(xa.indexOf(subplotXa) === -1) xa.push(subplotXa); - if(ya.indexOf(subplotYa) === -1) ya.push(subplotYa); - } - - function isDirectionActive(axList, activeVal) { - for(i = 0; i < axList.length; i++) { - if(!axList[i].fixedrange) return activeVal; - } - return ''; - } - - var allaxes = xa.concat(ya), - xActive = isDirectionActive(xa, ew), - yActive = isDirectionActive(ya, ns), - cursor = getDragCursor(yActive + xActive, fullLayout.dragmode), - dragClass = ns + ew + 'drag'; - - var dragger3 = plotinfo.draglayer.selectAll('.' + dragClass).data([0]); - dragger3.enter().append('rect') - .classed('drag', true) - .classed(dragClass, true) - .style({fill: 'transparent', 'stroke-width': 0}) - .attr('data-subplot', plotinfo.id); - dragger3.call(Plotly.Drawing.setRect, x, y, w, h) - .call(fx.setCursor,cursor); - var dragger = dragger3.node(); - - // still need to make the element if the axes are disabled - // but nuke its events (except for maindrag which needs them for hover) - // and stop there - if(!yActive && !xActive) { - dragger.onmousedown = null; - dragger.style.pointerEvents = (ns + ew === 'nsew') ? 'all' : 'none'; - return dragger; - } - - function forceNumbers(axRange) { - axRange[0] = Number(axRange[0]); - axRange[1] = Number(axRange[1]); - } - - var dragOptions = { - element: dragger, - gd: gd, - plotinfo: plotinfo, - xaxes: xa, - yaxes: ya, - doubleclick: doubleClick, - prepFn: function(e, startX, startY) { - var dragModeNow = gd._fullLayout.dragmode; - if(ns + ew === 'nsew') { - // main dragger handles all drag modes, and changes - // to pan (or to zoom if it already is pan) on shift - if(e.shiftKey) { - if(dragModeNow === 'pan') dragModeNow = 'zoom'; - else dragModeNow = 'pan'; - } - } - // all other draggers just pan - else dragModeNow = 'pan'; - - if(dragModeNow === 'lasso') dragOptions.minDrag = 1; - else dragOptions.minDrag = undefined; - - if(dragModeNow === 'zoom') { - dragOptions.moveFn = zoomMove; - dragOptions.doneFn = zoomDone; - zoomPrep(e, startX, startY); - } - else if(dragModeNow === 'pan') { - dragOptions.moveFn = plotDrag; - dragOptions.doneFn = dragDone; - clearSelect(); - } - else if(dragModeNow === 'select' || dragModeNow === 'lasso') { - prepSelect(e, startX, startY, dragOptions, dragModeNow); - } - } - }; - - fx.dragElement(dragOptions); - - var x0, - y0, - box, - lum, - path0, - dimmed, - zoomMode, - zb, - corners; - - function zoomPrep(e, startX, startY) { - var dragBBox = dragger.getBoundingClientRect(); - x0 = startX - dragBBox.left; - y0 = startY - dragBBox.top; - box = {l: x0, r: x0, w: 0, t: y0, b: y0, h: 0}; - lum = gd._hmpixcount ? - (gd._hmlumcount / gd._hmpixcount) : - tinycolor(gd._fullLayout.plot_bgcolor).getLuminance(); - path0 = path0 = 'M0,0H'+pw+'V'+ph+'H0V0'; - dimmed = false; - zoomMode = 'xy'; - - zb = plotinfo.plot.append('path') - .attr('class', 'zoombox') - .style({ - 'fill': lum>0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)', - 'stroke-width': 0 - }) - .attr('d', path0 + 'Z'); - - corners = plotinfo.plot.append('path') - .attr('class', 'zoombox-corners') - .style({ - fill: Plotly.Color.background, - stroke: Plotly.Color.defaultLine, - 'stroke-width': 1, - opacity: 0 - }) - .attr('d','M0,0Z'); - - clearSelect(); - for(i = 0; i < allaxes.length; i++) forceNumbers(allaxes[i].range); - } - - function clearSelect() { - // until we get around to persistent selections, remove the outline - // here. The selection itself will be removed when the plot redraws - // at the end. - plotinfo.plot.selectAll('.select-outline').remove(); - } - - function zoomMove(dx0, dy0) { - var x1 = Math.max(0, Math.min(pw, dx0 + x0)), - y1 = Math.max(0, Math.min(ph, dy0 + y0)), - dx = Math.abs(x1 - x0), - dy = Math.abs(y1 - y0), - clen = Math.floor(Math.min(dy, dx, MINZOOM) / 2); - - box.l = Math.min(x0, x1); - box.r = Math.max(x0, x1); - box.t = Math.min(y0, y1); - box.b = Math.max(y0, y1); - - // look for small drags in one direction or the other, - // and only drag the other axis - if(!yActive || dy < Math.min(Math.max(dx * 0.6, MINDRAG), MINZOOM)) { - if(dx < MINDRAG) { - zoomMode = ''; - box.r = box.l; - box.t = box.b; - corners.attr('d', 'M0,0Z'); - } - else { - box.t = 0; - box.b = ph; - zoomMode = 'x'; - corners.attr('d', - 'M' + (box.l - 0.5) + ',' + (y0 - MINZOOM - 0.5) + - 'h-3v' + (2 * MINZOOM + 1) + 'h3ZM' + - (box.r + 0.5) + ',' + (y0 - MINZOOM - 0.5) + - 'h3v' + (2 * MINZOOM + 1) + 'h-3Z'); - } - } - else if(!xActive || dx < Math.min(dy * 0.6, MINZOOM)) { - box.l = 0; - box.r = pw; - zoomMode = 'y'; - corners.attr('d', - 'M' + (x0 - MINZOOM - 0.5) + ',' + (box.t - 0.5) + - 'v-3h' + (2 * MINZOOM + 1) + 'v3ZM' + - (x0 - MINZOOM - 0.5) + ',' + (box.b + 0.5) + - 'v3h' + (2 * MINZOOM + 1) + 'v-3Z'); - } - else { - zoomMode = 'xy'; - corners.attr('d', - 'M'+(box.l-3.5)+','+(box.t-0.5+clen)+'h3v'+(-clen)+ - 'h'+clen+'v-3h-'+(clen+3)+'ZM'+ - (box.r+3.5)+','+(box.t-0.5+clen)+'h-3v'+(-clen)+ - 'h'+(-clen)+'v-3h'+(clen+3)+'ZM'+ - (box.r+3.5)+','+(box.b+0.5-clen)+'h-3v'+clen+ - 'h'+(-clen)+'v3h'+(clen+3)+'ZM'+ - (box.l-3.5)+','+(box.b+0.5-clen)+'h3v'+clen+ - 'h'+clen+'v3h-'+(clen+3)+'Z'); - } - box.w = box.r - box.l; - box.h = box.b - box.t; - - // Not sure about the addition of window.scrollX/Y... - // seems to work but doesn't seem robust. - zb.attr('d', - path0+'M'+(box.l)+','+(box.t)+'v'+(box.h)+ - 'h'+(box.w)+'v-'+(box.h)+'h-'+(box.w)+'Z'); - if(!dimmed) { - zb.transition() - .style('fill', lum>0.2 ? 'rgba(0,0,0,0.4)' : - 'rgba(255,255,255,0.3)') - .duration(200); - corners.transition() - .style('opacity',1) - .duration(200); - dimmed = true; - } - } - - function zoomAxRanges(axList, r0Fraction, r1Fraction) { - var i, - axi, - axRange; - - for(i = 0; i < axList.length; i++) { - axi = axList[i]; - if(axi.fixedrange) continue; - - axRange = axi.range; - axi.range = [ - axRange[0] + (axRange[1] - axRange[0]) * r0Fraction, - axRange[0] + (axRange[1] - axRange[0]) * r1Fraction - ]; - } - } - - function zoomDone(dragged, numClicks) { - if(Math.min(box.h, box.w) < MINDRAG * 2) { - if(numClicks === 2) doubleClick(); - - return removeZoombox(gd); - } - - if(zoomMode === 'xy' || zoomMode === 'x') zoomAxRanges(xa, box.l / pw, box.r / pw); - if(zoomMode === 'xy' || zoomMode === 'y') zoomAxRanges(ya, (ph - box.b) / ph, (ph - box.t) / ph); - - removeZoombox(gd); - dragTail(zoomMode); - - if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { - Plotly.Lib.notifier('Double-click to
zoom back out','long'); - SHOWZOOMOUTTIP = false; - } - } - - function dragDone(dragged, numClicks) { - var singleEnd = (ns + ew).length === 1; - if(dragged) dragTail(); - else if(numClicks === 2 && !singleEnd) doubleClick(); - else if(numClicks === 1 && singleEnd) { - var ax = ns ? ya[0] : xa[0], - end = (ns==='s' || ew==='w') ? 0 : 1, - attrStr = ax._name + '.range[' + end + ']', - initialText = getEndText(ax, end), - hAlign = 'left', - vAlign = 'middle'; - - if(ax.fixedrange) return; - - if(ns) { - vAlign = (ns === 'n') ? 'top' : 'bottom'; - if(ax.side === 'right') hAlign = 'right'; - } - else if(ew === 'e') hAlign = 'right'; - - dragger3 - .call(Plotly.util.makeEditable, null, { - immediate: true, - background: fullLayout.paper_bgcolor, - text: String(initialText), - fill: ax.tickfont ? ax.tickfont.color : '#444', - horizontalAlign: hAlign, - verticalAlign: vAlign - }) - .on('edit', function(text) { - var v = ax.type==='category' ? ax.c2l(text) : ax.d2l(text); - if(v !== undefined) { - Plotly.relayout(gd, attrStr, v); - } - }); - } - } - - // scroll zoom, on all draggers except corners - var scrollViewBox = [0,0,pw,ph], - // wait a little after scrolling before redrawing - redrawTimer = null, - REDRAWDELAY = 300, - mainplot = plotinfo.mainplot ? - fullLayout._plots[plotinfo.mainplot] : plotinfo; - - function zoomWheel(e) { - // deactivate mousewheel scrolling on embedded graphs - // devs can override this with layout._enablescrollzoom, - // but _ ensures this setting won't leave their page - if(!gd._context.scrollZoom && !fullLayout._enablescrollzoom) { - return; - } - var pc = gd.querySelector('.plotly'); - - // if the plot has scrollbars (more than a tiny excess) - // disable scrollzoom too. - if(pc.scrollHeight-pc.clientHeight>10 || - pc.scrollWidth-pc.clientWidth>10) { - return; - } - - clearTimeout(redrawTimer); - - var wheelDelta = -e.deltaY; - if(!isFinite(wheelDelta)) wheelDelta = e.wheelDelta / 10; - if(!isFinite(wheelDelta)) { - console.log('did not find wheel motion attributes', e); - return; - } - - var zoom = Math.exp(-Math.min(Math.max(wheelDelta, -20), 20) / 100), - gbb = mainplot.draglayer.select('.nsewdrag') - .node().getBoundingClientRect(), - xfrac = (e.clientX - gbb.left) / gbb.width, - vbx0 = scrollViewBox[0] + scrollViewBox[2]*xfrac, - yfrac = (gbb.bottom - e.clientY)/gbb.height, - vby0 = scrollViewBox[1]+scrollViewBox[3]*(1-yfrac), - i; - - function zoomWheelOneAxis(ax, centerFraction, zoom) { - if(ax.fixedrange) return; - forceNumbers(ax.range); - var axRange = ax.range, - v0 = axRange[0] + (axRange[1] - axRange[0]) * centerFraction; - ax.range = [v0 + (axRange[0] - v0) * zoom, v0 + (axRange[1] - v0) * zoom]; - } - - if(ew) { - for(i = 0; i < xa.length; i++) zoomWheelOneAxis(xa[i], xfrac, zoom); - scrollViewBox[2] *= zoom; - scrollViewBox[0] = vbx0 - scrollViewBox[2] * xfrac; - } - if(ns) { - for(i = 0; i < ya.length; i++) zoomWheelOneAxis(ya[i], yfrac, zoom); - scrollViewBox[3] *= zoom; - scrollViewBox[1] = vby0 - scrollViewBox[3] * (1 - yfrac); - } - - // viewbox redraw at first - updateViewBoxes(scrollViewBox); - ticksAndAnnotations(ns,ew); - - // then replot after a delay to make sure - // no more scrolling is coming - redrawTimer = setTimeout(function() { - scrollViewBox = [0,0,pw,ph]; - dragTail(); - }, REDRAWDELAY); - - return Plotly.Lib.pauseEvent(e); - } - - // everything but the corners gets wheel zoom - if(ns.length*ew.length!==1) { - // still seems to be some confusion about onwheel vs onmousewheel... - if(dragger.onwheel!==undefined) dragger.onwheel = zoomWheel; - else if(dragger.onmousewheel!==undefined) dragger.onmousewheel = zoomWheel; - } - - // plotDrag: move the plot in response to a drag - function plotDrag(dx,dy) { - function dragAxList(axList, pix) { - for(var i = 0; i < axList.length; i++) { - var axi = axList[i]; - if(!axi.fixedrange) { - axi.range = [axi._r[0] - pix / axi._m, axi._r[1] - pix / axi._m]; - } - } - } - - if(xActive === 'ew' || yActive === 'ns') { - if(xActive) dragAxList(xa, dx); - if(yActive) dragAxList(ya, dy); - updateViewBoxes([xActive ? -dx : 0, yActive ? -dy : 0, pw, ph]); - ticksAndAnnotations(yActive, xActive); - return; - } - - // common transform for dragging one end of an axis - // d>0 is compressing scale (cursor is over the plot, - // the axis end should move with the cursor) - // d<0 is expanding (cursor is off the plot, axis end moves - // nonlinearly so you can expand far) - function dZoom(d) { - return 1-((d>=0) ? Math.min(d,0.9) : - 1/(1/Math.max(d,-0.3)+3.222)); - } - - // dz: set a new value for one end (0 or 1) of an axis array ax, - // and return a pixel shift for that end for the viewbox - // based on pixel drag distance d - // TODO: this makes (generally non-fatal) errors when you get - // near floating point limits - function dz(ax, end, d) { - var otherEnd = 1 - end, - movedi = 0; - for(var i = 0; i < ax.length; i++) { - var axi = ax[i]; - if(axi.fixedrange) continue; - movedi = i; - axi.range[end] = axi._r[otherEnd] + - (axi._r[end] - axi._r[otherEnd]) / dZoom(d / axi._length); - } - return ax[movedi]._length * (ax[movedi]._r[end] - ax[movedi].range[end]) / - (ax[movedi]._r[end] - ax[movedi]._r[otherEnd]); - } - - if(xActive === 'w') dx = dz(xa, 0, dx); - else if(xActive === 'e') dx = dz(xa, 1, -dx); - else if(!xActive) dx = 0; - - if(yActive === 'n') dy = dz(ya, 1, dy); - else if(yActive === 's') dy = dz(ya, 0, -dy); - else if(!yActive) dy = 0; - - updateViewBoxes([ - (xActive === 'w') ? dx : 0, - (yActive === 'n') ? dy : 0, - pw - dx, - ph - dy - ]); - ticksAndAnnotations(yActive, xActive); - } - - function ticksAndAnnotations(ns, ew) { - var activeAxIds = [], - i; - - function pushActiveAxIds(axList) { - for(i = 0; i < axList.length; i++) { - if(!axList[i].fixedrange) activeAxIds.push(axList[i]._id); - } - } - - if(ew) pushActiveAxIds(xa); - if(ns) pushActiveAxIds(ya); - - for(i = 0; i < activeAxIds.length; i++) { - Plotly.Axes.doTicks(gd, activeAxIds[i], true); - } - - function redrawObjs(objArray, module) { - var obji; - for(i = 0; i < objArray.length; i++) { - obji = objArray[i]; - if((ew && activeAxIds.indexOf(obji.xref) !== -1) || - (ns && activeAxIds.indexOf(obji.yref) !== -1)) { - module.draw(gd, i); - } - } - } - - redrawObjs(fullLayout.annotations || [], Plotly.Annotations); - redrawObjs(fullLayout.shapes || [], Plotly.Shapes); - } - - function doubleClick() { - var doubleClickConfig = gd._context.doubleClick, - axList = (xActive ? xa : []).concat(yActive ? ya : []), - attrs = {}; - - var ax, i; - - if(doubleClickConfig === 'autosize') { - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - if(!ax.fixedrange) attrs[ax._name + '.autorange'] = true; - } - } - else if(doubleClickConfig === 'reset') { - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - - if(!ax._rangeInitial) { - attrs[ax._name + '.autorange'] = true; - } - else { - attrs[ax._name + '.range'] = ax._rangeInitial.slice(); - } - } - } - else if(doubleClickConfig === 'reset+autosize') { - for(i = 0; i < axList.length; i++) { - ax = axList[i]; - - if(ax.fixedrange) continue; - if(ax._rangeInitial === undefined || - ax.range[0] === ax._rangeInitial[0] && - ax.range[1] === ax._rangeInitial[1] - ) { - attrs[ax._name + '.autorange'] = true; - } - else attrs[ax._name + '.range'] = ax._rangeInitial.slice(); - } - } - - gd.emit('plotly_doubleclick', null); - Plotly.relayout(gd, attrs); - } - - // dragTail - finish a drag event with a redraw - function dragTail(zoommode) { - var attrs = {}; - // revert to the previous axis settings, then apply the new ones - // through relayout - this lets relayout manage undo/redo - for(var i = 0; i < allaxes.length; i++) { - var axi = allaxes[i]; - if(zoommode && zoommode.indexOf(axi._id.charAt(0))===-1) { - continue; - } - if(axi._r[0] !== axi.range[0]) attrs[axi._name+'.range[0]'] = axi.range[0]; - if(axi._r[1] !== axi.range[1]) attrs[axi._name+'.range[1]'] = axi.range[1]; - - axi.range=axi._r.slice(); - } - - updateViewBoxes([0,0,pw,ph]); - Plotly.relayout(gd,attrs); - } - - // updateViewBoxes - find all plot viewboxes that should be - // affected by this drag, and update them. look for all plots - // sharing an affected axis (including the one being dragged) - function updateViewBoxes(viewBox) { - var plotinfos = fullLayout._plots, - subplots = Object.keys(plotinfos), - i, - plotinfo2, - xa2, - ya2, - editX, - editY; - - for(i = 0; i < subplots.length; i++) { - plotinfo2 = plotinfos[subplots[i]]; - xa2 = plotinfo2.x(); - ya2 = plotinfo2.y(); - editX = ew && xa.indexOf(xa2)!==-1 && !xa2.fixedrange; - editY = ns && ya.indexOf(ya2)!==-1 && !ya2.fixedrange; - - if(editX || editY) { - var newVB = [0,0,xa2._length,ya2._length]; - if(editX) { - newVB[0] = viewBox[0]; - newVB[2] = viewBox[2]; - } - if(editY) { - newVB[1] = viewBox[1]; - newVB[3] = viewBox[3]; - } - plotinfo2.plot.attr('viewBox',newVB.join(' ')); - } - } - } - - return dragger; -} - -function getEndText(ax, end) { - var initialVal = ax.range[end], - diff = Math.abs(initialVal - ax.range[1 - end]), - dig; - - if(ax.type === 'date') { - return Plotly.Lib.ms2DateTime(initialVal, diff); - } - else if(ax.type==='log') { - dig = Math.ceil(Math.max(0, -Math.log(diff) / Math.LN10)) + 3; - return d3.format('.' + dig + 'g')(Math.pow(10, initialVal)); - } - else { // linear numeric (or category... but just show numbers here) - dig = Math.floor(Math.log(Math.abs(initialVal)) / Math.LN10) - - Math.floor(Math.log(diff) / Math.LN10) + 4; - return d3.format('.'+String(dig)+'g')(initialVal); - } -} - -function finishDrag(gd) { - gd._dragging = false; - if(gd._replotPending) Plotly.plot(gd); -} - -function removeZoombox(gd) { - d3.select(gd) - .selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners') - .remove(); -} - -// for automatic alignment on dragging, <1/3 means left align, -// >2/3 means right, and between is center. Pick the right fraction -// based on where you are, and return the fraction corresponding to -// that position on the object -fx.dragAlign = function(v, dv, v0, v1, anchor) { - var vmin = (v-v0)/(v1-v0), - vmax = vmin+dv/(v1-v0), - vc = (vmin+vmax)/2; - - // explicitly specified anchor - if(anchor==='left' || anchor==='bottom') return vmin; - if(anchor==='center' || anchor==='middle') return vc; - if(anchor==='right' || anchor==='top') return vmax; - - // automatic based on position - if(vmin<(2/3)-vc) return vmin; - if(vmax>(4/3)-vc) return vmax; - return vc; -}; - - -// set cursors pointing toward the closest corner/side, -// to indicate alignment -// x and y are 0-1, fractions of the plot area -var cursorset = [['sw-resize','s-resize','se-resize'], - ['w-resize','move','e-resize'], - ['nw-resize','n-resize','ne-resize']]; -fx.dragCursors = function(x,y,xanchor,yanchor) { - if(xanchor==='left') x=0; - else if(xanchor==='center') x=1; - else if(xanchor==='right') x=2; - else x = Plotly.Lib.constrain(Math.floor(x*3),0,2); - - if(yanchor==='bottom') y=0; - else if(yanchor==='middle') y=1; - else if(yanchor==='top') y=2; - else y = Plotly.Lib.constrain(Math.floor(y*3),0,2); - - return cursorset[y][x]; -}; - -/** - * Abstracts click & drag interactions - * @param {object} options with keys: - * element (required) the DOM element to drag - * prepFn (optional) function(event, startX, startY) - * executed on mousedown - * startX and startY are the clientX and clientY pixel position - * of the mousedown event - * moveFn (optional) function(dx, dy, dragged) - * executed on move - * dx and dy are the net pixel offset of the drag, - * dragged is true/false, has the mouse moved enough to - * constitute a drag - * doneFn (optional) function(dragged, numClicks) - * executed on mouseup, or mouseout of window since - * we don't get events after that - * dragged is as in moveFn - * numClicks is how many clicks we've registered within - * a doubleclick time - */ -fx.dragElement = function(options) { - var gd = Plotly.Lib.getPlotDiv(options.element) || {}, - numClicks = 1, - DBLCLICKDELAY = constants.DBLCLICKDELAY, - startX, - startY, - newMouseDownTime, - dragCover, - initialTarget; - - if(!gd._mouseDownTime) gd._mouseDownTime = 0; - - function onStart(e) { - // make dragging and dragged into properties of gd - // so that others can look at and modify them - gd._dragged = false; - gd._dragging = true; - startX = e.clientX; - startY = e.clientY; - initialTarget = e.target; - - newMouseDownTime = (new Date()).getTime(); - if(newMouseDownTime - gd._mouseDownTime < DBLCLICKDELAY) { - // in a click train - numClicks += 1; - } - else { - // new click train - numClicks = 1; - gd._mouseDownTime = newMouseDownTime; - } - - if(options.prepFn) options.prepFn(e, startX, startY); - - dragCover = coverSlip(); - - dragCover.onmousemove = onMove; - dragCover.onmouseup = onDone; - dragCover.onmouseout = onDone; - - dragCover.style.cursor = window.getComputedStyle(options.element).cursor; - - return Plotly.Lib.pauseEvent(e); - } - - function onMove(e) { - var dx = e.clientX - startX, - dy = e.clientY - startY, - minDrag = options.minDrag || constants.MINDRAG; - - if(Math.abs(dx) < minDrag) dx = 0; - if(Math.abs(dy) < minDrag) dy = 0; - if(dx||dy) { - gd._dragged = true; - fx.unhover(gd); - } - - if(options.moveFn) options.moveFn(dx, dy, gd._dragged); - - return Plotly.Lib.pauseEvent(e); - } - - function onDone(e) { - dragCover.onmousemove = null; - dragCover.onmouseup = null; - dragCover.onmouseout = null; - Plotly.Lib.removeElement(dragCover); - - if(!gd._dragging) { - gd._dragged = false; - return; - } - gd._dragging = false; - - // don't count as a dblClick unless the mouseUp is also within - // the dblclick delay - if((new Date()).getTime() - gd._mouseDownTime > DBLCLICKDELAY) { - numClicks = Math.max(numClicks - 1, 1); - } - - if(options.doneFn) options.doneFn(gd._dragged, numClicks); - - if(!gd._dragged) { - var e2 = document.createEvent('MouseEvents'); - e2.initEvent('click', true, true); - initialTarget.dispatchEvent(e2); - } - - finishDrag(gd); - - gd._dragged = false; - - return Plotly.Lib.pauseEvent(e); - } - - options.element.onmousedown = onStart; - options.element.style.pointerEvents = 'all'; -}; - -function coverSlip() { - var cover = document.createElement('div'); - - cover.className = 'dragcover'; - var cStyle = cover.style; - cStyle.position = 'fixed'; - cStyle.left = 0; - cStyle.right = 0; - cStyle.top = 0; - cStyle.bottom = 0; - cStyle.zIndex = 999999999; - cStyle.background = 'none'; - - document.body.appendChild(cover); - - return cover; -} - -fx.setCursor = function(el3,csr) { - (el3.attr('class')||'').split(' ').forEach(function(cls) { - if(cls.indexOf('cursor-')===0) { el3.classed(cls,false); } - }); - if(csr) { el3.classed('cursor-'+csr, true); } -}; - // for bar charts and others with finite-size objects: you must be inside // it to see its hover info, so distance is infinite outside. // But make distance inside be at least 1/4 MAXDIST, and a little bigger diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js index ba30f95305f..731d9b9d81d 100644 --- a/src/plots/cartesian/layout_attributes.js +++ b/src/plots/cartesian/layout_attributes.js @@ -17,6 +17,17 @@ var rangeSelectorAttrs = require('../../components/rangeselector/attributes'); module.exports = { + color: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: [ + 'Sets default for all colors associated with this axis', + 'all at once: line, font, tick, and grid colors.', + 'Grid color is lightened by blending this with the plot background', + 'Individual pieces can override this.' + ].join(' ') + }, title: { valType: 'string', role: 'info', diff --git a/src/plots/cartesian/layout_defaults.js b/src/plots/cartesian/layout_defaults.js index 0dea80e4b93..2fae5aaace3 100644 --- a/src/plots/cartesian/layout_defaults.js +++ b/src/plots/cartesian/layout_defaults.js @@ -11,6 +11,7 @@ var Lib = require('../../lib'); var Plots = require('../plots'); +var Color = require('../../components/color'); var RangeSlider = require('../../components/rangeslider'); var RangeSelector = require('../../components/rangeselector'); @@ -107,7 +108,16 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { yaList = yaListCartesian.concat(yaListGl2d).sort(axSort), axesList = xaList.concat(yaList); - axesList.concat(yaList).forEach(function(axName) { + // plot_bgcolor only makes sense if there's a (2D) plot! + // TODO: bgcolor for each subplot, to inherit from the main one + var plot_bgcolor = Color.background; + if(xaList.length && yaList.length) { + plot_bgcolor = Lib.coerce(layoutIn, layoutOut, Plots.layoutAttributes, 'plot_bgcolor'); + } + + var bgColor = Color.combine(plot_bgcolor, layoutOut.paper_bgcolor); + + axesList.forEach(function(axName) { var axLetter = axName.charAt(0), axLayoutIn = layoutIn[axName] || {}, axLayoutOut = {}, @@ -117,7 +127,8 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { outerTicks: outerTicks[axName], showGrid: !noGrids[axName], name: axName, - data: fullData + data: fullData, + bgColor: bgColor }, positioningOptions = { letter: axLetter, @@ -157,10 +168,4 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { RangeSelector.supplyLayoutDefaults(axLayoutIn, axLayoutOut, layoutOut, counterAxes); } }); - - // plot_bgcolor only makes sense if there's a (2D) plot! - // TODO: bgcolor for each subplot, to inherit from the main one - if(xaList.length && yaList.length) { - Lib.coerce(layoutIn, layoutOut, Plots.layoutAttributes, 'plot_bgcolor'); - } }; diff --git a/src/plots/cartesian/select.js b/src/plots/cartesian/select.js index 502f0ea6923..3c82d1ccdff 100644 --- a/src/plots/cartesian/select.js +++ b/src/plots/cartesian/select.js @@ -71,15 +71,27 @@ module.exports = function prepSelect(e, startX, startY, dragOptions, mode) { trace = cd[0].trace; if(!trace._module || !trace._module.selectPoints) continue; - if(xAxisIds.indexOf(trace.xaxis) === -1) continue; - if(yAxisIds.indexOf(trace.yaxis) === -1) continue; - - searchTraces.push({ - selectPoints: trace._module.selectPoints, - cd: cd, - xaxis: axes.getFromId(gd, trace.xaxis), - yaxis: axes.getFromId(gd, trace.yaxis) - }); + if(dragOptions.subplot) { + if(trace.subplot !== dragOptions.subplot) continue; + + searchTraces.push({ + selectPoints: trace._module.selectPoints, + cd: cd, + xaxis: dragOptions.xaxes[0], + yaxis: dragOptions.yaxes[0] + }); + } + else { + if(xAxisIds.indexOf(trace.xaxis) === -1) continue; + if(yAxisIds.indexOf(trace.yaxis) === -1) continue; + + searchTraces.push({ + selectPoints: trace._module.selectPoints, + cd: cd, + xaxis: axes.getFromId(gd, trace.xaxis), + yaxis: axes.getFromId(gd, trace.yaxis) + }); + } } function axValue(ax) { diff --git a/src/plots/cartesian/set_convert.js b/src/plots/cartesian/set_convert.js index 57574b9fc8d..e35c039429e 100644 --- a/src/plots/cartesian/set_convert.js +++ b/src/plots/cartesian/set_convert.js @@ -65,7 +65,7 @@ module.exports = function setConvert(ax) { // set scaling to pixels ax.setScale = function() { - var gs = ax._td._fullLayout._size, + var gs = ax._gd._fullLayout._size, i; // TODO cleaner way to handle this case @@ -74,7 +74,7 @@ module.exports = function setConvert(ax) { // make sure we have a domain (pull it in from the axis // this one is overlaying if necessary) if(ax.overlaying) { - var ax2 = axisIds.getFromId(ax._td, ax.overlaying); + var ax2 = axisIds.getFromId(ax._gd, ax.overlaying); ax.domain = ax2.domain; } @@ -116,7 +116,7 @@ module.exports = function setConvert(ax) { Lib.notifier( 'Something went wrong with axis scaling', 'long'); - ax._td._replotting = false; + ax._gd._replotting = false; throw new Error('axis scaling'); } }; diff --git a/src/plots/cartesian/tick_defaults.js b/src/plots/cartesian/tick_label_defaults.js similarity index 71% rename from src/plots/cartesian/tick_defaults.js rename to src/plots/cartesian/tick_label_defaults.js index 7e8440617ec..11608362711 100644 --- a/src/plots/cartesian/tick_defaults.js +++ b/src/plots/cartesian/tick_label_defaults.js @@ -11,24 +11,11 @@ var Lib = require('../../lib'); -var layoutAttributes = require('./layout_attributes'); - /** * options: inherits font, outerTicks, noHover from axes.handleAxisDefaults */ -module.exports = function handleTickDefaults(containerIn, containerOut, coerce, axType, options) { - var tickLen = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'ticklen'), - tickWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickwidth'), - tickColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickcolor'), - showTicks = coerce('ticks', (options.outerTicks || tickLen || tickWidth || tickColor) ? 'outside' : ''); - - if(!showTicks) { - delete containerOut.ticklen; - delete containerOut.tickwidth; - delete containerOut.tickcolor; - } - +module.exports = function handleTickLabelDefaults(containerIn, containerOut, coerce, axType, options) { var showAttrDflt = getShowAttrDflt(containerIn); var tickPrefix = coerce('tickprefix'); @@ -39,7 +26,16 @@ module.exports = function handleTickDefaults(containerIn, containerOut, coerce, var showTickLabels = coerce('showticklabels'); if(showTickLabels) { - Lib.coerceFont(coerce, 'tickfont', options.font || {}); + var font = options.font || {}; + // as with titlefont.color, inherit axis.color only if one was + // explicitly provided + var dfltFontColor = (containerOut.color === containerIn.color) ? + containerOut.color : font.color; + Lib.coerceFont(coerce, 'tickfont', { + family: font.family, + size: font.size, + color: dfltFontColor + }); coerce('tickangle'); if(axType !== 'category') { diff --git a/src/plots/cartesian/tick_mark_defaults.js b/src/plots/cartesian/tick_mark_defaults.js new file mode 100644 index 00000000000..8a05652182e --- /dev/null +++ b/src/plots/cartesian/tick_mark_defaults.js @@ -0,0 +1,31 @@ +/** +* 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 layoutAttributes = require('./layout_attributes'); + + +/** + * options: inherits outerTicks from axes.handleAxisDefaults + */ +module.exports = function handleTickDefaults(containerIn, containerOut, coerce, options) { + var tickLen = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'ticklen'), + tickWidth = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickwidth'), + tickColor = Lib.coerce2(containerIn, containerOut, layoutAttributes, 'tickcolor', containerOut.color), + showTicks = coerce('ticks', (options.outerTicks || tickLen || tickWidth || tickColor) ? 'outside' : ''); + + if(!showTicks) { + delete containerOut.ticklen; + delete containerOut.tickwidth; + delete containerOut.tickcolor; + } +}; diff --git a/src/plots/geo/geo.js b/src/plots/geo/geo.js index 9a20153fa53..8161a965e23 100644 --- a/src/plots/geo/geo.js +++ b/src/plots/geo/geo.js @@ -17,6 +17,8 @@ var Color = require('../../components/color'); var Drawing = require('../../components/drawing'); var Axes = require('../../plots/cartesian/axes'); +var filterVisible = require('../../lib/filter_visible'); + var addProjectionsToD3 = require('./projections'); var createGeoScale = require('./set_scale'); var createGeoZoom = require('./zoom'); @@ -139,20 +141,6 @@ proto.plot = function(geoData, fullLayout, promises) { // to avoid making multiple request while streaming }; -// filter out non-visible trace -// geo plot routine use the classic join/enter/exit pattern to update traces -function filterData(dataIn) { - var dataOut = []; - - for(var i = 0; i < dataIn.length; i++) { - var trace = dataIn[i]; - - if(trace.visible === true) dataOut.push(trace); - } - - return dataOut; -} - proto.onceTopojsonIsLoaded = function(geoData, geoLayout) { var i; @@ -190,7 +178,7 @@ proto.onceTopojsonIsLoaded = function(geoData, geoLayout) { var moduleData = traceHash[moduleNames[i]]; var _module = moduleData[0]._module; - _module.plot(this, filterData(moduleData), geoLayout); + _module.plot(this, filterVisible(moduleData), geoLayout); } this.traceHash = traceHash; @@ -500,7 +488,7 @@ function createMockAxis(fullLayout) { type: 'linear', showexponent: 'all', exponentformat: Axes.layoutAttributes.exponentformat.dflt, - _td: { _fullLayout: fullLayout } + _gd: { _fullLayout: fullLayout } }; Axes.setConvert(mockAxis); diff --git a/src/plots/geo/layout/attributes.js b/src/plots/geo/layout/attributes.js index e6f6823d5e2..a81b27bac1b 100644 --- a/src/plots/geo/layout/attributes.js +++ b/src/plots/geo/layout/attributes.js @@ -11,7 +11,7 @@ module.exports = { geo: { - valType: 'geoid', + valType: 'subplotid', role: 'info', dflt: 'geo', description: [ diff --git a/src/plots/geo/layout/defaults.js b/src/plots/geo/layout/defaults.js index 8e852eb5ac4..5919cbfafcb 100644 --- a/src/plots/geo/layout/defaults.js +++ b/src/plots/geo/layout/defaults.js @@ -9,8 +9,7 @@ 'use strict'; -var Lib = require('../../../lib'); -var Plots = require('../../plots'); +var handleSubplotDefaults = require('../../subplot_defaults'); var constants = require('../../../constants/geo_constants'); var layoutAttributes = require('./layout_attributes'); var supplyGeoAxisLayoutDefaults = require('./axis_defaults'); @@ -19,31 +18,12 @@ var supplyGeoAxisLayoutDefaults = require('./axis_defaults'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { if(!layoutOut._hasGeo) return; - var geos = Plots.findSubplotIds(fullData, 'geo'), - geosLength = geos.length; - - var geoLayoutIn, geoLayoutOut; - - function coerce(attr, dflt) { - return Lib.coerce(geoLayoutIn, geoLayoutOut, layoutAttributes, attr, dflt); - } - - for(var i = 0; i < geosLength; i++) { - var geo = geos[i]; - - // geo traces get a layout geo for free! - if(layoutIn[geo]) geoLayoutIn = layoutIn[geo]; - else geoLayoutIn = layoutIn[geo] = {}; - - geoLayoutIn = layoutIn[geo]; - geoLayoutOut = {}; - - coerce('domain.x'); - coerce('domain.y', [i / geosLength, (i + 1) / geosLength]); - - handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce); - layoutOut[geo] = geoLayoutOut; - } + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: 'geo', + attributes: layoutAttributes, + handleDefaults: handleGeoDefaults, + partition: 'y' + }); }; function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) { diff --git a/src/plots/gl3d/index.js b/src/plots/gl3d/index.js index 032b0714e30..d0286ee52c7 100644 --- a/src/plots/gl3d/index.js +++ b/src/plots/gl3d/index.js @@ -102,7 +102,7 @@ exports.initAxes = function(gd) { for(var j = 0; j < 3; ++j) { var axisName = axesNames[j]; var ax = sceneLayout[axisName]; - ax._td = gd; + ax._gd = gd; } } }; diff --git a/src/plots/gl3d/layout/attributes.js b/src/plots/gl3d/layout/attributes.js index 98e1719b930..0b1937947ca 100644 --- a/src/plots/gl3d/layout/attributes.js +++ b/src/plots/gl3d/layout/attributes.js @@ -11,7 +11,7 @@ module.exports = { scene: { - valType: 'sceneid', + valType: 'subplotid', role: 'info', dflt: 'scene', description: [ diff --git a/src/plots/gl3d/layout/axis_attributes.js b/src/plots/gl3d/layout/axis_attributes.js index dbc8811bf18..15bfb6b483a 100644 --- a/src/plots/gl3d/layout/axis_attributes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -67,6 +67,7 @@ module.exports = { dflt: true, description: 'Sets whether or not this axis is labeled' }, + color: axesAttrs.color, title: axesAttrs.title, titlefont: axesAttrs.titlefont, type: axesAttrs.type, diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js index c0473aefbf4..37ee79dcd23 100644 --- a/src/plots/gl3d/layout/axis_defaults.js +++ b/src/plots/gl3d/layout/axis_defaults.js @@ -8,14 +8,19 @@ 'use strict'; +var colorMix = require('tinycolor2').mix; var Lib = require('../../../lib'); + var layoutAttributes = require('./axis_attributes'); var handleAxisDefaults = require('../../cartesian/axis_defaults'); var axesNames = ['xaxis', 'yaxis', 'zaxis']; var noop = function() {}; +// TODO: hard-coded lightness fraction based on gridline default colors +// that differ from other subplot types. +var gridLightness = 100 * (204 - 0x44) / (255 - 0x44); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { var containerIn, containerOut; @@ -40,10 +45,11 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { font: options.font, letter: axName[0], data: options.data, - showGrid: true + showGrid: true, + bgColor: options.bgColor }); - coerce('gridcolor'); + coerce('gridcolor', colorMix(containerOut.color, options.bgColor, gridLightness).toRgbString()); coerce('title', axName[0]); // shouldn't this be on-par with 2D? containerOut.setScale = noop; @@ -51,10 +57,10 @@ module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { if(coerce('showspikes')) { coerce('spikesides'); coerce('spikethickness'); - coerce('spikecolor'); + coerce('spikecolor', containerOut.color); } - if(coerce('showbackground')) coerce('backgroundcolor'); coerce('showaxeslabels'); + if(coerce('showbackground')) coerce('backgroundcolor'); } }; diff --git a/src/plots/gl3d/layout/defaults.js b/src/plots/gl3d/layout/defaults.js index c4a107f9b38..066f7ec2fa4 100644 --- a/src/plots/gl3d/layout/defaults.js +++ b/src/plots/gl3d/layout/defaults.js @@ -9,8 +9,9 @@ 'use strict'; -var Lib = require('../../../lib'); -var Plots = require('../../plots'); +var Color = require('../../../components/color'); + +var handleSubplotDefaults = require('../../subplot_defaults'); var layoutAttributes = require('./layout_attributes'); var supplyGl3dAxisLayoutDefaults = require('./axis_defaults'); @@ -18,103 +19,89 @@ var supplyGl3dAxisLayoutDefaults = require('./axis_defaults'); module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { if(!layoutOut._hasGL3D) return; - var scenes = Plots.findSubplotIds(fullData, 'gl3d'), - scenesLength = scenes.length; - - var sceneLayoutIn, sceneLayoutOut; - - function coerce(attr, dflt) { - return Lib.coerce(sceneLayoutIn, sceneLayoutOut, layoutAttributes, attr, dflt); - } + var hasNon3D = ( + layoutOut._hasCartesian || + layoutOut._hasGeo || + layoutOut._hasGL2D || + layoutOut._hasPie || + layoutOut._hasTernary + ); // some layout-wide attribute are used in all scenes // if 3D is the only visible plot type function getDfltFromLayout(attr) { - var isOnlyGL3D = !( - layoutOut._hasCartesian || - layoutOut._hasGeo || - layoutOut._hasGL2D || - layoutOut._hasPie - ); + if(hasNon3D) return; var isValid = layoutAttributes[attr].values.indexOf(layoutIn[attr]) !== -1; + if(isValid) return layoutIn[attr]; + } - if(isOnlyGL3D && isValid) return layoutIn[attr]; + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: 'gl3d', + attributes: layoutAttributes, + handleDefaults: handleGl3dDefaults, + font: layoutOut.font, + fullData: fullData, + getDfltFromLayout: getDfltFromLayout, + paper_bgcolor: layoutOut.paper_bgcolor + }); +}; + +function handleGl3dDefaults(sceneLayoutIn, sceneLayoutOut, coerce, opts) { + /* + * Scene numbering proceeds as follows + * scene + * scene2 + * scene3 + * + * and d.scene will be undefined or some number or number string + * + * Also write back a blank scene object to user layout so that some + * attributes like aspectratio can be written back dynamically. + */ + + var bgcolor = coerce('bgcolor'), + bgColorCombined = Color.combine(bgcolor, opts.paper_bgcolor); + + var cameraKeys = Object.keys(layoutAttributes.camera); + + for(var j = 0; j < cameraKeys.length; j++) { + coerce('camera.' + cameraKeys[j] + '.x'); + coerce('camera.' + cameraKeys[j] + '.y'); + coerce('camera.' + cameraKeys[j] + '.z'); } - for(var i = 0; i < scenesLength; i++) { - var scene = scenes[i]; - - /* - * Scene numbering proceeds as follows - * scene - * scene2 - * scene3 - * - * and d.scene will be undefined or some number or number string - * - * Also write back a blank scene object to user layout so that some - * attributes like aspectratio can be written back dynamically. - */ - - // gl3d traces get a layout scene for free! - if(layoutIn[scene]) sceneLayoutIn = layoutIn[scene]; - else layoutIn[scene] = sceneLayoutIn = {}; - - sceneLayoutOut = layoutOut[scene] || {}; - - coerce('bgcolor'); - - var cameraKeys = Object.keys(layoutAttributes.camera); - - for(var j = 0; j < cameraKeys.length; j++) { - coerce('camera.' + cameraKeys[j] + '.x'); - coerce('camera.' + cameraKeys[j] + '.y'); - coerce('camera.' + cameraKeys[j] + '.z'); - } - - coerce('domain.x', [i / scenesLength, (i+1) / scenesLength]); - coerce('domain.y'); - - /* - * coerce to positive number (min 0) but also do not accept 0 (>0 not >=0) - * note that 0's go false with the !! call - */ - var hasAspect = !!coerce('aspectratio.x') && - !!coerce('aspectratio.y') && - !!coerce('aspectratio.z'); - - var defaultAspectMode = hasAspect ? 'manual' : 'auto'; - var aspectMode = coerce('aspectmode', defaultAspectMode); - - /* - * We need aspectratio object in all the Layouts as it is dynamically set - * in the calculation steps, ie, we cant set the correct data now, it happens later. - * We must also account for the case the user sends bad ratio data with 'manual' set - * for the mode. In this case we must force change it here as the default coerce - * misses it above. - */ - if(!hasAspect) { - sceneLayoutIn.aspectratio = sceneLayoutOut.aspectratio = {x: 1, y: 1, z: 1}; - - if(aspectMode === 'manual') sceneLayoutOut.aspectmode = 'auto'; - } - - /* - * scene arrangements need to be implemented: For now just splice - * along the horizontal direction. ie. - * x:[0,1] -> x:[0,0.5], x:[0.5,1] -> - * x:[0, 0.333] x:[0.333,0.666] x:[0.666, 1] - */ - supplyGl3dAxisLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { - font: layoutOut.font, - scene: scene, - data: fullData - }); - - coerce('dragmode', getDfltFromLayout('dragmode')); - coerce('hovermode', getDfltFromLayout('hovermode')); - - layoutOut[scene] = sceneLayoutOut; + /* + * coerce to positive number (min 0) but also do not accept 0 (>0 not >=0) + * note that 0's go false with the !! call + */ + var hasAspect = !!coerce('aspectratio.x') && + !!coerce('aspectratio.y') && + !!coerce('aspectratio.z'); + + var defaultAspectMode = hasAspect ? 'manual' : 'auto'; + var aspectMode = coerce('aspectmode', defaultAspectMode); + + /* + * We need aspectratio object in all the Layouts as it is dynamically set + * in the calculation steps, ie, we cant set the correct data now, it happens later. + * We must also account for the case the user sends bad ratio data with 'manual' set + * for the mode. In this case we must force change it here as the default coerce + * misses it above. + */ + if(!hasAspect) { + sceneLayoutIn.aspectratio = sceneLayoutOut.aspectratio = {x: 1, y: 1, z: 1}; + + if(aspectMode === 'manual') sceneLayoutOut.aspectmode = 'auto'; } -}; + + supplyGl3dAxisLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { + font: opts.font, + scene: opts.id, + data: opts.fullData, + bgColor: bgColorCombined + }); + + coerce('dragmode', opts.getDfltFromLayout('dragmode')); + coerce('hovermode', opts.getDfltFromLayout('hovermode')); +} diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 41781deca30..601251a1a8d 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -188,6 +188,10 @@ module.exports = { valType: 'boolean', dflt: false }, + _hasTernary: { + valType: 'boolean', + dflt: false + }, _composedModules: { '*': 'Fx' }, @@ -200,6 +204,7 @@ module.exports = { 'geo': 'geo', 'legend': 'Legend', 'annotations': 'Annotations', - 'shapes': 'Shapes' + 'shapes': 'Shapes', + 'ternary': 'ternary' } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index d719ffc6c81..2e5a0aec57a 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -471,6 +471,7 @@ plots.supplyDefaults = function(gd) { else if(plots.traceIs(fullTrace, 'geo')) newFullLayout._hasGeo = true; else if(plots.traceIs(fullTrace, 'pie')) newFullLayout._hasPie = true; else if(plots.traceIs(fullTrace, 'gl2d')) newFullLayout._hasGL2D = true; + else if(plots.traceIs(fullTrace, 'ternary')) newFullLayout._hasTernary = true; else if('r' in fullTrace) newFullLayout._hasPolar = true; _module = fullTrace._module; @@ -508,7 +509,7 @@ plots.supplyDefaults = function(gd) { axList = Plotly.Axes.list(gd); for(i = 0; i < axList.length; i++) { ax = axList[i]; - ax._td = gd; + ax._gd = gd; ax.setScale(); } @@ -635,6 +636,7 @@ plots.supplyDataDefaults = function(traceIn, i, layout) { // differently for 3D cases. coerceSubplotAttr('gl3d', 'scene'); coerceSubplotAttr('geo', 'geo'); + coerceSubplotAttr('ternary', 'subplot'); // module-specific attributes --- note: we need to send a trace into // the 3D modules to have it removed from the webgl context. @@ -643,7 +645,7 @@ plots.supplyDataDefaults = function(traceIn, i, layout) { traceOut._module = _module; } - // gets overwritten in pie and geo + // gets overwritten in pie, geo and ternary modules if(visible) coerce('hoverinfo', (layout._dataLength === 1) ? 'x+y+z+text' : undefined); if(_module && visible) _module.supplyDefaults(traceIn, traceOut, defaultColor, layout); @@ -715,6 +717,7 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { coerce('_hasGeo'); coerce('_hasPie'); coerce('_hasGL2D'); + coerce('_hasTernary'); }; plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { diff --git a/src/plots/subplot_defaults.js b/src/plots/subplot_defaults.js new file mode 100644 index 00000000000..15e42217b87 --- /dev/null +++ b/src/plots/subplot_defaults.js @@ -0,0 +1,73 @@ +/** +* 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'); + + +/** + * Find and supply defaults to all subplots of a given type + * This handles subplots that are contained within one container - so + * gl3d, geo, ternary... but not 2d axes which have separate x and y axes + * finds subplots, coerces their `domain` attributes, then calls the + * given handleDefaults function to fill in everything else. + * + * layoutIn: the complete user-supplied input layout + * layoutOut: the complete finished layout + * fullData: the finished data array, used only to find subplots + * opts: { + * type: subplot type string + * attributes: subplot attributes object + * partition: 'x' or 'y', which direction to divide domain space by default + * (default 'x', ie side-by-side subplots) + * TODO: this option is only here because 3D and geo made opposite + * choices in this regard previously and I didn't want to change it. + * Instead we should do: + * - something consistent + * - something more square (4 cuts 2x2, 5/6 cuts 2x3, etc.) + * - something that includes all subplot types in one arrangement, + * now that we can have them together! + * handleDefaults: function of (subplotLayoutIn, subplotLayoutOut, coerce, opts) + * this opts object is passed through to handleDefaults, so attach any + * additional items needed by this function here as well + * } + */ +module.exports = function handleSubplotDefaults(layoutIn, layoutOut, fullData, opts) { + var subplotType = opts.type, + subplotAttributes = opts.attributes, + handleDefaults = opts.handleDefaults, + partition = opts.partition || 'x'; + + var ids = Plots.findSubplotIds(fullData, subplotType), + idsLength = ids.length; + + var subplotLayoutIn, subplotLayoutOut; + + function coerce(attr, dflt) { + return Lib.coerce(subplotLayoutIn, subplotLayoutOut, subplotAttributes, attr, dflt); + } + + for(var i = 0; i < idsLength; i++) { + var id = ids[i]; + + // ternary traces get a layout ternary for free! + if(layoutIn[id]) subplotLayoutIn = layoutIn[id]; + else subplotLayoutIn = layoutIn[id] = {}; + + layoutOut[id] = subplotLayoutOut = {}; + + coerce('domain.' + partition, [i / idsLength, (i + 1) / idsLength]); + coerce('domain.' + {x: 'y', y: 'x'}[partition]); + + opts.id = id; + handleDefaults(subplotLayoutIn, subplotLayoutOut, coerce, opts); + } +}; diff --git a/src/plots/ternary/index.js b/src/plots/ternary/index.js new file mode 100644 index 00000000000..2db0226490d --- /dev/null +++ b/src/plots/ternary/index.js @@ -0,0 +1,72 @@ +/** +* 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 Ternary = require('./ternary'); + +var Plots = require('../../plots/plots'); + + +exports.name = 'ternary'; + +exports.attr = 'subplot'; + +exports.idRoot = 'ternary'; + +exports.idRegex = /^ternary([2-9]|[1-9][0-9]+)?$/; + +exports.attrRegex = /^ternary([2-9]|[1-9][0-9]+)?$/; + +exports.attributes = require('./layout/attributes'); + +exports.layoutAttributes = require('./layout/layout_attributes'); + +exports.supplyLayoutDefaults = require('./layout/defaults'); + +exports.plot = function plotTernary(gd) { + var fullLayout = gd._fullLayout, + fullData = gd._fullData, + ternaryIds = Plots.getSubplotIds(fullLayout, 'ternary'); + + for(var i = 0; i < ternaryIds.length; i++) { + var ternaryId = ternaryIds[i], + fullTernaryData = Plots.getSubplotData(fullData, 'ternary', ternaryId), + ternary = fullLayout[ternaryId]._ternary; + + // If ternary is not instantiated, create one! + if(ternary === undefined) { + ternary = new Ternary({ + id: ternaryId, + graphDiv: gd, + container: fullLayout._ternarylayer.node() + }, + fullLayout + ); + + fullLayout[ternaryId]._ternary = ternary; + } + + ternary.plot(fullTernaryData, fullLayout, gd._promises); + } +}; + +exports.clean = function(newFullData, newFullLayout, oldFullData, oldFullLayout) { + var oldTernaryKeys = Plots.getSubplotIds(oldFullLayout, 'ternary'); + + for(var i = 0; i < oldTernaryKeys.length; i++) { + var oldTernaryKey = oldTernaryKeys[i]; + var oldTernary = oldFullLayout[oldTernaryKey]._ternary; + + if(!newFullLayout[oldTernaryKey] && !!oldTernary) { + oldTernary.plotContainer.remove(); + oldTernary.clipDef.remove(); + } + } +}; diff --git a/src/plots/ternary/layout/attributes.js b/src/plots/ternary/layout/attributes.js new file mode 100644 index 00000000000..891f39df7ae --- /dev/null +++ b/src/plots/ternary/layout/attributes.js @@ -0,0 +1,24 @@ +/** +* 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'; + + +module.exports = { + subplot: { + valType: 'subplotid', + role: 'info', + dflt: 'ternary', + description: [ + 'Sets a reference between this trace\'s data coordinates and', + 'a ternary subplot.', + 'If *ternary* (the default value), the data refer to `layout.ternary`.', + 'If *ternary2*, the data refer to `layout.ternary2`, and so on.' + ].join(' ') + } +}; diff --git a/src/plots/ternary/layout/axis_attributes.js b/src/plots/ternary/layout/axis_attributes.js new file mode 100644 index 00000000000..42b00778682 --- /dev/null +++ b/src/plots/ternary/layout/axis_attributes.js @@ -0,0 +1,62 @@ +/** +* 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 axesAttrs = require('../../cartesian/layout_attributes'); +var extendFlat = require('../../../lib/extend').extendFlat; + + +module.exports = { + title: axesAttrs.title, + titlefont: axesAttrs.titlefont, + color: axesAttrs.color, + // ticks + tickmode: axesAttrs.tickmode, + nticks: extendFlat({}, axesAttrs.nticks, {dflt: 6, min: 1}), + tick0: axesAttrs.tick0, + dtick: axesAttrs.dtick, + tickvals: axesAttrs.tickvals, + ticktext: axesAttrs.ticktext, + ticks: axesAttrs.ticks, + ticklen: axesAttrs.ticklen, + tickwidth: axesAttrs.tickwidth, + tickcolor: axesAttrs.tickcolor, + showticklabels: axesAttrs.showticklabels, + showtickprefix: axesAttrs.showtickprefix, + tickprefix: axesAttrs.tickprefix, + showticksuffix: axesAttrs.showticksuffix, + ticksuffix: axesAttrs.ticksuffix, + showexponent: axesAttrs.showexponent, + exponentformat: axesAttrs.exponentformat, + tickfont: axesAttrs.tickfont, + tickangle: axesAttrs.tickangle, + tickformat: axesAttrs.tickformat, + hoverformat: axesAttrs.hoverformat, + // lines and grids + showline: extendFlat({}, axesAttrs.showline, {dflt: true}), + linecolor: axesAttrs.linecolor, + linewidth: axesAttrs.linewidth, + showgrid: extendFlat({}, axesAttrs.showgrid, {dflt: true}), + gridcolor: axesAttrs.gridcolor, + gridwidth: axesAttrs.gridwidth, + // range + min: { + valType: 'number', + dflt: 0, + role: 'info', + min: 0, + description: [ + 'The minimum value visible on this axis.', + 'The maximum is determined by the sum minus the minimum', + 'values of the other two axes. The full view corresponds to', + 'all the minima set to zero.' + ].join(' ') + } +}; diff --git a/src/plots/ternary/layout/axis_defaults.js b/src/plots/ternary/layout/axis_defaults.js new file mode 100644 index 00000000000..bc48a9b1a05 --- /dev/null +++ b/src/plots/ternary/layout/axis_defaults.js @@ -0,0 +1,94 @@ +/** +* 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 colorMix = require('tinycolor2').mix; + +var Lib = require('../../../lib'); + +var layoutAttributes = require('./axis_attributes'); +var handleTickLabelDefaults = require('../../cartesian/tick_label_defaults'); +var handleTickMarkDefaults = require('../../cartesian/tick_mark_defaults'); +var handleTickValueDefaults = require('../../cartesian/tick_value_defaults'); + + +module.exports = function supplyLayoutDefaults(containerIn, containerOut, options) { + + function coerce(attr, dflt) { + return Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); + } + + function coerce2(attr, dflt) { + return Lib.coerce2(containerIn, containerOut, layoutAttributes, attr, dflt); + } + + containerOut.type = 'linear'; // no other types allowed for ternary + + var dfltColor = coerce('color'); + // if axis.color was provided, use it for fonts too; otherwise, + // inherit from global font color in case that was provided. + var dfltFontColor = (dfltColor === containerIn.color) ? dfltColor : options.font.color; + + var axName = containerOut._name, + letterUpper = axName.charAt(0).toUpperCase(), + dfltTitle = 'Component ' + letterUpper; + + var title = coerce('title', dfltTitle); + containerOut._hovertitle = title === dfltTitle ? title : letterUpper; + + Lib.coerceFont(coerce, 'titlefont', { + family: options.font.family, + size: Math.round(options.font.size * 1.2), + color: dfltFontColor + }); + + // range is just set by 'min' - max is determined by the other axes mins + coerce('min'); + + handleTickValueDefaults(containerIn, containerOut, coerce, 'linear'); + handleTickLabelDefaults(containerIn, containerOut, coerce, 'linear', + { noHover: false }); + handleTickMarkDefaults(containerIn, containerOut, coerce, 'linear', + { outerticks: false }); + + // TODO - below is a bit repetitious from cartesian still... + + var showTickLabels = coerce('showticklabels'); + if(showTickLabels) { + Lib.coerceFont(coerce, 'tickfont', { + family: options.font.family, + size: options.font.size, + color: dfltFontColor + }); + coerce('tickangle'); + coerce('tickformat'); + } + + coerce('hoverformat'); + + var lineColor = coerce2('linecolor', dfltColor), + lineWidth = coerce2('linewidth'), + showLine = coerce('showline', !!lineColor || !!lineWidth); + + if(!showLine) { + delete containerOut.linecolor; + delete containerOut.linewidth; + } + + // default grid color is darker here (60%, vs cartesian default ~91%) + // because the grid is not square so the eye needs heavier cues to follow + var gridColor = coerce2('gridcolor', colorMix(dfltColor, options.bgColor, 60).toRgbString()), + gridWidth = coerce2('gridwidth'), + showGridLines = coerce('showgrid', !!gridColor || !!gridWidth); + + if(!showGridLines) { + delete containerOut.gridcolor; + delete containerOut.gridwidth; + } +}; diff --git a/src/plots/ternary/layout/defaults.js b/src/plots/ternary/layout/defaults.js new file mode 100644 index 00000000000..047208ec562 --- /dev/null +++ b/src/plots/ternary/layout/defaults.js @@ -0,0 +1,63 @@ +/** +* 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 Color = require('../../../components/color'); + +var handleSubplotDefaults = require('../../subplot_defaults'); +var layoutAttributes = require('./layout_attributes'); +var handleAxisDefaults = require('./axis_defaults'); + +var axesNames = ['aaxis', 'baxis', 'caxis']; + +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { + if(!layoutOut._hasTernary) return; + + handleSubplotDefaults(layoutIn, layoutOut, fullData, { + type: 'ternary', + attributes: layoutAttributes, + handleDefaults: handleTernaryDefaults, + font: layoutOut.font, + paper_bgcolor: layoutOut.paper_bgcolor + }); +}; + +function handleTernaryDefaults(ternaryLayoutIn, ternaryLayoutOut, coerce, options) { + var bgColor = coerce('bgcolor'); + var sum = coerce('sum'); + options.bgColor = Color.combine(bgColor, options.paper_bgcolor); + var axName, containerIn, containerOut; + + // TODO: allow most (if not all) axis attributes to be set + // in the outer container and used as defaults in the individual axes? + + for(var j = 0; j < axesNames.length; j++) { + axName = axesNames[j]; + containerIn = ternaryLayoutIn[axName] || {}; + containerOut = ternaryLayoutOut[axName] = {_name: axName}; + + handleAxisDefaults(containerIn, containerOut, options); + } + + // if the min values contradict each other, set them all to default (0) + // and delete *all* the inputs so the user doesn't get confused later by + // changing one and having them all change. + var aaxis = ternaryLayoutOut.aaxis, + baxis = ternaryLayoutOut.baxis, + caxis = ternaryLayoutOut.caxis; + if(aaxis.min + baxis.min + caxis.min >= sum) { + aaxis.min = 0; + baxis.min = 0; + caxis.min = 0; + if(ternaryLayoutIn.aaxis) delete ternaryLayoutIn.aaxis.min; + if(ternaryLayoutIn.baxis) delete ternaryLayoutIn.baxis.min; + if(ternaryLayoutIn.caxis) delete ternaryLayoutIn.caxis.min; + } +} diff --git a/src/plots/ternary/layout/layout_attributes.js b/src/plots/ternary/layout/layout_attributes.js new file mode 100644 index 00000000000..bc304055430 --- /dev/null +++ b/src/plots/ternary/layout/layout_attributes.js @@ -0,0 +1,63 @@ +/** +* 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 colorAttrs = require('../../../components/color/attributes'); +var ternaryAxesAttrs = require('./axis_attributes'); + + +module.exports = { + domain: { + x: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number', min: 0, max: 1}, + {valType: 'number', min: 0, max: 1} + ], + dflt: [0, 1], + description: [ + 'Sets the horizontal domain of this subplot', + '(in plot fraction).' + ].join(' ') + }, + y: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number', min: 0, max: 1}, + {valType: 'number', min: 0, max: 1} + ], + dflt: [0, 1], + description: [ + 'Sets the vertical domain of this subplot', + '(in plot fraction).' + ].join(' ') + } + }, + bgcolor: { + valType: 'color', + role: 'style', + dflt: colorAttrs.background, + description: 'Set the background color of the subplot' + }, + sum: { + valType: 'number', + role: 'info', + dflt: 1, + min: 0, + description: [ + 'The number each triplet should sum to,', + 'and the maximum range of each axis' + ].join(' ') + }, + aaxis: ternaryAxesAttrs, + baxis: ternaryAxesAttrs, + caxis: ternaryAxesAttrs +}; diff --git a/src/plots/ternary/ternary.js b/src/plots/ternary/ternary.js new file mode 100644 index 00000000000..28f0518722c --- /dev/null +++ b/src/plots/ternary/ternary.js @@ -0,0 +1,697 @@ +/** +* 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 tinycolor = require('tinycolor2'); + +var Plotly = require('../../plotly'); +var Lib = require('../../lib'); +var Color = require('../../components/color'); +var Drawing = require('../../components/drawing'); +var setConvert = require('../cartesian/set_convert'); +var extendFlat = require('../../lib/extend').extendFlat; +var Axes = require('../cartesian/axes'); +var filterVisible = require('../../lib/filter_visible'); +var dragElement = require('../../components/dragelement'); +var Titles = require('../../components/titles'); +var prepSelect = require('../cartesian/select'); +var constants = require('../cartesian/constants'); +var fx = require('../cartesian/graph_interact'); + + +function Ternary(options, fullLayout) { + this.id = options.id; + this.graphDiv = options.graphDiv; + this.init(fullLayout); + this.makeFramework(); +} + +module.exports = Ternary; + +var proto = Ternary.prototype; + +proto.init = function(fullLayout) { + this.container = fullLayout._ternarylayer; + this.defs = fullLayout._defs; + this.layoutId = fullLayout._uid; + this.traceHash = {}; +}; + +proto.plot = function(ternaryData, fullLayout) { + var _this = this, + ternaryLayout = fullLayout[_this.id], + graphSize = fullLayout._size, + i; + + if(Lib.getPlotDiv(_this.plotContainer.node()) !== _this.graphDiv) { + // someone deleted the framework - remake it + // TODO: this is getting deleted in (cartesian) makePlotFramework + // turn that into idiomatic d3 (enter/exit, the piece I didn't know + // before was ordering selections) so we don't need this. + _this.init(_this.graphDiv._fullLayout); + _this.makeFramework(); + } + + _this.adjustLayout(ternaryLayout, graphSize); + + var traceHashOld = _this.traceHash; + var traceHash = {}; + + for(i = 0; i < ternaryData.length; i++) { + var trace = ternaryData[i]; + + traceHash[trace.type] = traceHash[trace.type] || []; + traceHash[trace.type].push(trace); + } + + var moduleNamesOld = Object.keys(traceHashOld); + var moduleNames = Object.keys(traceHash); + + // when a trace gets deleted, make sure that its module's + // plot method is called so that it is properly + // removed from the DOM. + for(i = 0; i < moduleNamesOld.length; i++) { + var moduleName = moduleNamesOld[i]; + + if(moduleNames.indexOf(moduleName) === -1) { + var fakeModule = traceHashOld[moduleName][0]; + fakeModule.visible = false; + traceHash[moduleName] = [fakeModule]; + } + } + + moduleNames = Object.keys(traceHash); + + for(i = 0; i < moduleNames.length; i++) { + var moduleData = traceHash[moduleNames[i]]; + var _module = moduleData[0]._module; + + _module.plot(_this, filterVisible(moduleData), ternaryLayout); + } + + _this.traceHash = traceHash; + + _this.layers.plotbg.select('path').call(Color.fill, ternaryLayout.bgcolor); +}; + +proto.makeFramework = function() { + var _this = this; + + var defGroup = _this.defs.selectAll('g.clips') + .data([0]); + defGroup.enter().append('g') + .classed('clips', true); + + // clippath for this ternary subplot + var clipId = 'clip' + _this.layoutId + _this.id; + _this.clipDef = defGroup.selectAll('#' + clipId) + .data([0]); + _this.clipDef.enter().append('clipPath').attr('id', clipId) + .append('path').attr('d', 'M0,0Z'); + + // container for everything in this ternary subplot + _this.plotContainer = _this.container.selectAll('g.' + _this.id) + .data([0]); + _this.plotContainer.enter().append('g') + .classed(_this.id, true); + + _this.layers = {}; + + // inside that container, we have one container for the data, and + // one each for the three axes around it. + var plotLayers = [ + 'draglayer', + 'plotbg', + 'backplot', + 'grids', + 'frontplot', + 'zoom', + 'aaxis', 'baxis', 'caxis', 'axlines' + ]; + var toplevel = _this.plotContainer.selectAll('g.toplevel') + .data(plotLayers); + toplevel.enter().append('g') + .attr('class', function(d) { return 'toplevel ' + d; }) + .each(function(d) { + var s = d3.select(this); + _this.layers[d] = s; + + // containers for different trace types. + // NOTE - this is different from cartesian, where all traces + // are in front of grids. Here I'm putting maps behind the grids + // so the grids will always be visible if they're requested. + // Perhaps we want that for cartesian too? + if(d === 'frontplot') s.append('g').classed('scatterlayer', true); + else if(d === 'backplot') s.append('g').classed('maplayer', true); + else if(d === 'plotbg') s.append('path').attr('d', 'M0,0Z'); + else if(d === 'axlines') { + s.selectAll('path').data(['aline', 'bline', 'cline']) + .enter().append('path').each(function(d) { + d3.select(this).classed(d, true); + }); + } + }); + + var grids = _this.plotContainer.select('.grids').selectAll('g.grid') + .data(['agrid', 'bgrid', 'cgrid']); + grids.enter().append('g') + .attr('class', function(d) { return 'grid ' + d; }) + .each(function(d) { _this.layers[d] = d3.select(this); }); + + _this.plotContainer.selectAll('.backplot,.frontplot,.grids') + .call(Drawing.setClipUrl, clipId); + + _this.initInteractions(); +}; + +var w_over_h = Math.sqrt(4/3); + +proto.adjustLayout = function(ternaryLayout, graphSize) { + var _this = this, + domain = ternaryLayout.domain, + xDomainCenter = (domain.x[0] + domain.x[1]) / 2, + yDomainCenter = (domain.y[0] + domain.y[1]) / 2, + xDomain = domain.x[1] - domain.x[0], + yDomain = domain.y[1] - domain.y[0], + wmax = xDomain * graphSize.w, + hmax = yDomain * graphSize.h, + sum = ternaryLayout.sum, + amin = ternaryLayout.aaxis.min, + bmin = ternaryLayout.baxis.min, + cmin = ternaryLayout.caxis.min; + + var x0, y0, w, h, xDomainFinal, yDomainFinal; + + if(wmax > w_over_h * hmax) { + h = hmax; + w = h * w_over_h; + } + else { + w = wmax; + h = w / w_over_h; + } + + xDomainFinal = xDomain * w / wmax; + yDomainFinal = yDomain * h / hmax; + + x0 = graphSize.l + graphSize.w * xDomainCenter - w / 2; + y0 = graphSize.t + graphSize.h * (1 - yDomainCenter) - h / 2; + + _this.x0 = x0; + _this.y0 = y0; + _this.w = w; + _this.h = h; + _this.sum = sum; + + // set up the x and y axis objects we'll use to lay out the points + _this.xaxis = { + type: 'linear', + range: [amin + 2 * cmin - sum, sum - amin - 2 * bmin], + domain: [ + xDomainCenter - xDomainFinal / 2, + xDomainCenter + xDomainFinal / 2 + ], + _id: 'x', + _gd: _this.graphDiv + }; + setConvert(_this.xaxis); + _this.xaxis.setScale(); + + _this.yaxis = { + type: 'linear', + range: [amin, sum - bmin - cmin], + domain: [ + yDomainCenter - yDomainFinal / 2, + yDomainCenter + yDomainFinal / 2 + ], + _id: 'y', + _gd: _this.graphDiv + }; + setConvert(_this.yaxis); + _this.yaxis.setScale(); + + // set up the modified axes for tick drawing + var yDomain0 = _this.yaxis.domain[0]; + + // aaxis goes up the left side. Set it up as a y axis, but with + // fictitious angles and domain, but then rotate and translate + // it into place at the end + var aaxis = _this.aaxis = extendFlat({}, ternaryLayout.aaxis, { + range: [amin, sum - bmin - cmin], + side: 'left', + _counterangle: 30, + // tickangle = 'auto' means 0 anyway for a y axis, need to coerce to 0 here + // so we can shift by 30. + tickangle: (+ternaryLayout.aaxis.tickangle || 0) - 30, + domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], + _axislayer: _this.layers.aaxis, + _gridlayer: _this.layers.agrid, + _pos: 0, //_this.xaxis.domain[0] * graphSize.w, + _gd: _this.graphDiv, + _id: 'y', + _length: w, + _gridpath: 'M0,0l' + h + ',-' + (w / 2) + }); + setConvert(aaxis); + + // baxis goes across the bottom (backward). We can set it up as an x axis + // without any enclosing transformation. + var baxis = _this.baxis = extendFlat({}, ternaryLayout.baxis, { + range: [sum - amin - cmin, bmin], + side: 'bottom', + _counterangle: 30, + domain: _this.xaxis.domain, + _axislayer: _this.layers.baxis, + _gridlayer: _this.layers.bgrid, + _counteraxis: _this.aaxis, + _pos: 0, //(1 - yDomain0) * graphSize.h, + _gd: _this.graphDiv, + _id: 'x', + _length: w, + _gridpath: 'M0,0l-' + (w / 2) + ',-' + h + }); + setConvert(baxis); + aaxis._counteraxis = baxis; + + // caxis goes down the right side. Set it up as a y axis, with + // post-transformation similar to aaxis + var caxis = _this.caxis = extendFlat({}, ternaryLayout.caxis, { + range: [sum - amin - bmin, cmin], + side: 'right', + _counterangle: 30, + tickangle: (+ternaryLayout.caxis.tickangle || 0) + 30, + domain: [yDomain0, yDomain0 + yDomainFinal * w_over_h], + _axislayer: _this.layers.caxis, + _gridlayer: _this.layers.cgrid, + _counteraxis: _this.baxis, + _pos: 0, //_this.xaxis.domain[1] * graphSize.w, + _gd: _this.graphDiv, + _id: 'y', + _length: w, + _gridpath: 'M0,0l-' + h + ',' + (w / 2) + }); + setConvert(caxis); + + var triangleClip = 'M' + x0 + ',' + (y0 + h) + 'h' + w + 'l-' + (w/2) + ',-' + h + 'Z'; + _this.clipDef.select('path').attr('d', triangleClip); + _this.layers.plotbg.select('path').attr('d', triangleClip); + + var plotTransform = 'translate(' + x0 + ',' + y0 + ')'; + _this.plotContainer.selectAll('.scatterlayer,.maplayer,.zoom') + .attr('transform', plotTransform); + + // TODO: shift axes to accommodate linewidth*sin(30) tick mark angle + + var bTransform = 'translate(' + x0 + ',' + (y0 + h) + ')'; + + _this.layers.baxis.attr('transform', bTransform); + _this.layers.bgrid.attr('transform', bTransform); + + var aTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(30)'; + _this.layers.aaxis.attr('transform', aTransform); + _this.layers.agrid.attr('transform', aTransform); + + var cTransform = 'translate(' + (x0 + w / 2) + ',' + y0 + ')rotate(-30)'; + _this.layers.caxis.attr('transform', cTransform); + _this.layers.cgrid.attr('transform', cTransform); + + _this.drawAxes(true); + + // remove crispEdges - all the off-square angles in ternary plots + // make these counterproductive. + _this.plotContainer.selectAll('.crisp').classed('crisp', false); + + var axlines = _this.layers.axlines; + axlines.select('.aline') + .attr('d', aaxis.showline ? + 'M' + x0 + ',' + (y0 + h) + 'l' + (w / 2) + ',-' + h : 'M0,0') + .call(Color.stroke, aaxis.linecolor || '#000') + .style('stroke-width', (aaxis.linewidth || 0) + 'px'); + axlines.select('.bline') + .attr('d', baxis.showline ? + 'M' + x0 + ',' + (y0 + h) + 'h' + w : 'M0,0') + .call(Color.stroke, baxis.linecolor || '#000') + .style('stroke-width', (baxis.linewidth || 0) + 'px'); + axlines.select('.cline') + .attr('d', caxis.showline ? + 'M' + (x0 + w / 2) + ',' + y0 + 'l' + (w / 2) + ',' + h : 'M0,0') + .call(Color.stroke, caxis.linecolor || '#000') + .style('stroke-width', (caxis.linewidth || 0) + 'px'); +}; + +proto.drawAxes = function(doTitles) { + var _this = this, + gd = _this.graphDiv, + titlesuffix = _this.id.substr(7) + 'title', + aaxis = _this.aaxis, + baxis = _this.baxis, + caxis = _this.caxis; + // 3rd arg true below skips titles, so we can configure them + // correctly later on. + Axes.doTicks(gd, aaxis, true); + Axes.doTicks(gd, baxis, true); + Axes.doTicks(gd, caxis, true); + + if(doTitles) { + var apad = Math.max(aaxis.showticklabels ? aaxis.tickfont.size / 2 : 0, + (caxis.showticklabels ? caxis.tickfont.size * 0.75 : 0) + + (caxis.ticks === 'outside' ? caxis.ticklen * 0.87 : 0)); + Titles.draw(gd, 'a' + titlesuffix, { + propContainer: aaxis, + propName: _this.id + '.aaxis.title', + dfltName: 'Component A', + attributes: { + x: _this.x0 + _this.w / 2, + y: _this.y0 - aaxis.titlefont.size / 3 - apad, + 'text-anchor': 'middle' + } + }); + + var bpad = (baxis.showticklabels ? baxis.tickfont.size : 0) + + (baxis.ticks === 'outside' ? baxis.ticklen : 0) + 3; + + Titles.draw(gd, 'b' + titlesuffix, { + propContainer: baxis, + propName: _this.id + '.baxis.title', + dfltName: 'Component B', + attributes: { + x: _this.x0 - bpad, + y: _this.y0 + _this.h + baxis.titlefont.size * 0.83 + bpad, + 'text-anchor': 'middle' + } + }); + + Titles.draw(gd, 'c' + titlesuffix, { + propContainer: caxis, + propName: _this.id + '.caxis.title', + dfltName: 'Component C', + attributes: { + x: _this.x0 + _this.w + bpad, + y: _this.y0 + _this.h + caxis.titlefont.size * 0.83 + bpad, + 'text-anchor': 'middle' + } + }); + } +}; + +// hard coded paths for zoom corners +// uses the same sizing as cartesian, length is MINZOOM/2, width is 3px +var CLEN = constants.MINZOOM / 2 + 0.87; +var BLPATH = 'm-0.87,.5h' + CLEN + 'v3h-' + (CLEN + 5.2) + + 'l' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) + + 'l2.6,1.5l-' + (CLEN / 2) + ',' + (CLEN * 0.87) + 'Z'; +var BRPATH = 'm0.87,.5h-' + CLEN + 'v3h' + (CLEN + 5.2) + + 'l-' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) + + 'l-2.6,1.5l' + (CLEN / 2) + ',' + (CLEN * 0.87) + 'Z'; +var TOPPATH = 'm0,1l' + (CLEN / 2) + ',' + (CLEN * 0.87) + + 'l2.6,-1.5l-' + (CLEN / 2 + 2.6) + ',-' + (CLEN * 0.87 + 4.5) + + 'l-' + (CLEN / 2 + 2.6) + ',' + (CLEN * 0.87 + 4.5) + + 'l2.6,1.5l' + (CLEN / 2) + ',-' + (CLEN * 0.87) + 'Z'; +var STARTMARKER = 'm0.5,0.5h5v-2h-5v-5h-2v5h-5v2h5v5h2Z'; + +// I guess this could be shared with cartesian... but for now it's separate. +var SHOWZOOMOUTTIP = true; + +proto.initInteractions = function() { + var _this = this, + dragger = _this.layers.plotbg.select('path').node(), + gd = _this.graphDiv, + zoomContainer = _this.layers.zoom; + + // use plotbg for the main interactions + var dragOptions = { + element: dragger, + gd: gd, + plotinfo: {plot: zoomContainer}, + doubleclick: doubleClick, + subplot: _this.id, + prepFn: function(e, startX, startY) { + // these aren't available yet when initInteractions + // is called + dragOptions.xaxes = [_this.xaxis]; + dragOptions.yaxes = [_this.yaxis]; + var dragModeNow = gd._fullLayout.dragmode; + if(e.shiftKey) { + if(dragModeNow === 'pan') dragModeNow = 'zoom'; + else dragModeNow = 'pan'; + } + + if(dragModeNow === 'lasso') dragOptions.minDrag = 1; + else dragOptions.minDrag = undefined; + + if(dragModeNow === 'zoom') { + dragOptions.moveFn = zoomMove; + dragOptions.doneFn = zoomDone; + zoomPrep(e, startX, startY); + } + else if(dragModeNow === 'pan') { + dragOptions.moveFn = plotDrag; + dragOptions.doneFn = dragDone; + panPrep(); + clearSelect(); + } + else if(dragModeNow === 'select' || dragModeNow === 'lasso') { + prepSelect(e, startX, startY, dragOptions, dragModeNow); + } + } + }; + + var x0, y0, mins0, span0, mins, lum, path0, dimmed, zb, corners; + + function zoomPrep(e, startX, startY) { + var dragBBox = dragger.getBoundingClientRect(); + x0 = startX - dragBBox.left; + y0 = startY - dragBBox.top; + mins0 = { + a: _this.aaxis.range[0], + b: _this.baxis.range[1], + c: _this.caxis.range[1] + }; + mins = mins0; + span0 = _this.aaxis.range[1] - mins0.a; + lum = tinycolor(_this.graphDiv._fullLayout[_this.id].bgcolor).getLuminance(); + path0 = 'M0,' + _this.h + 'L' + (_this.w / 2) +', 0L' + _this.w + ',' + _this.h + 'Z'; + dimmed = false; + + zb = zoomContainer.append('path') + .attr('class', 'zoombox') + .style({ + 'fill': lum>0.2 ? 'rgba(0,0,0,0)' : 'rgba(255,255,255,0)', + 'stroke-width': 0 + }) + .attr('d', path0); + + corners = zoomContainer.append('path') + .attr('class', 'zoombox-corners') + .style({ + fill: Plotly.Color.background, + stroke: Plotly.Color.defaultLine, + 'stroke-width': 1, + opacity: 0 + }) + .attr('d','M0,0Z'); + + clearSelect(); + } + + function getAFrac(x, y) { return 1 - (y / _this.h); } + function getBFrac(x, y) { return 1 - ((x + (_this.h - y) / Math.sqrt(3)) / _this.w); } + function getCFrac(x, y) { return ((x - (_this.h - y) / Math.sqrt(3)) / _this.w); } + + function zoomMove(dx0, dy0) { + var x1 = x0 + dx0, + y1 = y0 + dy0, + afrac = Math.max(0, Math.min(1, getAFrac(x0, y0), getAFrac(x1, y1))), + bfrac = Math.max(0, Math.min(1, getBFrac(x0, y0), getBFrac(x1, y1))), + cfrac = Math.max(0, Math.min(1, getCFrac(x0, y0), getCFrac(x1, y1))), + xLeft = ((afrac / 2) + cfrac) * _this.w, + xRight = (1 - (afrac / 2) - bfrac) * _this.w, + xCenter = (xLeft + xRight) / 2, + xSpan = xRight - xLeft, + yBottom = (1 - afrac) * _this.h, + yTop = yBottom - xSpan / w_over_h; + + if(xSpan < constants.MINZOOM) { + mins = mins0; + zb.attr('d', path0); + corners.attr('d', 'M0,0Z'); + } + else { + mins = { + a: mins0.a + afrac * span0, + b: mins0.b + bfrac * span0, + c: mins0.c + cfrac * span0 + }; + zb.attr('d', path0 + 'M' + xLeft + ',' + yBottom + + 'H' + xRight + 'L' + xCenter + ',' + yTop + + 'L' + xLeft + ',' + yBottom + 'Z'); + corners.attr('d', 'M' + x0 + ',' + y0 + STARTMARKER + + 'M' + xLeft + ',' + yBottom + BLPATH + + 'M' + xRight + ',' + yBottom + BRPATH + + 'M' + xCenter + ',' + yTop + TOPPATH); + } + + if(!dimmed) { + zb.transition() + .style('fill', lum>0.2 ? 'rgba(0,0,0,0.4)' : + 'rgba(255,255,255,0.3)') + .duration(200); + corners.transition() + .style('opacity',1) + .duration(200); + dimmed = true; + } + } + + function zoomDone(dragged, numClicks) { + if(mins === mins0) { + if(numClicks === 2) doubleClick(); + + return removeZoombox(gd); + } + + removeZoombox(gd); + + var attrs = {}; + attrs[_this.id + '.aaxis.min'] = mins.a; + attrs[_this.id + '.baxis.min'] = mins.b; + attrs[_this.id + '.caxis.min'] = mins.c; + + Plotly.relayout(gd, attrs); + + if(SHOWZOOMOUTTIP && gd.data && gd._context.showTips) { + Lib.notifier('Double-click to
zoom back out','long'); + SHOWZOOMOUTTIP = false; + } + } + + function panPrep() { + mins0 = { + a: _this.aaxis.range[0], + b: _this.baxis.range[1], + c: _this.caxis.range[1] + }; + mins = mins0; + } + + function plotDrag(dx, dy) { + var dxScaled = dx / _this.xaxis._m, + dyScaled = dy / _this.yaxis._m; + mins = { + a: mins0.a - dyScaled, + b: mins0.b + (dxScaled + dyScaled) / 2, + c: mins0.c - (dxScaled - dyScaled) / 2 + }; + var minsorted = [mins.a, mins.b, mins.c].sort(), + minindices = { + a: minsorted.indexOf(mins.a), + b: minsorted.indexOf(mins.b), + c: minsorted.indexOf(mins.c) + }; + if(minsorted[0] < 0) { + if(minsorted[1] + minsorted[0] / 2 < 0) { + minsorted[2] += minsorted[0] + minsorted[1]; + minsorted[0] = minsorted[1] = 0; + } + else { + minsorted[2] += minsorted[0] / 2; + minsorted[1] += minsorted[0] / 2; + minsorted[0] = 0; + } + mins = { + a: minsorted[minindices.a], + b: minsorted[minindices.b], + c: minsorted[minindices.c] + }; + dy = (mins0.a - mins.a) * _this.yaxis._m; + dx = (mins0.c - mins.c - mins0.b + mins.b) * _this.xaxis._m; + } + + // move the data (translate, don't redraw) + var plotTransform = 'translate(' + (_this.x0 + dx) + ',' + (_this.y0 + dy) + ')'; + _this.plotContainer.selectAll('.scatterlayer,.maplayer') + .attr('transform', plotTransform); + + // move the ticks + _this.aaxis.range = [mins.a, _this.sum - mins.b - mins.c]; + _this.baxis.range = [_this.sum - mins.a - mins.c, mins.b]; + _this.caxis.range = [_this.sum - mins.a - mins.b, mins.c]; + + _this.drawAxes(false); + _this.plotContainer.selectAll('.crisp').classed('crisp', false); + } + + function dragDone(dragged, numClicks) { + if(dragged) { + var attrs = {}; + attrs[_this.id + '.aaxis.min'] = mins.a; + attrs[_this.id + '.baxis.min'] = mins.b; + attrs[_this.id + '.caxis.min'] = mins.c; + + Plotly.relayout(gd, attrs); + } + else if(numClicks === 2) doubleClick(); + } + + function clearSelect() { + // until we get around to persistent selections, remove the outline + // here. The selection itself will be removed when the plot redraws + // at the end. + _this.plotContainer.selectAll('.select-outline').remove(); + } + + function doubleClick() { + var attrs = {}; + attrs[_this.id + '.aaxis.min'] = 0; + attrs[_this.id + '.baxis.min'] = 0; + attrs[_this.id + '.caxis.min'] = 0; + gd.emit('plotly_doubleclick', null); + Plotly.relayout(gd, attrs); + } + + dragElement.init(dragOptions); + + // finally, set up hover and click + dragger.onmousemove = function(evt) { + fx.hover(gd, evt, _this.id); + gd._fullLayout._lasthover = dragger; + gd._fullLayout._hoversubplot = _this.id; + }; + + dragger.onmouseout = function(evt) { + if(gd._dragging) return; + + dragElement.unhover(gd, evt); + }; + + dragger.onclick = function(evt) { + fx.click(gd, evt); + }; + + // make a fake plotinfo for fx.hover + // it hardly uses it, could probably be refactored out... + // but specifying subplot by name does seem nice for js applications + // that want to hook into this. + if(!gd._fullLayout._plots) gd._fullLayout._plots = {}; + gd._fullLayout._plots[_this.id] = { + overlays: [], + xaxis: _this.xaxis, + yaxis: _this.yaxis, + x: function() { return _this.xaxis; }, + y: function() { return _this.yaxis; } + }; +}; + +function removeZoombox(gd) { + d3.select(gd) + .selectAll('.zoombox,.js-zoombox-backdrop,.js-zoombox-menu,.zoombox-corners') + .remove(); +} diff --git a/src/traces/bar/hover.js b/src/traces/bar/hover.js index a667baaec02..673edd2e4ce 100644 --- a/src/traces/bar/hover.js +++ b/src/traces/bar/hover.js @@ -21,7 +21,7 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode) { xa = pointData.xa, ya = pointData.ya, barDelta = (hovermode==='closest') ? - t.barwidth/2 : t.dbar*(1-xa._td._fullLayout.bargap)/2, + t.barwidth/2 : t.dbar*(1-xa._gd._fullLayout.bargap)/2, barPos; if(hovermode!=='closest') barPos = function(di) { return di.p; }; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index ed2934662c9..bea712eb9fb 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -130,7 +130,10 @@ module.exports = function plot(gd, cdpie) { } function handleMouseOut(evt) { - Fx.unhover(gd, evt, 'pie'); + gd.emit('plotly_unhover', { + points: [evt] + }); + if(hasHoverData) { Fx.loneUnhover(fullLayout._hoverlayer.node()); hasHoverData = false; diff --git a/src/traces/scatter/defaults.js b/src/traces/scatter/defaults.js index c18250b8208..3690160053f 100644 --- a/src/traces/scatter/defaults.js +++ b/src/traces/scatter/defaults.js @@ -17,6 +17,7 @@ var subTypes = require('./subtypes'); var handleXYDefaults = require('./xy_defaults'); var handleMarkerDefaults = require('./marker_defaults'); var handleLineDefaults = require('./line_defaults'); +var handleLineShapeDefaults = require('./line_shape_defaults'); var handleTextDefaults = require('./text_defaults'); var handleFillColorDefaults = require('./fillcolor_defaults'); var errorBarsSupplyDefaults = require('../../components/errorbars/defaults'); @@ -40,7 +41,7 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout if(subTypes.hasLines(traceOut)) { handleLineDefaults(traceIn, traceOut, defaultColor, coerce); - lineShapeDefaults(traceIn, traceOut, coerce); + handleLineShapeDefaults(traceIn, traceOut, coerce); coerce('connectgaps'); } @@ -59,14 +60,9 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout coerce('fill'); if(traceOut.fill !== 'none') { handleFillColorDefaults(traceIn, traceOut, defaultColor, coerce); - if(!subTypes.hasLines(traceOut)) lineShapeDefaults(traceIn, traceOut, coerce); + if(!subTypes.hasLines(traceOut)) handleLineShapeDefaults(traceIn, traceOut, coerce); } errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); errorBarsSupplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); }; - -function lineShapeDefaults(traceIn, traceOut, coerce) { - var shape = coerce('line.shape'); - if(shape === 'spline') coerce('line.smoothing'); -} diff --git a/src/traces/scatter/index.js b/src/traces/scatter/index.js index a7a9cafd08e..3b576a561d0 100644 --- a/src/traces/scatter/index.js +++ b/src/traces/scatter/index.js @@ -40,7 +40,7 @@ Scatter.meta = { 'The data visualized as scatter point or lines is set in `x` and `y`.', 'Text (appearing either on the chart or on hover only) is via `text`.', 'Bubble charts are achieved by setting `marker.size` and/or `marker.color`', - 'to a numerical arrays.' + 'to numerical arrays.' ].join(' ') }; diff --git a/src/traces/scatter/line_shape_defaults.js b/src/traces/scatter/line_shape_defaults.js new file mode 100644 index 00000000000..421144f3a2f --- /dev/null +++ b/src/traces/scatter/line_shape_defaults.js @@ -0,0 +1,17 @@ +/** +* 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'; + + +// common to 'scatter' and 'scatterternary' +module.exports = function handleLineShapeDefaults(traceIn, traceOut, coerce) { + var shape = coerce('line.shape'); + if(shape === 'spline') coerce('line.smoothing'); +}; diff --git a/src/traces/scattergeo/attributes.js b/src/traces/scattergeo/attributes.js index 724cd12bc8d..969c7a16970 100644 --- a/src/traces/scattergeo/attributes.js +++ b/src/traces/scattergeo/attributes.js @@ -47,7 +47,7 @@ module.exports = { mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}), text: extendFlat({}, scatterAttrs.text, { description: [ - 'Sets text elements associated with each (lon,lat) pair.', + 'Sets text elements associated with each (lon,lat) pair', 'or item in `locations`.', 'If a single string, the same string appears over', 'all the data points.', diff --git a/src/traces/scatterternary/attributes.js b/src/traces/scatterternary/attributes.js new file mode 100644 index 00000000000..697b86fcb92 --- /dev/null +++ b/src/traces/scatterternary/attributes.js @@ -0,0 +1,118 @@ +/** +* 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 scatterAttrs = require('../scatter/attributes'); +var plotAttrs = require('../../plots/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterMarkerAttrs = scatterAttrs.marker, + scatterLineAttrs = scatterAttrs.line, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; + + +module.exports = { + a: { + valType: 'data_array', + description: [ + 'Sets the quantity of component `a` in each data point.', + 'If `a`, `b`, and `c` are all provided, they need not be', + 'normalized, only the relative values matter. If only two', + 'arrays are provided they must be normalized to match', + '`ternary.sum`.' + ].join(' ') + }, + b: { + valType: 'data_array', + description: [ + 'Sets the quantity of component `a` in each data point.', + 'If `a`, `b`, and `c` are all provided, they need not be', + 'normalized, only the relative values matter. If only two', + 'arrays are provided they must be normalized to match', + '`ternary.sum`.' + ].join(' ') + }, + c: { + valType: 'data_array', + description: [ + 'Sets the quantity of component `a` in each data point.', + 'If `a`, `b`, and `c` are all provided, they need not be', + 'normalized, only the relative values matter. If only two', + 'arrays are provided they must be normalized to match', + '`ternary.sum`.' + ].join(' ') + }, + sum: { + valType: 'number', + role: 'info', + dflt: 0, + min: 0, + description: [ + 'The number each triplet should sum to,', + 'if only two of `a`, `b`, and `c` are provided.', + 'This overrides `ternary.sum` to normalize this specific', + 'trace, but does not affect the values displayed on the axes.', + '0 (or missing) means to use ternary.sum' + ].join(' ') + }, + mode: extendFlat({}, scatterAttrs.mode, {dflt: 'markers'}), + text: extendFlat({}, scatterAttrs.text, { + description: [ + 'Sets text elements associated with each (a,b,c) point.', + 'If a single string, the same string appears over', + 'all the data points.', + 'If an array of strings, the items are mapped in order to the', + 'the data points in (a,b,c).' + ].join(' ') + }), + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + shape: extendFlat({}, scatterLineAttrs.shape, + {values: ['linear', 'spline']}), + smoothing: scatterLineAttrs.smoothing + }, + connectgaps: scatterAttrs.connectgaps, + marker: { + symbol: scatterMarkerAttrs.symbol, + opacity: scatterMarkerAttrs.opacity, + maxdisplayed: scatterMarkerAttrs.maxdisplayed, + size: scatterMarkerAttrs.size, + sizeref: scatterMarkerAttrs.sizeref, + sizemin: scatterMarkerAttrs.sizemin, + sizemode: scatterMarkerAttrs.sizemode, + color: scatterMarkerAttrs.color, + colorscale: scatterMarkerAttrs.colorscale, + cauto: scatterMarkerAttrs.cauto, + cmax: scatterMarkerAttrs.cmax, + cmin: scatterMarkerAttrs.cmin, + autocolorscale: scatterMarkerAttrs.autocolorscale, + reversescale: scatterMarkerAttrs.reversescale, + showscale: scatterMarkerAttrs.showscale, + line: { + color: scatterMarkerLineAttrs.color, + width: scatterMarkerLineAttrs.width, + colorscale: scatterMarkerLineAttrs.colorscale, + cauto: scatterMarkerLineAttrs.cauto, + cmax: scatterMarkerLineAttrs.cmax, + cmin: scatterMarkerLineAttrs.cmin, + autocolorscale: scatterMarkerLineAttrs.autocolorscale, + reversescale: scatterMarkerLineAttrs.reversescale + } + }, + textfont: scatterAttrs.textfont, + textposition: scatterAttrs.textposition, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { + flags: ['a', 'b', 'c', 'text', 'name'] + }), + _nestedModules: { + 'marker.colorbar': 'Colorbar' + } +}; diff --git a/src/traces/scatterternary/calc.js b/src/traces/scatterternary/calc.js new file mode 100644 index 00000000000..ae439af07a6 --- /dev/null +++ b/src/traces/scatterternary/calc.js @@ -0,0 +1,97 @@ +/** +* 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 isNumeric = require('fast-isnumeric'); + +var Axes = require('../../plots/cartesian/axes'); +var Lib = require('../../lib'); + +var subTypes = require('../scatter/subtypes'); +var calcMarkerColorscale = require('../scatter/marker_colorscale_calc'); + +var dataArrays = ['a', 'b', 'c']; +var arraysToFill = {a: ['b', 'c'], b: ['a', 'c'], c: ['a', 'b']}; + + +module.exports = function calc(gd, trace) { + var ternary = gd._fullLayout[trace.subplot], + displaySum = ternary.sum, + normSum = trace.sum || displaySum; + + var i, j, dataArray, newArray, fillArray1, fillArray2; + + // fill in one missing component + for(i = 0; i < dataArrays.length; i++) { + dataArray = dataArrays[i]; + if(trace[dataArray]) continue; + + fillArray1 = trace[arraysToFill[dataArray][0]]; + fillArray2 = trace[arraysToFill[dataArray][1]]; + newArray = new Array(fillArray1.length); + for(j = 0; j < fillArray1.length; j++) { + newArray[j] = normSum - fillArray1[j] - fillArray2[j]; + } + trace[dataArray] = newArray; + } + + // make the calcdata array + var serieslen = trace.a.length; + var cd = new Array(serieslen); + var a, b, c, norm, x, y; + for(i = 0; i < serieslen; i++) { + a = trace.a[i]; + b = trace.b[i]; + c = trace.c[i]; + if(isNumeric(a) && isNumeric(b) && isNumeric(c)) { + a = +a; + b = +b; + c = +c; + norm = displaySum / (a + b + c); + if(norm !== 1) { + a *= norm; + b *= norm; + c *= norm; + } + // map a, b, c onto x and y where the full scale of y + // is [0, sum], and x is [-sum, sum] + // TODO: this makes `a` always the top, `b` the bottom left, + // and `c` the bottom right. Do we want options to rearrange + // these? + y = a; + x = c - b; + cd[i] = {x: x, y: y, a: a, b: b, c: c}; + } + else cd[i] = {x: false, y: false}; + } + + // fill in some extras + var marker, s; + if(subTypes.hasMarkers(trace)) { + // Treat size like x or y arrays --- Run d2c + // this needs to go before ppad computation + marker = trace.marker; + s = marker.size; + + if(Array.isArray(s)) { + var ax = {type: 'linear'}; + Axes.setConvert(ax); + s = ax.makeCalcdata(trace.marker, 'size'); + if(s.length > serieslen) s.splice(serieslen, s.length - serieslen); + } + } + + calcMarkerColorscale(trace); + + // this has migrated up from arraysToCalcdata as we have a reference to 's' here + if(typeof s !== undefined) Lib.mergeArray(s, cd, 'ms'); + + return cd; +}; diff --git a/src/traces/scatterternary/defaults.js b/src/traces/scatterternary/defaults.js new file mode 100644 index 00000000000..8094cb3fa7e --- /dev/null +++ b/src/traces/scatterternary/defaults.js @@ -0,0 +1,91 @@ +/** +* 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 constants = require('../scatter/constants'); +var subTypes = require('../scatter/subtypes'); +var handleMarkerDefaults = require('../scatter/marker_defaults'); +var handleLineDefaults = require('../scatter/line_defaults'); +var handleLineShapeDefaults = require('../scatter/line_shape_defaults'); +var handleTextDefaults = require('../scatter/text_defaults'); + +var attributes = require('./attributes'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Lib.coerce(traceIn, traceOut, attributes, attr, dflt); + } + + var a = coerce('a'), + b = coerce('b'), + c = coerce('c'), + len; + + // allow any one array to be missing, len is the minimum length of those + // present. Note that after coerce data_array's are either Arrays (which + // are truthy even if empty) or undefined. As in scatter, an empty array + // is different from undefined, because it can signify that this data is + // not known yet but expected in the future + if(a) { + len = a.length; + if(b) { + len = Math.min(len, b.length); + if(c) len = Math.min(len, c.length); + } + else if(c) len = Math.min(len, c.length); + else len = 0; + } + else if(b && c) { + len = Math.min(b.length, c.length); + } + + if(!len) { + traceOut.visible = false; + return; + } + + // cut all data arrays down to same length + if(a && len < a.length) traceOut.a = a.slice(0, len); + if(b && len < b.length) traceOut.b = b.slice(0, len); + if(c && len < c.length) traceOut.c = c.slice(0, len); + + coerce('sum'); + + coerce('text'); + + var defaultMode = len < constants.PTS_LINESONLY ? 'lines+markers' : 'lines'; + coerce('mode', defaultMode); + + if(subTypes.hasLines(traceOut)) { + handleLineDefaults(traceIn, traceOut, defaultColor, coerce); + handleLineShapeDefaults(traceIn, traceOut, coerce); + coerce('connectgaps'); + } + + if(subTypes.hasMarkers(traceOut)) { + handleMarkerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if(subTypes.hasText(traceOut)) { + handleTextDefaults(traceIn, traceOut, layout, coerce); + } + + if(subTypes.hasMarkers(traceOut) || subTypes.hasText(traceOut)) { + coerce('marker.maxdisplayed'); + } + + coerce('hoverinfo', (layout._dataLength === 1) ? 'a+b+c+text' : undefined); + + // until 'fill' and 'fillcolor' are supported + traceOut.fill = 'none'; +}; diff --git a/src/traces/scatterternary/hover.js b/src/traces/scatterternary/hover.js new file mode 100644 index 00000000000..212b7d18d83 --- /dev/null +++ b/src/traces/scatterternary/hover.js @@ -0,0 +1,48 @@ +/** +* 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 scatterHover = require('../scatter/hover'); +var Axes = require('../../plots/cartesian/axes'); + + +module.exports = function hoverPoints(pointData, xval, yval, hovermode) { + var scatterPointData = scatterHover(pointData, xval, yval, hovermode); + if(!scatterPointData || scatterPointData[0].index === false) return; + + var newPointData = scatterPointData[0], + cdi = newPointData.cd[newPointData.index]; + + newPointData.a = cdi.a; + newPointData.b = cdi.b; + newPointData.c = cdi.c; + + newPointData.xLabelVal = undefined; + newPointData.yLabelVal = undefined; + // TODO: nice formatting, and label by axis title, for a, b, and c? + + var trace = newPointData.trace, + ternary = trace._ternary, + hoverinfo = trace.hoverinfo.split('+'), + text = []; + + function textPart(ax, val) { + text.push(ax._hovertitle + ': ' + Axes.tickText(ax, val, 'hover').text); + } + + if(hoverinfo.indexOf('all') !== -1) hoverinfo = ['a', 'b', 'c']; + if(hoverinfo.indexOf('a') !== -1) textPart(ternary.aaxis, cdi.a); + if(hoverinfo.indexOf('b') !== -1) textPart(ternary.baxis, cdi.b); + if(hoverinfo.indexOf('c') !== -1) textPart(ternary.caxis, cdi.c); + + newPointData.extraText = text.join('
'); + + return scatterPointData; +}; diff --git a/src/traces/scatterternary/index.js b/src/traces/scatterternary/index.js new file mode 100644 index 00000000000..5fd1953cf50 --- /dev/null +++ b/src/traces/scatterternary/index.js @@ -0,0 +1,34 @@ +/** +* 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 ScatterTernary = {}; + +ScatterTernary.attributes = require('./attributes'); +ScatterTernary.supplyDefaults = require('./defaults'); +ScatterTernary.colorbar = require('../scatter/colorbar'); +ScatterTernary.calc = require('./calc'); +ScatterTernary.plot = require('./plot'); +ScatterTernary.style = require('./style'); +ScatterTernary.hoverPoints = require('./hover'); +ScatterTernary.selectPoints = require('./select'); + +ScatterTernary.moduleType = 'trace'; +ScatterTernary.name = 'scatterternary'; +ScatterTernary.basePlotModule = require('../../plots/ternary'); +ScatterTernary.categories = ['ternary', 'symbols', 'markerColorscale', 'showLegend']; +ScatterTernary.meta = { + hrName: 'scatter_ternary', + description: [ + 'Provides similar functionality to the *scatter* type but on a ternary phase diagram.', + 'The data is provided by at least two arrays out of `a`, `b`, `c` triplets.' + ].join(' ') +}; + +module.exports = ScatterTernary; diff --git a/src/traces/scatterternary/plot.js b/src/traces/scatterternary/plot.js new file mode 100644 index 00000000000..81eccb22518 --- /dev/null +++ b/src/traces/scatterternary/plot.js @@ -0,0 +1,39 @@ +/** +* 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 scatterPlot = require('../scatter/plot'); + + +module.exports = function plot(ternary, data) { + // mimic cartesian plotinfo + var plotinfo = { + x: function() { return ternary.xaxis; }, + y: function() { return ternary.yaxis; }, + plot: ternary.plotContainer + }; + + var calcdata = new Array(data.length), + fullCalcdata = ternary.graphDiv.calcdata; + + for(var i = 0; i < fullCalcdata.length; i++) { + var j = data.indexOf(fullCalcdata[i][0].trace); + + if(j === -1) continue; + + calcdata[j] = fullCalcdata[i]; + + // while we're here and have references to both the Ternary object + // and fullData, connect the two (for use by hover) + data[j]._ternary = ternary; + } + + scatterPlot(ternary.graphDiv, plotinfo, calcdata); +}; diff --git a/src/traces/scatterternary/select.js b/src/traces/scatterternary/select.js new file mode 100644 index 00000000000..bca550e040c --- /dev/null +++ b/src/traces/scatterternary/select.js @@ -0,0 +1,33 @@ +/** +* 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 scatterSelect = require('../scatter/select'); + + +module.exports = function selectPoints(searchInfo, polygon) { + var selection = scatterSelect(searchInfo, polygon); + if(!selection) return; + + var cd = searchInfo.cd, + pt, cdi, i; + + for(i = 0; i < selection.length; i++) { + pt = selection[i]; + cdi = cd[pt.pointNumber]; + pt.a = cdi.a; + pt.b = cdi.b; + pt.c = cdi.c; + delete pt.x; + delete pt.y; + } + + return selection; +}; diff --git a/src/traces/scatterternary/style.js b/src/traces/scatterternary/style.js new file mode 100644 index 00000000000..7b92f4d3200 --- /dev/null +++ b/src/traces/scatterternary/style.js @@ -0,0 +1,24 @@ +/** +* 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 scatterStyle = require('../scatter/style'); + + +module.exports = function style(graphDiv) { + for(var i = 0; i < graphDiv._modules.length; i++) { + // we're just going to call scatter style... if we already + // called it, don't need to redo. + // Later though we may want differences, or we may make style + // more specific in its scope, then we can remove this. + if(graphDiv._modules[i].name === 'scatter') return; + } + scatterStyle(graphDiv); +}; diff --git a/test/image/baselines/gl3d_cufflinks.png b/test/image/baselines/gl3d_cufflinks.png index 7be1febeebd..809e2296303 100644 Binary files a/test/image/baselines/gl3d_cufflinks.png and b/test/image/baselines/gl3d_cufflinks.png differ diff --git a/test/image/baselines/plot_types.png b/test/image/baselines/plot_types.png index dfe2fe6d4f3..2dadfb539a2 100644 Binary files a/test/image/baselines/plot_types.png and b/test/image/baselines/plot_types.png differ diff --git a/test/image/baselines/size_margins.png b/test/image/baselines/size_margins.png index afe9bfebab2..6c3f930a746 100644 Binary files a/test/image/baselines/size_margins.png and b/test/image/baselines/size_margins.png differ diff --git a/test/image/baselines/ternary_array_styles.png b/test/image/baselines/ternary_array_styles.png new file mode 100644 index 00000000000..f58c8741dd4 Binary files /dev/null and b/test/image/baselines/ternary_array_styles.png differ diff --git a/test/image/baselines/ternary_lines.png b/test/image/baselines/ternary_lines.png new file mode 100644 index 00000000000..9cf20933031 Binary files /dev/null and b/test/image/baselines/ternary_lines.png differ diff --git a/test/image/baselines/ternary_markers.png b/test/image/baselines/ternary_markers.png new file mode 100644 index 00000000000..f131b3266f5 Binary files /dev/null and b/test/image/baselines/ternary_markers.png differ diff --git a/test/image/baselines/ternary_multiple.png b/test/image/baselines/ternary_multiple.png new file mode 100644 index 00000000000..bb6d078a5ab Binary files /dev/null and b/test/image/baselines/ternary_multiple.png differ diff --git a/test/image/baselines/ternary_simple.png b/test/image/baselines/ternary_simple.png new file mode 100644 index 00000000000..c6d5869de40 Binary files /dev/null and b/test/image/baselines/ternary_simple.png differ diff --git a/test/image/mocks/plot_types.json b/test/image/mocks/plot_types.json index 8d29b07d163..8ac1cd0abab 100644 --- a/test/image/mocks/plot_types.json +++ b/test/image/mocks/plot_types.json @@ -11,8 +11,7 @@ 1, 2 ], - "legendgroup": "group", - "uid": "d036e4" + "uid": "574a94" }, { "type": "scatter3d", @@ -31,8 +30,7 @@ 1, 2 ], - "legendgroup": "group", - "uid": "a4b919" + "uid": "cd61e2" }, { "type": "scattergeo", @@ -47,7 +45,7 @@ 0, 45 ], - "uid": "cdc474" + "uid": "d8595c" }, { "type": "pie", @@ -63,27 +61,46 @@ ], "domain": { "x": [ - 0.5, - 1 + 0.33, + 0.67 ], "y": [ 0.5, 1 ] }, - "uid": "586104" + "uid": "d7e376" + }, + { + "type": "scatterternary", + "a": [ + 2, + 1, + 1 + ], + "b": [ + 1, + 2, + 1 + ], + "c": [ + 1, + 1, + 2.12345 + ], + "uid": "609928" } ], "layout": { "xaxis": { "domain": [ 0, - 0.5 + 0.33 ], "type": "linear", "range": [ - 0.8603667136812412, - 3.139633286318759 + 0.8719530783206721, + 3.128046921679328 ], "autorange": true }, @@ -94,16 +111,30 @@ ], "type": "linear", "range": [ - 0.9078947368421053, - 2.0921052631578947 + 0.9202898550724637, + 2.079710144927536 ], "autorange": true }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0.33, + 0.67 + ] + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0.33, + 0.67 + ] + }, "scene": { "domain": { "x": [ 0, - 0.5 + 0.33 ], "y": [ 0.5, @@ -119,8 +150,8 @@ "geo": { "domain": { "x": [ - 0.5, - 1 + 0.33, + 0.67 ], "y": [ 0, @@ -128,6 +159,34 @@ ] } }, + "ternary": { + "domain": { + "x": [ + 0.67, + 1 + ], + "y": [ + 0.5, + 1 + ] + }, + "aaxis": { + "title": "A" + }, + "baxis": { + "title": "B" + }, + "caxis": { + "title": "C" + } + }, + "margin": { + "t": 25, + "l": 25, + "r": 25, + "b": 25 + }, + "showlegend": false, "height": 450, "width": 1000, "autosize": true diff --git a/test/image/mocks/ternary_array_styles.json b/test/image/mocks/ternary_array_styles.json new file mode 100644 index 00000000000..c127c4c3b22 --- /dev/null +++ b/test/image/mocks/ternary_array_styles.json @@ -0,0 +1,116 @@ +{ + "data": [ + { + "type": "scatterternary", + "a": [ + 0.2, + 0.3, + null, + 0.1 + ], + "b": [ + 0.1, + 0.2, + null, + 0.4 + ], + "c": [ + 0.1, + 0.2, + null, + 0.1 + ], + "marker": { + "symbol": [ + "square", + "cross", + null, + "diamond" + ], + "size": [ + 10, + 20, + null, + 30 + ], + "color": [ + 20, + 30, + null, + 10 + ], + "showscale": true, + "line": { + "width": [ + 3, + 5, + null, + 2 + ], + "color": [ + 10, + null, + 30, + 20 + ], + "colorscale": "Greens", + "cmin": 10, + "cmax": 30 + }, + "cmin": 10, + "cmax": 30, + "colorscale": [ + [ + 0, + "rgb(220,220,220)" + ], + [ + 0.2, + "rgb(245,195,157)" + ], + [ + 0.4, + "rgb(245,160,105)" + ], + [ + 1, + "rgb(178,10,28)" + ] + ] + }, + "line": { + "dash": "dash", + "shape": "spline", + "width": 3 + }, + "connectgaps": true, + "uid": "35ff50" + } + ], + "layout": { + "ternary": { + "aaxis": { + "showline": true, + "ticks": "inside", + "showgrid": true, + "color": "#ccc" + }, + "baxis": { + "showline": true, + "ticks": "inside", + "showgrid": true, + "color": "#0f0", + "title": "chocolate", + "min": 0.2 + }, + "caxis": { + "showline": true, + "ticks": "inside", + "showgrid": true + } + }, + "height": 450, + "width": 700, + "autosize": false + } +} diff --git a/test/image/mocks/ternary_lines.json b/test/image/mocks/ternary_lines.json new file mode 100644 index 00000000000..bae46288fa1 --- /dev/null +++ b/test/image/mocks/ternary_lines.json @@ -0,0 +1,399 @@ +{ + "data": [ + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 0, + 10, + 0 + ], + "b": [ + 100, + 90, + 90 + ], + "c": [ + 0, + 0, + 10 + ], + "line": { + "color": "#c00" + }, + "uid": "4a9e8d" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 0, + 10, + 15, + 0 + ], + "b": [ + 90, + 90, + 85, + 70 + ], + "c": [ + 10, + 0, + 0, + 30 + ], + "line": { + "color": "#c00" + }, + "uid": "6b700b" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 0, + 15, + 20, + 20, + 5, + 5, + 0 + ], + "b": [ + 70, + 85, + 80, + 53, + 53, + 45, + 50 + ], + "c": [ + 30, + 0, + 0, + 32, + 42, + 50, + 50 + ], + "line": { + "color": "#c00" + }, + "uid": "0e280c" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 20, + 35, + 35, + 28, + 20 + ], + "b": [ + 80, + 65, + 45, + 45, + 53 + ], + "c": [ + 0, + 0, + 20, + 27, + 32 + ], + "line": { + "color": "#c00" + }, + "uid": "40fccc" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 35, + 35, + 55 + ], + "b": [ + 65, + 45, + 45 + ], + "c": [ + 0, + 20, + 0 + ], + "line": { + "color": "#c00" + }, + "uid": "d990f2" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 55, + 100, + 60, + 40, + 40 + ], + "b": [ + 45, + 0, + 0, + 20, + 45 + ], + "c": [ + 0, + 0, + 40, + 40, + 15 + ], + "line": { + "color": "#c00" + }, + "uid": "b6c367" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 40, + 40, + 28, + 28 + ], + "b": [ + 45, + 20, + 20, + 45 + ], + "c": [ + 15, + 40, + 52, + 27 + ], + "line": { + "color": "#c00" + }, + "uid": "90d4e2" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 60, + 40, + 40 + ], + "b": [ + 0, + 0, + 20 + ], + "c": [ + 40, + 60, + 40 + ], + "line": { + "color": "#c00" + }, + "uid": "c483e4" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 28, + 28, + 40, + 40 + ], + "b": [ + 0, + 20, + 20, + 0 + ], + "c": [ + 72, + 52, + 40, + 60 + ], + "line": { + "color": "#c00" + }, + "uid": "9b3fa9" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 0, + 28, + 28, + 12, + 12, + 0 + ], + "b": [ + 50, + 22, + 0, + 0, + 8, + 20 + ], + "c": [ + 50, + 50, + 72, + 88, + 80, + 80 + ], + "line": { + "color": "#c00" + }, + "uid": "5c8c5a" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 0, + 0, + 12, + 12 + ], + "b": [ + 0, + 20, + 8, + 0 + ], + "c": [ + 100, + 80, + 80, + 88 + ], + "line": { + "color": "#c00" + }, + "uid": "845650" + }, + { + "type": "scatterternary", + "mode": "lines", + "a": [ + 28, + 28, + 5, + 5, + 20 + ], + "b": [ + 45, + 22, + 45, + 53, + 53 + ], + "c": [ + 27, + 50, + 50, + 42, + 32 + ], + "line": { + "color": "#c00" + }, + "uid": "ca74b0" + } + ], + "layout": { + "ternary": { + "aaxis": { + "title": "Clay", + "min": 0.01, + "linewidth": 2, + "ticks": "outside", + "ticklen": 8, + "showgrid": true, + "tickvals": [ + 0.2, + 0.4, + 0.6, + 0.8 + ], + "ticktext": [ + "20%", + "40%", + "60%", + "80%" + ] + }, + "baxis": { + "title": "Sand", + "min": 0.01, + "linewidth": 2, + "ticks": "outside", + "ticklen": 8, + "showgrid": true, + "tickvals": [ + 0.2, + 0.4, + 0.6, + 0.8 + ], + "ticktext": [ + "20%", + "40%", + "60%", + "80%" + ] + }, + "caxis": { + "title": "Silt", + "min": 0.01, + "linewidth": 2, + "ticks": "outside", + "ticklen": 8, + "showgrid": true, + "tickvals": [ + 0.2, + 0.4, + 0.6, + 0.8 + ], + "ticktext": [ + "20%", + "40%", + "60%", + "80%" + ] + } + }, + "showlegend": false, + "width": 700, + "height": 450, + "autosize": false + } +} diff --git a/test/image/mocks/ternary_markers.json b/test/image/mocks/ternary_markers.json new file mode 100644 index 00000000000..12e33c13448 --- /dev/null +++ b/test/image/mocks/ternary_markers.json @@ -0,0 +1,121 @@ +{ + "data": [ + { + "type": "scatterternary", + "mode": "markers", + "a": [ + 75, + 70, + 75, + 5, + 10, + 10, + 20, + 10, + 15, + 10, + 20 + ], + "b": [ + 25, + 10, + 20, + 60, + 80, + 90, + 70, + 20, + 5, + 10, + 10 + ], + "c": [ + 0, + 20, + 5, + 35, + 10, + 0, + 10, + 70, + 80, + 80, + 70 + ], + "text": [ + "point 1", + "point 2", + "point 3", + "point 4", + "point 5", + "point 6", + "point 7", + "point 8", + "point 9", + "point 10", + "point 11" + ], + "marker": { + "symbol": 100, + "color": "#DB7365", + "size": 14, + "line": { + "width": 2 + } + }, + "uid": "de1eca" + } + ], + "layout": { + "ternary": { + "sum": 100, + "aaxis": { + "titlefont": { + "size": 20 + }, + "tickfont": { + "size": 15 + }, + "tickcolor": "rgba(0, 0, 0, 0)", + "ticklen": 5, + "showline": true, + "showgrid": true, + "title": "Journalist", + "tickangle": 0 + }, + "baxis": { + "titlefont": { + "size": 20 + }, + "tickfont": { + "size": 15 + }, + "tickcolor": "rgba(0, 0, 0, 0)", + "ticklen": 5, + "showline": true, + "showgrid": true, + "title": "
Developer", + "tickangle": 45 + }, + "caxis": { + "titlefont": { + "size": 20 + }, + "tickfont": { + "size": 15 + }, + "tickcolor": "rgba(0, 0, 0, 0)", + "ticklen": 5, + "showline": true, + "showgrid": true, + "title": "
Designer", + "tickangle": -45 + }, + "bgcolor": "#fff1e0" + }, + "paper_bgcolor": "#fff1e0", + "height": 450, + "width": 700, + "autosize": false + } +} diff --git a/test/image/mocks/ternary_multiple.json b/test/image/mocks/ternary_multiple.json new file mode 100644 index 00000000000..b41308d655f --- /dev/null +++ b/test/image/mocks/ternary_multiple.json @@ -0,0 +1,95 @@ +{ + "data": [ + { + "type": "scatterternary", + "mode": "markers", + "a": [ + 0.1, + 0.2, + 0.3 + ], + "b": [ + 0.3, + 0.2, + 0.1 + ], + "name": "c missing", + "uid": "757d30" + }, + { + "type": "scatterternary", + "mode": "markers", + "a": [ + 0.1, + 0.2, + 0.3 + ], + "c": [ + 0.3, + 0.2, + 0.1 + ], + "subplot": "ternary2", + "name": "b missing", + "uid": "6c0753" + }, + { + "type": "scatterternary", + "mode": "markers", + "b": [ + 0.1, + 0.2, + 0.3 + ], + "c": [ + 0.3, + 0.2, + 0.1 + ], + "subplot": "ternary3", + "name": "a missing", + "uid": "12520d" + } + ], + "layout": { + "ternary": { + "domain": { + "x": [ + 0, + 0.5 + ], + "y": [ + 0, + 0.4 + ] + } + }, + "ternary2": { + "domain": { + "x": [ + 0, + 0.5 + ], + "y": [ + 0.6, + 1 + ] + } + }, + "ternary3": { + "domain": { + "x": [ + 0.5, + 1 + ], + "y": [ + 0.25, + 0.75 + ] + } + }, + "height": 450, + "width": 700, + "autosize": false + } +} diff --git a/test/image/mocks/ternary_simple.json b/test/image/mocks/ternary_simple.json new file mode 100644 index 00000000000..ea1d78ff2a3 --- /dev/null +++ b/test/image/mocks/ternary_simple.json @@ -0,0 +1,52 @@ +{ + "data": [ + { + "a": [ + 2, + 1, + 1 + ], + "b": [ + 1, + 2, + 1 + ], + "c": [ + 1, + 1, + 2.12345 + ], + "type": "scatterternary", + "uid": "412fa8" + } + ], + "layout": { + "ternary": { + "aaxis": { + "showline": true, + "ticks": "outside", + "showgrid": true, + "color": "#ccc", + "min": 0.231 + }, + "baxis": { + "showline": true, + "ticks": "outside", + "showgrid": true, + "color": "#0f0", + "title": "chocolate", + "min": 0.2 + }, + "caxis": { + "showline": true, + "ticks": "outside", + "showgrid": true, + "min": 0.11 + }, + "bgcolor": "#eee" + }, + "height": 450, + "width": 700, + "autosize": true + } +} diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index abb8a3c9953..f820e830263 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -3,6 +3,7 @@ var PlotlyInternal = require('@src/plotly'); var Plots = require('@src/plots/plots'); var Lib = require('@src/lib'); var Color = require('@src/components/color'); +var tinycolor = require('tinycolor2'); var handleTickValueDefaults = require('@src/plots/cartesian/tick_value_defaults'); var Axes = PlotlyInternal.Axes; @@ -221,10 +222,11 @@ describe('Test axes', function() { yaxis: {} }; supplyLayoutDefaults(layoutIn, layoutOut, fullData); + var lightLine = tinycolor(Color.lightLine).toRgbString(); expect(layoutOut.xaxis.gridwidth).toBe(1); - expect(layoutOut.xaxis.gridcolor).toBe(Color.lightLine); + expect(tinycolor(layoutOut.xaxis.gridcolor).toRgbString()).toBe(lightLine); expect(layoutOut.yaxis.gridwidth).toBe(1); - expect(layoutOut.yaxis.gridcolor).toBe(Color.lightLine); + expect(tinycolor(layoutOut.yaxis.gridcolor).toRgbString()).toBe(lightLine); }); it('should set gridcolor/gridwidth to undefined if showgrid is false', function() { @@ -319,6 +321,67 @@ describe('Test axes', function() { supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutOut._hasCartesian).toBe(undefined); }); + + it('should use \'axis.color\' as default for \'axis.titlefont.color\'', function() { + layoutIn = { + xaxis: { color: 'red' }, + yaxis: {}, + yaxis2: { titlefont: { color: 'yellow' } } + }; + + layoutOut.font = { color: 'blue' }, + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.titlefont.color).toEqual('red'); + expect(layoutOut.yaxis.titlefont.color).toEqual('blue'); + expect(layoutOut.yaxis2.titlefont.color).toEqual('yellow'); + }); + + it('should use \'axis.color\' as default for \'axis.linecolor\'', function() { + layoutIn = { + xaxis: { showline: true, color: 'red' }, + yaxis: { linecolor: 'blue' }, + yaxis2: { showline: true } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.linecolor).toEqual('red'); + expect(layoutOut.yaxis.linecolor).toEqual('blue'); + expect(layoutOut.yaxis2.linecolor).toEqual('#444'); + }); + + it('should use \'axis.color\' as default for \'axis.zerolinecolor\'', function() { + layoutIn = { + xaxis: { showzeroline: true, color: 'red' }, + yaxis: { zerolinecolor: 'blue' }, + yaxis2: { showzeroline: true } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.zerolinecolor).toEqual('red'); + expect(layoutOut.yaxis.zerolinecolor).toEqual('blue'); + expect(layoutOut.yaxis2.zerolinecolor).toEqual('#444'); + }); + + it('should use combo of \'axis.color\', bgcolor and lightFraction as default for \'axis.gridcolor\'', function() { + layoutIn = { + paper_bgcolor: 'green', + plot_bgcolor: 'yellow', + xaxis: { showgrid: true, color: 'red' }, + yaxis: { gridcolor: 'blue' }, + yaxis2: { showgrid: true } + }; + + var bgColor = Color.combine('yellow', 'green'), + frac = 100 * (0xe - 0x4) / (0xf - 0x4); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.xaxis.gridcolor) + .toEqual(tinycolor.mix('red', bgColor, frac).toRgbString()); + expect(layoutOut.yaxis.gridcolor).toEqual('blue'); + expect(layoutOut.yaxis2.gridcolor) + .toEqual(tinycolor.mix('#444', bgColor, frac).toRgbString()); + }); }); describe('handleTickDefaults', function() { diff --git a/test/jasmine/tests/dragelement_test.js b/test/jasmine/tests/dragelement_test.js new file mode 100644 index 00000000000..924f7f3bcaf --- /dev/null +++ b/test/jasmine/tests/dragelement_test.js @@ -0,0 +1,301 @@ +var dragElement = require('@src/components/dragelement'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var mouseEvent = require('../assets/mouse_event'); + + +describe('dragElement', function() { + 'use strict'; + + beforeEach(function() { + this.gd = createGraphDiv(); + this.hoverlayer = document.createElement('div'); + this.element = document.createElement('div'); + + this.gd.className = 'js-plotly-plot'; + this.gd._fullLayout = { + _hoverlayer: d3.select(this.hoverlayer) + }; + this.element.innerHTML = 'drag element'; + + this.gd.appendChild(this.element); + this.element.appendChild(this.hoverlayer); + + var clientRect = this.element.getBoundingClientRect(); + this.x = clientRect.left + clientRect.width / 2; + this.y = clientRect.top + clientRect.height / 2; + }); + + afterEach(destroyGraphDiv); + + it('should init drag element', function() { + var options = { element: this.element }; + dragElement.init(options); + + expect(this.element.style.pointerEvents).toEqual('all', 'with pointer event style'); + expect(this.element.onmousedown).not.toBe(null, 'with on mousedown handler'); + }); + + it('should pass event, startX and startY to prepFn on mousedown', function() { + var args = []; + var options = { + element: this.element, + prepFn: function(event, startX, startY) { + args = [event, startX, startY]; + } + }; + dragElement.init(options); + + mouseEvent('mousedown', this.x, this.y); + mouseEvent('mouseup', this.x, this.y); + + expect(args[0]).not.toBe(null); + expect(args[1]).toEqual(Math.floor(this.x)); + expect(args[2]).toEqual(Math.floor(this.y)); + }); + + it('should pass dragged, dx and dy to moveFn on mousemove', function() { + var args = []; + var options = { + element: this.element, + moveFn: function(dx, dy, dragged) { + args = [dx, dy, dragged]; + } + }; + dragElement.init(options); + + mouseEvent('mousedown', this.x, this.y); + mouseEvent('mousemove', this.x + 10, this.y + 10); + mouseEvent('mouseup', this.x, this.y); + + expect(args[0]).toEqual(10); + expect(args[1]).toEqual(10); + expect(args[2]).toBe(true); + }); + + it('should pass dragged and numClicks to doneFn on mouseup & mouseout', function() { + var args = []; + var options = { + element: this.element, + doneFn: function(dragged, numClicks) { + args = [dragged, numClicks]; + } + }; + dragElement.init(options); + + mouseEvent('mousedown', this.x, this.y); + mouseEvent('mouseup', this.x, this.y); + + expect(args[0]).toBe(false); + expect(args[1]).toEqual(1); + + mouseEvent('mousedown', this.x, this.y); + mouseEvent('mousemove', this.x + 10, this.y + 10); + mouseEvent('mouseout', this.x, this.y); + + expect(args[0]).toBe(true); + expect(args[1]).toEqual(2); + }); + + it('should add a cover slip div to the DOM', function() { + function countCoverSlip() { + return d3.selectAll('.dragcover').size(); + } + + var options = { element: this.element }; + dragElement.init(options); + + expect(countCoverSlip()).toEqual(0); + + mouseEvent('mousedown', this.x, this.y); + expect(countCoverSlip()).toEqual(1); + + mouseEvent('mousemove', this.x + 10, this.y + 10); + expect(countCoverSlip()).toEqual(1); + + mouseEvent('mouseout', this.x, this.y); + expect(countCoverSlip()).toEqual(0); + }); + + it('should fire off click event when down/up without dragging', function() { + var options = { element: this.element }; + dragElement.init(options); + + var mockObj = { + handleClick: function() {}, + dummy: function() {} + }; + spyOn(mockObj, 'handleClick'); + spyOn(mockObj, 'dummy'); + + this.element.onclick = mockObj.handleClick; + + mouseEvent('mousedown', this.x, this.y); + mouseEvent('mouseup', this.x, this.y); + + expect(mockObj.handleClick).toHaveBeenCalled(); + + this.element.onclick = mockObj.dummy; + + mouseEvent('mousedown', this.x, this.y); + mouseEvent('mousemove', this.x + 10, this.y + 10); + mouseEvent('mouseup', this.x, this.y); + + expect(mockObj.dummy).not.toHaveBeenCalled(); + }); +}); + +describe('dragElement.getCursor', function() { + 'use strict'; + + var getCursor = dragElement.getCursor; + + it('should return sw-resize when x < 1/3, y < 1/3', function() { + var cursor = getCursor(0.2, 0); + expect(cursor).toEqual('sw-resize'); + + cursor = getCursor(1, 0, 'left'); + expect(cursor).toEqual('sw-resize', 'with left xanchor'); + + cursor = getCursor(0.3, 1, null, 'bottom'); + expect(cursor).toEqual('sw-resize', 'with bottom yanchor'); + }); + + it('should return s-resize when 1/3 < x < 2/3, y < 1/3', function() { + var cursor = getCursor(0.4, 0.3); + expect(cursor).toEqual('s-resize'); + + cursor = getCursor(0, 0, 'center'); + expect(cursor).toEqual('s-resize', 'with center xanchor'); + + cursor = getCursor(0.63, 1, null, 'bottom'); + expect(cursor).toEqual('s-resize', 'with bottom yanchor'); + }); + + it('should return se-resize when x > 2/3, y < 1/3', function() { + var cursor = getCursor(0.9, 0.1); + expect(cursor).toEqual('se-resize'); + + cursor = getCursor(0, 0, 'right'); + expect(cursor).toEqual('se-resize', 'with right xanchor'); + + cursor = getCursor(0.63, 1, null, 'bottom'); + expect(cursor).toEqual('s-resize', 'with bottom yanchor'); + }); + + it('should return w-resize when x < 1/3, 1/3 < y < 2/3', function() { + var cursor = getCursor(0.1, 0.4); + expect(cursor).toEqual('w-resize'); + + cursor = getCursor(0.9, 0.5, 'left'); + expect(cursor).toEqual('w-resize', 'with left xanchor'); + + cursor = getCursor(0.1, 0.1, null, 'middle'); + expect(cursor).toEqual('w-resize', 'with middle yanchor'); + }); + + it('should return move when 1/3 < x < 2/3, 1/3 < y < 2/3', function() { + var cursor = getCursor(0.4, 0.4); + expect(cursor).toEqual('move'); + + cursor = getCursor(0.9, 0.5, 'center'); + expect(cursor).toEqual('move', 'with center xanchor'); + + cursor = getCursor(0.4, 0.1, null, 'middle'); + expect(cursor).toEqual('move', 'with middle yanchor'); + }); + + it('should return e-resize when x > 1/3, 1/3 < y < 2/3', function() { + var cursor = getCursor(0.8, 0.4); + expect(cursor).toEqual('e-resize'); + + cursor = getCursor(0.09, 0.5, 'right'); + expect(cursor).toEqual('e-resize', 'with right xanchor'); + + cursor = getCursor(0.9, 0.1, null, 'middle'); + expect(cursor).toEqual('e-resize', 'with middle yanchor'); + }); + + it('should return nw-resize when x > 1/3, y > 2/3', function() { + var cursor = getCursor(0.2, 0.7); + expect(cursor).toEqual('nw-resize'); + + cursor = getCursor(0.9, 0.9, 'left'); + expect(cursor).toEqual('nw-resize', 'with left xanchor'); + + cursor = getCursor(0.1, 0.1, null, 'top'); + expect(cursor).toEqual('nw-resize', 'with top yanchor'); + }); + + it('should return nw-resize when 1/3 < x < 2/3, y > 2/3', function() { + var cursor = getCursor(0.4, 0.7); + expect(cursor).toEqual('n-resize'); + + cursor = getCursor(0.9, 0.9, 'center'); + expect(cursor).toEqual('n-resize', 'with center xanchor'); + + cursor = getCursor(0.5, 0.1, null, 'top'); + expect(cursor).toEqual('n-resize', 'with top yanchor'); + }); + + it('should return nw-resize when x > 2/3, y > 2/3', function() { + var cursor = getCursor(0.7, 0.7); + expect(cursor).toEqual('ne-resize'); + + cursor = getCursor(0.09, 0.9, 'right'); + expect(cursor).toEqual('ne-resize', 'with right xanchor'); + + cursor = getCursor(0.8, 0.1, null, 'top'); + expect(cursor).toEqual('ne-resize', 'with top yanchor'); + }); +}); + +describe('dragElement.align', function() { + 'use strict'; + + var align = dragElement.align; + + it('should return min value if anchor is set to \'bottom\' or \'left\'', function() { + var al = align(0, 1, 0, 1, 'bottom'); + expect(al).toEqual(0); + + al = align(0, 1, 0, 1, 'left'); + expect(al).toEqual(0); + }); + + it('should return max value if anchor is set to \'top\' or \'right\'', function() { + var al = align(0, 1, 0, 1, 'top'); + expect(al).toEqual(1); + + al = align(0, 1, 0, 1, 'right'); + expect(al).toEqual(1); + }); + + it('should return center value if anchor is set to \'middle\' or \'center\'', function() { + var al = align(0, 1, 0, 1, 'middle'); + expect(al).toEqual(0.5); + + al = align(0, 1, 0, 1, 'center'); + expect(al).toEqual(0.5); + }); + + it('should return center value if anchor is set to \'middle\' or \'center\'', function() { + var al = align(0, 1, 0, 1, 'middle'); + expect(al).toEqual(0.5); + + al = align(0, 1, 0, 1, 'center'); + expect(al).toEqual(0.5); + }); + + it('should return min value ', function() { + var al = align(0, 1, 0, 1); + expect(al).toEqual(0); + }); + + it('should return max value ', function() { + var al = align(1, 1, 0, 1); + expect(al).toEqual(2); + }); +}); diff --git a/test/jasmine/tests/gl3daxes_test.js b/test/jasmine/tests/gl3daxes_test.js index 5b3f0f5934e..1e4b61912bd 100644 --- a/test/jasmine/tests/gl3daxes_test.js +++ b/test/jasmine/tests/gl3daxes_test.js @@ -11,7 +11,8 @@ describe('Test Gl3dAxes', function() { var options = { font: 'Open Sans', scene: {id: 'scene'}, - data: [{x: [], y: []}] + data: [{x: [], y: []}], + bgColor: '#fff' }; beforeEach(function() { @@ -30,7 +31,7 @@ describe('Test Gl3dAxes', function() { 'showspikes': true, 'spikesides': true, 'spikethickness': 2, - 'spikecolor': 'rgb(0,0,0)', + 'spikecolor': '#444', 'showbackground': false, 'showaxeslabels': true }, @@ -42,7 +43,7 @@ describe('Test Gl3dAxes', function() { 'showspikes': true, 'spikesides': true, 'spikethickness': 2, - 'spikecolor': 'rgb(0,0,0)', + 'spikecolor': '#444', 'showbackground': false, 'showaxeslabels': true }, @@ -54,7 +55,7 @@ describe('Test Gl3dAxes', function() { 'showspikes': true, 'spikesides': true, 'spikethickness': 2, - 'spikecolor': 'rgb(0,0,0)', + 'spikecolor': '#444', 'showbackground': false, 'showaxeslabels': true } @@ -64,14 +65,14 @@ describe('Test Gl3dAxes', function() { var keys = Object.keys(validObject); for(var i = 0; i < keys.length; i++) { var k = keys[i]; - if(validObject[k] !== testObject[k]) return false; + expect(validObject[k]).toBe(testObject[k]); } return true; } supplyLayoutDefaults(layoutIn, layoutOut, options); ['xaxis', 'yaxis', 'zaxis'].forEach(function(axis) { - expect(checkKeys(expected[axis], layoutOut[axis])).toBe(true); + checkKeys(expected[axis], layoutOut[axis]); }); }); }); diff --git a/test/jasmine/tests/gl3dlayout_test.js b/test/jasmine/tests/gl3dlayout_test.js index 466a24f3314..8a2f4f81cdc 100644 --- a/test/jasmine/tests/gl3dlayout_test.js +++ b/test/jasmine/tests/gl3dlayout_test.js @@ -1,5 +1,8 @@ var Gl3d = require('@src/plots/gl3d'); +var tinycolor = require('tinycolor2'); +var Color = require('@src/components/color'); + describe('Test Gl3d layout defaults', function() { 'use strict'; @@ -226,5 +229,27 @@ describe('Test Gl3d layout defaults', function() { supplyLayoutDefaults(layoutIn, layoutOut, fullData); expect(layoutIn.scene).toBe(undefined); }); + + it('should use combo of \'axis.color\', bgcolor and lightFraction as default for \'axis.gridcolor\'', function() { + layoutIn = { + paper_bgcolor: 'green', + scene: { + bgcolor: 'yellow', + xaxis: { showgrid: true, color: 'red' }, + yaxis: { gridcolor: 'blue' }, + zaxis: { showgrid: true } + } + }; + + var bgColor = Color.combine('yellow', 'green'), + frac = 100 * (204 - 0x44) / (255 - 0x44); + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.scene.xaxis.gridcolor) + .toEqual(tinycolor.mix('red', bgColor, frac).toRgbString()); + expect(layoutOut.scene.yaxis.gridcolor).toEqual('blue'); + expect(layoutOut.scene.zaxis.gridcolor) + .toEqual(tinycolor.mix('#444', bgColor, frac).toRgbString()); + }); }); }); diff --git a/test/jasmine/tests/lib_test.js b/test/jasmine/tests/lib_test.js index fd1a2f5c28b..3b11c1868d9 100644 --- a/test/jasmine/tests/lib_test.js +++ b/test/jasmine/tests/lib_test.js @@ -1,5 +1,11 @@ var Lib = require('@src/lib'); -var Plots = require('@src/plots/plots'); +var setCursor = require('@src/lib/setcursor'); + +var d3 = require('d3'); +var PlotlyInternal = require('@src/plotly'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var Plots = PlotlyInternal.Plots; describe('Test lib.js:', function() { @@ -588,6 +594,46 @@ describe('Test lib.js:', function() { }); + + describe('subplotid valtype', function() { + var dflt = 'slice'; + var idAttrs = { + pizza: { + valType: 'subplotid', + dflt: dflt + } + }; + + var goodVals = ['slice', 'slice2', 'slice1492']; + + goodVals.forEach(function(goodVal) { + it('should allow "' + goodVal + '"', function() { + expect(coerce({pizza: goodVal}, {}, idAttrs, 'pizza')) + .toEqual(goodVal); + }); + }); + + var badVals = [ + 'slice0', + 'slice1', + 'Slice2', + '2slice', + '2', + 2, + 'slice2 ', + 'slice2.0', + ' slice2', + 'slice 2', + 'slice01' + ]; + + badVals.forEach(function(badVal) { + it('should not allow "' + badVal + '"', function() { + expect(coerce({pizza: badVal}, {}, idAttrs, 'pizza')) + .toEqual(dflt); + }); + }); + }); }); describe('coerceFont', function() { @@ -675,4 +721,53 @@ describe('Test lib.js:', function() { }); }); + describe('setCursor', function() { + + beforeEach(function() { + this.el3 = d3.select(createGraphDiv()); + }); + + afterEach(destroyGraphDiv); + + it('should assign cursor- class', function() { + setCursor(this.el3, 'one'); + + expect(this.el3.attr('class')).toEqual('cursor-one'); + }); + + it('should assign cursor- class while present non-cursor- classes', function() { + this.el3.classed('one', true); + this.el3.classed('two', true); + this.el3.classed('three', true); + setCursor(this.el3, 'one'); + + expect(this.el3.attr('class')).toEqual('one two three cursor-one'); + }); + + it('should update class from one cursor- class to another', function() { + this.el3.classed('cursor-one', true); + setCursor(this.el3, 'two'); + + expect(this.el3.attr('class')).toEqual('cursor-two'); + }); + + it('should update multiple cursor- classes', function() { + this.el3.classed('cursor-one', true); + this.el3.classed('cursor-two', true); + this.el3.classed('cursor-three', true); + setCursor(this.el3, 'four'); + + expect(this.el3.attr('class')).toEqual('cursor-four'); + }); + + it('should remove cursor- if no new class is given', function() { + this.el3.classed('cursor-one', true); + this.el3.classed('cursor-two', true); + this.el3.classed('cursor-three', true); + setCursor(this.el3); + + expect(this.el3.attr('class')).toEqual(''); + }); + }); + }); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index 933aafd28f9..9931091d431 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -309,9 +309,6 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; gd._fullLayout._hasGL3D = true; - gd._fullLayout._hasGeo = false; - gd._fullLayout._hasGL2D = false; - gd._fullLayout._hasPie = false; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -327,10 +324,7 @@ describe('ModeBar', function() { var gd = getMockGraphInfo(); gd._fullLayout._hasCartesian = true; - gd._fullLayout._hasGL3D = false; gd._fullLayout._hasGeo = true; - gd._fullLayout._hasGL2D = false; - gd._fullLayout._hasPie = false; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; @@ -355,9 +349,6 @@ describe('ModeBar', function() { _module: {selectPoints: true} }]; gd._fullLayout.xaxis = {fixedrange: false}; - gd._fullLayout._hasGL3D = false; - gd._fullLayout._hasGeo = false; - gd._fullLayout._hasGL2D = false; gd._fullLayout._hasPie = true; manageModeBar(gd); @@ -373,11 +364,77 @@ describe('ModeBar', function() { ]); var gd = getMockGraphInfo(); - gd._fullLayout._hasCartesian = false; gd._fullLayout._hasGL3D = true; gd._fullLayout._hasGeo = true; - gd._fullLayout._hasGL2D = false; - gd._fullLayout._hasPie = false; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (un-selectable ternary version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasTernary = true; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (selectable ternary version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d', 'select2d', 'lasso2d'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasTernary = true; + gd._fullData = [{ + type: 'scatterternary', + visible: true, + mode: 'markers', + _module: {selectPoints: true} + }]; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (ternary + cartesian version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['zoom2d', 'pan2d'], + ['hoverClosestCartesian', 'hoverCompareCartesian'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasTernary = true; + gd._fullLayout._hasCartesian = true; + + manageModeBar(gd); + var modeBar = gd._fullLayout._modeBar; + + checkButtons(modeBar, buttons, 1); + }); + + it('creates mode bar (ternary + gl3d version)', function() { + var buttons = getButtons([ + ['toImage', 'sendDataToCloud'], + ['resetViews', 'toggleHover'] + ]); + + var gd = getMockGraphInfo(); + gd._fullLayout._hasTernary = true; + gd._fullLayout._hasGL3D = true; manageModeBar(gd); var modeBar = gd._fullLayout._modeBar; diff --git a/test/jasmine/tests/plotschema_test.js b/test/jasmine/tests/plotschema_test.js index b57be39ea3a..7064557f0af 100644 --- a/test/jasmine/tests/plotschema_test.js +++ b/test/jasmine/tests/plotschema_test.js @@ -91,7 +91,7 @@ describe('plot schema', function() { it('all subplot objects should contain _isSubplotObj', function() { var IS_SUBPLOT_OBJ = '_isSubplotObj', - astrs = ['xaxis', 'yaxis', 'scene', 'geo'], + astrs = ['xaxis', 'yaxis', 'scene', 'geo', 'ternary'], list = []; // check if the subplot objects have '_isSubplotObj' diff --git a/test/jasmine/tests/scatterternary_test.js b/test/jasmine/tests/scatterternary_test.js new file mode 100644 index 00000000000..e2d6c293085 --- /dev/null +++ b/test/jasmine/tests/scatterternary_test.js @@ -0,0 +1,318 @@ +var Plotly = require('@lib'); +var Lib = require('@src/lib'); +var ScatterTernary = require('@src/traces/scatterternary'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var customMatchers = require('../assets/custom_matchers'); + + +describe('scatterternary defaults', function() { + 'use strict'; + + var supplyDefaults = ScatterTernary.supplyDefaults; + + var traceIn, traceOut; + + var defaultColor = '#444', + layout = {}; + + beforeEach(function() { + traceOut = {}; + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (base case)', function() { + traceIn = { + a: [1, 2, 3], + b: [1, 2, 3], + c: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'c\' is missing case)', function() { + traceIn = { + a: [1, 2, 3], + b: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'b\' is missing case)', function() { + traceIn = { + a: [1, 2, 3], + c: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\' is missing case)', function() { + traceIn = { + b: [1, 2, 3], + c: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).not.toBe(true); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'b\ and \'c\' are missing case)', function() { + traceIn = { + a: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\ and \'c\' are missing case)', function() { + traceIn = { + b: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (\'a\ and \'b\' are missing case)', function() { + traceIn = { + c: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it('should allow one of \'a\', \'b\' or \'c\' to be missing (all are missing case)', function() { + traceIn = {}; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.visible).toBe(false); + }); + + it('should truncate data arrays to the same length (\'c\' is shortest case)', function() { + traceIn = { + a: [1, 2, 3], + b: [1, 2], + c: [1] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.a).toEqual([1]); + expect(traceOut.b).toEqual([1]); + expect(traceOut.c).toEqual([1]); + }); + + it('should truncate data arrays to the same length (\'a\' is shortest case)', function() { + traceIn = { + a: [1], + b: [1, 2, 3], + c: [1, 2] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.a).toEqual([1]); + expect(traceOut.b).toEqual([1]); + expect(traceOut.c).toEqual([1]); + }); + + it('should truncate data arrays to the same length (\'a\' is shortest case)', function() { + traceIn = { + a: [1, 2], + b: [1], + c: [1, 2, 3] + }; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.a).toEqual([1]); + expect(traceOut.b).toEqual([1]); + expect(traceOut.c).toEqual([1]); + }); + it('should include \'name\' in \'hoverinfo\' default if multi trace graph', function() { + traceIn = { + a: [1, 2, 3], + b: [1, 2, 3], + c: [1, 2, 3] + }; + layout._dataLength = 2; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoverinfo).toBe('all'); + }); + + it('should not include \'name\' in \'hoverinfo\' default if single trace graph', function() { + traceIn = { + a: [1, 2, 3], + b: [1, 2, 3], + c: [1, 2, 3] + }; + layout._dataLength = 1; + + supplyDefaults(traceIn, traceOut, defaultColor, layout); + expect(traceOut.hoverinfo).toBe('a+b+c+text'); + }); +}); + +describe('scatterternary calc', function() { + 'use strict'; + + var calc = ScatterTernary.calc; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + var gd, trace, cd; + + beforeEach(function() { + gd = { + _fullLayout: { + ternary: { sum: 1 } + } + }; + + trace = { + subplot: 'ternary', + sum: 1 + }; + }); + + it('should fill in missing component (case \'c\')', function() { + trace.a = [0.1, 0.3, 0.6]; + trace.b = [0.3, 0.6, 0.1]; + + calc(gd, trace); + expect(trace.c).toBeCloseToArray([0.6, 0.1, 0.3]); + }); + + it('should fill in missing component (case \'b\')', function() { + trace.a = [0.1, 0.3, 0.6]; + trace.c = [0.1, 0.3, 0.2]; + + calc(gd, trace); + expect(trace.b).toBeCloseToArray([0.8, 0.4, 0.2]); + }); + + it('should fill in missing component (case \'a\')', function() { + trace.b = [0.1, 0.3, 0.6]; + trace.c = [0.8, 0.4, 0.1]; + + calc(gd, trace); + expect(trace.a).toBeCloseToArray([0.1, 0.3, 0.3]); + }); + + it('should skip over non-numeric values', function() { + trace.a = [0.1, 'a', 0.6]; + trace.b = [0.1, 0.3, null]; + trace.c = [8, 0.4, 0.1]; + + cd = calc(gd, trace); + + expect(objectToArray(cd[0])).toBeCloseToArray([ + 0.963414634, 0.012195121, 0.012195121, 0.012195121, 0.975609756 + ]); + expect(cd[1]).toEqual({ x: false, y: false }); + expect(cd[2]).toEqual({ x: false, y: false }); + }); + + function objectToArray(obj) { + return Object.keys(obj).map(function(k) { + return obj[k]; + }); + } + +}); + +describe('scatterternary plot and hover', function() { + 'use strict'; + + var mock = require('@mocks/ternary_simple.json'); + + afterAll(destroyGraphDiv); + + beforeAll(function(done) { + var gd = createGraphDiv(); + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + it('should put scatterternary trace in \'frontplot\' node', function() { + var nodes = d3.select('.frontplot').selectAll('.scatter'); + + expect(nodes.size()).toEqual(1); + }); + + it('should generate one line path per trace', function() { + var nodes = d3.selectAll('path.js-line'); + + expect(nodes.size()).toEqual(mock.data.length); + }); + + it('should generate as many points as there are data points', function() { + var nodes = d3.selectAll('path.point'); + + expect(nodes.size()).toEqual(mock.data[0].a.length); + }); +}); + +describe('scatterternary hover', function() { + 'use strict'; + + var hoverPoints = ScatterTernary.hoverPoints; + + var gd, pointData; + + beforeAll(function(done) { + gd = createGraphDiv(); + + var data = [{ + type: 'scatterternary', + a: [0.1, 0.2, 0.3], + b: [0.3, 0.2, 0.1], + c: [0.1, 0.4, 0.5] + }]; + + Plotly.plot(gd, data).then(done); + }); + + beforeEach(function() { + var cd = gd.calcdata, + ternary = gd._fullLayout.ternary._ternary; + + pointData = { + index: false, + distance: 20, + cd: cd[0], + trace: cd[0][0].trace, + xa: ternary.xaxis, + ya: ternary.yaxis + }; + + }); + + afterAll(destroyGraphDiv); + + it('should generate extra text field on hover', function() { + var xval = 0.42, + yval = 0.37, + hovermode = 'closest'; + + var scatterPointData = hoverPoints(pointData, xval, yval, hovermode); + + expect(scatterPointData[0].extraText).toEqual( + 'Component A: 0.3333333
Component B: 0.1111111
Component C: 0.5555556' + ); + + expect(scatterPointData[0].xLabelVal).toBeUndefined(); + expect(scatterPointData[0].yLabelVal).toBeUndefined(); + }); + +}); diff --git a/test/jasmine/tests/ternary_test.js b/test/jasmine/tests/ternary_test.js new file mode 100644 index 00000000000..167d46c4b54 --- /dev/null +++ b/test/jasmine/tests/ternary_test.js @@ -0,0 +1,298 @@ +var Plotly = require('@lib'); +var Lib = require('@src/lib'); +var DBLCLICKDELAY = require('@src/plots/cartesian/constants').DBLCLICKDELAY; + +var supplyLayoutDefaults = require('@src/plots/ternary/layout/defaults'); + +var d3 = require('d3'); +var createGraphDiv = require('../assets/create_graph_div'); +var destroyGraphDiv = require('../assets/destroy_graph_div'); +var mouseEvent = require('../assets/mouse_event'); +var customMatchers = require('../assets/custom_matchers'); + + +describe('ternary plots', function() { + 'use strict'; + + beforeAll(function() { + jasmine.addMatchers(customMatchers); + }); + + afterEach(destroyGraphDiv); + + describe('with scatterternary trace(s)', function() { + var mock = require('@mocks/ternary_simple.json'); + var gd; + + var pointPos = [391, 219]; + var blankPos = [200, 200]; + + beforeEach(function(done) { + gd = createGraphDiv(); + + var mockCopy = Lib.extendDeep({}, mock); + + Plotly.plot(gd, mockCopy.data, mockCopy.layout).then(done); + }); + + it('should be able to toggle trace visibility', function(done) { + expect(countTraces('scatter')).toEqual(1); + + Plotly.restyle(gd, 'visible', false).then(function() { + expect(countTraces('scatter')).toEqual(0); + + return Plotly.restyle(gd, 'visible', true); + }).then(function() { + expect(countTraces('scatter')).toEqual(1); + + return Plotly.restyle(gd, 'visible', 'legendonly'); + }).then(function() { + expect(countTraces('scatter')).toEqual(0); + + return Plotly.restyle(gd, 'visible', true); + }).then(function() { + expect(countTraces('scatter')).toEqual(1); + + done(); + }); + }); + + it('should be able to delete and add traces', function(done) { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces('scatter')).toEqual(1); + + Plotly.deleteTraces(gd, [0]).then(function() { + expect(countTernarySubplot()).toEqual(0); + expect(countTraces('scatter')).toEqual(0); + + var trace = Lib.extendDeep({}, mock.data[0]); + + return Plotly.addTraces(gd, [trace]); + }).then(function() { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces('scatter')).toEqual(1); + + var trace = Lib.extendDeep({}, mock.data[0]); + + return Plotly.addTraces(gd, [trace]); + }).then(function() { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces('scatter')).toEqual(2); + + return Plotly.deleteTraces(gd, [0]); + }).then(function() { + expect(countTernarySubplot()).toEqual(1); + expect(countTraces('scatter')).toEqual(1); + + done(); + }); + }); + + it('should display to hover labels', function() { + var hoverLabels; + + mouseEvent('mousemove', blankPos[0], blankPos[1]); + hoverLabels = findHoverLabels(); + expect(hoverLabels.size()).toEqual(0, 'only on data points'); + + mouseEvent('mousemove', pointPos[0], pointPos[1]); + hoverLabels = findHoverLabels(); + expect(hoverLabels.size()).toEqual(1, 'one per data point'); + + var rows = hoverLabels.selectAll('tspan'); + expect(rows[0][0].innerHTML).toEqual('Component A: 0.5', 'with correct text'); + expect(rows[0][1].innerHTML).toEqual('B: 0.25', 'with correct text'); + expect(rows[0][2].innerHTML).toEqual('Component C: 0.25', 'with correct text'); + }); + + it('should respond to hover interactions by', function() { + var hoverCnt = 0, + unhoverCnt = 0; + + var hoverData, unhoverData; + + gd.on('plotly_hover', function(eventData) { + hoverCnt++; + hoverData = eventData.points[0]; + }); + + gd.on('plotly_unhover', function(eventData) { + unhoverCnt++; + unhoverData = eventData.points[0]; + }); + + mouseEvent('mousemove', blankPos[0], blankPos[1]); + expect(hoverData).toBe(undefined, 'not firing on blank points'); + expect(unhoverData).toBe(undefined, 'not firing on blank points'); + + mouseEvent('mousemove', pointPos[0], pointPos[1]); + expect(hoverData).not.toBe(undefined, 'firing on data points'); + expect(Object.keys(hoverData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'x', 'y', 'xaxis', 'yaxis' + ], 'returning the correct event data keys'); + expect(hoverData.curveNumber).toEqual(0, 'returning the correct curve number'); + expect(hoverData.pointNumber).toEqual(0, 'returning the correct point number'); + + mouseEvent('mouseout', pointPos[0], pointPos[1]); + expect(unhoverData).not.toBe(undefined, 'firing on data points'); + expect(Object.keys(unhoverData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'x', 'y', 'xaxis', 'yaxis' + ], 'returning the correct event data keys'); + expect(unhoverData.curveNumber).toEqual(0, 'returning the correct curve number'); + expect(unhoverData.pointNumber).toEqual(0, 'returning the correct point number'); + + expect(hoverCnt).toEqual(1); + expect(unhoverCnt).toEqual(1); + }); + + it('should respond to click interactions by', function() { + var ptData; + + gd.on('plotly_click', function(eventData) { + ptData = eventData.points[0]; + }); + + click(blankPos[0], blankPos[1]); + expect(ptData).toBe(undefined, 'not firing on blank points'); + + click(pointPos[0], pointPos[1]); + expect(ptData).not.toBe(undefined, 'firing on data points'); + expect(Object.keys(ptData)).toEqual([ + 'data', 'fullData', 'curveNumber', 'pointNumber', + 'x', 'y', 'xaxis', 'yaxis' + ], 'returning the correct event data keys'); + expect(ptData.curveNumber).toEqual(0, 'returning the correct curve number'); + expect(ptData.pointNumber).toEqual(0, 'returning the correct point number'); + }); + + it('should respond zoom drag interactions', function(done) { + assertRange(gd, [0.231, 0.2, 0.11]); + + Plotly.relayout(gd, 'ternary.aaxis.min', 0.1).then(function() { + assertRange(gd, [0.1, 0.2, 0.11]); + + return doubleClick(pointPos[0], pointPos[1]); + }).then(function() { + assertRange(gd, [0, 0, 0]); + + done(); + }); + }); + + }); + + function countTernarySubplot() { + return d3.selectAll('.ternary').size(); + } + + function countTraces(type) { + return d3.selectAll('.ternary').selectAll('g.trace.' + type).size(); + } + + function findHoverLabels() { + return d3.select('.hoverlayer').selectAll('g'); + } + + function click(x, y) { + mouseEvent('mousemove', x, y); + mouseEvent('mousedown', x, y); + mouseEvent('mouseup', x, y); + } + + function doubleClick(x, y) { + return new Promise(function(resolve) { + click(x, y); + + setTimeout(function() { + click(x, y); + resolve(); + }, DBLCLICKDELAY / 2); + }); + } + + function assertRange(gd, expected) { + var ternary = gd._fullLayout.ternary; + var actual = [ + ternary.aaxis.min, + ternary.baxis.min, + ternary.caxis.min + ]; + + expect(actual).toBeCloseToArray(expected); + } +}); + +describe('ternary defaults', function() { + 'use strict'; + + var layoutIn, layoutOut, fullData; + + beforeEach(function() { + // if hasTernary is not at this stage, the default step is skipped + layoutOut = { + _hasTernary: true, + font: { color: 'red' } + }; + + // needs a ternary-ref in a trace in order to be detected + fullData = [{ type: 'scatterternary', subplot: 'ternary' }]; + }); + + it('should fill empty containers', function() { + layoutIn = {}; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutIn).toEqual({ ternary: {} }); + expect(layoutOut.ternary.aaxis.type).toEqual('linear'); + expect(layoutOut.ternary.baxis.type).toEqual('linear'); + expect(layoutOut.ternary.caxis.type).toEqual('linear'); + }); + + it('should coerce \'min\' values to 0 and delete them for user data if they contradict', function() { + layoutIn = { + ternary: { + aaxis: { min: 1 }, + baxis: { min: 1 }, + caxis: { min: 1 }, + sum: 2 + } + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.ternary.aaxis.min).toEqual(0); + expect(layoutOut.ternary.baxis.min).toEqual(0); + expect(layoutOut.ternary.caxis.min).toEqual(0); + expect(layoutOut.ternary.sum).toEqual(2); + expect(layoutIn.ternary.aaxis.min).toBeUndefined(); + expect(layoutIn.ternary.baxis.min).toBeUndefined(); + expect(layoutIn.ternary.caxis.min).toBeUndefined(); + }); + + it('should default \'title\' to Component + _name', function() { + layoutIn = {}; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.ternary.aaxis.title).toEqual('Component A'); + expect(layoutOut.ternary.baxis.title).toEqual('Component B'); + expect(layoutOut.ternary.caxis.title).toEqual('Component C'); + }); + + it('should default \'gricolor\' to 60% dark', function() { + layoutIn = { + ternary: { + aaxis: { showgrid: true, color: 'red' }, + baxis: { showgrid: true }, + caxis: { gridcolor: 'black' }, + bgcolor: 'blue' + }, + paper_bgcolor: 'green' + }; + + supplyLayoutDefaults(layoutIn, layoutOut, fullData); + expect(layoutOut.ternary.aaxis.gridcolor).toEqual('rgb(102, 0, 153)'); + expect(layoutOut.ternary.baxis.gridcolor).toEqual('rgb(27, 27, 180)'); + expect(layoutOut.ternary.caxis.gridcolor).toEqual('black'); + }); +});