diff --git a/build/plotcss.js b/build/plotcss.js index afed129a1d9..669cf65d480 100644 --- a/build/plotcss.js +++ b/build/plotcss.js @@ -31,21 +31,21 @@ var rules = { "X .cursor-n-resize": "cursor:n-resize;", "X .cursor-ne-resize": "cursor:ne-resize;", "X .cursor-grab": "cursor:-webkit-grab;cursor:grab;", - "X .modebar": "position:absolute;top:2px;right:2px;z-index:1001;background:rgba(255,255,255,0.7);", + "X .modebar": "position:absolute;top:2px;right:2px;z-index:1001;", "X .modebar--hover": "opacity:0;-webkit-transition:opacity 0.3s ease 0s;-moz-transition:opacity 0.3s ease 0s;-ms-transition:opacity 0.3s ease 0s;-o-transition:opacity 0.3s ease 0s;transition:opacity 0.3s ease 0s;", "X:hover .modebar--hover": "opacity:1;", "X .modebar-group": "float:left;display:inline-block;box-sizing:border-box;margin-left:8px;position:relative;vertical-align:middle;white-space:nowrap;", - "X .modebar-group:first-child": "margin-left:0px;", - "X .modebar-btn": "position:relative;font-size:16px;padding:3px 4px;cursor:pointer;line-height:normal;box-sizing:border-box;", + "X .modebar-btn": "position:relative;font-size:16px;padding:3px 4px;height:22px;cursor:pointer;line-height:normal;box-sizing:border-box;", "X .modebar-btn svg": "position:relative;top:2px;", - "X .modebar-btn path": "fill:rgba(0,31,95,0.3);", - "X .modebar-btn.active path,X .modebar-btn:hover path": "fill:rgba(0,22,72,0.5);", - "X .modebar-btn.modebar-btn--logo": "padding:3px 1px;", - "X .modebar-btn.modebar-btn--logo path": "fill:#447adb !important;", + "X .modebar.vertical": "top:-1px;", + "X .modebar.vertical .modebar-group": "display:block;float:none;margin-left:0px;margin-bottom:8px;", + "X .modebar.vertical .modebar-group .modebar-btn": "display:block;text-align:center;", "X [data-title]:before,X [data-title]:after": "position:absolute;-webkit-transform:translate3d(0, 0, 0);-moz-transform:translate3d(0, 0, 0);-ms-transform:translate3d(0, 0, 0);-o-transform:translate3d(0, 0, 0);transform:translate3d(0, 0, 0);display:none;opacity:0;z-index:1001;pointer-events:none;top:110%;right:50%;", "X [data-title]:hover:before,X [data-title]:hover:after": "display:block;opacity:1;", "X [data-title]:before": "content:'';position:absolute;background:transparent;border:6px solid transparent;z-index:1002;margin-top:-12px;border-bottom-color:#69738a;margin-right:-6px;", "X [data-title]:after": "content:attr(data-title);background:#69738a;color:white;padding:8px 10px;font-size:12px;line-height:12px;white-space:nowrap;margin-right:-18px;border-radius:2px;", + "X .vertical [data-title]:before,X .vertical [data-title]:after": "top:0%;right:200%;", + "X .vertical [data-title]:before": "border:6px solid transparent;border-left-color:#69738a;margin-top:8px;margin-right:-30px;", "X .select-outline": "fill:none;stroke-width:1;shape-rendering:crispEdges;", "X .select-outline-1": "stroke:white;", "X .select-outline-2": "stroke:black;stroke-dasharray:2px 2px;", diff --git a/build/ploticon.js b/build/ploticon.js index f7bcbcd4781..eb6ecadccac 100644 --- a/build/ploticon.js +++ b/build/ploticon.js @@ -32,13 +32,13 @@ module.exports = { 'transform': 'matrix(1 0 0 -1 0 850)' }, 'zoom_plus': { - 'width': 1000, + 'width': 875, 'height': 1000, 'path': 'm1 787l0-875 875 0 0 875-875 0z m687-500l-187 0 0-187-125 0 0 187-188 0 0 125 188 0 0 187 125 0 0-187 187 0 0-125z', 'transform': 'matrix(1 0 0 -1 0 850)' }, 'zoom_minus': { - 'width': 1000, + 'width': 875, 'height': 1000, 'path': 'm0 788l0-876 875 0 0 876-875 0z m688-500l-500 0 0 125 500 0 0-125z', 'transform': 'matrix(1 0 0 -1 0 850)' @@ -120,5 +120,9 @@ module.exports = { 'height': 1000, 'path': 'M512 409c0-57-46-104-103-104-57 0-104 47-104 104 0 57 47 103 104 103 57 0 103-46 103-103z m-327-39l92 0 0 92-92 0z m-185 0l92 0 0 92-92 0z m370-186l92 0 0 93-92 0z m0-184l92 0 0 92-92 0z', 'transform': 'matrix(1.5 0 0 -1.5 0 850)' + }, + 'newplotlylogo': { + 'name': 'newplotlylogo', + 'svg': 'plotly-logomark' } }; diff --git a/src/components/modebar/modebar.js b/src/components/modebar/modebar.js index 445ad3e5088..6928d133aa3 100644 --- a/src/components/modebar/modebar.js +++ b/src/components/modebar/modebar.js @@ -14,7 +14,7 @@ var isNumeric = require('fast-isnumeric'); var Lib = require('../../lib'); var Icons = require('../../../build/ploticon'); - +var Parser = new DOMParser(); /** * UI controller for interactive plots @@ -45,13 +45,29 @@ var proto = ModeBar.prototype; proto.update = function(graphInfo, buttons) { this.graphInfo = graphInfo; - var context = this.graphInfo._context; + var context = this.graphInfo._context, + fullLayout = this.graphInfo._fullLayout, + modeBarId = 'modebar-' + fullLayout._uid; + + this.element.setAttribute('id', modeBarId); + this._uid = modeBarId; if(context.displayModeBar === 'hover') { this.element.className = 'modebar modebar--hover'; } else this.element.className = 'modebar'; + if(fullLayout.modebar.orientation === 'v') { + this.element.className += ' vertical'; + buttons = buttons.reverse(); + } + + Lib.deleteRelatedStyleRule(modeBarId); + Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId, 'background-color: ' + fullLayout.modebar.bgcolor); + Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn .icon path', 'fill: ' + fullLayout.modebar.color); + Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn:hover .icon path', 'fill: ' + fullLayout.modebar.activecolor); + Lib.addRelatedStyleRule(modeBarId, '#' + modeBarId + ' .modebar-btn.active .icon path', 'fill: ' + fullLayout.modebar.activecolor); + // if buttons or logo have changed, redraw modebar interior var needsNewButtons = !this.hasButtons(buttons); var needsNewLogo = (this.hasLogo !== context.displaylogo); @@ -65,7 +81,12 @@ proto.update = function(graphInfo, buttons) { this.updateButtons(buttons); if(context.displaylogo) { - this.element.appendChild(this.getLogo()); + if(fullLayout.modebar.orientation === 'v') { + this.element.prepend(this.getLogo()); + } else { + this.element.appendChild(this.getLogo()); + } + this.hasLogo = true; } } @@ -173,6 +194,7 @@ proto.createButton = function(config) { * @Param {object} thisIcon * @Param {number} thisIcon.width * @Param {string} thisIcon.path + * @Param {string} thisIcon.color * @Return {HTMLelement} */ proto.createIcon = function(thisIcon) { @@ -180,24 +202,34 @@ proto.createIcon = function(thisIcon) { Number(thisIcon.height) : thisIcon.ascent - thisIcon.descent, svgNS = 'http://www.w3.org/2000/svg', - icon = document.createElementNS(svgNS, 'svg'), - path = document.createElementNS(svgNS, 'path'); + icon; - icon.setAttribute('height', '1em'); - icon.setAttribute('width', (thisIcon.width / iconHeight) + 'em'); - icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' ')); + if(thisIcon.path) { + icon = document.createElementNS(svgNS, 'svg'); + icon.setAttribute('viewBox', [0, 0, thisIcon.width, iconHeight].join(' ')); + icon.setAttribute('class', 'icon'); + + var path = document.createElementNS(svgNS, 'path'); + path.setAttribute('d', thisIcon.path); - path.setAttribute('d', thisIcon.path); + if(thisIcon.transform) { + path.setAttribute('transform', thisIcon.transform); + } + else if(thisIcon.ascent !== undefined) { + // Legacy icon transform calculation + path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')'); + } - if(thisIcon.transform) { - path.setAttribute('transform', thisIcon.transform); + icon.appendChild(path); } - else if(thisIcon.ascent !== undefined) { - // Legacy icon transform calculation - path.setAttribute('transform', 'matrix(1 0 0 -1 0 ' + thisIcon.ascent + ')'); + + if(thisIcon.svg) { + var svgDoc = Parser.parseFromString(thisIcon.svg, 'application/xml'); + icon = svgDoc.childNodes[0]; } - icon.appendChild(path); + icon.setAttribute('height', '1em'); + icon.setAttribute('width', '1em'); return icon; }; @@ -272,7 +304,7 @@ proto.getLogo = function() { a.setAttribute('data-title', Lib._(this.graphInfo, 'Produced with Plotly')); a.className = 'modebar-btn plotlyjsicon modebar-btn--logo'; - a.appendChild(this.createIcon(Icons.plotlylogo)); + a.appendChild(this.createIcon(Icons.newplotlylogo)); group.appendChild(a); return group; @@ -288,6 +320,7 @@ proto.removeAllButtons = function() { proto.destroy = function() { Lib.removeElement(this.container.querySelector('.modebar')); + Lib.deleteRelatedStyleRule(this._uid); }; function createModeBar(gd, buttons) { diff --git a/src/css/_modebar.scss b/src/css/_modebar.scss index adb85a6878a..a7e128bb573 100644 --- a/src/css/_modebar.scss +++ b/src/css/_modebar.scss @@ -3,7 +3,6 @@ top: 2px; right: 2px; z-index: 1001; - background: rgba(255,255,255,0.7); } .modebar--hover { @@ -23,17 +22,13 @@ position: relative; vertical-align: middle; white-space: nowrap; - - &:first-child { - margin-left: 0px; - } } - .modebar-btn { position: relative; font-size: 16px; padding: 3px 4px; + height: 22px; /* display: inline-block; including this breaks 3d interaction in .embed mode. Chrome bug? */ cursor: pointer; line-height: normal; @@ -44,19 +39,22 @@ top: 2px; } - path { - fill: rgba(0,31,95,0.3); - } + &.modebar-btn--logo { - &.active path, &:hover path { - fill: rgba(0,22,72,0.5); } +} - &.modebar-btn--logo { - padding: 3px 1px; +.modebar.vertical { + top: -1px; + .modebar-group { + display: block; + float: none; + margin-left: 0px; + margin-bottom: 8px; - path { - fill: $color-brand-primary !important; + .modebar-btn { + display: block; + text-align: center; } } } diff --git a/src/css/_tooltip.scss b/src/css/_tooltip.scss index 1f113a4af7f..596ed44d936 100644 --- a/src/css/_tooltip.scss +++ b/src/css/_tooltip.scss @@ -51,7 +51,7 @@ $successColor: hsl(121, 32%, 40%) !default; opacity: 1; } - // Arrow + // Top arrow &:before { content: ''; position: absolute; @@ -78,3 +78,18 @@ $successColor: hsl(121, 32%, 40%) !default; border-radius: 2px; } } + +.vertical [data-title] { + &:before, &:after { + top: 0%; + right: 200%; + } + + // Right arrow + &:before { + border: $arrowBorderWidth solid transparent; + border-left-color: $defaultColor; + margin-top: $verticalPadding; + margin-right: -1 * ($arrowOffsetX + 2 * $arrowBorderWidth); + } +} diff --git a/src/fonts/ploticon/ploticon.svg b/src/fonts/ploticon/ploticon.svg index 5007169983b..e2a07ec54d4 100644 --- a/src/fonts/ploticon/ploticon.svg +++ b/src/fonts/ploticon/ploticon.svg @@ -11,8 +11,8 @@ - - + + @@ -27,5 +27,25 @@ + + diff --git a/src/lib/index.js b/src/lib/index.js index 50a1e39948b..e0b6576d8ca 100644 --- a/src/lib/index.js +++ b/src/lib/index.js @@ -679,14 +679,24 @@ lib.removeElement = function(el) { * by all calls to this function */ lib.addStyleRule = function(selector, styleString) { - if(!lib.styleSheet) { - var style = document.createElement('style'); + lib.addRelatedStyleRule('global', selector, styleString); +}; + +/** + * for dynamically adding style rules + * to a stylesheet uniquely identified by a uid + */ +lib.addRelatedStyleRule = function(uid, selector, styleString) { + var id = 'plotly.js-style-' + uid, + style = document.getElementById(id); + if(!style) { + style = document.createElement('style'); + style.setAttribute('id', id); // WebKit hack :( style.appendChild(document.createTextNode('')); document.head.appendChild(style); - lib.styleSheet = style.sheet; } - var styleSheet = lib.styleSheet; + var styleSheet = style.sheet; if(styleSheet.insertRule) { styleSheet.insertRule(selector + '{' + styleString + '}', 0); @@ -697,6 +707,15 @@ lib.addStyleRule = function(selector, styleString) { else lib.warn('addStyleRule failed'); }; +/** + * to remove from the page a stylesheet identified by a given uid + */ +lib.deleteRelatedStyleRule = function(uid) { + var id = 'plotly.js-style-' + uid, + style = document.getElementById(id); + if(style) style.remove(); +}; + lib.isIE = function() { return typeof window.navigator.msSaveBlob !== 'undefined'; }; diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js index 9e422717d4c..103a0b234f7 100644 --- a/src/plots/layout_attributes.js +++ b/src/plots/layout_attributes.js @@ -224,5 +224,34 @@ module.exports = { 'or a logo image, for example. To omit one of these items on the plot,', 'make an item with matching `templateitemname` and `visible: false`.' ].join(' ') + }, + modebar: { + orientation: { + valType: 'enumerated', + values: ['v', 'h'], + dflt: 'h', + role: 'info', + editType: 'modebar', + description: 'Sets the orientation of the modebar.' + }, + bgcolor: { + valType: 'color', + role: 'style', + editType: 'modebar', + description: 'Sets the background color of the modebar.' + }, + color: { + valType: 'color', + role: 'style', + editType: 'modebar', + description: 'Sets the color of the icons in the modebar.' + }, + activecolor: { + valType: 'color', + role: 'style', + editType: 'modebar', + description: 'Sets the color of the active or hovered on icons in the modebar.' + }, + editType: 'modebar' } }; diff --git a/src/plots/plots.js b/src/plots/plots.js index 8f172d14ba0..648b340094b 100644 --- a/src/plots/plots.js +++ b/src/plots/plots.js @@ -1334,6 +1334,12 @@ plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut, formatObj) { coerce('datarevision'); + coerce('modebar.orientation'); + coerce('modebar.bgcolor', Color.addOpacity(layoutOut.paper_bgcolor, 0.5)); + var modebarDefaultColor = Color.contrast(Color.rgb(layoutOut.modebar.bgcolor)); + coerce('modebar.color', Color.addOpacity(modebarDefaultColor, 0.3)); + coerce('modebar.activecolor', Color.addOpacity(modebarDefaultColor, 0.7)); + Registry.getComponentMethod( 'calendars', 'handleDefaults' diff --git a/tasks/util/pull_font_svg.js b/tasks/util/pull_font_svg.js index b509b98a512..5fe807d6de4 100644 --- a/tasks/util/pull_font_svg.js +++ b/tasks/util/pull_font_svg.js @@ -2,7 +2,7 @@ var fs = require('fs'); var xml2js = require('xml2js'); var parser = new xml2js.Parser(); - +var builder = new xml2js.Builder({ headless: true, rootName: 'svg', renderOpts: {'newline': ''}}); module.exports = function pullFontSVG(data, pathOut) { parser.parseString(data, function(err, result) { @@ -28,6 +28,17 @@ module.exports = function pullFontSVG(data, pathOut) { }; }); + // Load SVG + var svgs = result.svg.defs[0].svg; + svgs.forEach(function(svg) { + var name = svg.$.id; + delete svg.$.id; + chars[name] = { + name: name, + svg: builder.buildObject(svg) + }; + }); + // turn remaining double quotes into single var charStr = JSON.stringify(chars, null, 4).replace(/\"/g, '\''); diff --git a/test/jasmine/tests/modebar_test.js b/test/jasmine/tests/modebar_test.js index d0e447d21a4..8c2267bb3cf 100644 --- a/test/jasmine/tests/modebar_test.js +++ b/test/jasmine/tests/modebar_test.js @@ -9,6 +9,7 @@ var Registry = require('@src/registry'); var createGraphDiv = require('../assets/create_graph_div'); var destroyGraphDiv = require('../assets/destroy_graph_div'); var selectButton = require('../assets/modebar_button'); +var failTest = require('../assets/fail_test'); describe('ModeBar', function() { @@ -29,10 +30,17 @@ describe('ModeBar', function() { function getMockGraphInfo(xaxes, yaxes) { return { _fullLayout: { + _uid: '6ea6a7', dragmode: 'zoom', _paperdiv: d3.select(getMockContainerTree()), _has: Plots._hasPlotType, - _subplots: {xaxis: xaxes || [], yaxis: yaxes || []} + _subplots: {xaxis: xaxes || [], yaxis: yaxes || []}, + modebar: { + orientation: 'h', + bgcolor: 'rgba(255,255,255,0.7)', + color: 'rgba(0, 31, 95, 0.3)', + activecolor: 'rgba(0, 31, 95, 1)' + } }, _fullData: [], _context: { @@ -291,6 +299,7 @@ describe('ModeBar', function() { modeBar.destroy(); expect(modeBarParent.querySelector('.modebar')).toBeNull(); + }); }); @@ -1229,4 +1238,104 @@ describe('ModeBar', function() { }); }); }); + + describe('modebar styling', function() { + var gd, + colors = ['rgba(128, 128, 128, 0.7)', 'rgba(255, 0, 128, 0.2)'], + targetBtn = 'pan2d', button, style; + + beforeEach(function() { + gd = createGraphDiv(); + }); + + afterEach(function() { + Plotly.purge(gd); + destroyGraphDiv(); + }); + + function checkButtonColor(button, color) { + var paths = button.node.querySelector('path'); + var style = window.getComputedStyle(paths); + expect(style.fill).toBe(color); + } + + it('create an associated style element and destroy it on purge', function(done) { + var styleSelector, style; + Plotly.plot(gd, [], {}) + .then(function() { + styleSelector = 'style[id*="modebar-' + gd._fullLayout._uid + '"]'; + + style = document.querySelector(styleSelector); + expect(style).toBeTruthy(); + }) + .then(function() { + Plotly.purge(gd); + style = document.querySelector(styleSelector); + expect(style).toBeNull(); + }) + .then(done); + }); + + it('changes icon colors', function(done) { + Plotly.plot(gd, [], {modebar: { color: colors[0]}}) + .then(function() { + button = selectButton(gd._fullLayout._modeBar, targetBtn); + checkButtonColor(button, colors[0]); + }) + .then(function() {Plotly.relayout(gd, 'modebar.color', colors[1]);}) + .then(function() { + checkButtonColor(button, colors[1]); + }) + .catch(failTest) + .then(done); + }); + + it('changes active icon colors', function(done) { + Plotly.plot(gd, [], {modebar: { activecolor: colors[0]}}) + .then(function() { + button = selectButton(gd._fullLayout._modeBar, targetBtn); + button.click(); + checkButtonColor(button, colors[0]); + }) + .then(function() {Plotly.relayout(gd, 'modebar.activecolor', colors[1]);}) + .then(function() { + checkButtonColor(button, colors[1]); + }) + .catch(failTest) + .then(done); + }); + + it('changes background color', function(done) { + Plotly.plot(gd, [], {modebar: { bgcolor: colors[0]}}) + .then(function() { + style = window.getComputedStyle(gd._fullLayout._modeBar.element); + expect(style.backgroundColor).toBe(colors[0]); + }) + .then(function() {Plotly.relayout(gd, 'modebar.bgcolor', colors[1]);}) + .then(function() { + style = window.getComputedStyle(gd._fullLayout._modeBar.element); + expect(style.backgroundColor).toBe(colors[1]); + }) + .catch(failTest) + .then(done); + }); + + it('changes orientation', function(done) { + var modeBarEl, size; + + Plotly.plot(gd, [], {modebar: { orientation: 'v' }}) + .then(function() { + modeBarEl = gd._fullLayout._modeBar.element; + size = modeBarEl.getBoundingClientRect(); + expect(size.width < size.height).toBeTruthy(); + }) + .then(function() {Plotly.relayout(gd, 'modebar.orientation', 'h');}) + .catch(failTest) + .then(function() { + size = modeBarEl.getBoundingClientRect(); + expect(size.width > size.height).toBeTruthy(); + }) + .then(done); + }); + }); });