UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

1,196 lines (998 loc) 37.7 kB
/** * Copyright 2012-2020, Plotly, Inc. * All rights reserved. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ 'use strict'; var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var tinycolor = require('tinycolor2'); var Registry = require('../../registry'); var Color = require('../color'); var Colorscale = require('../colorscale'); var Lib = require('../../lib'); var strTranslate = Lib.strTranslate; var svgTextUtils = require('../../lib/svg_text_utils'); var xmlnsNamespaces = require('../../constants/xmlns_namespaces'); var alignment = require('../../constants/alignment'); var LINE_SPACING = alignment.LINE_SPACING; var DESELECTDIM = require('../../constants/interactions').DESELECTDIM; var subTypes = require('../../traces/scatter/subtypes'); var makeBubbleSizeFn = require('../../traces/scatter/make_bubble_size_func'); var appendArrayPointValue = require('../../components/fx/helpers').appendArrayPointValue; var drawing = module.exports = {}; // ----------------------------------------------------- // styling functions for plot elements // ----------------------------------------------------- drawing.font = function(s, family, size, color) { // also allow the form font(s, {family, size, color}) if(Lib.isPlainObject(family)) { color = family.color; size = family.size; family = family.family; } if(family) s.style('font-family', family); if(size + 1) s.style('font-size', size + 'px'); if(color) s.call(Color.fill, color); }; /* * Positioning helpers * Note: do not use `setPosition` with <text> nodes modified by * `svgTextUtils.convertToTspans`. Use `svgTextUtils.positionText` * instead, so that <tspan.line> elements get updated to match. */ drawing.setPosition = function(s, x, y) { s.attr('x', x).attr('y', y); }; drawing.setSize = function(s, w, h) { s.attr('width', w).attr('height', h); }; drawing.setRect = function(s, x, y, w, h) { s.call(drawing.setPosition, x, y).call(drawing.setSize, w, h); }; /** Translate node * * @param {object} d : calcdata point item * @param {sel} sel : d3 selction of node to translate * @param {object} xa : corresponding full xaxis object * @param {object} ya : corresponding full yaxis object * * @return {boolean} : * true if selection got translated * false if selection could not get translated */ drawing.translatePoint = function(d, sel, xa, ya) { var x = xa.c2p(d.x); var y = ya.c2p(d.y); if(isNumeric(x) && isNumeric(y) && sel.node()) { // for multiline text this works better if(sel.node().nodeName === 'text') { sel.attr('x', x).attr('y', y); } else { sel.attr('transform', strTranslate(x, y)); } } else { return false; } return true; }; drawing.translatePoints = function(s, xa, ya) { s.each(function(d) { var sel = d3.select(this); drawing.translatePoint(d, sel, xa, ya); }); }; drawing.hideOutsideRangePoint = function(d, sel, xa, ya, xcalendar, ycalendar) { sel.attr( 'display', (xa.isPtWithinRange(d, xcalendar) && ya.isPtWithinRange(d, ycalendar)) ? null : 'none' ); }; drawing.hideOutsideRangePoints = function(traceGroups, subplot) { if(!subplot._hasClipOnAxisFalse) return; var xa = subplot.xaxis; var ya = subplot.yaxis; traceGroups.each(function(d) { var trace = d[0].trace; var xcalendar = trace.xcalendar; var ycalendar = trace.ycalendar; var selector = Registry.traceIs(trace, 'bar-like') ? '.bartext' : '.point,.textpoint'; traceGroups.selectAll(selector).each(function(d) { drawing.hideOutsideRangePoint(d, d3.select(this), xa, ya, xcalendar, ycalendar); }); }); }; drawing.crispRound = function(gd, lineWidth, dflt) { // for lines that disable antialiasing we want to // make sure the width is an integer, and at least 1 if it's nonzero if(!lineWidth || !isNumeric(lineWidth)) return dflt || 0; // but not for static plots - these don't get antialiased anyway. if(gd._context.staticPlot) return lineWidth; if(lineWidth < 1) return 1; return Math.round(lineWidth); }; drawing.singleLineStyle = function(d, s, lw, lc, ld) { s.style('fill', 'none'); var line = (((d || [])[0] || {}).trace || {}).line || {}; var lw1 = lw || line.width || 0; var dash = ld || line.dash || ''; Color.stroke(s, lc || line.color); drawing.dashLine(s, dash, lw1); }; drawing.lineGroupStyle = function(s, lw, lc, ld) { s.style('fill', 'none') .each(function(d) { var line = (((d || [])[0] || {}).trace || {}).line || {}; var lw1 = lw || line.width || 0; var dash = ld || line.dash || ''; d3.select(this) .call(Color.stroke, lc || line.color) .call(drawing.dashLine, dash, lw1); }); }; drawing.dashLine = function(s, dash, lineWidth) { lineWidth = +lineWidth || 0; dash = drawing.dashStyle(dash, lineWidth); s.style({ 'stroke-dasharray': dash, 'stroke-width': lineWidth + 'px' }); }; drawing.dashStyle = function(dash, lineWidth) { lineWidth = +lineWidth || 1; var dlw = Math.max(lineWidth, 3); if(dash === 'solid') dash = ''; else if(dash === 'dot') dash = dlw + 'px,' + dlw + 'px'; else if(dash === 'dash') dash = (3 * dlw) + 'px,' + (3 * dlw) + 'px'; else if(dash === 'longdash') dash = (5 * dlw) + 'px,' + (5 * dlw) + 'px'; else if(dash === 'dashdot') { dash = (3 * dlw) + 'px,' + dlw + 'px,' + dlw + 'px,' + dlw + 'px'; } else if(dash === 'longdashdot') { dash = (5 * dlw) + 'px,' + (2 * dlw) + 'px,' + dlw + 'px,' + (2 * dlw) + 'px'; } // otherwise user wrote the dasharray themselves - leave it be return dash; }; // Same as fillGroupStyle, except in this case the selection may be a transition drawing.singleFillStyle = function(sel) { var node = d3.select(sel.node()); var data = node.data(); var fillcolor = (((data[0] || [])[0] || {}).trace || {}).fillcolor; if(fillcolor) { sel.call(Color.fill, fillcolor); } }; drawing.fillGroupStyle = function(s) { s.style('stroke-width', 0) .each(function(d) { var shape = d3.select(this); // N.B. 'd' won't be a calcdata item when // fill !== 'none' on a segment-less and marker-less trace if(d[0].trace) { shape.call(Color.fill, d[0].trace.fillcolor); } }); }; var SYMBOLDEFS = require('./symbol_defs'); drawing.symbolNames = []; drawing.symbolFuncs = []; drawing.symbolNeedLines = {}; drawing.symbolNoDot = {}; drawing.symbolNoFill = {}; drawing.symbolList = []; Object.keys(SYMBOLDEFS).forEach(function(k) { var symDef = SYMBOLDEFS[k]; var n = symDef.n; drawing.symbolList.push( n, String(n), k, n + 100, String(n + 100), k + '-open' ); drawing.symbolNames[n] = k; drawing.symbolFuncs[n] = symDef.f; if(symDef.needLine) { drawing.symbolNeedLines[n] = true; } if(symDef.noDot) { drawing.symbolNoDot[n] = true; } else { drawing.symbolList.push( n + 200, String(n + 200), k + '-dot', n + 300, String(n + 300), k + '-open-dot' ); } if(symDef.noFill) { drawing.symbolNoFill[n] = true; } }); var MAXSYMBOL = drawing.symbolNames.length; // add a dot in the middle of the symbol var DOTPATH = 'M0,0.5L0.5,0L0,-0.5L-0.5,0Z'; drawing.symbolNumber = function(v) { if(isNumeric(v)) { v = +v; } else if(typeof v === 'string') { var vbase = 0; if(v.indexOf('-open') > 0) { vbase = 100; v = v.replace('-open', ''); } if(v.indexOf('-dot') > 0) { vbase += 200; v = v.replace('-dot', ''); } v = drawing.symbolNames.indexOf(v); if(v >= 0) { v += vbase; } } return (v % 100 >= MAXSYMBOL || v >= 400) ? 0 : Math.floor(Math.max(v, 0)); }; function makePointPath(symbolNumber, r) { var base = symbolNumber % 100; return drawing.symbolFuncs[base](r) + (symbolNumber >= 200 ? DOTPATH : ''); } var HORZGRADIENT = {x1: 1, x2: 0, y1: 0, y2: 0}; var VERTGRADIENT = {x1: 0, x2: 0, y1: 1, y2: 0}; var stopFormatter = d3.format('~.1f'); var gradientInfo = { radial: {node: 'radialGradient'}, radialreversed: {node: 'radialGradient', reversed: true}, horizontal: {node: 'linearGradient', attrs: HORZGRADIENT}, horizontalreversed: {node: 'linearGradient', attrs: HORZGRADIENT, reversed: true}, vertical: {node: 'linearGradient', attrs: VERTGRADIENT}, verticalreversed: {node: 'linearGradient', attrs: VERTGRADIENT, reversed: true} }; /** * gradient: create and apply a gradient fill * * @param {object} sel: d3 selection to apply this gradient to * You can use `selection.call(Drawing.gradient, ...)` * @param {DOM element} gd: the graph div `sel` is part of * @param {string} gradientID: a unique (within this plot) identifier * for this gradient, so that we don't create unnecessary definitions * @param {string} type: 'radial', 'horizontal', or 'vertical', optionally with * 'reversed' at the end. Normally radial goes center to edge, * horizontal goes right to left, and vertical goes bottom to top * @param {array} colorscale: as in attribute values, [[fraction, color], ...] * @param {string} prop: the property to apply to, 'fill' or 'stroke' */ drawing.gradient = function(sel, gd, gradientID, type, colorscale, prop) { var len = colorscale.length; var info = gradientInfo[type]; var colorStops = new Array(len); for(var i = 0; i < len; i++) { if(info.reversed) { colorStops[len - 1 - i] = [stopFormatter((1 - colorscale[i][0]) * 100), colorscale[i][1]]; } else { colorStops[i] = [stopFormatter(colorscale[i][0] * 100), colorscale[i][1]]; } } var fullLayout = gd._fullLayout; var fullID = 'g' + fullLayout._uid + '-' + gradientID; var gradient = fullLayout._defs.select('.gradients') .selectAll('#' + fullID) .data([type + colorStops.join(';')], Lib.identity); gradient.exit().remove(); gradient.enter() .append(info.node) .each(function() { var el = d3.select(this); if(info.attrs) el.attr(info.attrs); el.attr('id', fullID); var stops = el.selectAll('stop') .data(colorStops); stops.exit().remove(); stops.enter().append('stop'); stops.each(function(d) { var tc = tinycolor(d[1]); d3.select(this).attr({ offset: d[0] + '%', 'stop-color': Color.tinyRGB(tc), 'stop-opacity': tc.getAlpha() }); }); }); sel.style(prop, getFullUrl(fullID, gd)) .style(prop + '-opacity', null); var className2query = function(s) { return '.' + s.attr('class').replace(/\s/g, '.'); }; var k = className2query(d3.select(sel.node().parentNode)) + '>' + className2query(sel); fullLayout._gradientUrlQueryParts[k] = 1; }; /* * Make the gradients container and clear out any previous gradients. * We never collect all the gradients we need in one place, * so we can't ever remove gradients that have stopped being useful, * except all at once before a full redraw. * The upside of this is arbitrary points can share gradient defs */ drawing.initGradients = function(gd) { var fullLayout = gd._fullLayout; var gradientsGroup = Lib.ensureSingle(fullLayout._defs, 'g', 'gradients'); gradientsGroup.selectAll('linearGradient,radialGradient').remove(); // initialize stash of query parts filled in Drawing.gradient, // used to fix URL strings during image exports fullLayout._gradientUrlQueryParts = {}; }; drawing.pointStyle = function(s, trace, gd) { if(!s.size()) return; var fns = drawing.makePointStyleFns(trace); s.each(function(d) { drawing.singlePointStyle(d, d3.select(this), trace, fns, gd); }); }; drawing.singlePointStyle = function(d, sel, trace, fns, gd) { var marker = trace.marker; var markerLine = marker.line; sel.style('opacity', fns.selectedOpacityFn ? fns.selectedOpacityFn(d) : (d.mo === undefined ? marker.opacity : d.mo) ); if(fns.ms2mrc) { var r; // handle multi-trace graph edit case if(d.ms === 'various' || marker.size === 'various') { r = 3; } else { r = fns.ms2mrc(d.ms); } // store the calculated size so hover can use it d.mrc = r; if(fns.selectedSizeFn) { r = d.mrc = fns.selectedSizeFn(d); } // turn the symbol into a sanitized number var x = drawing.symbolNumber(d.mx || marker.symbol) || 0; // save if this marker is open // because that impacts how to handle colors d.om = x % 200 >= 100; sel.attr('d', makePointPath(x, r)); } var perPointGradient = false; var fillColor, lineColor, lineWidth; // 'so' is suspected outliers, for box plots if(d.so) { lineWidth = markerLine.outlierwidth; lineColor = markerLine.outliercolor; fillColor = marker.outliercolor; } else { var markerLineWidth = (markerLine || {}).width; lineWidth = ( d.mlw + 1 || markerLineWidth + 1 || // TODO: we need the latter for legends... can we get rid of it? (d.trace ? (d.trace.marker.line || {}).width : 0) + 1 ) - 1 || 0; if('mlc' in d) lineColor = d.mlcc = fns.lineScale(d.mlc); // weird case: array wasn't long enough to apply to every point else if(Lib.isArrayOrTypedArray(markerLine.color)) lineColor = Color.defaultLine; else lineColor = markerLine.color; if(Lib.isArrayOrTypedArray(marker.color)) { fillColor = Color.defaultLine; perPointGradient = true; } if('mc' in d) { fillColor = d.mcc = fns.markerScale(d.mc); } else { fillColor = marker.color || 'rgba(0,0,0,0)'; } if(fns.selectedColorFn) { fillColor = fns.selectedColorFn(d); } } if(d.om) { // open markers can't have zero linewidth, default to 1px, // and use fill color as stroke color sel.call(Color.stroke, fillColor) .style({ 'stroke-width': (lineWidth || 1) + 'px', fill: 'none' }); } else { sel.style('stroke-width', (d.isBlank ? 0 : lineWidth) + 'px'); var markerGradient = marker.gradient; var gradientType = d.mgt; if(gradientType) perPointGradient = true; else gradientType = markerGradient && markerGradient.type; // for legend - arrays will propagate through here, but we don't need // to treat it as per-point. if(Array.isArray(gradientType)) { gradientType = gradientType[0]; if(!gradientInfo[gradientType]) gradientType = 0; } if(gradientType && gradientType !== 'none') { var gradientColor = d.mgc; if(gradientColor) perPointGradient = true; else gradientColor = markerGradient.color; var gradientID = trace.uid; if(perPointGradient) gradientID += '-' + d.i; drawing.gradient(sel, gd, gradientID, gradientType, [[0, gradientColor], [1, fillColor]], 'fill'); } else { Color.fill(sel, fillColor); } if(lineWidth) { Color.stroke(sel, lineColor); } } }; drawing.makePointStyleFns = function(trace) { var out = {}; var marker = trace.marker; // allow array marker and marker line colors to be // scaled by given max and min to colorscales out.markerScale = drawing.tryColorscale(marker, ''); out.lineScale = drawing.tryColorscale(marker, 'line'); if(Registry.traceIs(trace, 'symbols')) { out.ms2mrc = subTypes.isBubble(trace) ? makeBubbleSizeFn(trace) : function() { return (marker.size || 6) / 2; }; } if(trace.selectedpoints) { Lib.extendFlat(out, drawing.makeSelectedPointStyleFns(trace)); } return out; }; drawing.makeSelectedPointStyleFns = function(trace) { var out = {}; var selectedAttrs = trace.selected || {}; var unselectedAttrs = trace.unselected || {}; var marker = trace.marker || {}; var selectedMarker = selectedAttrs.marker || {}; var unselectedMarker = unselectedAttrs.marker || {}; var mo = marker.opacity; var smo = selectedMarker.opacity; var usmo = unselectedMarker.opacity; var smoIsDefined = smo !== undefined; var usmoIsDefined = usmo !== undefined; if(Lib.isArrayOrTypedArray(mo) || smoIsDefined || usmoIsDefined) { out.selectedOpacityFn = function(d) { var base = d.mo === undefined ? marker.opacity : d.mo; if(d.selected) { return smoIsDefined ? smo : base; } else { return usmoIsDefined ? usmo : DESELECTDIM * base; } }; } var mc = marker.color; var smc = selectedMarker.color; var usmc = unselectedMarker.color; if(smc || usmc) { out.selectedColorFn = function(d) { var base = d.mcc || mc; if(d.selected) { return smc || base; } else { return usmc || base; } }; } var ms = marker.size; var sms = selectedMarker.size; var usms = unselectedMarker.size; var smsIsDefined = sms !== undefined; var usmsIsDefined = usms !== undefined; if(Registry.traceIs(trace, 'symbols') && (smsIsDefined || usmsIsDefined)) { out.selectedSizeFn = function(d) { var base = d.mrc || ms / 2; if(d.selected) { return smsIsDefined ? sms / 2 : base; } else { return usmsIsDefined ? usms / 2 : base; } }; } return out; }; drawing.makeSelectedTextStyleFns = function(trace) { var out = {}; var selectedAttrs = trace.selected || {}; var unselectedAttrs = trace.unselected || {}; var textFont = trace.textfont || {}; var selectedTextFont = selectedAttrs.textfont || {}; var unselectedTextFont = unselectedAttrs.textfont || {}; var tc = textFont.color; var stc = selectedTextFont.color; var utc = unselectedTextFont.color; out.selectedTextColorFn = function(d) { var base = d.tc || tc; if(d.selected) { return stc || base; } else { if(utc) return utc; else return stc ? base : Color.addOpacity(base, DESELECTDIM); } }; return out; }; drawing.selectedPointStyle = function(s, trace) { if(!s.size() || !trace.selectedpoints) return; var fns = drawing.makeSelectedPointStyleFns(trace); var marker = trace.marker || {}; var seq = []; if(fns.selectedOpacityFn) { seq.push(function(pt, d) { pt.style('opacity', fns.selectedOpacityFn(d)); }); } if(fns.selectedColorFn) { seq.push(function(pt, d) { Color.fill(pt, fns.selectedColorFn(d)); }); } if(fns.selectedSizeFn) { seq.push(function(pt, d) { var mx = d.mx || marker.symbol || 0; var mrc2 = fns.selectedSizeFn(d); pt.attr('d', makePointPath(drawing.symbolNumber(mx), mrc2)); // save for Drawing.selectedTextStyle d.mrc2 = mrc2; }); } if(seq.length) { s.each(function(d) { var pt = d3.select(this); for(var i = 0; i < seq.length; i++) { seq[i](pt, d); } }); } }; drawing.tryColorscale = function(marker, prefix) { var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker; if(cont) { var colorArray = cont.color; if((cont.colorscale || cont._colorAx) && Lib.isArrayOrTypedArray(colorArray)) { return Colorscale.makeColorScaleFuncFromTrace(cont); } } return Lib.identity; }; var TEXTOFFSETSIGN = { start: 1, end: -1, middle: 0, bottom: 1, top: -1 }; function textPointPosition(s, textPosition, fontSize, markerRadius) { var group = d3.select(s.node().parentNode); var v = textPosition.indexOf('top') !== -1 ? 'top' : textPosition.indexOf('bottom') !== -1 ? 'bottom' : 'middle'; var h = textPosition.indexOf('left') !== -1 ? 'end' : textPosition.indexOf('right') !== -1 ? 'start' : 'middle'; // if markers are shown, offset a little more than // the nominal marker size // ie 2/1.6 * nominal, bcs some markers are a bit bigger var r = markerRadius ? markerRadius / 0.8 + 1 : 0; var numLines = (svgTextUtils.lineCount(s) - 1) * LINE_SPACING + 1; var dx = TEXTOFFSETSIGN[h] * r; var dy = fontSize * 0.75 + TEXTOFFSETSIGN[v] * r + (TEXTOFFSETSIGN[v] - 1) * numLines * fontSize / 2; // fix the overall text group position s.attr('text-anchor', h); group.attr('transform', strTranslate(dx, dy)); } function extracTextFontSize(d, trace) { var fontSize = d.ts || trace.textfont.size; return (isNumeric(fontSize) && fontSize > 0) ? fontSize : 0; } // draw text at points drawing.textPointStyle = function(s, trace, gd) { if(!s.size()) return; var selectedTextColorFn; if(trace.selectedpoints) { var fns = drawing.makeSelectedTextStyleFns(trace); selectedTextColorFn = fns.selectedTextColorFn; } var texttemplate = trace.texttemplate; var fullLayout = gd._fullLayout; s.each(function(d) { var p = d3.select(this); var text = texttemplate ? Lib.extractOption(d, trace, 'txt', 'texttemplate') : Lib.extractOption(d, trace, 'tx', 'text'); if(!text && text !== 0) { p.remove(); return; } if(texttemplate) { var labels = trace._module.formatLabels ? trace._module.formatLabels(d, trace, fullLayout) : {}; var pointValues = {}; appendArrayPointValue(pointValues, trace, d.i); var meta = trace._meta || {}; text = Lib.texttemplateString(text, labels, fullLayout._d3locale, pointValues, d, meta); } var pos = d.tp || trace.textposition; var fontSize = extracTextFontSize(d, trace); var fontColor = selectedTextColorFn ? selectedTextColorFn(d) : (d.tc || trace.textfont.color); p.call(drawing.font, d.tf || trace.textfont.family, fontSize, fontColor) .text(text) .call(svgTextUtils.convertToTspans, gd) .call(textPointPosition, pos, fontSize, d.mrc); }); }; drawing.selectedTextStyle = function(s, trace) { if(!s.size() || !trace.selectedpoints) return; var fns = drawing.makeSelectedTextStyleFns(trace); s.each(function(d) { var tx = d3.select(this); var tc = fns.selectedTextColorFn(d); var tp = d.tp || trace.textposition; var fontSize = extracTextFontSize(d, trace); Color.fill(tx, tc); textPointPosition(tx, tp, fontSize, d.mrc2 || d.mrc); }); }; // generalized Catmull-Rom splines, per // http://www.cemyuksel.com/research/catmullrom_param/catmullrom.pdf var CatmullRomExp = 0.5; drawing.smoothopen = function(pts, smoothness) { if(pts.length < 3) { return 'M' + pts.join('L');} var path = 'M' + pts[0]; var tangents = []; var i; for(i = 1; i < pts.length - 1; i++) { tangents.push(makeTangent(pts[i - 1], pts[i], pts[i + 1], smoothness)); } path += 'Q' + tangents[0][0] + ' ' + pts[1]; for(i = 2; i < pts.length - 1; i++) { path += 'C' + tangents[i - 2][1] + ' ' + tangents[i - 1][0] + ' ' + pts[i]; } path += 'Q' + tangents[pts.length - 3][1] + ' ' + pts[pts.length - 1]; return path; }; drawing.smoothclosed = function(pts, smoothness) { if(pts.length < 3) { return 'M' + pts.join('L') + 'Z'; } var path = 'M' + pts[0]; var pLast = pts.length - 1; var tangents = [makeTangent(pts[pLast], pts[0], pts[1], smoothness)]; var i; for(i = 1; i < pLast; i++) { tangents.push(makeTangent(pts[i - 1], pts[i], pts[i + 1], smoothness)); } tangents.push( makeTangent(pts[pLast - 1], pts[pLast], pts[0], smoothness) ); for(i = 1; i <= pLast; i++) { path += 'C' + tangents[i - 1][1] + ' ' + tangents[i][0] + ' ' + pts[i]; } path += 'C' + tangents[pLast][1] + ' ' + tangents[0][0] + ' ' + pts[0] + 'Z'; return path; }; function makeTangent(prevpt, thispt, nextpt, smoothness) { var d1x = prevpt[0] - thispt[0]; var d1y = prevpt[1] - thispt[1]; var d2x = nextpt[0] - thispt[0]; var d2y = nextpt[1] - thispt[1]; var d1a = Math.pow(d1x * d1x + d1y * d1y, CatmullRomExp / 2); var d2a = Math.pow(d2x * d2x + d2y * d2y, CatmullRomExp / 2); var numx = (d2a * d2a * d1x - d1a * d1a * d2x) * smoothness; var numy = (d2a * d2a * d1y - d1a * d1a * d2y) * smoothness; var denom1 = 3 * d2a * (d1a + d2a); var denom2 = 3 * d1a * (d1a + d2a); return [ [ d3.round(thispt[0] + (denom1 && numx / denom1), 2), d3.round(thispt[1] + (denom1 && numy / denom1), 2) ], [ d3.round(thispt[0] - (denom2 && numx / denom2), 2), d3.round(thispt[1] - (denom2 && numy / denom2), 2) ] ]; } // step paths - returns a generator function for paths // with the given step shape var STEPPATH = { hv: function(p0, p1) { return 'H' + d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2); }, vh: function(p0, p1) { return 'V' + d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2); }, hvh: function(p0, p1) { return 'H' + d3.round((p0[0] + p1[0]) / 2, 2) + 'V' + d3.round(p1[1], 2) + 'H' + d3.round(p1[0], 2); }, vhv: function(p0, p1) { return 'V' + d3.round((p0[1] + p1[1]) / 2, 2) + 'H' + d3.round(p1[0], 2) + 'V' + d3.round(p1[1], 2); } }; var STEPLINEAR = function(p0, p1) { return 'L' + d3.round(p1[0], 2) + ',' + d3.round(p1[1], 2); }; drawing.steps = function(shape) { var onestep = STEPPATH[shape] || STEPLINEAR; return function(pts) { var path = 'M' + d3.round(pts[0][0], 2) + ',' + d3.round(pts[0][1], 2); for(var i = 1; i < pts.length; i++) { path += onestep(pts[i - 1], pts[i]); } return path; }; }; // off-screen svg render testing element, shared by the whole page // uses the id 'js-plotly-tester' and stores it in drawing.tester drawing.makeTester = function() { var tester = Lib.ensureSingleById(d3.select('body'), 'svg', 'js-plotly-tester', function(s) { s.attr(xmlnsNamespaces.svgAttrs) .style({ position: 'absolute', left: '-10000px', top: '-10000px', width: '9000px', height: '9000px', 'z-index': '1' }); }); // browsers differ on how they describe the bounding rect of // the svg if its contents spill over... so make a 1x1px // reference point we can measure off of. var testref = Lib.ensureSingle(tester, 'path', 'js-reference-point', function(s) { s.attr('d', 'M0,0H1V1H0Z') .style({ 'stroke-width': 0, fill: 'black' }); }); drawing.tester = tester; drawing.testref = testref; }; /* * use our offscreen tester to get a clientRect for an element, * in a reference frame where it isn't translated (or transformed) and * its anchor point is at (0,0) * always returns a copy of the bbox, so the caller can modify it safely * * @param {SVGElement} node: the element to measure. If possible this should be * a <text> or MathJax <g> element that's already passed through * `convertToTspans` because in that case we can cache the results, but it's * possible to pass in any svg element. * * @param {boolean} inTester: is this element already in `drawing.tester`? * If you are measuring a dummy element, rather than one you really intend * to use on the plot, making it in `drawing.tester` in the first place * allows us to test faster because it cuts out cloning and appending it. * * @param {string} hash: for internal use only, if we already know the cache key * for this element beforehand. * * @return {object}: a plain object containing the width, height, left, right, * top, and bottom of `node` */ drawing.savedBBoxes = {}; var savedBBoxesCount = 0; var maxSavedBBoxes = 10000; drawing.bBox = function(node, inTester, hash) { /* * Cache elements we've already measured so we don't have to * remeasure the same thing many times * We have a few bBox callers though who pass a node larger than * a <text> or a MathJax <g>, such as an axis group containing many labels. * These will not generate a hash (unless we figure out an appropriate * hash key for them) and thus we will not hash them. */ if(!hash) hash = nodeHash(node); var out; if(hash) { out = drawing.savedBBoxes[hash]; if(out) return Lib.extendFlat({}, out); } else if(node.childNodes.length === 1) { /* * If we have only one child element, which is itself hashable, make * a new hash from this element plus its x,y,transform * These bounding boxes *include* x,y,transform - mostly for use by * callers trying to avoid overlaps (ie titles) */ var innerNode = node.childNodes[0]; hash = nodeHash(innerNode); if(hash) { var x = +innerNode.getAttribute('x') || 0; var y = +innerNode.getAttribute('y') || 0; var transform = innerNode.getAttribute('transform'); if(!transform) { // in this case, just varying x and y, don't bother caching // the final bBox because the alteration is quick. var innerBB = drawing.bBox(innerNode, false, hash); if(x) { innerBB.left += x; innerBB.right += x; } if(y) { innerBB.top += y; innerBB.bottom += y; } return innerBB; } /* * else we have a transform - rather than make a complicated * (and error-prone and probably slow) transform parser/calculator, * just continue on calculating the boundingClientRect of the group * and use the new composite hash to cache it. * That said, `innerNode.transform.baseVal` is an array of * `SVGTransform` objects, that *do* seem to have a nice matrix * multiplication interface that we could use to avoid making * another getBoundingClientRect call... */ hash += '~' + x + '~' + y + '~' + transform; out = drawing.savedBBoxes[hash]; if(out) return Lib.extendFlat({}, out); } } var testNode, tester; if(inTester) { testNode = node; } else { tester = drawing.tester.node(); // copy the node to test into the tester testNode = node.cloneNode(true); tester.appendChild(testNode); } // standardize its position (and newline tspans if any) d3.select(testNode) .attr('transform', null) .call(svgTextUtils.positionText, 0, 0); var testRect = testNode.getBoundingClientRect(); var refRect = drawing.testref .node() .getBoundingClientRect(); if(!inTester) tester.removeChild(testNode); var bb = { height: testRect.height, width: testRect.width, left: testRect.left - refRect.left, top: testRect.top - refRect.top, right: testRect.right - refRect.left, bottom: testRect.bottom - refRect.top }; // make sure we don't have too many saved boxes, // or a long session could overload on memory // by saving boxes for long-gone elements if(savedBBoxesCount >= maxSavedBBoxes) { drawing.savedBBoxes = {}; savedBBoxesCount = 0; } // cache this bbox if(hash) drawing.savedBBoxes[hash] = bb; savedBBoxesCount++; return Lib.extendFlat({}, bb); }; // capture everything about a node (at least in our usage) that // impacts its bounding box, given that bBox clears x, y, and transform function nodeHash(node) { var inputText = node.getAttribute('data-unformatted'); if(inputText === null) return; return inputText + node.getAttribute('data-math') + node.getAttribute('text-anchor') + node.getAttribute('style'); } /** * Set clipPath URL in a way that work for all situations. * * In details, graphs on pages with <base> HTML tags need to prepend * the clip path ids with the page's base url EXCEPT during toImage exports. * * @param {d3 selection} s : node to add clip-path attribute * @param {string} localId : local clip-path (w/o base url) id * @param {DOM element || object} gd * - context._baseUrl {string} * - context._exportedPlot {boolean} */ drawing.setClipUrl = function(s, localId, gd) { s.attr('clip-path', getFullUrl(localId, gd)); }; function getFullUrl(localId, gd) { if(!localId) return null; var context = gd._context; var baseUrl = context._exportedPlot ? '' : (context._baseUrl || ''); return 'url(\'' + baseUrl + '#' + localId + '\')'; } drawing.getTranslate = function(element) { // Note the separator [^\d] between x and y in this regex // We generally use ',' but IE will convert it to ' ' var re = /.*\btranslate\((-?\d*\.?\d*)[^-\d]*(-?\d*\.?\d*)[^\d].*/; var getter = element.attr ? 'attr' : 'getAttribute'; var transform = element[getter]('transform') || ''; var translate = transform.replace(re, function(match, p1, p2) { return [p1, p2].join(' '); }) .split(' '); return { x: +translate[0] || 0, y: +translate[1] || 0 }; }; drawing.setTranslate = function(element, x, y) { var re = /(\btranslate\(.*?\);?)/; var getter = element.attr ? 'attr' : 'getAttribute'; var setter = element.attr ? 'attr' : 'setAttribute'; var transform = element[getter]('transform') || ''; x = x || 0; y = y || 0; transform = transform.replace(re, '').trim(); transform += strTranslate(x, y); transform = transform.trim(); element[setter]('transform', transform); return transform; }; drawing.getScale = function(element) { var re = /.*\bscale\((\d*\.?\d*)[^\d]*(\d*\.?\d*)[^\d].*/; var getter = element.attr ? 'attr' : 'getAttribute'; var transform = element[getter]('transform') || ''; var translate = transform.replace(re, function(match, p1, p2) { return [p1, p2].join(' '); }) .split(' '); return { x: +translate[0] || 1, y: +translate[1] || 1 }; }; drawing.setScale = function(element, x, y) { var re = /(\bscale\(.*?\);?)/; var getter = element.attr ? 'attr' : 'getAttribute'; var setter = element.attr ? 'attr' : 'setAttribute'; var transform = element[getter]('transform') || ''; x = x || 1; y = y || 1; transform = transform.replace(re, '').trim(); transform += 'scale(' + x + ',' + y + ')'; transform = transform.trim(); element[setter]('transform', transform); return transform; }; var SCALE_RE = /\s*sc.*/; drawing.setPointGroupScale = function(selection, xScale, yScale) { xScale = xScale || 1; yScale = yScale || 1; if(!selection) return; // The same scale transform for every point: var scale = (xScale === 1 && yScale === 1) ? '' : 'scale(' + xScale + ',' + yScale + ')'; selection.each(function() { var t = (this.getAttribute('transform') || '').replace(SCALE_RE, ''); t += scale; t = t.trim(); this.setAttribute('transform', t); }); }; var TEXT_POINT_LAST_TRANSLATION_RE = /translate\([^)]*\)\s*$/; drawing.setTextPointsScale = function(selection, xScale, yScale) { if(!selection) return; selection.each(function() { var transforms; var el = d3.select(this); var text = el.select('text'); if(!text.node()) return; var x = parseFloat(text.attr('x') || 0); var y = parseFloat(text.attr('y') || 0); var existingTransform = (el.attr('transform') || '').match(TEXT_POINT_LAST_TRANSLATION_RE); if(xScale === 1 && yScale === 1) { transforms = []; } else { transforms = [ strTranslate(x, y), 'scale(' + xScale + ',' + yScale + ')', strTranslate(-x, -y), ]; } if(existingTransform) { transforms.push(existingTransform); } el.attr('transform', transforms.join('')); }); };