UNPKG

plotly.js

Version:

The open source javascript graphing library that powers plotly

267 lines (229 loc) 9.63 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 Plots = require('../../plots/plots'); var Registry = require('../../registry'); var Lib = require('../../lib'); var strTranslate = Lib.strTranslate; var Drawing = require('../drawing'); var Color = require('../color'); var svgTextUtils = require('../../lib/svg_text_utils'); var interactConstants = require('../../constants/interactions'); var OPPOSITE_SIDE = require('../../constants/alignment').OPPOSITE_SIDE; var numStripRE = / [XY][0-9]* /; /** * Titles - (re)draw titles on the axes and plot: * @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 * placeholder - placeholder text for an empty editable title * [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 <g> element already exists to hold this * title, include here. Otherwise it will go in fullLayout._infolayer * _meta {object (optional} - meta key-value to for title with * Lib.templateString, default to fullLayout._meta, if not provided * * @return {selection} d3 selection of title container group */ function draw(gd, titleClass, options) { var cont = options.propContainer; var prop = options.propName; var placeholder = options.placeholder; var traceIndex = options.traceIndex; var avoid = options.avoid || {}; var attributes = options.attributes; var transform = options.transform; var group = options.containerGroup; var fullLayout = gd._fullLayout; var opacity = 1; var isplaceholder = false; var title = cont.title; var txt = (title && title.text ? title.text : '').trim(); var font = title && title.font ? title.font : {}; var fontFamily = font.family; var fontSize = font.size; var fontColor = font.color; // only make this title editable if we positively identify its property // as one that has editing enabled. var editAttr; if(prop === 'title.text') editAttr = 'titleText'; else if(prop.indexOf('axis') !== -1) editAttr = 'axisTitleText'; else if(prop.indexOf('colorbar' !== -1)) editAttr = 'colorbarTitleText'; var editable = gd._context.edits[editAttr]; if(txt === '') opacity = 0; // look for placeholder text while stripping out numbers from eg X2, Y3 // this is just for backward compatibility with the old version that had // "Click to enter X2 title" and may have gotten saved in some old plots, // we don't want this to show up when these are displayed. else if(txt.replace(numStripRE, ' % ') === placeholder.replace(numStripRE, ' % ')) { opacity = 0.2; isplaceholder = true; if(!editable) txt = ''; } if(options._meta) { txt = Lib.templateString(txt, options._meta); } else if(fullLayout._meta) { txt = Lib.templateString(txt, fullLayout._meta); } var elShouldExist = txt || editable; if(!group) { group = Lib.ensureSingle(fullLayout._infolayer, 'g', 'g-' + titleClass); } var el = group.selectAll('text') .data(elShouldExist ? [0] : []); el.enter().append('text'); el.text(txt) // this is hacky, but convertToTspans uses the class // to determine whether to rotate mathJax... // 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', titleClass); el.exit().remove(); if(!elShouldExist) return group; function titleLayout(titleEl) { Lib.syncOrAsync([drawTitle, scootTitle], titleEl); } function drawTitle(titleEl) { var transformVal; if(transform) { transformVal = ''; if(transform.rotate) { transformVal += 'rotate(' + [transform.rotate, attributes.x, attributes.y] + ')'; } if(transform.offset) { transformVal += strTranslate(0, transform.offset); } } else { transformVal = null; } titleEl.attr('transform', transformVal); titleEl.style({ 'font-family': fontFamily, 'font-size': d3.round(fontSize, 2) + 'px', fill: Color.rgb(fontColor), opacity: opacity * Color.opacity(fontColor), 'font-weight': Plots.fontWeight }) .attr(attributes) .call(svgTextUtils.convertToTspans, gd); return Plots.previousPromises(gd); } function scootTitle(titleElIn) { var titleGroup = d3.select(titleElIn.node().parentNode); if(avoid && avoid.selection && avoid.side && txt) { titleGroup.attr('transform', null); // move toward avoid.side (= left, right, top, bottom) if needed // can include pad (pixels, default 2) var backside = OPPOSITE_SIDE[avoid.side]; var shiftSign = (avoid.side === 'left' || avoid.side === 'top') ? -1 : 1; var pad = isNumeric(avoid.pad) ? avoid.pad : 2; var titlebb = Drawing.bBox(titleGroup.node()); var paperbb = { left: 0, top: 0, right: fullLayout.width, bottom: fullLayout.height }; var maxshift = avoid.maxShift || shiftSign * (paperbb[avoid.side] - titlebb[avoid.side]); var shift = 0; // Prevent the title going off the paper if(maxshift < 0) { shift = maxshift; } else { // so we don't have to offset each avoided element, // give the title the opposite offset var offsetLeft = avoid.offsetLeft || 0; var offsetTop = avoid.offsetTop || 0; titlebb.left -= offsetLeft; titlebb.right -= offsetLeft; titlebb.top -= offsetTop; titlebb.bottom -= offsetTop; // iterate over a set of elements (avoid.selection) // to avoid collisions with avoid.selection.each(function() { var avoidbb = Drawing.bBox(this); if(Lib.bBoxIntersect(titlebb, avoidbb, pad)) { shift = Math.max(shift, shiftSign * ( avoidbb[avoid.side] - titlebb[backside]) + pad); } }); shift = Math.min(maxshift, shift); } if(shift > 0 || maxshift < 0) { var shiftTemplate = { left: [-shift, 0], right: [shift, 0], top: [0, -shift], bottom: [0, shift] }[avoid.side]; titleGroup.attr('transform', strTranslate(shiftTemplate[0], shiftTemplate[1])); } } } el.call(titleLayout); function setPlaceholder() { opacity = 0; isplaceholder = true; el.text(placeholder) .on('mouseover.opacity', function() { d3.select(this).transition() .duration(interactConstants.SHOW_PLACEHOLDER).style('opacity', 1); }) .on('mouseout.opacity', function() { d3.select(this).transition() .duration(interactConstants.HIDE_PLACEHOLDER).style('opacity', 0); }); } if(editable) { if(!txt) setPlaceholder(); else el.on('.opacity', null); el.call(svgTextUtils.makeEditable, {gd: gd}) .on('edit', function(text) { if(traceIndex !== undefined) { Registry.call('_guiRestyle', gd, prop, text, traceIndex); } else { Registry.call('_guiRelayout', gd, prop, text); } }) .on('cancel', function() { this.text(this.attr('data-unformatted')) .call(titleLayout); }) .on('input', function(d) { this.text(d || ' ') .call(svgTextUtils.positionText, attributes.x, attributes.y); }); } el.classed('js-placeholder', isplaceholder); return group; } module.exports = { draw: draw };