diff --git a/src/components/annotations/draw.js b/src/components/annotations/draw.js index 2fdb1086924..6244fc1f408 100644 --- a/src/components/annotations/draw.js +++ b/src/components/annotations/draw.js @@ -212,8 +212,8 @@ function drawRaw(gd, options, index, subplotId, xa, ya) { var font = options.font; - var text = fullLayout.meta ? - Lib.templateString(options.text, {meta: fullLayout.meta}) : + var text = fullLayout._meta ? + Lib.templateString(options.text, fullLayout._meta) : options.text; var annText = annTextGroupInner.append('text') diff --git a/src/components/colorbar/draw.js b/src/components/colorbar/draw.js index 95b9660b447..b01ff451a37 100644 --- a/src/components/colorbar/draw.js +++ b/src/components/colorbar/draw.js @@ -140,6 +140,7 @@ function makeColorBarData(gd) { opts._id = 'cb' + trace.uid + (allowsMultiplotCbs && contName ? '-' + contName : ''); opts._traceIndex = trace.index; opts._propPrefix = (contName ? contName + '.' : '') + 'colorbar.'; + opts._meta = trace._meta; calcOpts(); out.push(opts); } @@ -156,6 +157,7 @@ function makeColorBarData(gd) { opts = initOpts(cont.colorbar); opts._id = 'cb' + k; opts._propPrefix = k + '.colorbar.'; + opts._meta = fullLayout._meta; cbOpt = {min: 'cmin', max: 'cmax'}; if(colorAxOpts[0] !== 'heatmap') { @@ -281,6 +283,7 @@ function drawColorBar(g, opts, gd) { propContainer: ax, propName: opts._propPrefix + 'title', traceIndex: opts._traceIndex, + _meta: opts._meta, placeholder: fullLayout._dfltTitle.colorbar, containerGroup: g.select('.' + cn.cbtitle) }; diff --git a/src/components/fx/hover.js b/src/components/fx/hover.js index 73cac1a5821..7fe5f429c81 100644 --- a/src/components/fx/hover.js +++ b/src/components/fx/hover.js @@ -887,8 +887,8 @@ function createHoverText(hoverData, opts, gd) { if(d.nameOverride !== undefined) d.name = d.nameOverride; if(d.name) { - if(fullLayout.meta) { - d.name = Lib.templateString(d.name, {meta: fullLayout.meta}); + if(d.trace._meta) { + d.name = Lib.templateString(d.name, d.trace._meta); } name = plainText(d.name, d.nameLength); } @@ -925,7 +925,7 @@ function createHoverText(hoverData, opts, gd) { } // hovertemplate - var d3locale = gd._fullLayout._d3locale; + var d3locale = fullLayout._d3locale; var hovertemplate = d.hovertemplate || false; var hovertemplateLabels = d.hovertemplateLabels || d; var eventData = d.eventData[0] || {}; @@ -935,7 +935,7 @@ function createHoverText(hoverData, opts, gd) { hovertemplateLabels, d3locale, eventData, - {meta: fullLayout.meta} + d.trace._meta ); text = text.replace(EXTRA_STRING_REGEX, function(match, extra) { diff --git a/src/components/legend/draw.js b/src/components/legend/draw.js index 1ded8cba129..50558b1513f 100644 --- a/src/components/legend/draw.js +++ b/src/components/legend/draw.js @@ -404,8 +404,8 @@ function drawTexts(g, gd, maxLength) { var isEditable = gd._context.edits.legendText && !isPie; var name = isPie ? legendItem.label : trace.name; - if(fullLayout.meta) { - name = Lib.templateString(name, {meta: fullLayout.meta}); + if(trace._meta) { + name = Lib.templateString(name, trace._meta); } var textEl = Lib.ensureSingle(g, 'text', 'legendtext'); diff --git a/src/components/rangeselector/draw.js b/src/components/rangeselector/draw.js index 927e982516b..34f431eaf78 100644 --- a/src/components/rangeselector/draw.js +++ b/src/components/rangeselector/draw.js @@ -149,14 +149,14 @@ function drawButtonText(button, selectorLayout, d, gd) { }); text.call(Drawing.font, selectorLayout.font) - .text(getLabel(d, gd._fullLayout.meta)) + .text(getLabel(d, gd._fullLayout._meta)) .call(textLayout); } -function getLabel(opts, meta) { +function getLabel(opts, _meta) { if(opts.label) { - return meta ? - Lib.templateString(opts.label, {meta: meta}) : + return _meta ? + Lib.templateString(opts.label, _meta) : opts.label; } diff --git a/src/components/sliders/draw.js b/src/components/sliders/draw.js index f411ce7f909..319fee92248 100644 --- a/src/components/sliders/draw.js +++ b/src/components/sliders/draw.js @@ -315,10 +315,8 @@ function drawCurrentValue(sliderGroup, sliderOpts, valueOverride) { str += valueOverride; } else { var curVal = sliderOpts.steps[sliderOpts.active].label; - var meta = sliderOpts._gd._fullLayout.meta; - if(meta) { - curVal = Lib.templateString(curVal, {meta: meta}); - } + var _meta = sliderOpts._gd._fullLayout._meta; + if(_meta) curVal = Lib.templateString(curVal, _meta); str += curVal; } @@ -367,10 +365,8 @@ function drawLabel(item, data, sliderOpts) { }); var tx = data.step.label; - var meta = sliderOpts._gd._fullLayout.meta; - if(meta) { - tx = Lib.templateString(tx, {meta: meta}); - } + var _meta = sliderOpts._gd._fullLayout._meta; + if(_meta) tx = Lib.templateString(tx, _meta); text.call(Drawing.font, sliderOpts.font) .text(tx) diff --git a/src/components/titles/index.js b/src/components/titles/index.js index d761382ee8e..96c5a5fc550 100644 --- a/src/components/titles/index.js +++ b/src/components/titles/index.js @@ -53,6 +53,8 @@ var numStripRE = / [XY][0-9]* /; * 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 + * _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 */ @@ -97,8 +99,10 @@ function draw(gd, titleClass, options) { if(!editable) txt = ''; } - if(fullLayout.meta) { - txt = Lib.templateString(txt, {meta: fullLayout.meta}); + if(options._meta) { + txt = Lib.templateString(txt, options._meta); + } else if(fullLayout._meta) { + txt = Lib.templateString(txt, fullLayout._meta); } var elShouldExist = txt || editable; diff --git a/src/components/updatemenus/draw.js b/src/components/updatemenus/draw.js index d5737704bf2..58c21b9122b 100644 --- a/src/components/updatemenus/draw.js +++ b/src/components/updatemenus/draw.js @@ -438,10 +438,8 @@ function drawItemText(item, menuOpts, itemOpts, gd) { }); var tx = itemOpts.label; - var meta = gd._fullLayout.meta; - if(meta) { - tx = Lib.templateString(tx, {meta: meta}); - } + var _meta = gd._fullLayout._meta; + if(_meta) tx = Lib.templateString(tx, _meta); text.call(Drawing.font, menuOpts.font) .text(tx) diff --git a/src/plots/attributes.js b/src/plots/attributes.js index cbdf5fd3e8e..bbcdd96d882 100644 --- a/src/plots/attributes.js +++ b/src/plots/attributes.js @@ -102,6 +102,25 @@ module.exports = { 'DOM elements' ].join(' ') }, + meta: { + valType: 'any', + arrayOk: true, + role: 'info', + editType: 'plot', + description: [ + 'Assigns extra meta information associated with this trace', + 'that can be used in various text attributes.', + 'Attributes such as trace `name`, graph, axis and colorbar `title.text`, annotation `text`', + '`rangeselector`, `updatemenues` and `sliders` `label` text', + 'all support `meta`.', + 'To access the trace `meta` values in an attribute in the same trace, simply use', + '`%{meta[i]}` where `i` is the index or key of the `meta`', + 'item in question.', + 'To access trace `meta` in layout attributes, use', + '`%{data[n[.meta[i]}` where `i` is the index or key of the `meta`', + 'and `n` is the trace index.' + ].join(' ') + }, // N.B. these cannot be 'data_array' as they do not have the same length as // other data arrays and arrayOk attributes in general diff --git a/src/plots/gl3d/layout/convert.js b/src/plots/gl3d/layout/convert.js index a065203fcb4..5d0aea9ad6a 100644 --- a/src/plots/gl3d/layout/convert.js +++ b/src/plots/gl3d/layout/convert.js @@ -83,8 +83,8 @@ proto.merge = function(fullLayout, sceneLayout) { } // Axes labels - opts.labels[i] = fullLayout.meta ? - Lib.templateString(axes.title.text, {meta: fullLayout.meta}) : + opts.labels[i] = fullLayout._meta ? + Lib.templateString(axes.title.text, fullLayout._meta) : axes.title.text; if('font' in axes.title) { diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 66b55d13da8..cbcd32af91d 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -410,7 +410,9 @@ module.exports = { }, meta: { - valType: 'data_array', + valType: 'any', + arrayOk: true, + role: 'info', editType: 'plot', description: [ 'Assigns extra meta information that can be used in various `text` attributes.', @@ -418,7 +420,9 @@ module.exports = { '`trace.name` in legend items, `rangeselector`, `updatemenues` and `sliders` `label` text', 'all support `meta`. One can access `meta` fields using template strings:', '`%{meta[i]}` where `i` is the index of the `meta`', - 'item in question.' + 'item in question.', + '`meta` can also be an object for example `{key: value}` which can be accessed', + '%{meta[key]}.' ].join(' ') }, diff --git a/src/plots/plots.js b/src/plots/plots.js index 9d9b942fc4f..287d4dfaa9c 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -6,7 +6,6 @@ * LICENSE file in the root directory of this source tree. */ - 'use strict'; var d3 = require('d3'); @@ -483,6 +482,10 @@ plots.supplyDefaults = function(gd, opts) { oldFullLayout._zoomlayer.selectAll('.select-outline').remove(); } + + // fill in meta helpers + fillMetaTextHelpers(newFullData, newFullLayout); + // relink functions and _ attributes to promote consistency between plots relinkPrivateKeys(newFullLayout, oldFullLayout); @@ -698,6 +701,38 @@ function getFormatter(formatObj, separators) { return d3.locale(formatObj); } +function fillMetaTextHelpers(newFullData, newFullLayout) { + var _meta; + var meta4data = []; + + if(newFullLayout.meta) { + _meta = newFullLayout._meta = { + meta: newFullLayout.meta, + layout: {meta: newFullLayout.meta} + }; + } + + for(var i = 0; i < newFullData.length; i++) { + var trace = newFullData[i]; + + if(trace.meta) { + meta4data[trace.index] = trace._meta = {meta: trace.meta}; + } else if(newFullLayout.meta) { + trace._meta = {meta: newFullLayout.meta}; + } + if(newFullLayout.meta) { + trace._meta.layout = {meta: newFullLayout.meta}; + } + } + + if(meta4data.length) { + if(!_meta) { + _meta = newFullLayout._meta = {}; + } + _meta.data = meta4data; + } +} + // Create storage for all of the data related to frames and transitions: plots.createTransitionData = function(gd) { // Set up the default keyframe if it doesn't exist: @@ -1236,6 +1271,7 @@ plots.supplyTraceDefaults = function(traceIn, traceOut, colorIndex, layout, trac if(visible) { coerce('customdata'); coerce('ids'); + coerce('meta'); if(Registry.traceIs(traceOut, 'showLegend')) { traceOut._dfltShowLegend = true; diff --git a/src/traces/pie/plot.js b/src/traces/pie/plot.js index 71167677eee..05bf81e8bf8 100644 --- a/src/traces/pie/plot.js +++ b/src/traces/pie/plot.js @@ -204,9 +204,10 @@ function plot(gd, cdpie) { s.attr('data-notex', 1); }); - var txt = fullLayout.meta ? - Lib.templateString(trace.title.text, {meta: fullLayout.meta}) : - trace.title.text; + var txt = trace.title.text; + if(trace._meta) { + txt = Lib.templateString(txt, trace._meta); + } titleText.text(txt) .attr({ @@ -481,18 +482,18 @@ function determineInsideTextFont(trace, pt, layoutFont) { } function prerenderTitles(cdpie, gd) { - var fullLayout = gd._fullLayout; - var cd0, trace; + // Determine the width and height of the title for each pie. for(var i = 0; i < cdpie.length; i++) { cd0 = cdpie[i][0]; trace = cd0.trace; if(trace.title.text) { - var txt = fullLayout.meta ? - Lib.templateString(trace.title.text, {meta: fullLayout.meta}) : - trace.title.text; + var txt = trace.title.text; + if(trace._meta) { + txt = Lib.templateString(txt, trace._meta); + } var dummyTitle = Drawing.tester.append('text') .attr('data-notex', 1) diff --git a/test/image/baselines/trace_metatext.png b/test/image/baselines/trace_metatext.png new file mode 100644 index 00000000000..c0f3c45443a Binary files /dev/null and b/test/image/baselines/trace_metatext.png differ diff --git a/test/image/mocks/trace_metatext.json b/test/image/mocks/trace_metatext.json new file mode 100644 index 00000000000..492f75fd120 --- /dev/null +++ b/test/image/mocks/trace_metatext.json @@ -0,0 +1,131 @@ +{ + "data": [{ + "x": ["2019-01-01", "2019-02-10", "2019-03-24"], + "y": [1, 2, 1], + "meta": {"colname": "A", "colname2": "B"}, + "name": "TRACE %{meta.colname} | %{meta.colname2} ( %{layout.meta.global} )" + }, { + "type": "scatterpolar", + "r": [1, 2, 1], + "meta": {"radii": "Rz"}, + "name": "%{meta.radii} !!" + }, { + "type": "scatterternary", + "a": [2, 1, 1], + "b": [1, 2, 1], + "c": [1, 1, 2.12], + "meta": {"hello": "HeLlO"}, + "name": "!! %{meta.hello} !!" + }, { + "type": "surface", + "z": [[1, 2, 3], [1, 2, 1], [3, 2, 1]], + "meta": {"d": "Z"}, + "colorbar": { + "title": {"text": "Product %{meta.d}", "side": "right"}, + "len": 0.3 + } + }, { + "type": "pie", + "labels": ["a", "b", "c"], + "values": [1, 2, 3], + "domain": {"row": 0, "column": 1}, + "meta": {"column": "full-time"}, + "title": {"text": "Employee %{meta.column} -- %{layout.meta.global}"} + }], + "layout": { + "meta": { + "global": "GrApH", + "item": "mEtA" + }, + + "grid": {"rows": 3, "columns": 2, "xgap": 0.2, "ygap": 0.3}, + "width": 700, + "height": 860, + "margin": {"b": 100}, + + "title": {"text": "%{meta.global} using trace %{meta.item}"}, + "xaxis": { + "domain": {"row": 0, "column": 0}, + "title": {"text": "Worth more than %{data[0].meta.colname} | %{data[0].meta.colname2}"}, + "rangeselector": { + "buttons": [{ + "step": "all" + }, { + "step": "month", + "count": 2, + "label": "backup %{data[0].meta.colname2}" + }] + } + }, + "yaxis": { + "domain": {"row": 0, "column": 0}, + "title": {"text": "$ by %{data[0].meta.colname2}"} + }, + "polar": { + "bgcolor": "#d3d3d3", + "domain": {"row": 1, "column": 0}, + "angularaxis": { + "showticklabels": false + }, + "radialaxis": { + "title": { + "text": "%{data[1].meta.radii} ->", + "font": {"color": "red"} + } + } + }, + "ternary": { + "domain": {"row": 2, "column": 0}, + "aaxis": {"title": {"text": "%{data[2].meta.hello}"}}, + "baxis": {"title": {"text": "%{meta.global} | %{data[2].meta.hello}"}}, + "caxis": {"title": {"text": "%{data[2].meta.hello} | %{meta.item}"}} + }, + "scene": { + "domain": {"row": 1, "column": 1}, + "camera": {"eye": {"x": -0.05, "y": 3.07, "z": 0.105}}, + "zaxis": {"title": {"text": "___ %{data[3].meta.d} ___"}}, + "annotations": [{ + "text": "Look at %{data[3].meta.d}", + "bgcolor": "#d3d3d3", + "borderpad": 2, + "bordercolor": "#000", + "borderwidth": "1", + "x": 1, + "y": 1, + "z": 2 + }] + }, + + "annotations": [{ + "text": "N.B. --- %{data[0].meta.colname2}", + "xref": "x", + "yref": "y", + "x": "2019-02-10", + "y": 2 + }], + + "updatemenus": [{ + "buttons": [{ + "label": "Btn-%{data[1].meta.radii}", + "method": "restyle", + "args": ["visible", false] + }], + "y": 1, + "yanchor": "bottom" + }], + + "sliders": [{ + "len": 0.3, + "x": 0.7, + "steps": [{ + "label": "step-%{data[2].meta.hello}", + "method": "restyle", + "args": ["marker.color", "red"] + }, { + "label": "step-%{data[4].meta.column}", + "method": "restyle", + "args": ["marker.color", "blue"] + }] + }] + } +} diff --git a/test/jasmine/tests/hover_label_test.js b/test/jasmine/tests/hover_label_test.js index b6d35d9387d..6b3e6192684 100644 --- a/test/jasmine/tests/hover_label_test.js +++ b/test/jasmine/tests/hover_label_test.js @@ -1975,6 +1975,26 @@ describe('hover info', function() { .catch(failTest) .then(done); }); + + it('should work with trace meta references', function(done) { + var gd = document.getElementById('graph'); + + Plotly.update(gd, { + meta: {yname: 'Yy', xname: 'Xx'}, + hovertemplate: 'TRACE -- %{meta.yname}%{meta.xname}' + }) + .then(function() { + Fx.hover('graph', evt, 'xy'); + + assertHoverLabelContent({ + nums: 'TRACE -- Yy', + name: 'Xx', + axis: '0.388' + }); + }) + .catch(failTest) + .then(done); + }); }); it('should work with trace.name linked to layout.meta', function(done) {