diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000000..2190b0e1c7c --- /dev/null +++ b/.jshintrc @@ -0,0 +1,72 @@ +{ + // environments + "browser": true, // Define globals exposed by modern browsers. + "jquery": false, // Define globals exposed by jQuery. + "node": true, // Define globals exposed by Node.js. + "jasmine": true, // Define globals exposed by jasmine + + // enforcing (true means bug us about it) + "camelcase": false, // Force all variable names to use either camelCase style or UPPER_CASE with underscores. + "curly": false, // This option requires you to always put curly braces around blocks in loops and conditionals. + "eqeqeq": true, // Prohibit use of == and != in favor of === and !==. + "es3": true, // This option tells JSHint that your code needs to adhere to ECMAScript 3 spec (old browsers) + "forin": false, // This option requires all for in loops to filter object's items. + "freeze": true, // This options prohibits overwriting prototypes of native objects such as Array, Date and so on. + "immed": true, // This option prohibits the use of immediate function invocations without wrapping them in parentheses. + "indent": 4, // Enforce tab width of 4 spaces. + "latedef": "nofunc", // Prohibit use of a variable before it is defined. + "maxcomplexity": false, // This option lets you control cyclomatic complexity throughout your code. + "maxdepth": 6, // This option lets you control how nested do you want your blocks to be + "maxlen": 120, // Enforce line length to 120 characters + "newcap": true, // Require capitalized names for constructor functions. + "noarg": true, // This option prohibits the use of arguments.caller and arguments.callee. + "noempty": true, // This option warns when you have an empty block in your code. + "nonbsp": true, // This option warns about "non-breaking whitespace" characters. + "nonew": true, // This option prohibits the use of constructor functions for side-effects. + "plusplus": false, // This option prohibits the use of unary increment and decrement operators. + "quotmark": "single", // Enforce use of single quotation marks for strings. + "strict": true, // Enforce placing 'use strict' at the top function scope + "trailing": true, // Prohibit trailing whitespace. + "undef": true, // Prohibit use of explicitly undeclared variables. + "unused": true, // Warn when variables are defined but never used. + + // relaxing (true means DON'T bug us about it) + "asi": false, // This option suppresses warnings about missing semicolons. + "boss": false, // This option suppresses warnings about the use of assignments in cases where comparisons are expected. + "debug": false, // This option suppresses warnings about the debugger statements in your code. + "eqnull": true, // Suppress warnings about == null comparisons. + "esnext": true, // This option tells JSHint that your code uses ECMAScript 6 specific syntax. + "evil": false, // This option suppresses warnings about the use of eval. + "expr": false, // This option suppresses warnings about the use of expressions where normally you would expect to see assignments or function calls. + "funcscope": false, // This option suppresses warnings about declaring variables inside of control structures while accessing them later from the outside. + "globalstrict": false, // This option suppresses warnings about the use of global strict mode. + "iterator": false, // This option suppresses warnings about the __iterator__ property. + "lastsemic": false, // This option suppresses warnings about missing semicolons + "laxbreak": false, // This option suppresses most of the warnings about possibly unsafe line breakings in your code. + "laxcomma": false, // This option suppresses warnings about comma-first coding style + "loopfunc": false, // This option suppresses warnings about functions inside of loops. + "maxerr": 500, // This options allows you to set the maximum amount of warnings JSHint will produce before giving up. + "moz": false, // This options tells JSHint that your code uses Mozilla JavaScript extensions. + "multistr": false, // This option suppresses warnings about multi-line strings. + "notypeof": false, // This option suppresses warnings about invalid typeof operator values. + "proto": false, // This option suppresses warnings about the __proto__ property. + "scripturl": false, // This option suppresses warnings about the use of script-targeted URLs—such as javascript:... + "shadow": false, // This option suppresses warnings about variable shadowing + "sub": false, // This option suppresses warnings about using [] notation when it can be expressed in dot notation + "supernew": false, // This option suppresses warnings about "weird" constructions like new function () { ... } and new Object; + "validthis": false, // This option suppresses warnings about possible strict violations when the code is running in strict mode + "noyield": false, // This option suppresses warnings about generator functions with no yield statement in them. + + // global pre defined variables + "predef": [ + "JSON", + "Uint8Array", + "Uint16Array", + "Uint32Array", + "Int8Array", + "Int16Array", + "Int32Array", + "Float32Array", + "Float64Array" + ] +} diff --git a/devtools/test_dashboard/test-geo.js b/devtools/test_dashboard/test-geo.js index d6c307b1442..cf27d98e4e3 100644 --- a/devtools/test_dashboard/test-geo.js +++ b/devtools/test_dashboard/test-geo.js @@ -1,6 +1,6 @@ var plotButtons = require('./buttons'); -var figDir = '../../test/image/baseline/geo_'; +var figDir = '../../test/image/baselines/geo_'; var plots = {}; diff --git a/package.json b/package.json index e44a8776301..4f52ce7cdda 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "The premier javascript graphing library", "license": "MIT", - "main": "./src/plotly.js", + "main": "./src/index.js", "repository": { "type": "git", "url": "https://github.com/plotly/plotly.js.git" @@ -43,9 +43,11 @@ ], "scripts": { "preprocess": "node tasks/preprocess.js", + "postinstall": "npm run preprocess", "bundle": "node tasks/bundle.js", "build": "npm run preprocess && npm run bundle", "watch": "node tasks/watch_plotly.js", + "lint": "cd src && jshint . || true", "test-jasmine": "karma start test/jasmine/karma.conf.js", "test-image": "echo TODO", "test": "npm run test-jasmine && npm test-image", @@ -97,6 +99,7 @@ "browserify-transform-tools": "^1.5.0", "ecstatic": "^1.2.0", "jasmine-core": "^2.3.4", + "jshint": "^2.8.0", "karma": "^0.13.15", "karma-browserify": "^4.4.0", "karma-chrome-launcher": "^0.2.1", diff --git a/src/geo/geo-assets.js b/src/assets/geo_assets.js similarity index 100% rename from src/geo/geo-assets.js rename to src/assets/geo_assets.js diff --git a/src/geo/topojson/africa_110m.json b/src/assets/topojson/africa_110m.json similarity index 100% rename from src/geo/topojson/africa_110m.json rename to src/assets/topojson/africa_110m.json diff --git a/src/geo/topojson/africa_50m.json b/src/assets/topojson/africa_50m.json similarity index 100% rename from src/geo/topojson/africa_50m.json rename to src/assets/topojson/africa_50m.json diff --git a/src/geo/topojson/asia_110m.json b/src/assets/topojson/asia_110m.json similarity index 100% rename from src/geo/topojson/asia_110m.json rename to src/assets/topojson/asia_110m.json diff --git a/src/geo/topojson/asia_50m.json b/src/assets/topojson/asia_50m.json similarity index 100% rename from src/geo/topojson/asia_50m.json rename to src/assets/topojson/asia_50m.json diff --git a/src/geo/topojson/europe_110m.json b/src/assets/topojson/europe_110m.json similarity index 100% rename from src/geo/topojson/europe_110m.json rename to src/assets/topojson/europe_110m.json diff --git a/src/geo/topojson/europe_50m.json b/src/assets/topojson/europe_50m.json similarity index 100% rename from src/geo/topojson/europe_50m.json rename to src/assets/topojson/europe_50m.json diff --git a/src/geo/topojson/north-america_110m.json b/src/assets/topojson/north-america_110m.json similarity index 100% rename from src/geo/topojson/north-america_110m.json rename to src/assets/topojson/north-america_110m.json diff --git a/src/geo/topojson/north-america_50m.json b/src/assets/topojson/north-america_50m.json similarity index 100% rename from src/geo/topojson/north-america_50m.json rename to src/assets/topojson/north-america_50m.json diff --git a/src/geo/topojson/south-america_110m.json b/src/assets/topojson/south-america_110m.json similarity index 100% rename from src/geo/topojson/south-america_110m.json rename to src/assets/topojson/south-america_110m.json diff --git a/src/geo/topojson/south-america_50m.json b/src/assets/topojson/south-america_50m.json similarity index 100% rename from src/geo/topojson/south-america_50m.json rename to src/assets/topojson/south-america_50m.json diff --git a/src/geo/topojson/usa_110m.json b/src/assets/topojson/usa_110m.json similarity index 100% rename from src/geo/topojson/usa_110m.json rename to src/assets/topojson/usa_110m.json diff --git a/src/geo/topojson/usa_50m.json b/src/assets/topojson/usa_50m.json similarity index 100% rename from src/geo/topojson/usa_50m.json rename to src/assets/topojson/usa_50m.json diff --git a/src/geo/topojson/world_110m.json b/src/assets/topojson/world_110m.json similarity index 100% rename from src/geo/topojson/world_110m.json rename to src/assets/topojson/world_110m.json diff --git a/src/geo/topojson/world_50m.json b/src/assets/topojson/world_50m.json similarity index 100% rename from src/geo/topojson/world_50m.json rename to src/assets/topojson/world_50m.json diff --git a/src/components/annotations/arrow_paths.js b/src/components/annotations/arrow_paths.js new file mode 100644 index 00000000000..64c662f6ae0 --- /dev/null +++ b/src/components/annotations/arrow_paths.js @@ -0,0 +1,49 @@ +/** + * centerx is a center of scaling tuned for maximum scalability of + * the arrowhead ie throughout mag=0.3..3 the head is joined smoothly + * to the line, but the endpoint moves. + * backoff is the distance to move the arrowhead, and the end of the + * line, in order to end at the right place + * + * TODO: option to have the pointed-to point a little in front of the + * end of the line, as people tend to want a bit of a gap there... + */ +module.exports = [ + // no arrow + '', + // wide with flat back + { + path: 'M-2.4,-3V3L0.6,0Z', + backoff: 0.6 + }, + // narrower with flat back + { + path: 'M-3.7,-2.5V2.5L1.3,0Z', + backoff: 1.3 + }, + // barbed + { + path: 'M-4.45,-3L-1.65,-0.2V0.2L-4.45,3L1.55,0Z', + backoff: 1.55 + }, + // wide line-drawn + { + path: 'M-2.2,-2.2L-0.2,-0.2V0.2L-2.2,2.2L-1.4,3L1.6,0L-1.4,-3Z', + backoff: 1.6 + }, + // narrower line-drawn + { + path: 'M-4.4,-2.1L-0.6,-0.2V0.2L-4.4,2.1L-4,3L2,0L-4,-3Z', + backoff: 2 + }, + // circle + { + path: 'M2,0A2,2 0 1,1 0,-2A2,2 0 0,1 2,0Z', + backoff: 0 + }, + // square + { + path: 'M2,2V-2H-2V2Z', + backoff: 0 + } +]; diff --git a/src/components/annotations/attributes.js b/src/components/annotations/attributes.js new file mode 100644 index 00000000000..2338a81707c --- /dev/null +++ b/src/components/annotations/attributes.js @@ -0,0 +1,240 @@ +var Plotly = require('../../plotly'); +var ARROWPATHS = require('./arrow_paths'); +var fontAttrs = require('../../plots/font_attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +module.exports = { + _isLinkedToArray: true, + + text: { + valType: 'string', + role: 'info', + description: [ + 'Sets the text associated with this annotation.', + 'Plotly uses a subset of HTML tags to do things like', + 'newline (
), bold (), italics (),', + 'hyperlinks (). Tags , , ', + ' are also supported.' + ].join(' ') + }, + textangle: { + valType: 'angle', + dflt: 0, + role: 'style', + description: [ + 'Sets the angle at which the `text` is drawn', + 'with respect to the horizontal.' + ].join(' ') + }, + font: extendFlat({}, fontAttrs, { + description: 'Sets the annotation text font.' + }), + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 1, + role: 'style', + description: 'Sets the opacity of the annotation (text + arrow).' + }, + align: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + dflt: 'center', + role: 'style', + description: [ + 'Sets the vertical alignment of the `text` with', + 'respect to the set `x` and `y` position.', + 'Has only an effect if `text` spans more two or more lines', + '(i.e. `text` contains one or more
HTML tags).' + ].join(' ') + }, + bgcolor: { + valType: 'color', + dflt: 'rgba(0,0,0,0)', + role: 'style', + description: 'Sets the background color of the annotation.' + }, + bordercolor: { + valType: 'color', + dflt: 'rgba(0,0,0,0)', + role: 'style', + description: [ + 'Sets the color of the border enclosing the annotation `text`.' + ].join(' ') + }, + borderpad: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: [ + 'Sets the padding (in px) between the `text`', + 'and the enclosing border.' + ].join(' ') + }, + borderwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: [ + 'Sets the width (in px) of the border enclosing', + 'the annotation `text`.' + ].join(' ') + }, + // arrow + showarrow: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the annotation is drawn with an arrow.', + 'If *true*, `text` is placed near the arrow\'s tail.', + 'If *false*, `text` lines up with the `x` and `y` provided.' + ].join(' ') + }, + arrowcolor: { + valType: 'color', + role: 'style', + description: 'Sets the color of the annotation arrow.' + }, + arrowhead: { + valType: 'integer', + min: 0, + max: ARROWPATHS.length, + dflt: 1, + role: 'style', + description: 'Sets the annotation arrow head style.' + }, + arrowsize: { + valType: 'number', + min: 0.3, + dflt: 1, + role: 'style', + description: 'Sets the size (in px) of annotation arrow head.' + }, + arrowwidth: { + valType: 'number', + min: 0.1, + role: 'style', + description: 'Sets the width (in px) of annotation arrow.' + }, + ax: { + valType: 'number', + dflt: -10, + role: 'info', + description: [ + 'Sets the x component of the arrow tail about the arrow head.', + 'A positive (negative) component corresponds to an arrow pointing', + 'from right to left (left to right)' + ].join(' ') + }, + ay: { + valType: 'number', + dflt: -30, + role: 'info', + description: [ + 'Sets the y component of the arrow tail about the arrow head.', + 'A positive (negative) component corresponds to an arrow pointing', + 'from bottom to top (top to bottom)' + ].join(' ') + }, + // positioning + xref: { + valType: 'enumerated', + values: [ + 'paper', + Plotly.Plots.subplotsRegistry.cartesian.idRegex.x.toString() + ], + role: 'info', + description: [ + 'Sets the annotation\'s x coordinate axis.', + 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', + 'refers to an x coordinate', + 'If set to *paper*, the `x` position refers to the distance from', + 'the left side of the plotting area in normalized coordinates', + 'where 0 (1) corresponds to the left (right) side.' + ].join(' ') + }, + x: { + valType: 'number', + role: 'info', + description: [ + 'Sets the annotation\'s x position.', + 'Note that dates and categories are converted to numbers.' + ].join(' ') + }, + xanchor: { + valType: 'enumerated', + values: ['auto', 'left', 'center', 'right'], + dflt: 'auto', + role: 'info', + description: [ + 'Sets the annotation\'s horizontal position anchor', + 'This anchor binds the `x` position to the *left*, *center*', + 'or *right* of the annotation.', + 'For example, if `x` is set to 1, `xref` to *paper* and', + '`xanchor` to *right* then the right-most portion of the', + 'annotation lines up with the right-most edge of the', + 'plotting area.', + 'If *auto*, the anchor is equivalent to *center* for', + 'data-referenced annotations', + 'whereas for paper-referenced, the anchor picked corresponds', + 'to the closest side.' + ].join(' ') + }, + yref: { + valType: 'enumerated', + values: [ + 'paper', + Plotly.Plots.subplotsRegistry.cartesian.idRegex.y.toString() + ], + role: 'info', + description: [ + 'Sets the annotation\'s y coordinate axis.', + 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', + 'refers to an y coordinate', + 'If set to *paper*, the `y` position refers to the distance from', + 'the bottom of the plotting area in normalized coordinates', + 'where 0 (1) corresponds to the bottom (top).' + ].join(' ') + }, + y: { + valType: 'number', + role: 'info', + description: [ + 'Sets the annotation\'s y position.', + 'Note that dates and categories are converted to numbers.' + ].join(' ') + }, + yanchor: { + valType: 'enumerated', + values: ['auto', 'top', 'middle', 'bottom'], + dflt: 'auto', + role: 'info', + description: [ + 'Sets the annotation\'s vertical position anchor', + 'This anchor binds the `y` position to the *top*, *middle*', + 'or *bottom* of the annotation.', + 'For example, if `y` is set to 1, `yref` to *paper* and', + '`yanchor` to *top* then the top-most portion of the', + 'annotation lines up with the top-most edge of the', + 'plotting area.', + 'If *auto*, the anchor is equivalent to *middle* for', + 'data-referenced annotations', + 'whereas for paper-referenced, the anchor picked corresponds', + 'to the closest side.' + ].join(' ') + }, + + _deprecated: { + ref: { + valType: 'string', + role: 'info', + description: [ + 'Obsolete. Set `xref` and `yref` separately instead.' + ].join(' ') + } + } +}; diff --git a/src/annotations.js b/src/components/annotations/index.js similarity index 79% rename from src/annotations.js rename to src/components/annotations/index.js index e58f7a3c0b9..c829af7c3de 100644 --- a/src/annotations.js +++ b/src/components/annotations/index.js @@ -1,293 +1,14 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var annotations = module.exports = {}; -// centerx is a center of scaling tuned for maximum scalability of -// the arrowhead ie throughout mag=0.3..3 the head is joined smoothly -// to the line, but the endpoint moves. -// backoff is the distance to move the arrowhead, and the end of the -// line, in order to end at the right place -// TODO: option to have the pointed-to point a little in front of the -// end of the line, as people tend to want a bit of a gap there... -annotations.ARROWPATHS = [ - // no arrow - '', - // wide with flat back - { - path: 'M-2.4,-3V3L0.6,0Z', - backoff: 0.6 - }, - // narrower with flat back - { - path: 'M-3.7,-2.5V2.5L1.3,0Z', - backoff: 1.3 - }, - // barbed - { - path: 'M-4.45,-3L-1.65,-0.2V0.2L-4.45,3L1.55,0Z', - backoff: 1.55 - }, - // wide line-drawn - { - path: 'M-2.2,-2.2L-0.2,-0.2V0.2L-2.2,2.2L-1.4,3L1.6,0L-1.4,-3Z', - backoff: 1.6 - }, - // narrower line-drawn - { - path: 'M-4.4,-2.1L-0.6,-0.2V0.2L-4.4,2.1L-4,3L2,0L-4,-3Z', - backoff: 2 - }, - // circle - { - path: 'M2,0A2,2 0 1,1 0,-2A2,2 0 0,1 2,0Z', - backoff: 0 - }, - // square - { - path: 'M2,2V-2H-2V2Z', - backoff: 0 - } -]; - -annotations.layoutAttributes = { - _isLinkedToArray: true, - - text: { - valType: 'string', - role: 'info', - description: [ - 'Sets the text associated with this annotation.', - 'Plotly uses a subset of HTML tags to do things like', - 'newline (
), bold (), italics (),', - 'hyperlinks (). Tags , , ', - ' are also supported.' - ].join(' ') - }, - textangle: { - valType: 'angle', - dflt: 0, - role: 'style', - description: [ - 'Sets the angle at which the `text` is drawn', - 'with respect to the horizontal.' - ].join(' ') - }, - font: Plotly.Lib.extendFlat({}, Plotly.Plots.fontAttrs, { - description: 'Sets the annotation text font.' - }), - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - role: 'style', - description: 'Sets the opacity of the annotation (text + arrow).' - }, - align: { - valType: 'enumerated', - values: ['left', 'center', 'right'], - dflt: 'center', - role: 'style', - description: [ - 'Sets the vertical alignment of the `text` with', - 'respect to the set `x` and `y` position.', - 'Has only an effect if `text` spans more two or more lines', - '(i.e. `text` contains one or more
HTML tags).' - ].join(' ') - }, - bgcolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'style', - description: 'Sets the background color of the annotation.' - }, - bordercolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'style', - description: [ - 'Sets the color of the border enclosing the annotation `text`.' - ].join(' ') - }, - borderpad: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the padding (in px) between the `text`', - 'and the enclosing border.' - ].join(' ') - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the width (in px) of the border enclosing', - 'the annotation `text`.' - ].join(' ') - }, - // arrow - showarrow: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the annotation is drawn with an arrow.', - 'If *true*, `text` is placed near the arrow\'s tail.', - 'If *false*, `text` lines up with the `x` and `y` provided.' - ].join(' ') - }, - arrowcolor: { - valType: 'color', - role: 'style', - description: 'Sets the color of the annotation arrow.' - }, - arrowhead: { - valType: 'integer', - min: 0, - max: annotations.ARROWPATHS.length, - dflt: 1, - role: 'style', - description: 'Sets the annotation arrow head style.' - }, - arrowsize: { - valType: 'number', - min: 0.3, - dflt: 1, - role: 'style', - description: 'Sets the size (in px) of annotation arrow head.' - }, - arrowwidth: { - valType: 'number', - min: 0.1, - role: 'style', - description: 'Sets the width (in px) of annotation arrow.' - }, - ax: { - valType: 'number', - dflt: -10, - role: 'info', - description: [ - 'Sets the x component of the arrow tail about the arrow head.', - 'A positive (negative) component corresponds to an arrow pointing', - 'from right to left (left to right)' - ].join(' ') - }, - ay: { - valType: 'number', - dflt: -30, - role: 'info', - description: [ - 'Sets the y component of the arrow tail about the arrow head.', - 'A positive (negative) component corresponds to an arrow pointing', - 'from bottom to top (top to bottom)' - ].join(' ') - }, - // positioning - xref: { - valType: 'enumerated', - values: [ - 'paper', - Plotly.Plots.subplotsRegistry.cartesian.idRegex.x.toString() - ], - role: 'info', - description: [ - 'Sets the annotation\'s x coordinate axis.', - 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left side of the plotting area in normalized coordinates', - 'where 0 (1) corresponds to the left (right) side.' - ].join(' ') - }, - x: { - valType: 'number', - role: 'info', - description: [ - 'Sets the annotation\'s x position.', - 'Note that dates and categories are converted to numbers.' - ].join(' ') - }, - xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'auto', - role: 'info', - description: [ - 'Sets the annotation\'s horizontal position anchor', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the annotation.', - 'For example, if `x` is set to 1, `xref` to *paper* and', - '`xanchor` to *right* then the right-most portion of the', - 'annotation lines up with the right-most edge of the', - 'plotting area.', - 'If *auto*, the anchor is equivalent to *center* for', - 'data-referenced annotations', - 'whereas for paper-referenced, the anchor picked corresponds', - 'to the closest side.' - ].join(' ') - }, - yref: { - valType: 'enumerated', - values: [ - 'paper', - Plotly.Plots.subplotsRegistry.cartesian.idRegex.y.toString() - ], - role: 'info', - description: [ - 'Sets the annotation\'s y coordinate axis.', - 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to an y coordinate', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plotting area in normalized coordinates', - 'where 0 (1) corresponds to the bottom (top).' - ].join(' ') - }, - y: { - valType: 'number', - role: 'info', - description: [ - 'Sets the annotation\'s y position.', - 'Note that dates and categories are converted to numbers.' - ].join(' ') - }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'auto', - role: 'info', - description: [ - 'Sets the annotation\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the annotation.', - 'For example, if `y` is set to 1, `yref` to *paper* and', - '`yanchor` to *top* then the top-most portion of the', - 'annotation lines up with the top-most edge of the', - 'plotting area.', - 'If *auto*, the anchor is equivalent to *middle* for', - 'data-referenced annotations', - 'whereas for paper-referenced, the anchor picked corresponds', - 'to the closest side.' - ].join(' ') - }, - - _deprecated: { - ref: { - valType: 'string', - role: 'info', - description: [ - 'Obsolete. Set `xref` and `yref` separately instead.' - ].join(' ') - } - } -}; +annotations.ARROWPATHS = require('./arrow_paths'); + +annotations.layoutAttributes = require('./attributes'); annotations.supplyLayoutDefaults = function(layoutIn, layoutOut) { var containerIn = layoutIn.annotations || [], @@ -302,9 +23,7 @@ function handleAnnotationDefaults(annIn, fullLayout) { var annOut = {}; function coerce(attr, dflt) { - return Plotly.Lib.coerce(annIn, annOut, - annotations.layoutAttributes, - attr, dflt); + return Plotly.Lib.coerce(annIn, annOut, annotations.layoutAttributes, attr, dflt); } coerce('opacity'); diff --git a/src/components/color/attributes.js b/src/components/color/attributes.js new file mode 100644 index 00000000000..dac67382220 --- /dev/null +++ b/src/components/color/attributes.js @@ -0,0 +1,19 @@ +// IMPORTANT - default colors should be in hex for compatibility +exports.defaults = [ + '#1f77b4', // muted blue + '#ff7f0e', // safety orange + '#2ca02c', // cooked asparagus green + '#d62728', // brick red + '#9467bd', // muted purple + '#8c564b', // chestnut brown + '#e377c2', // raspberry yogurt pink + '#7f7f7f', // middle gray + '#bcbd22', // curry yellow-green + '#17becf' // blue-teal +]; + +exports.defaultLine = '#444'; + +exports.lightLine = '#eee'; + +exports.background = '#fff'; diff --git a/src/color.js b/src/components/color/index.js similarity index 89% rename from src/color.js rename to src/components/color/index.js index 4e7780b6629..2f4dfac805d 100644 --- a/src/color.js +++ b/src/components/color/index.js @@ -5,23 +5,11 @@ var isNumeric = require('fast-isnumeric'); var color = module.exports = {}; -// IMPORTANT - default colors should be in hex for grid.js -color.defaults = [ - '#1f77b4', // muted blue - '#ff7f0e', // safety orange - '#2ca02c', // cooked asparagus green - '#d62728', // brick red - '#9467bd', // muted purple - '#8c564b', // chestnut brown - '#e377c2', // raspberry yogurt pink - '#7f7f7f', // middle gray - '#bcbd22', // curry yellow-green - '#17becf' // blue-teal -]; - -color.defaultLine = '#444'; -color.lightLine = '#eee'; -color.background = '#fff'; +var colorAttrs = require('./attributes'); +color.defaults = colorAttrs.defaults; +color.defaultLine = colorAttrs.defaultLine; +color.lightLine = colorAttrs.lightLine; +color.background = colorAttrs.background; color.tinyRGB = function(tc) { var c = tc.toRgb(); diff --git a/src/components/colorbar/attributes.js b/src/components/colorbar/attributes.js new file mode 100644 index 00000000000..0dd0b65f01d --- /dev/null +++ b/src/components/colorbar/attributes.js @@ -0,0 +1,182 @@ +var axesAttrs = require('../../plots/cartesian/layout_attributes'); +var fontAttrs = require('../../plots/font_attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +module.exports = { +// TODO: only right is supported currently +// orient: { +// valType: 'enumerated', +// role: 'info', +// values: ['left', 'right', 'top', 'bottom'], +// dflt: 'right', +// description: [ +// 'Determines which side are the labels on', +// '(so left and right make vertical bars, etc.)' +// ].join(' ') +// }, + thicknessmode: { + valType: 'enumerated', + values: ['fraction', 'pixels'], + role: 'style', + dflt: 'pixels', + description: [ + 'Determines whether this color bar\'s thickness', + '(i.e. the measure in the constant color direction)', + 'is set in units of plot *fraction* or in *pixels*.', + 'Use `thickness` to set the value.' + ].join(' ') + }, + thickness: { + valType: 'number', + role: 'style', + min: 0, + dflt: 30, + description: [ + 'Sets the thickness of the color bar', + 'This measure excludes the size of the padding, ticks and labels.' + ].join(' ') + }, + lenmode: { + valType: 'enumerated', + values: ['fraction', 'pixels'], + role: 'info', + dflt: 'fraction', + description: [ + 'Determines whether this color bar\'s length', + '(i.e. the measure in the color variation direction)', + 'is set in units of plot *fraction* or in *pixels.', + 'Use `len` to set the value.' + ].join(' ') + }, + len: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: [ + 'Sets the length of the color bar', + 'This measure excludes the padding of both ends.', + 'That is, the color bar length is this length minus the', + 'padding on both ends.' + ].join(' ') + }, + x: { + valType: 'number', + dflt: 1.02, + min: -2, + max: 3, + role: 'style', + description: [ + 'Sets the x position of the color bar (in plot fraction).' + ].join(' ') + }, + xanchor: { + valType: 'enumerated', + values: ['left', 'center', 'right'], + dflt: 'left', + role: 'style', + description: [ + 'Sets this color bar\'s horizontal position anchor.', + 'This anchor binds the `x` position to the *left*, *center*', + 'or *right* of the color bar.' + ].join(' ') + }, + xpad: { + valType: 'number', + role: 'style', + min: 0, + dflt: 10, + description: 'Sets the amount of padding (in px) along the x direction.' + }, + y: { + valType: 'number', + role: 'style', + dflt: 0.5, + min: -2, + max: 3, + description: [ + 'Sets the y position of the color bar (in plot fraction).' + ].join(' ') + }, + yanchor: { + valType: 'enumerated', + values: ['top', 'middle', 'bottom'], + role: 'style', + dflt: 'middle', + description: [ + 'Sets this color bar\'s vertical position anchor', + 'This anchor binds the `y` position to the *top*, *middle*', + 'or *bottom* of the color bar.' + ].join(' ') + }, + ypad: { + valType: 'number', + role: 'style', + min: 0, + dflt: 10, + description: 'Sets the amount of padding (in px) along the y direction.' + }, + // a possible line around the bar itself + outlinecolor: axesAttrs.linecolor, + outlinewidth: axesAttrs.linewidth, + // Should outlinewidth have {dflt: 0} ? + // another possible line outside the padding and tick labels + bordercolor: axesAttrs.linecolor, + borderwidth: { + valType: 'number', + role: 'style', + min: 0, + dflt: 0, + description: [ + 'Sets the width (in px) or the border enclosing this color bar.' + ].join(' ') + }, + bgcolor: { + valType: 'color', + role: 'style', + dflt: 'rgba(0,0,0,0)', + description: 'Sets the color of padded area.' + }, + // tick and title properties named and function exactly as in axes + tickmode: axesAttrs.tickmode, + nticks: axesAttrs.nticks, + tick0: axesAttrs.tick0, + dtick: axesAttrs.dtick, + tickvals: axesAttrs.tickvals, + ticktext: axesAttrs.ticktext, + ticks: extendFlat({}, axesAttrs.ticks, {dflt: ''}), + ticklen: axesAttrs.ticklen, + tickwidth: axesAttrs.tickwidth, + tickcolor: axesAttrs.tickcolor, + showticklabels: axesAttrs.showticklabels, + tickfont: axesAttrs.tickfont, + tickangle: axesAttrs.tickangle, + tickformat: axesAttrs.tickformat, + tickprefix: axesAttrs.tickprefix, + showtickprefix: axesAttrs.showtickprefix, + ticksuffix: axesAttrs.ticksuffix, + showticksuffix: axesAttrs.showticksuffix, + exponentformat: axesAttrs.exponentformat, + showexponent: axesAttrs.showexponent, + title: { + valType: 'string', + role: 'info', + dflt: 'Click to enter colorscale title', + description: 'Sets the title of the color bar.' + }, + titlefont: extendFlat({}, fontAttrs, { + description: [ + 'Sets this color bar\'s title font.' + ].join(' ') + }), + titleside: { + valType: 'enumerated', + values: ['right', 'top', 'bottom'], + role: 'style', + dflt: 'top', + description: [ + 'Determines the location of the colorbar title', + 'with respect to the color bar.' + ].join(' ') + } +}; diff --git a/src/colorbar.js b/src/components/colorbar/index.js similarity index 76% rename from src/colorbar.js rename to src/components/colorbar/index.js index 74d14df5557..0f6073ead96 100644 --- a/src/colorbar.js +++ b/src/components/colorbar/index.js @@ -1,9 +1,10 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); + var colorbar = module.exports = function(td, id) { // opts: options object, containing everything from attributes // plus a few others that are the equivalent of the colorbar "data" @@ -238,7 +239,7 @@ var colorbar = module.exports = function(td, id) { if(['top','bottom'].indexOf(opts.titleside)!==-1) { // draw the title so we know how much room it needs // when we squish the axis - Plotly.Plots.titles(td, cbAxisOut._id + 'title'); + Plotly.Titles.draw(td, cbAxisOut._id + 'title'); } function drawAxis(){ @@ -519,187 +520,7 @@ var colorbar = module.exports = function(td, id) { return component; }; -var axesAttrs = Plotly.Axes.layoutAttributes, - extendFlat = Plotly.Lib.extendFlat; - -colorbar.attributes = { -// TODO: only right is supported currently -// orient: { -// valType: 'enumerated', -// role: 'info', -// values: ['left', 'right', 'top', 'bottom'], -// dflt: 'right', -// description: [ -// 'Determines which side are the labels on', -// '(so left and right make vertical bars, etc.)' -// ].join(' ') -// }, - thicknessmode: { - valType: 'enumerated', - values: ['fraction', 'pixels'], - role: 'style', - dflt: 'pixels', - description: [ - 'Determines whether this color bar\'s thickness', - '(i.e. the measure in the constant color direction)', - 'is set in units of plot *fraction* or in *pixels*.', - 'Use `thickness` to set the value.' - ].join(' ') - }, - thickness: { - valType: 'number', - role: 'style', - min: 0, - dflt: 30, - description: [ - 'Sets the thickness of the color bar', - 'This measure excludes the size of the padding, ticks and labels.' - ].join(' ') - }, - lenmode: { - valType: 'enumerated', - values: ['fraction', 'pixels'], - role: 'info', - dflt: 'fraction', - description: [ - 'Determines whether this color bar\'s length', - '(i.e. the measure in the color variation direction)', - 'is set in units of plot *fraction* or in *pixels.', - 'Use `len` to set the value.' - ].join(' ') - }, - len: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the length of the color bar', - 'This measure excludes the padding of both ends.', - 'That is, the color bar length is this length minus the', - 'padding on both ends.' - ].join(' ') - }, - x: { - valType: 'number', - dflt: 1.02, - min: -2, - max: 3, - role: 'style', - description: [ - 'Sets the x position of the color bar (in plot fraction).' - ].join(' ') - }, - xanchor: { - valType: 'enumerated', - values: ['left', 'center', 'right'], - dflt: 'left', - role: 'style', - description: [ - 'Sets this color bar\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the color bar.' - ].join(' ') - }, - xpad: { - valType: 'number', - role: 'style', - min: 0, - dflt: 10, - description: 'Sets the amount of padding (in px) along the x direction.' - }, - y: { - valType: 'number', - role: 'style', - dflt: 0.5, - min: -2, - max: 3, - description: [ - 'Sets the y position of the color bar (in plot fraction).' - ].join(' ') - }, - yanchor: { - valType: 'enumerated', - values: ['top', 'middle', 'bottom'], - role: 'style', - dflt: 'middle', - description: [ - 'Sets this color bar\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the color bar.' - ].join(' ') - }, - ypad: { - valType: 'number', - role: 'style', - min: 0, - dflt: 10, - description: 'Sets the amount of padding (in px) along the y direction.' - }, - // a possible line around the bar itself - outlinecolor: axesAttrs.linecolor, - outlinewidth: axesAttrs.linewidth, - // Should outlinewidth have {dflt: 0} ? - // another possible line outside the padding and tick labels - bordercolor: axesAttrs.linecolor, - borderwidth: { - valType: 'number', - role: 'style', - min: 0, - dflt: 0, - description: [ - 'Sets the width (in px) or the border enclosing this color bar.' - ].join(' ') - }, - bgcolor: { - valType: 'color', - role: 'style', - dflt: 'rgba(0,0,0,0)', - description: 'Sets the color of padded area.' - }, - // tick and title properties named and function exactly as in axes - tickmode: axesAttrs.tickmode, - nticks: axesAttrs.nticks, - tick0: axesAttrs.tick0, - dtick: axesAttrs.dtick, - tickvals: axesAttrs.tickvals, - ticktext: axesAttrs.ticktext, - ticks: extendFlat({}, axesAttrs.ticks, {dflt: ''}), - ticklen: axesAttrs.ticklen, - tickwidth: axesAttrs.tickwidth, - tickcolor: axesAttrs.tickcolor, - showticklabels: axesAttrs.showticklabels, - tickfont: axesAttrs.tickfont, - tickangle: axesAttrs.tickangle, - tickformat: axesAttrs.tickformat, - tickprefix: axesAttrs.tickprefix, - showtickprefix: axesAttrs.showtickprefix, - ticksuffix: axesAttrs.ticksuffix, - showticksuffix: axesAttrs.showticksuffix, - exponentformat: axesAttrs.exponentformat, - showexponent: axesAttrs.showexponent, - title: { - valType: 'string', - role: 'info', - dflt: 'Click to enter colorscale title', - description: 'Sets the title of the color bar.' - }, - titlefont: extendFlat({}, Plotly.Plots.fontAttrs, { - description: [ - 'Sets this color bar\'s title font.' - ].join(' ') - }), - titleside: { - valType: 'enumerated', - values: ['right', 'top', 'bottom'], - role: 'style', - dflt: 'top', - description: [ - 'Determines the location of the colorbar title', - 'with respect to the color bar.' - ].join(' ') - } -}; +colorbar.attributes = require('./attributes'); colorbar.supplyDefaults = function(containerIn, containerOut, layout) { var colorbarOut = containerOut.colorbar = {}, @@ -771,68 +592,3 @@ colorbar.traceColorbar = function(gd, cd) { Plotly.Lib.markTime('done colorbar'); }; - -colorbar.traceColorbarAttributes = { - zauto: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines the whether or not the color domain is computed', - 'with respect to the input data.' - ].join(' ') - }, - zmin: { - valType: 'number', - role: 'info', - dflt: null, - description: 'Sets the lower bound of color domain.' - }, - zmax: { - valType: 'number', - role: 'info', - dflt: null, - description: 'Sets the upper bound of color domain.' - }, - colorscale: { - valType: 'colorscale', - role: 'style', - description: 'Sets the colorscale.' - }, - autocolorscale: { - valType: 'boolean', - role: 'style', - dflt: true, // gets overrode in 'heatmap' & 'surface' for backwards comp. - description: [ - 'Determines whether or not the colorscale is picked using the sign of', - 'the input z values.' - ].join(' ') - }, - reversescale: { - valType: 'boolean', - role: 'style', - dflt: false, - description: 'Reverses the colorscale.' - }, - showscale: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not a colorbar is displayed for this trace.' - ].join(' ') - }, - - _deprecated: { - scl: { - valType: 'colorscale', - role: 'style', - description: 'Renamed to `colorscale`.' - }, - reversescl: { - valType: 'boolean', - role: 'style', - description: 'Renamed to `reversescale`.' - } - } -}; diff --git a/src/components/colorscale/attributes.js b/src/components/colorscale/attributes.js new file mode 100644 index 00000000000..61def16d1bc --- /dev/null +++ b/src/components/colorscale/attributes.js @@ -0,0 +1,64 @@ +module.exports = { + zauto: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines the whether or not the color domain is computed', + 'with respect to the input data.' + ].join(' ') + }, + zmin: { + valType: 'number', + role: 'info', + dflt: null, + description: 'Sets the lower bound of color domain.' + }, + zmax: { + valType: 'number', + role: 'info', + dflt: null, + description: 'Sets the upper bound of color domain.' + }, + colorscale: { + valType: 'colorscale', + role: 'style', + description: 'Sets the colorscale.' + }, + autocolorscale: { + valType: 'boolean', + role: 'style', + dflt: true, // gets overrode in 'heatmap' & 'surface' for backwards comp. + description: [ + 'Determines whether or not the colorscale is picked using the sign of', + 'the input z values.' + ].join(' ') + }, + reversescale: { + valType: 'boolean', + role: 'style', + dflt: false, + description: 'Reverses the colorscale.' + }, + showscale: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether or not a colorbar is displayed for this trace.' + ].join(' ') + }, + + _deprecated: { + scl: { + valType: 'colorscale', + role: 'style', + description: 'Renamed to `colorscale`.' + }, + reversescl: { + valType: 'boolean', + role: 'style', + description: 'Renamed to `reversescale`.' + } + } +}; diff --git a/src/colorscale.js b/src/components/colorscale/index.js similarity index 62% rename from src/colorscale.js rename to src/components/colorscale/index.js index 4f0aa7c06ea..48f213c5e1a 100644 --- a/src/colorscale.js +++ b/src/components/colorscale/index.js @@ -1,97 +1,17 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var tinycolor = require('tinycolor2'); var isNumeric = require('fast-isnumeric'); var colorscale = module.exports = {}; -colorscale.scales = { - 'Greys':[[0,'rgb(0,0,0)'],[1,'rgb(255,255,255)']], - - 'YIGnBu':[[0,'rgb(8, 29, 88)'],[0.125,'rgb(37, 52, 148)'], - [0.25,'rgb(34, 94, 168)'],[0.375,'rgb(29, 145, 192)'], - [0.5,'rgb(65, 182, 196)'],[0.625,'rgb(127, 205, 187)'], - [0.75,'rgb(199, 233, 180)'],[0.875,'rgb(237, 248, 217)'], - [1,'rgb(255, 255, 217)']], - - 'Greens':[[0,'rgb(0, 68, 27)'],[0.125,'rgb(0, 109, 44)'], - [0.25,'rgb(35, 139, 69)'],[0.375,'rgb(65, 171, 93)'], - [0.5,'rgb(116, 196, 118)'],[0.625,'rgb(161, 217, 155)'], - [0.75,'rgb(199, 233, 192)'],[0.875,'rgb(229, 245, 224)'], - [1,'rgb(247, 252, 245)']], - - 'YIOrRd':[[0,'rgb(128, 0, 38)'],[0.125,'rgb(189, 0, 38)'], - [0.25,'rgb(227, 26, 28)'],[0.375,'rgb(252, 78, 42)'], - [0.5,'rgb(253, 141, 60)'],[0.625,'rgb(254, 178, 76)'], - [0.75,'rgb(254, 217, 118)'],[0.875,'rgb(255, 237, 160)'], - [1,'rgb(255, 255, 204)']], - - 'Bluered':[[0,'rgb(0,0,255)'],[1,'rgb(255,0,0)']], - - // modified RdBu based on - // www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf - 'RdBu':[[0,'rgb(5, 10, 172)'],[0.35,'rgb(106, 137, 247)'], - [0.5,'rgb(190,190,190)'],[0.6,'rgb(220, 170, 132)'], - [0.7,'rgb(230, 145, 90)'],[1,'rgb(178, 10, 28)']], - // Scale for non-negative numeric values - 'Reds': [[0, 'rgb(220, 220, 220)'], [0.2,'rgb(245, 195, 157)'], - [0.4,'rgb(245, 160, 105)'], [1, 'rgb(178, 10, 28)']], - // Scale for non-positive numeric values - 'Blues': [[0, 'rgb(5, 10, 172)'], [0.35, 'rgb(40, 60, 190)'], - [0.5, 'rgb(70, 100, 245)'], [0.6, 'rgb(90, 120, 245)'], - [0.7, 'rgb(106, 137, 247)'], [1, 'rgb(220, 220, 220)']], - - 'Picnic':[[0,'rgb(0,0,255)'],[0.1,'rgb(51,153,255)'], - [0.2,'rgb(102,204,255)'],[0.3,'rgb(153,204,255)'], - [0.4,'rgb(204,204,255)'],[0.5,'rgb(255,255,255)'], - [0.6,'rgb(255,204,255)'],[0.7,'rgb(255,153,255)'], - [0.8,'rgb(255,102,204)'],[0.9,'rgb(255,102,102)'], - [1,'rgb(255,0,0)']], - - 'Rainbow':[[0,'rgb(150,0,90)'],[0.125,'rgb(0, 0, 200)'], - [0.25,'rgb(0, 25, 255)'],[0.375,'rgb(0, 152, 255)'], - [0.5,'rgb(44, 255, 150)'],[0.625,'rgb(151, 255, 0)'], - [0.75,'rgb(255, 234, 0)'],[0.875,'rgb(255, 111, 0)'], - [1,'rgb(255, 0, 0)']], - - 'Portland':[[0,'rgb(12,51,131)'],[0.25,'rgb(10,136,186)'], - [0.5,'rgb(242,211,56)'],[0.75,'rgb(242,143,56)'], - [1,'rgb(217,30,30)']], - - 'Jet':[[0,'rgb(0,0,131)'],[0.125,'rgb(0,60,170)'], - [0.375,'rgb(5,255,255)'],[0.625,'rgb(255,255,0)'], - [0.875,'rgb(250,0,0)'],[1,'rgb(128,0,0)']], - - 'Hot':[[0,'rgb(0,0,0)'],[0.3,'rgb(230,0,0)'], - [0.6,'rgb(255,210,0)'],[1,'rgb(255,255,255)']], - - 'Blackbody':[[0,'rgb(0,0,0)'],[0.2,'rgb(230,0,0)'], - [0.4,'rgb(230,210,0)'],[0.7,'rgb(255,255,255)'], - [1,'rgb(160,200,255)']], - - 'Earth':[[0,'rgb(0,0,130)'],[0.1,'rgb(0,180,180)'], - [0.2,'rgb(40,210,40)'],[0.4,'rgb(230,230,50)'], - [0.6,'rgb(120,70,20)'],[1,'rgb(255,255,255)']], - - 'Electric':[[0,'rgb(0,0,0)'],[0.15,'rgb(30,0,100)'], - [0.4,'rgb(120,0,100)'],[0.6,'rgb(160,90,0)'], - [0.8,'rgb(230,200,0)'],[1,'rgb(255,250,220)']], - - 'Viridis': [[0,'#440154'],[0.06274509803921569,'#48186a'], - [0.12549019607843137,'#472d7b'],[0.18823529411764706,'#424086'], - [0.25098039215686274,'#3b528b'],[0.3137254901960784,'#33638d'], - [0.3764705882352941,'#2c728e'],[0.4392156862745098,'#26828e'], - [0.5019607843137255,'#21918c'],[0.5647058823529412,'#1fa088'], - [0.6274509803921569,'#28ae80'],[0.6901960784313725,'#3fbc73'], - [0.7529411764705882,'#5ec962'],[0.8156862745098039,'#84d44b'], - [0.8784313725490196,'#addc30'],[0.9411764705882353,'#d8e219'], - [1,'#fde725']] -}; - +colorscale.scales = require('./scales'); colorscale.defaultScale = colorscale.scales.RdBu; +colorscale.attributes = require('./attributes'); + function isValidScaleArray(scl) { var isValid = true, highestVal = 0, diff --git a/src/components/colorscale/scales.js b/src/components/colorscale/scales.js new file mode 100644 index 00000000000..c5163c91a62 --- /dev/null +++ b/src/components/colorscale/scales.js @@ -0,0 +1,82 @@ +module.exports = { + 'Greys':[[0,'rgb(0,0,0)'],[1,'rgb(255,255,255)']], + + 'YIGnBu':[[0,'rgb(8, 29, 88)'],[0.125,'rgb(37, 52, 148)'], + [0.25,'rgb(34, 94, 168)'],[0.375,'rgb(29, 145, 192)'], + [0.5,'rgb(65, 182, 196)'],[0.625,'rgb(127, 205, 187)'], + [0.75,'rgb(199, 233, 180)'],[0.875,'rgb(237, 248, 217)'], + [1,'rgb(255, 255, 217)']], + + 'Greens':[[0,'rgb(0, 68, 27)'],[0.125,'rgb(0, 109, 44)'], + [0.25,'rgb(35, 139, 69)'],[0.375,'rgb(65, 171, 93)'], + [0.5,'rgb(116, 196, 118)'],[0.625,'rgb(161, 217, 155)'], + [0.75,'rgb(199, 233, 192)'],[0.875,'rgb(229, 245, 224)'], + [1,'rgb(247, 252, 245)']], + + 'YIOrRd':[[0,'rgb(128, 0, 38)'],[0.125,'rgb(189, 0, 38)'], + [0.25,'rgb(227, 26, 28)'],[0.375,'rgb(252, 78, 42)'], + [0.5,'rgb(253, 141, 60)'],[0.625,'rgb(254, 178, 76)'], + [0.75,'rgb(254, 217, 118)'],[0.875,'rgb(255, 237, 160)'], + [1,'rgb(255, 255, 204)']], + + 'Bluered':[[0,'rgb(0,0,255)'],[1,'rgb(255,0,0)']], + + // modified RdBu based on + // www.sandia.gov/~kmorel/documents/ColorMaps/ColorMapsExpanded.pdf + 'RdBu':[[0,'rgb(5, 10, 172)'],[0.35,'rgb(106, 137, 247)'], + [0.5,'rgb(190,190,190)'],[0.6,'rgb(220, 170, 132)'], + [0.7,'rgb(230, 145, 90)'],[1,'rgb(178, 10, 28)']], + // Scale for non-negative numeric values + 'Reds': [[0, 'rgb(220, 220, 220)'], [0.2,'rgb(245, 195, 157)'], + [0.4,'rgb(245, 160, 105)'], [1, 'rgb(178, 10, 28)']], + // Scale for non-positive numeric values + 'Blues': [[0, 'rgb(5, 10, 172)'], [0.35, 'rgb(40, 60, 190)'], + [0.5, 'rgb(70, 100, 245)'], [0.6, 'rgb(90, 120, 245)'], + [0.7, 'rgb(106, 137, 247)'], [1, 'rgb(220, 220, 220)']], + + 'Picnic':[[0,'rgb(0,0,255)'],[0.1,'rgb(51,153,255)'], + [0.2,'rgb(102,204,255)'],[0.3,'rgb(153,204,255)'], + [0.4,'rgb(204,204,255)'],[0.5,'rgb(255,255,255)'], + [0.6,'rgb(255,204,255)'],[0.7,'rgb(255,153,255)'], + [0.8,'rgb(255,102,204)'],[0.9,'rgb(255,102,102)'], + [1,'rgb(255,0,0)']], + + 'Rainbow':[[0,'rgb(150,0,90)'],[0.125,'rgb(0, 0, 200)'], + [0.25,'rgb(0, 25, 255)'],[0.375,'rgb(0, 152, 255)'], + [0.5,'rgb(44, 255, 150)'],[0.625,'rgb(151, 255, 0)'], + [0.75,'rgb(255, 234, 0)'],[0.875,'rgb(255, 111, 0)'], + [1,'rgb(255, 0, 0)']], + + 'Portland':[[0,'rgb(12,51,131)'],[0.25,'rgb(10,136,186)'], + [0.5,'rgb(242,211,56)'],[0.75,'rgb(242,143,56)'], + [1,'rgb(217,30,30)']], + + 'Jet':[[0,'rgb(0,0,131)'],[0.125,'rgb(0,60,170)'], + [0.375,'rgb(5,255,255)'],[0.625,'rgb(255,255,0)'], + [0.875,'rgb(250,0,0)'],[1,'rgb(128,0,0)']], + + 'Hot':[[0,'rgb(0,0,0)'],[0.3,'rgb(230,0,0)'], + [0.6,'rgb(255,210,0)'],[1,'rgb(255,255,255)']], + + 'Blackbody':[[0,'rgb(0,0,0)'],[0.2,'rgb(230,0,0)'], + [0.4,'rgb(230,210,0)'],[0.7,'rgb(255,255,255)'], + [1,'rgb(160,200,255)']], + + 'Earth':[[0,'rgb(0,0,130)'],[0.1,'rgb(0,180,180)'], + [0.2,'rgb(40,210,40)'],[0.4,'rgb(230,230,50)'], + [0.6,'rgb(120,70,20)'],[1,'rgb(255,255,255)']], + + 'Electric':[[0,'rgb(0,0,0)'],[0.15,'rgb(30,0,100)'], + [0.4,'rgb(120,0,100)'],[0.6,'rgb(160,90,0)'], + [0.8,'rgb(230,200,0)'],[1,'rgb(255,250,220)']], + + 'Viridis': [[0,'#440154'],[0.06274509803921569,'#48186a'], + [0.12549019607843137,'#472d7b'],[0.18823529411764706,'#424086'], + [0.25098039215686274,'#3b528b'],[0.3137254901960784,'#33638d'], + [0.3764705882352941,'#2c728e'],[0.4392156862745098,'#26828e'], + [0.5019607843137255,'#21918c'],[0.5647058823529412,'#1fa088'], + [0.6274509803921569,'#28ae80'],[0.6901960784313725,'#3fbc73'], + [0.7529411764705882,'#5ec962'],[0.8156862745098039,'#84d44b'], + [0.8784313725490196,'#addc30'],[0.9411764705882353,'#d8e219'], + [1,'#fde725']] +}; diff --git a/src/drawing.js b/src/components/drawing/index.js similarity index 57% rename from src/drawing.js rename to src/components/drawing/index.js index 1cd1526be04..1b17ca6486c 100644 --- a/src/drawing.js +++ b/src/components/drawing/index.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); @@ -110,471 +110,14 @@ drawing.fillGroupStyle = function(s) { }); }; -// marker symbol definitions -// users can specify markers either by number or name -// add 100 (or '-open') and you get an open marker -// open markers have no fill and use line color as the stroke color -// add 200 (or '-dot') and you get a dot in the middle -// add both and you get both -var SYMBOLDEFS = { - circle: { - n: 0, - f: function(r) { - var rs = d3.round(r,2); - return 'M'+rs+',0A'+rs+','+rs+' 0 1,1 0,-'+rs+ - 'A'+rs+','+rs+' 0 0,1 '+rs+',0Z'; - } - }, - square: { - n: 1, - f: function(r) { - var rs = d3.round(r,2); - return 'M'+rs+','+rs+'H-'+rs+'V-'+rs+'H'+rs+'Z'; - } - }, - diamond: { - n: 2, - f: function(r) { - var rd = d3.round(r*1.3,2); - return 'M'+rd+',0L0,'+rd+'L-'+rd+',0L0,-'+rd+'Z'; - } - }, - cross: { - n: 3, - f: function(r) { - var rc = d3.round(r*0.4,2), - rc2 = d3.round(r*1.2,2); - return 'M'+rc2+','+rc+'H'+rc+'V'+rc2+'H-'+rc+ - 'V'+rc+'H-'+rc2+'V-'+rc+'H-'+rc+'V-'+rc2+ - 'H'+rc+'V-'+rc+'H'+rc2+'Z'; - } - }, - x: { - n: 4, - f: function(r) { - var rx = d3.round(r*0.8/Math.sqrt(2),2), - ne = 'l'+rx+','+rx, - se = 'l'+rx+',-'+rx, - sw = 'l-'+rx+',-'+rx, - nw = 'l-'+rx+','+rx; - return 'M0,'+rx+ne+se+sw+se+sw+nw+sw+nw+ne+nw+ne+'Z'; - } - }, - 'triangle-up': { - n: 5, - f: function(r) { - var rt = d3.round(r*2/Math.sqrt(3),2), - r2 = d3.round(r/2,2), - rs = d3.round(r,2); - return 'M-'+rt+','+r2+'H'+rt+'L0,-'+rs+'Z'; - } - }, - 'triangle-down': { - n: 6, - f: function(r) { - var rt = d3.round(r*2/Math.sqrt(3),2), - r2 = d3.round(r/2,2), - rs = d3.round(r,2); - return 'M-'+rt+',-'+r2+'H'+rt+'L0,'+rs+'Z'; - } - }, - 'triangle-left': { - n: 7, - f: function(r) { - var rt = d3.round(r*2/Math.sqrt(3),2), - r2 = d3.round(r/2,2), - rs = d3.round(r,2); - return 'M'+r2+',-'+rt+'V'+rt+'L-'+rs+',0Z'; - } - }, - 'triangle-right': { - n: 8, - f: function(r) { - var rt = d3.round(r*2/Math.sqrt(3),2), - r2 = d3.round(r/2,2), - rs = d3.round(r,2); - return 'M-'+r2+',-'+rt+'V'+rt+'L'+rs+',0Z'; - } - }, - 'triangle-ne': { - n: 9, - f: function(r) { - var r1 = d3.round(r*0.6,2), - r2 = d3.round(r*1.2,2); - return 'M-'+r2+',-'+r1+'H'+r1+'V'+r2+'Z'; - } - }, - 'triangle-se': { - n: 10, - f: function(r) { - var r1 = d3.round(r*0.6,2), - r2 = d3.round(r*1.2,2); - return 'M'+r1+',-'+r2+'V'+r1+'H-'+r2+'Z'; - } - }, - 'triangle-sw': { - n: 11, - f: function(r) { - var r1 = d3.round(r*0.6,2), - r2 = d3.round(r*1.2,2); - return 'M'+r2+','+r1+'H-'+r1+'V-'+r2+'Z'; - } - }, - 'triangle-nw': { - n: 12, - f: function(r) { - var r1 = d3.round(r*0.6,2), - r2 = d3.round(r*1.2,2); - return 'M-'+r1+','+r2+'V-'+r1+'H'+r2+'Z'; - } - }, - pentagon: { - n: 13, - f: function(r) { - var x1 = d3.round(r*0.951,2), - x2 = d3.round(r*0.588,2), - y0 = d3.round(-r,2), - y1 = d3.round(r*-0.309,2), - y2 = d3.round(r*0.809,2); - return 'M'+x1+','+y1+'L'+x2+','+y2+'H-'+x2+ - 'L-'+x1+','+y1+'L0,'+y0+'Z'; - } - }, - hexagon: { - n: 14, - f: function(r) { - var y0 = d3.round(r,2), - y1 = d3.round(r/2,2), - x = d3.round(r*Math.sqrt(3)/2,2); - return 'M'+x+',-'+y1+'V'+y1+'L0,'+y0+ - 'L-'+x+','+y1+'V-'+y1+'L0,-'+y0+'Z'; - } - }, - hexagon2: { - n: 15, - f: function(r) { - var x0 = d3.round(r,2), - x1 = d3.round(r/2,2), - y = d3.round(r*Math.sqrt(3)/2,2); - return 'M-'+x1+','+y+'H'+x1+'L'+x0+ - ',0L'+x1+',-'+y+'H-'+x1+'L-'+x0+',0Z'; - } - }, - octagon: { - n: 16, - f: function(r) { - var a = d3.round(r*0.924,2), - b = d3.round(r*0.383,2); - return 'M-'+b+',-'+a+'H'+b+'L'+a+',-'+b+'V'+b+ - 'L'+b+','+a+'H-'+b+'L-'+a+','+b+'V-'+b+'Z'; - } - }, - star: { - n: 17, - f: function(r) { - var rs = r*1.4, - x1 = d3.round(rs*0.225,2), - x2 = d3.round(rs*0.951,2), - x3 = d3.round(rs*0.363,2), - x4 = d3.round(rs*0.588,2), - y0 = d3.round(-rs,2), - y1 = d3.round(rs*-0.309,2), - y3 = d3.round(rs*0.118,2), - y4 = d3.round(rs*0.809,2), - y5 = d3.round(rs*0.382,2); - return 'M'+x1+','+y1+'H'+x2+'L'+x3+','+y3+ - 'L'+x4+','+y4+'L0,'+y5+'L-'+x4+','+y4+ - 'L-'+x3+','+y3+'L-'+x2+','+y1+'H-'+x1+ - 'L0,'+y0+'Z'; - } - }, - hexagram: { - n: 18, - f: function(r) { - var y = d3.round(r*0.66,2), - x1 = d3.round(r*0.38,2), - x2 = d3.round(r*0.76,2); - return 'M-'+x2+',0l-'+x1+',-'+y+'h'+x2+ - 'l'+x1+',-'+y+'l'+x1+','+y+'h'+x2+ - 'l-'+x1+','+y+'l'+x1+','+y+'h-'+x2+ - 'l-'+x1+','+y+'l-'+x1+',-'+y+'h-'+x2+'Z'; - } - }, - 'star-triangle-up': { - n: 19, - f: function(r) { - var x = d3.round(r*Math.sqrt(3)*0.8,2), - y1 = d3.round(r*0.8,2), - y2 = d3.round(r*1.6,2), - rc = d3.round(r*4,2), - aPart = 'A '+rc+','+rc+' 0 0 1 '; - return 'M-'+x+','+y1+aPart+x+','+y1+ - aPart+'0,-'+y2+aPart+'-'+x+','+y1+'Z'; - } - }, - 'star-triangle-down': { - n: 20, - f: function(r) { - var x = d3.round(r*Math.sqrt(3)*0.8,2), - y1 = d3.round(r*0.8,2), - y2 = d3.round(r*1.6,2), - rc = d3.round(r*4,2), - aPart = 'A '+rc+','+rc+' 0 0 1 '; - return 'M'+x+',-'+y1+aPart+'-'+x+',-'+y1+ - aPart+'0,'+y2+aPart+x+',-'+y1+'Z'; - } - }, - 'star-square': { - n: 21, - f: function(r) { - var rp = d3.round(r*1.1,2), - rc = d3.round(r*2,2), - aPart = 'A '+rc+','+rc+' 0 0 1 '; - return 'M-'+rp+',-'+rp+aPart+'-'+rp+','+rp+ - aPart+rp+','+rp+aPart+rp+',-'+rp+ - aPart+'-'+rp+',-'+rp+'Z'; - } - }, - 'star-diamond': { - n: 22, - f: function(r) { - var rp = d3.round(r*1.4,2), - rc = d3.round(r*1.9,2), - aPart = 'A '+rc+','+rc+' 0 0 1 '; - return 'M-'+rp+',0'+aPart+'0,'+rp+ - aPart+rp+',0'+aPart+'0,-'+rp+ - aPart+'-'+rp+',0'+'Z'; - } - }, - 'diamond-tall': { - n: 23, - f: function(r) { - var x = d3.round(r*0.7,2), - y = d3.round(r*1.4,2); - return 'M0,'+y+'L'+x+',0L0,-'+y+'L-'+x+',0Z'; - } - }, - 'diamond-wide': { - n: 24, - f: function(r) { - var x = d3.round(r*1.4,2), - y = d3.round(r*0.7,2); - return 'M0,'+y+'L'+x+',0L0,-'+y+'L-'+x+',0Z'; - } - }, - hourglass: { - n: 25, - f: function(r) { - var rs = d3.round(r,2); - return 'M'+rs+','+rs+'H-'+rs+'L'+rs+',-'+rs+'H-'+rs+'Z'; - }, - noDot: true - }, - bowtie: { - n: 26, - f: function(r) { - var rs = d3.round(r,2); - return 'M'+rs+','+rs+'V-'+rs+'L-'+rs+','+rs+'V-'+rs+'Z'; - }, - noDot: true - }, - 'circle-cross': { - n: 27, - f: function(r) { - var rs = d3.round(r,2); - return 'M0,'+rs+'V-'+rs+'M'+rs+',0H-'+rs+ - 'M'+rs+',0A'+rs+','+rs+' 0 1,1 0,-'+rs+ - 'A'+rs+','+rs+' 0 0,1 '+rs+',0Z'; - }, - needLine: true, - noDot: true - }, - 'circle-x': { - n: 28, - f: function(r) { - var rs = d3.round(r,2), - rc = d3.round(r/Math.sqrt(2),2); - return 'M'+rc+','+rc+'L-'+rc+',-'+rc+ - 'M'+rc+',-'+rc+'L-'+rc+','+rc+ - 'M'+rs+',0A'+rs+','+rs+' 0 1,1 0,-'+rs+ - 'A'+rs+','+rs+' 0 0,1 '+rs+',0Z'; - }, - needLine: true, - noDot: true - }, - 'square-cross': { - n: 29, - f: function(r) { - var rs = d3.round(r,2); - return 'M0,'+rs+'V-'+rs+'M'+rs+',0H-'+rs+ - 'M'+rs+','+rs+'H-'+rs+'V-'+rs+'H'+rs+'Z'; - }, - needLine: true, - noDot: true - }, - 'square-x': { - n: 30, - f: function(r) { - var rs = d3.round(r,2); - return 'M'+rs+','+rs+'L-'+rs+',-'+rs+ - 'M'+rs+',-'+rs+'L-'+rs+','+rs+ - 'M'+rs+','+rs+'H-'+rs+'V-'+rs+'H'+rs+'Z'; - }, - needLine: true, - noDot: true - }, - 'diamond-cross': { - n: 31, - f: function(r) { - var rd = d3.round(r*1.3,2); - return 'M'+rd+',0L0,'+rd+'L-'+rd+',0L0,-'+rd+'Z'+ - 'M0,-'+rd+'V'+rd+'M-'+rd+',0H'+rd; - }, - needLine: true, - noDot: true - }, - 'diamond-x': { - n: 32, - f: function(r) { - var rd = d3.round(r*1.3,2), - r2 = d3.round(r*0.65,2); - return 'M'+rd+',0L0,'+rd+'L-'+rd+',0L0,-'+rd+'Z'+ - 'M-'+r2+',-'+r2+'L'+r2+','+r2+ - 'M-'+r2+','+r2+'L'+r2+',-'+r2; - }, - needLine: true, - noDot: true - }, - 'cross-thin': { - n: 33, - f: function(r) { - var rc = d3.round(r*1.4,2); - return 'M0,'+rc+'V-'+rc+'M'+rc+',0H-'+rc; - }, - needLine: true, - noDot: true - }, - 'x-thin': { - n: 34, - f: function(r) { - var rx = d3.round(r,2); - return 'M'+rx+','+rx+'L-'+rx+',-'+rx+ - 'M'+rx+',-'+rx+'L-'+rx+','+rx; - }, - needLine: true, - noDot: true - }, - asterisk: { - n: 35, - f: function(r) { - var rc = d3.round(r*1.2,2); - var rs = d3.round(r*0.85,2); - return 'M0,'+rc+'V-'+rc+'M'+rc+',0H-'+rc+ - 'M'+rs+','+rs+'L-'+rs+',-'+rs+ - 'M'+rs+',-'+rs+'L-'+rs+','+rs; - }, - needLine: true, - noDot: true - }, - hash: { - n: 36, - f: function(r) { - var r1 = d3.round(r/2,2), - r2 = d3.round(r,2); - return 'M'+r1+','+r2+'V-'+r2+ - 'm-'+r2+',0V'+r2+ - 'M'+r2+','+r1+'H-'+r2+ - 'm0,-'+r2+'H'+r2; - }, - needLine: true - }, - 'y-up': { - n: 37, - f: function(r) { - var x = d3.round(r*1.2,2), - y0 = d3.round(r*1.6,2), - y1 = d3.round(r*0.8,2); - return 'M-'+x+','+y1+'L0,0M'+x+','+y1+'L0,0M0,-'+y0+'L0,0'; - }, - needLine: true, - noDot: true - }, - 'y-down': { - n: 38, - f: function(r) { - var x = d3.round(r*1.2,2), - y0 = d3.round(r*1.6,2), - y1 = d3.round(r*0.8,2); - return 'M-'+x+',-'+y1+'L0,0M'+x+',-'+y1+'L0,0M0,'+y0+'L0,0'; - }, - needLine: true, - noDot: true - }, - 'y-left': { - n: 39, - f: function(r) { - var y = d3.round(r*1.2,2), - x0 = d3.round(r*1.6,2), - x1 = d3.round(r*0.8,2); - return 'M'+x1+','+y+'L0,0M'+x1+',-'+y+'L0,0M-'+x0+',0L0,0'; - }, - needLine: true, - noDot: true - }, - 'y-right': { - n: 40, - f: function(r) { - var y = d3.round(r*1.2,2), - x0 = d3.round(r*1.6,2), - x1 = d3.round(r*0.8,2); - return 'M-'+x1+','+y+'L0,0M-'+x1+',-'+y+'L0,0M'+x0+',0L0,0'; - }, - needLine: true, - noDot: true - }, - 'line-ew': { - n: 41, - f: function(r) { - var rc = d3.round(r*1.4,2); - return 'M'+rc+',0H-'+rc; - }, - needLine: true, - noDot: true - }, - 'line-ns': { - n: 42, - f: function(r) { - var rc = d3.round(r*1.4,2); - return 'M0,'+rc+'V-'+rc; - }, - needLine: true, - noDot: true - }, - 'line-ne': { - n: 43, - f: function(r) { - var rx = d3.round(r,2); - return 'M'+rx+',-'+rx+'L-'+rx+','+rx; - }, - needLine: true, - noDot: true - }, - 'line-nw': { - n: 44, - f: function(r) { - var rx = d3.round(r,2); - return 'M'+rx+','+rx+'L-'+rx+',-'+rx; - }, - needLine: true, - noDot: true - } -}; +var SYMBOLDEFS = require('./symbol_defs'); drawing.symbolNames = []; drawing.symbolFuncs = []; drawing.symbolNeedLines = {}; drawing.symbolNoDot = {}; drawing.symbolList = []; + Object.keys(SYMBOLDEFS).forEach(function(k) { var symDef = SYMBOLDEFS[k]; drawing.symbolList = drawing.symbolList.concat( diff --git a/src/components/drawing/symbol_defs.js b/src/components/drawing/symbol_defs.js new file mode 100644 index 00000000000..2c461f2f003 --- /dev/null +++ b/src/components/drawing/symbol_defs.js @@ -0,0 +1,465 @@ +'use strict'; + +var d3 = require('d3'); + +/** Marker symbol definitions + * users can specify markers either by number or name + * add 100 (or '-open') and you get an open marker + * open markers have no fill and use line color as the stroke color + * add 200 (or '-dot') and you get a dot in the middle + * add both and you get both + */ + +module.exports = { + circle: { + n: 0, + f: function(r) { + var rs = d3.round(r,2); + return 'M'+rs+',0A'+rs+','+rs+' 0 1,1 0,-'+rs+ + 'A'+rs+','+rs+' 0 0,1 '+rs+',0Z'; + } + }, + square: { + n: 1, + f: function(r) { + var rs = d3.round(r,2); + return 'M'+rs+','+rs+'H-'+rs+'V-'+rs+'H'+rs+'Z'; + } + }, + diamond: { + n: 2, + f: function(r) { + var rd = d3.round(r*1.3,2); + return 'M'+rd+',0L0,'+rd+'L-'+rd+',0L0,-'+rd+'Z'; + } + }, + cross: { + n: 3, + f: function(r) { + var rc = d3.round(r*0.4,2), + rc2 = d3.round(r*1.2,2); + return 'M'+rc2+','+rc+'H'+rc+'V'+rc2+'H-'+rc+ + 'V'+rc+'H-'+rc2+'V-'+rc+'H-'+rc+'V-'+rc2+ + 'H'+rc+'V-'+rc+'H'+rc2+'Z'; + } + }, + x: { + n: 4, + f: function(r) { + var rx = d3.round(r*0.8/Math.sqrt(2),2), + ne = 'l'+rx+','+rx, + se = 'l'+rx+',-'+rx, + sw = 'l-'+rx+',-'+rx, + nw = 'l-'+rx+','+rx; + return 'M0,'+rx+ne+se+sw+se+sw+nw+sw+nw+ne+nw+ne+'Z'; + } + }, + 'triangle-up': { + n: 5, + f: function(r) { + var rt = d3.round(r*2/Math.sqrt(3),2), + r2 = d3.round(r/2,2), + rs = d3.round(r,2); + return 'M-'+rt+','+r2+'H'+rt+'L0,-'+rs+'Z'; + } + }, + 'triangle-down': { + n: 6, + f: function(r) { + var rt = d3.round(r*2/Math.sqrt(3),2), + r2 = d3.round(r/2,2), + rs = d3.round(r,2); + return 'M-'+rt+',-'+r2+'H'+rt+'L0,'+rs+'Z'; + } + }, + 'triangle-left': { + n: 7, + f: function(r) { + var rt = d3.round(r*2/Math.sqrt(3),2), + r2 = d3.round(r/2,2), + rs = d3.round(r,2); + return 'M'+r2+',-'+rt+'V'+rt+'L-'+rs+',0Z'; + } + }, + 'triangle-right': { + n: 8, + f: function(r) { + var rt = d3.round(r*2/Math.sqrt(3),2), + r2 = d3.round(r/2,2), + rs = d3.round(r,2); + return 'M-'+r2+',-'+rt+'V'+rt+'L'+rs+',0Z'; + } + }, + 'triangle-ne': { + n: 9, + f: function(r) { + var r1 = d3.round(r*0.6,2), + r2 = d3.round(r*1.2,2); + return 'M-'+r2+',-'+r1+'H'+r1+'V'+r2+'Z'; + } + }, + 'triangle-se': { + n: 10, + f: function(r) { + var r1 = d3.round(r*0.6,2), + r2 = d3.round(r*1.2,2); + return 'M'+r1+',-'+r2+'V'+r1+'H-'+r2+'Z'; + } + }, + 'triangle-sw': { + n: 11, + f: function(r) { + var r1 = d3.round(r*0.6,2), + r2 = d3.round(r*1.2,2); + return 'M'+r2+','+r1+'H-'+r1+'V-'+r2+'Z'; + } + }, + 'triangle-nw': { + n: 12, + f: function(r) { + var r1 = d3.round(r*0.6,2), + r2 = d3.round(r*1.2,2); + return 'M-'+r1+','+r2+'V-'+r1+'H'+r2+'Z'; + } + }, + pentagon: { + n: 13, + f: function(r) { + var x1 = d3.round(r*0.951,2), + x2 = d3.round(r*0.588,2), + y0 = d3.round(-r,2), + y1 = d3.round(r*-0.309,2), + y2 = d3.round(r*0.809,2); + return 'M'+x1+','+y1+'L'+x2+','+y2+'H-'+x2+ + 'L-'+x1+','+y1+'L0,'+y0+'Z'; + } + }, + hexagon: { + n: 14, + f: function(r) { + var y0 = d3.round(r,2), + y1 = d3.round(r/2,2), + x = d3.round(r*Math.sqrt(3)/2,2); + return 'M'+x+',-'+y1+'V'+y1+'L0,'+y0+ + 'L-'+x+','+y1+'V-'+y1+'L0,-'+y0+'Z'; + } + }, + hexagon2: { + n: 15, + f: function(r) { + var x0 = d3.round(r,2), + x1 = d3.round(r/2,2), + y = d3.round(r*Math.sqrt(3)/2,2); + return 'M-'+x1+','+y+'H'+x1+'L'+x0+ + ',0L'+x1+',-'+y+'H-'+x1+'L-'+x0+',0Z'; + } + }, + octagon: { + n: 16, + f: function(r) { + var a = d3.round(r*0.924,2), + b = d3.round(r*0.383,2); + return 'M-'+b+',-'+a+'H'+b+'L'+a+',-'+b+'V'+b+ + 'L'+b+','+a+'H-'+b+'L-'+a+','+b+'V-'+b+'Z'; + } + }, + star: { + n: 17, + f: function(r) { + var rs = r*1.4, + x1 = d3.round(rs*0.225,2), + x2 = d3.round(rs*0.951,2), + x3 = d3.round(rs*0.363,2), + x4 = d3.round(rs*0.588,2), + y0 = d3.round(-rs,2), + y1 = d3.round(rs*-0.309,2), + y3 = d3.round(rs*0.118,2), + y4 = d3.round(rs*0.809,2), + y5 = d3.round(rs*0.382,2); + return 'M'+x1+','+y1+'H'+x2+'L'+x3+','+y3+ + 'L'+x4+','+y4+'L0,'+y5+'L-'+x4+','+y4+ + 'L-'+x3+','+y3+'L-'+x2+','+y1+'H-'+x1+ + 'L0,'+y0+'Z'; + } + }, + hexagram: { + n: 18, + f: function(r) { + var y = d3.round(r*0.66,2), + x1 = d3.round(r*0.38,2), + x2 = d3.round(r*0.76,2); + return 'M-'+x2+',0l-'+x1+',-'+y+'h'+x2+ + 'l'+x1+',-'+y+'l'+x1+','+y+'h'+x2+ + 'l-'+x1+','+y+'l'+x1+','+y+'h-'+x2+ + 'l-'+x1+','+y+'l-'+x1+',-'+y+'h-'+x2+'Z'; + } + }, + 'star-triangle-up': { + n: 19, + f: function(r) { + var x = d3.round(r*Math.sqrt(3)*0.8,2), + y1 = d3.round(r*0.8,2), + y2 = d3.round(r*1.6,2), + rc = d3.round(r*4,2), + aPart = 'A '+rc+','+rc+' 0 0 1 '; + return 'M-'+x+','+y1+aPart+x+','+y1+ + aPart+'0,-'+y2+aPart+'-'+x+','+y1+'Z'; + } + }, + 'star-triangle-down': { + n: 20, + f: function(r) { + var x = d3.round(r*Math.sqrt(3)*0.8,2), + y1 = d3.round(r*0.8,2), + y2 = d3.round(r*1.6,2), + rc = d3.round(r*4,2), + aPart = 'A '+rc+','+rc+' 0 0 1 '; + return 'M'+x+',-'+y1+aPart+'-'+x+',-'+y1+ + aPart+'0,'+y2+aPart+x+',-'+y1+'Z'; + } + }, + 'star-square': { + n: 21, + f: function(r) { + var rp = d3.round(r*1.1,2), + rc = d3.round(r*2,2), + aPart = 'A '+rc+','+rc+' 0 0 1 '; + return 'M-'+rp+',-'+rp+aPart+'-'+rp+','+rp+ + aPart+rp+','+rp+aPart+rp+',-'+rp+ + aPart+'-'+rp+',-'+rp+'Z'; + } + }, + 'star-diamond': { + n: 22, + f: function(r) { + var rp = d3.round(r*1.4,2), + rc = d3.round(r*1.9,2), + aPart = 'A '+rc+','+rc+' 0 0 1 '; + return 'M-'+rp+',0'+aPart+'0,'+rp+ + aPart+rp+',0'+aPart+'0,-'+rp+ + aPart+'-'+rp+',0'+'Z'; + } + }, + 'diamond-tall': { + n: 23, + f: function(r) { + var x = d3.round(r*0.7,2), + y = d3.round(r*1.4,2); + return 'M0,'+y+'L'+x+',0L0,-'+y+'L-'+x+',0Z'; + } + }, + 'diamond-wide': { + n: 24, + f: function(r) { + var x = d3.round(r*1.4,2), + y = d3.round(r*0.7,2); + return 'M0,'+y+'L'+x+',0L0,-'+y+'L-'+x+',0Z'; + } + }, + hourglass: { + n: 25, + f: function(r) { + var rs = d3.round(r,2); + return 'M'+rs+','+rs+'H-'+rs+'L'+rs+',-'+rs+'H-'+rs+'Z'; + }, + noDot: true + }, + bowtie: { + n: 26, + f: function(r) { + var rs = d3.round(r,2); + return 'M'+rs+','+rs+'V-'+rs+'L-'+rs+','+rs+'V-'+rs+'Z'; + }, + noDot: true + }, + 'circle-cross': { + n: 27, + f: function(r) { + var rs = d3.round(r,2); + return 'M0,'+rs+'V-'+rs+'M'+rs+',0H-'+rs+ + 'M'+rs+',0A'+rs+','+rs+' 0 1,1 0,-'+rs+ + 'A'+rs+','+rs+' 0 0,1 '+rs+',0Z'; + }, + needLine: true, + noDot: true + }, + 'circle-x': { + n: 28, + f: function(r) { + var rs = d3.round(r,2), + rc = d3.round(r/Math.sqrt(2),2); + return 'M'+rc+','+rc+'L-'+rc+',-'+rc+ + 'M'+rc+',-'+rc+'L-'+rc+','+rc+ + 'M'+rs+',0A'+rs+','+rs+' 0 1,1 0,-'+rs+ + 'A'+rs+','+rs+' 0 0,1 '+rs+',0Z'; + }, + needLine: true, + noDot: true + }, + 'square-cross': { + n: 29, + f: function(r) { + var rs = d3.round(r,2); + return 'M0,'+rs+'V-'+rs+'M'+rs+',0H-'+rs+ + 'M'+rs+','+rs+'H-'+rs+'V-'+rs+'H'+rs+'Z'; + }, + needLine: true, + noDot: true + }, + 'square-x': { + n: 30, + f: function(r) { + var rs = d3.round(r,2); + return 'M'+rs+','+rs+'L-'+rs+',-'+rs+ + 'M'+rs+',-'+rs+'L-'+rs+','+rs+ + 'M'+rs+','+rs+'H-'+rs+'V-'+rs+'H'+rs+'Z'; + }, + needLine: true, + noDot: true + }, + 'diamond-cross': { + n: 31, + f: function(r) { + var rd = d3.round(r*1.3,2); + return 'M'+rd+',0L0,'+rd+'L-'+rd+',0L0,-'+rd+'Z'+ + 'M0,-'+rd+'V'+rd+'M-'+rd+',0H'+rd; + }, + needLine: true, + noDot: true + }, + 'diamond-x': { + n: 32, + f: function(r) { + var rd = d3.round(r*1.3,2), + r2 = d3.round(r*0.65,2); + return 'M'+rd+',0L0,'+rd+'L-'+rd+',0L0,-'+rd+'Z'+ + 'M-'+r2+',-'+r2+'L'+r2+','+r2+ + 'M-'+r2+','+r2+'L'+r2+',-'+r2; + }, + needLine: true, + noDot: true + }, + 'cross-thin': { + n: 33, + f: function(r) { + var rc = d3.round(r*1.4,2); + return 'M0,'+rc+'V-'+rc+'M'+rc+',0H-'+rc; + }, + needLine: true, + noDot: true + }, + 'x-thin': { + n: 34, + f: function(r) { + var rx = d3.round(r,2); + return 'M'+rx+','+rx+'L-'+rx+',-'+rx+ + 'M'+rx+',-'+rx+'L-'+rx+','+rx; + }, + needLine: true, + noDot: true + }, + asterisk: { + n: 35, + f: function(r) { + var rc = d3.round(r*1.2,2); + var rs = d3.round(r*0.85,2); + return 'M0,'+rc+'V-'+rc+'M'+rc+',0H-'+rc+ + 'M'+rs+','+rs+'L-'+rs+',-'+rs+ + 'M'+rs+',-'+rs+'L-'+rs+','+rs; + }, + needLine: true, + noDot: true + }, + hash: { + n: 36, + f: function(r) { + var r1 = d3.round(r/2,2), + r2 = d3.round(r,2); + return 'M'+r1+','+r2+'V-'+r2+ + 'm-'+r2+',0V'+r2+ + 'M'+r2+','+r1+'H-'+r2+ + 'm0,-'+r2+'H'+r2; + }, + needLine: true + }, + 'y-up': { + n: 37, + f: function(r) { + var x = d3.round(r*1.2,2), + y0 = d3.round(r*1.6,2), + y1 = d3.round(r*0.8,2); + return 'M-'+x+','+y1+'L0,0M'+x+','+y1+'L0,0M0,-'+y0+'L0,0'; + }, + needLine: true, + noDot: true + }, + 'y-down': { + n: 38, + f: function(r) { + var x = d3.round(r*1.2,2), + y0 = d3.round(r*1.6,2), + y1 = d3.round(r*0.8,2); + return 'M-'+x+',-'+y1+'L0,0M'+x+',-'+y1+'L0,0M0,'+y0+'L0,0'; + }, + needLine: true, + noDot: true + }, + 'y-left': { + n: 39, + f: function(r) { + var y = d3.round(r*1.2,2), + x0 = d3.round(r*1.6,2), + x1 = d3.round(r*0.8,2); + return 'M'+x1+','+y+'L0,0M'+x1+',-'+y+'L0,0M-'+x0+',0L0,0'; + }, + needLine: true, + noDot: true + }, + 'y-right': { + n: 40, + f: function(r) { + var y = d3.round(r*1.2,2), + x0 = d3.round(r*1.6,2), + x1 = d3.round(r*0.8,2); + return 'M-'+x1+','+y+'L0,0M-'+x1+',-'+y+'L0,0M'+x0+',0L0,0'; + }, + needLine: true, + noDot: true + }, + 'line-ew': { + n: 41, + f: function(r) { + var rc = d3.round(r*1.4,2); + return 'M'+rc+',0H-'+rc; + }, + needLine: true, + noDot: true + }, + 'line-ns': { + n: 42, + f: function(r) { + var rc = d3.round(r*1.4,2); + return 'M0,'+rc+'V-'+rc; + }, + needLine: true, + noDot: true + }, + 'line-ne': { + n: 43, + f: function(r) { + var rx = d3.round(r,2); + return 'M'+rx+',-'+rx+'L-'+rx+','+rx; + }, + needLine: true, + noDot: true + }, + 'line-nw': { + n: 44, + f: function(r) { + var rx = d3.round(r,2); + return 'M'+rx+','+rx+'L-'+rx+',-'+rx; + }, + needLine: true, + noDot: true + } +}; diff --git a/src/components/errorbars/attributes.js b/src/components/errorbars/attributes.js new file mode 100644 index 00000000000..9d20961e876 --- /dev/null +++ b/src/components/errorbars/attributes.js @@ -0,0 +1,129 @@ +module.exports = { + visible: { + valType: 'boolean', + role: 'info', + description: [ + 'Determines whether or not this set of error bars is visible.' + ].join(' ') + }, + type: { + valType: 'enumerated', + values: ['percent', 'constant', 'sqrt', 'data'], + role: 'info', + description: [ + 'Determines the rule used to generate the error bars.', + + 'If *constant`, the bar lengths are of a constant value.', + 'Set this constant in `value`.', + + 'If *percent*, the bar lengths correspond to a percentage of', + 'underlying data. Set this percentage in `value`.', + + 'If *sqrt*, the bar lengths correspond to the sqaure of the', + 'underlying data.', + + 'If *array*, the bar lengths are set with data set `array`.' + ].join(' ') + }, + symmetric: { + valType: 'boolean', + role: 'info', + description: [ + 'Determines whether or not the error bars have the same length', + 'in both direction', + '(top/bottom for vertical bars, left/right for horizontal bars.' + ].join(' ') + }, + array: { + valType: 'data_array', + description: [ + 'Sets the data corresponding the length of each error bar.', + 'Values are plotted relative to the underlying data.' + ].join(' ') + }, + arrayminus: { + valType: 'data_array', + description: [ + 'Sets the data corresponding the length of each error bar in the', + 'bottom (left) direction for vertical (horizontal) bars', + 'Values are plotted relative to the underlying data.' + ].join(' ') + }, + value: { + valType: 'number', + min: 0, + dflt: 10, + role: 'info', + description: [ + 'Sets the value of either the percentage', + '(if `type` is set to *percent*) or the constant', + '(if `type` is set to *constant*) corresponding to the lengths of', + 'the error bars.' + ].join(' ') + }, + valueminus: { + valType: 'number', + min: 0, + dflt: 10, + role: 'info', + description: [ + 'Sets the value of either the percentage', + '(if `type` is set to *percent*) or the constant', + '(if `type` is set to *constant*) corresponding to the lengths of', + 'the error bars in the', + 'bottom (left) direction for vertical (horizontal) bars' + ].join(' ') + }, + traceref: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'info' + }, + tracerefminus: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'info' + }, + copy_ystyle: { + valType: 'boolean', + role: 'style' + }, + copy_zstyle: { + valType: 'boolean', + role: 'style' + }, + color: { + valType: 'color', + role: 'style', + description: 'Sets the stoke color of the error bars.' + }, + thickness: { + valType: 'number', + min: 0, + dflt: 2, + role: 'style', + description: 'Sets the thickness (in px) of the error bars.' + }, + width: { + valType: 'number', + min: 0, + role: 'style', + description: [ + 'Sets the width (in px) of the cross-bar at both ends', + 'of the error bars.' + ].join(' ') + }, + + _deprecated: { + opacity: { + valType: 'number', + role: 'style', + description: [ + 'Obsolete.', + 'Use the alpha channel in error bar `color` to set the opacity.' + ].join(' ') + } + } +}; diff --git a/src/errorbars.js b/src/components/errorbars/index.js similarity index 74% rename from src/errorbars.js rename to src/components/errorbars/index.js index 05e2aff4c17..39537a73030 100644 --- a/src/errorbars.js +++ b/src/components/errorbars/index.js @@ -1,142 +1,12 @@ 'use strict'; -/* jshint camelcase: false */ - -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var errorBars = module.exports = {}; -errorBars.attributes = { - visible: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not this set of error bars is visible.' - ].join(' ') - }, - type: { - valType: 'enumerated', - values: ['percent', 'constant', 'sqrt', 'data'], - role: 'info', - description: [ - 'Determines the rule used to generate the error bars.', - - 'If *constant`, the bar lengths are of a constant value.', - 'Set this constant in `value`.', - - 'If *percent*, the bar lengths correspond to a percentage of', - 'underlying data. Set this percentage in `value`.', - - 'If *sqrt*, the bar lengths correspond to the sqaure of the', - 'underlying data.', - - 'If *array*, the bar lengths are set with data set `array`.' - ].join(' ') - }, - symmetric: { - valType: 'boolean', - role: 'info', - description: [ - 'Determines whether or not the error bars have the same length', - 'in both direction', - '(top/bottom for vertical bars, left/right for horizontal bars.' - ].join(' ') - }, - array: { - valType: 'data_array', - description: [ - 'Sets the data corresponding the length of each error bar.', - 'Values are plotted relative to the underlying data.' - ].join(' ') - }, - arrayminus: { - valType: 'data_array', - description: [ - 'Sets the data corresponding the length of each error bar in the', - 'bottom (left) direction for vertical (horizontal) bars', - 'Values are plotted relative to the underlying data.' - ].join(' ') - }, - value: { - valType: 'number', - min: 0, - dflt: 10, - role: 'info', - description: [ - 'Sets the value of either the percentage', - '(if `type` is set to *percent*) or the constant', - '(if `type` is set to *constant*) corresponding to the lengths of', - 'the error bars.' - ].join(' ') - }, - valueminus: { - valType: 'number', - min: 0, - dflt: 10, - role: 'info', - description: [ - 'Sets the value of either the percentage', - '(if `type` is set to *percent*) or the constant', - '(if `type` is set to *constant*) corresponding to the lengths of', - 'the error bars in the', - 'bottom (left) direction for vertical (horizontal) bars' - ].join(' ') - }, - traceref: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'info' - }, - tracerefminus: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'info' - }, - copy_ystyle: { - valType: 'boolean', - role: 'style' - }, - copy_zstyle: { - valType: 'boolean', - role: 'style' - }, - color: { - valType: 'color', - role: 'style', - description: 'Sets the stoke color of the error bars.' - }, - thickness: { - valType: 'number', - min: 0, - dflt: 2, - role: 'style', - description: 'Sets the thickness (in px) of the error bars.' - }, - width: { - valType: 'number', - min: 0, - role: 'style', - description: [ - 'Sets the width (in px) of the cross-bar at both ends', - 'of the error bars.' - ].join(' ') - }, - - _deprecated: { - opacity: { - valType: 'number', - role: 'style', - description: [ - 'Obsolete.', - 'Use the alpha channel in error bar `color` to set the opacity.' - ].join(' ') - } - } -}; +errorBars.attributes = require('./attributes'); errorBars.supplyDefaults = function(traceIn, traceOut, defaultColor, opts) { var objName = 'error_' + opts.axis, diff --git a/src/components/legend/attributes.js b/src/components/legend/attributes.js new file mode 100644 index 00000000000..94c3033fc55 --- /dev/null +++ b/src/components/legend/attributes.js @@ -0,0 +1,96 @@ +var fontAttrs = require('../../plots/font_attributes'); +var colorAttrs = require('../color/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + + +module.exports = { + bgcolor: { + valType: 'color', + role: 'style', + description: 'Sets the legend background color.' + }, + bordercolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the color of the border enclosing the legend.' + }, + borderwidth: { + valType: 'number', + min: 0, + dflt: 0, + role: 'style', + description: 'Sets the width (in px) of the border enclosing the legend.' + }, + font: extendFlat({}, fontAttrs, { + description: 'Sets the font used to text the legend items.' + }), + traceorder: { + valType: 'flaglist', + flags: ['reversed', 'grouped'], + extras: ['normal'], + role: 'style', + description: [ + 'Determines the order at which the legend items are displayed.', + + 'If *normal*, the items are displayed top-to-bottom in the same', + 'order as the input data.', + + 'If *reversed*, the items are displayed in the opposite order', + 'as *normal*.', + + 'If *grouped*, the items are displayed in groups', + '(when a trace `legendgroup` is provided).', + + 'if *grouped+reversed*, the items are displayed in the opposite order', + 'as *grouped*.' + ].join(' ') + }, + tracegroupgap: { + valType: 'number', + min: 0, + dflt: 10, + role: 'style', + description: [ + 'Sets the amount of vertical space (in px) between legend groups.' + ].join(' ') + }, + x: { + valType: 'number', + min: -2, + max: 3, + dflt: 1.02, + role: 'style', + description: 'Sets the x position (in normalized coordinates) of the legend.' + }, + xanchor: { + valType: 'enumerated', + values: ['auto', 'left', 'center', 'right'], + dflt: 'left', + role: 'info', + description: [ + 'Sets the legend\'s horizontal position anchor.', + 'This anchor binds the `x` position to the *left*, *center*', + 'or *right* of the legend.' + ].join(' ') + }, + y: { + valType: 'number', + min: -2, + max: 3, + dflt: 1, + role: 'style', + description: 'Sets the y position (in normalized coordinates) of the legend.' + }, + yanchor: { + valType: 'enumerated', + values: ['auto', 'top', 'middle', 'bottom'], + dflt: 'auto', + role: 'info', + description: [ + 'Sets the legend\'s vertical position anchor', + 'This anchor binds the `y` position to the *top*, *middle*', + 'or *bottom* of the legend.' + ].join(' ') + } +}; diff --git a/src/legend.js b/src/components/legend/index.js similarity index 88% rename from src/legend.js rename to src/components/legend/index.js index 42fb366d050..9ee54e11ee0 100644 --- a/src/legend.js +++ b/src/components/legend/index.js @@ -1,103 +1,11 @@ 'use strict'; -/* jshint camelcase: false */ - -var Plotly = require('./plotly'), - d3 = require('d3'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); var legend = module.exports = {}; -legend.layoutAttributes = { - bgcolor: { - valType: 'color', - role: 'style', - description: 'Sets the legend background color.' - }, - bordercolor: { - valType: 'color', - dflt: Plotly.Color.defaultLine, - role: 'style', - description: 'Sets the color of the border enclosing the legend.' - }, - borderwidth: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: 'Sets the width (in px) of the border enclosing the legend.' - }, - font: Plotly.Lib.extendFlat({}, Plotly.Plots.fontAttrs, { - description: 'Sets the font used to text the legend items.' - }), - traceorder: { - valType: 'flaglist', - flags: ['reversed', 'grouped'], - extras: ['normal'], - role: 'style', - description: [ - 'Determines the order at which the legend items are displayed.', - - 'If *normal*, the items are displayed top-to-bottom in the same', - 'order as the input data.', - - 'If *reversed*, the items are displayed in the opposite order', - 'as *normal*.', - - 'If *grouped*, the items are displayed in groups', - '(when a trace `legendgroup` is provided).', - - 'if *grouped+reversed*, the items are displayed in the opposite order', - 'as *grouped*.' - ].join(' ') - }, - tracegroupgap: { - valType: 'number', - min: 0, - dflt: 10, - role: 'style', - description: [ - 'Sets the amount of vertical space (in px) between legend groups.' - ].join(' ') - }, - x: { - valType: 'number', - min: -2, - max: 3, - dflt: 1.02, - role: 'style', - description: 'Sets the x position (in normalized coordinates) of the legend.' - }, - xanchor: { - valType: 'enumerated', - values: ['auto', 'left', 'center', 'right'], - dflt: 'left', - role: 'info', - description: [ - 'Sets the legend\'s horizontal position anchor.', - 'This anchor binds the `x` position to the *left*, *center*', - 'or *right* of the legend.' - ].join(' ') - }, - y: { - valType: 'number', - min: -2, - max: 3, - dflt: 1, - role: 'style', - description: 'Sets the y position (in normalized coordinates) of the legend.' - }, - yanchor: { - valType: 'enumerated', - values: ['auto', 'top', 'middle', 'bottom'], - dflt: 'auto', - role: 'info', - description: [ - 'Sets the legend\'s vertical position anchor', - 'This anchor binds the `y` position to the *top*, *middle*', - 'or *bottom* of the legend.' - ].join(' ') - } -}; +legend.layoutAttributes = require('./attributes'); legend.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { var containerIn = layoutIn.legend || {}, diff --git a/src/modebar.js b/src/components/modebar/index.js similarity index 99% rename from src/modebar.js rename to src/components/modebar/index.js index c47fbbddb61..0396a1928af 100644 --- a/src/modebar.js +++ b/src/components/modebar/index.js @@ -1,7 +1,7 @@ 'use strict'; -var Plotly = require('./plotly'), - d3 = require('d3'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); /** * UI controller for interactive plots diff --git a/src/components/shapes/attributes.js b/src/components/shapes/attributes.js new file mode 100644 index 00000000000..f19c3384d17 --- /dev/null +++ b/src/components/shapes/attributes.js @@ -0,0 +1,134 @@ +var annAttrs = require('../annotations/attributes'); +var scatterAttrs = require('../../traces/scatter/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterLineAttrs = scatterAttrs.line; + +module.exports = { + _isLinkedToArray: true, + + type: { + valType: 'enumerated', + values: ['circle', 'rect', 'path', 'line'], + role: 'info', + description: [ + 'Specifies the shape type to be drawn.', + + 'If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)', + + 'If *circle*, a circle is drawn from', + '((`x0`+`x1`)/2, (`y0`+`y1`)/2))', + 'with radius', + '(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)', + + 'If *rect*, a rectangle is drawn linking', + '(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)', + + 'If *path*, draw a custom SVG path using `path`.' + ].join(' ') + }, + + xref: extendFlat({}, annAttrs.xref, { + description: [ + 'Sets the shape\'s x coordinate axis.', + 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', + 'refers to an x coordinate', + 'If set to *paper*, the `x` position refers to the distance from', + 'the left side of the plotting area in normalized coordinates', + 'where *0* (*1*) corresponds to the left (right) side.' + ].join(' ') + }), + x0: { + valType: 'any', + role: 'info', + description: [ + 'Sets the shape\'s starting x position.', + 'See `type` for more info.' + ].join(' ') + }, + x1: { + valType: 'any', + role: 'info', + description: [ + 'Sets the shape\'s end x position.', + 'See `type` for more info.' + ].join(' ') + }, + + yref: extendFlat({}, annAttrs.yref, { + description: [ + 'Sets the annotation\'s y coordinate axis.', + 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', + 'refers to an y coordinate', + 'If set to *paper*, the `y` position refers to the distance from', + 'the bottom of the plotting area in normalized coordinates', + 'where *0* (*1*) corresponds to the bottom (top).' + ].join(' ') + }), + y0: { + valType: 'any', + role: 'info', + description: [ + 'Sets the shape\'s starting y position.', + 'See `type` for more info.' + ].join(' ') + }, + y1: { + valType: 'any', + role: 'info', + description: [ + 'Sets the shape\'s end y position.', + 'See `type` for more info.' + ].join(' ') + }, + + path: { + valType: 'string', + role: 'info', + description: [ + 'For `type` *path* - a valid SVG path but with the pixel values', + 'replaced by data values. There are a few restrictions / quirks', + 'only absolute instructions, not relative. So the allowed segments', + 'are: M, L, H, V, Q, C, T, S, and Z', + 'arcs (A) are not allowed because radius rx and ry are relative.', + + 'In the future we could consider supporting relative commands,', + 'but we would have to decide on how to handle date and log axes.', + 'Note that even as is, Q and C Bezier paths that are smooth on', + 'linear axes may not be smooth on log, and vice versa.', + 'no chained "polybezier" commands - specify the segment type for', + 'each one.', + + 'On category axes, values are numbers scaled to the serial numbers', + 'of categories because using the categories themselves there would', + 'be no way to describe fractional positions', + 'On data axes: because space and T are both normal components of path', + 'strings, we can\'t use either to separate date from time parts.', + 'Therefore we\'ll use underscore for this purpose:', + '2015-02-21_13:45:56.789' + ].join(' ') + }, + + opacity: { + valType: 'number', + min: 0, + max: 1, + dflt: 1, + role: 'info', + description: 'Sets the opacity of the shape.' + }, + line: { + color: scatterLineAttrs.color, + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + role: 'info' + }, + fillcolor: { + valType: 'color', + dflt: 'rgba(0,0,0,0)', + role: 'info', + description: [ + 'Sets the color filling the shape\'s interior.' + ].join(' ') + } +}; diff --git a/src/shapes.js b/src/components/shapes/index.js similarity index 77% rename from src/shapes.js rename to src/components/shapes/index.js index 1459d2d2ea3..6ed2dd97c31 100644 --- a/src/shapes.js +++ b/src/components/shapes/index.js @@ -1,140 +1,11 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var isNumeric = require('fast-isnumeric'); var shapes = module.exports = {}; -var extendFlat = Plotly.Lib.extendFlat, - scatterLineAttrs = Plotly.Scatter.attributes.line; - -shapes.layoutAttributes = { - _isLinkedToArray: true, - type: { - valType: 'enumerated', - values: ['circle', 'rect', 'path', 'line'], - role: 'info', - description: [ - 'Specifies the shape type to be drawn.', - - 'If *line*, a line is drawn from (`x0`,`y0`) to (`x1`,`y1`)', - - 'If *circle*, a circle is drawn from', - '((`x0`+`x1`)/2, (`y0`+`y1`)/2))', - 'with radius', - '(|(`x0`+`x1`)/2 - `x0`|, |(`y0`+`y1`)/2 -`y0`)|)', - - 'If *rect*, a rectangle is drawn linking', - '(`x0`,`y0`), (`x1`,`y0`), (`x1`,`y1`), (`x0`,`y1`), (`x0`,`y0`)', - - 'If *path*, draw a custom SVG path using `path`.' - ].join(' ') - }, - - xref: extendFlat({}, Plotly.Annotations.layoutAttributes.xref, { - description: [ - 'Sets the shape\'s x coordinate axis.', - 'If set to an x axis id (e.g. *x* or *x2*), the `x` position', - 'refers to an x coordinate', - 'If set to *paper*, the `x` position refers to the distance from', - 'the left side of the plotting area in normalized coordinates', - 'where *0* (*1*) corresponds to the left (right) side.' - ].join(' ') - }), - x0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s starting x position.', - 'See `type` for more info.' - ].join(' ') - }, - x1: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s end x position.', - 'See `type` for more info.' - ].join(' ') - }, - - yref: extendFlat({}, Plotly.Annotations.layoutAttributes.yref, { - description: [ - 'Sets the annotation\'s y coordinate axis.', - 'If set to an y axis id (e.g. *y* or *y2*), the `y` position', - 'refers to an y coordinate', - 'If set to *paper*, the `y` position refers to the distance from', - 'the bottom of the plotting area in normalized coordinates', - 'where *0* (*1*) corresponds to the bottom (top).' - ].join(' ') - }), - y0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s starting y position.', - 'See `type` for more info.' - ].join(' ') - }, - y1: { - valType: 'any', - role: 'info', - description: [ - 'Sets the shape\'s end y position.', - 'See `type` for more info.' - ].join(' ') - }, - - path: { - valType: 'string', - role: 'info', - description: [ - 'For `type` *path* - a valid SVG path but with the pixel values', - 'replaced by data values. There are a few restrictions / quirks', - 'only absolute instructions, not relative. So the allowed segments', - 'are: M, L, H, V, Q, C, T, S, and Z', - 'arcs (A) are not allowed because radius rx and ry are relative.', - - 'In the future we could consider supporting relative commands,', - 'but we would have to decide on how to handle date and log axes.', - 'Note that even as is, Q and C Bezier paths that are smooth on', - 'linear axes may not be smooth on log, and vice versa.', - 'no chained "polybezier" commands - specify the segment type for', - 'each one.', - - 'On category axes, values are numbers scaled to the serial numbers', - 'of categories because using the categories themselves there would', - 'be no way to describe fractional positions', - 'On data axes: because space and T are both normal components of path', - 'strings, we can\'t use either to separate date from time parts.', - 'Therefore we\'ll use underscore for this purpose:', - '2015-02-21_13:45:56.789' - ].join(' ') - }, - - opacity: { - valType: 'number', - min: 0, - max: 1, - dflt: 1, - role: 'info', - description: 'Sets the opacity of the shape.' - }, - line: { - color: scatterLineAttrs.color, - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, - role: 'info' - }, - fillcolor: { - valType: 'color', - dflt: 'rgba(0,0,0,0)', - role: 'info', - description: [ - 'Sets the color filling the shape\'s interior.' - ].join(' ') - } -}; +shapes.layoutAttributes = require('./attributes'); shapes.supplyLayoutDefaults = function(layoutIn, layoutOut) { var containerIn = layoutIn.shapes || [], diff --git a/src/components/titles/index.js b/src/components/titles/index.js new file mode 100644 index 00000000000..e390d09107c --- /dev/null +++ b/src/components/titles/index.js @@ -0,0 +1,304 @@ +'use strict'; + +var Plotly = require('../../plotly'); +var d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var plots = Plotly.Plots; + +var Titles = module.exports = {}; + +// titles - (re)draw titles on the axes and plot +// title can be 'xtitle', 'ytitle', 'gtitle', +// or empty to draw all +Titles.draw = function(gd, title) { + if(!title) { + Plotly.Axes.listIds(gd).forEach(function(axId) { + Titles.draw(gd, axId + 'title'); + }); + Titles.draw(gd, 'gtitle'); + return; + } + + 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[Plotly.Axes.id2name(title.replace('title',''))] || fullLayout; + + var prop = cont===fullLayout ? 'title' : cont._name+'.title', + name = colorbar ? 'colorscale' : + ((cont._id||axletter).toUpperCase()+' axis'), + font = cont.titlefont.family, + fontSize = cont.titlefont.size, + fontColor = cont.titlefont.color, + x, + y, + transform='', + attr = {}, + 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 Plotly.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} : + Plotly.Axes.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))); + 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} : + Plotly.Axes.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))); + attr = {center: 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, + isplaceholder = false, + txt = cont.title.trim(); + if(txt === '') { opacity = 0; } + if(txt.match(/Click to enter .+ title/)) { + opacity = 0.2; + 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) + .data([0]); + group.enter().append('g') + .classed('g-'+title, true); + } + + var el = group.selectAll('text') + .data([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', title); + + function titleLayout(titleEl){ + Plotly.Lib.syncOrAsync([drawTitle,scootTitle], titleEl); + } + + function drawTitle(titleEl) { + titleEl.attr('transform', transform ? + 'rotate(' + [transform.rotate, options.x, options.y] + + ') translate(0, '+transform.offset+')' : + null); + titleEl.style({ + 'font-family': font, + 'font-size': d3.round(fontSize,2)+'px', + fill: Plotly.Color.rgb(fontColor), + opacity: opacity*Plotly.Color.opacity(fontColor), + 'font-weight': plots.fontWeight + }) + .attr(options) + .call(Plotly.util.convertToTspans) + .attr(options); + titleEl.selectAll('tspan.line') + .attr(options); + 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 shift = 0, + backside = { + left: 'right', + right: 'left', + top: 'bottom', + bottom: 'top' + }[avoid.side], + shiftSign = (['left','top'].indexOf(avoid.side)!==-1) ? + -1 : 1, + pad = isNumeric(avoid.pad) ? avoid.pad : 2, + titlebb = Plotly.Drawing.bBox(titleGroup.node()), + paperbb = { + left: 0, + top: 0, + right: fullLayout.width, + bottom: fullLayout.height + }, + maxshift = colorbar ? fullLayout.width: + (paperbb[avoid.side]-titlebb[avoid.side]) * + ((avoid.side==='left' || avoid.side==='top') ? -1 : 1); + // 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 + titlebb.left -= avoid.offsetLeft; + titlebb.right -= avoid.offsetLeft; + titlebb.top -= avoid.offsetTop; + titlebb.bottom -= avoid.offsetTop; + + // iterate over a set of elements (avoid.selection) + // to avoid collisions with + avoid.selection.each(function(){ + var avoidbb = Plotly.Drawing.bBox(this); + + if(Plotly.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', + 'translate(' + shiftTemplate + ')'); + } + } + } + + el.attr({'data-unformatted': txt}) + .call(titleLayout); + + var placeholderText = 'Click to enter '+name.replace(/\d+/,'')+' title'; + + function setPlaceholder(){ + opacity = 0; + isplaceholder = true; + txt = placeholderText; + fullLayout._infolayer.select('.'+title) + .attr({'data-unformatted': txt}) + .text(txt) + .on('mouseover.opacity',function(){ + d3.select(this).transition() + .duration(100).style('opacity',1); + }) + .on('mouseout.opacity',function(){ + d3.select(this).transition() + .duration(1000).style('opacity',0); + }); + } + + if(gd._context.editable){ + if(!txt) setPlaceholder(); + + el.call(Plotly.util.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); + }) + .on('cancel', function(){ + this.text(this.attr('data-unformatted')) + .call(titleLayout); + }) + .on('input', function(d){ + this.text(d || ' ').attr(options) + .selectAll('tspan.line') + .attr(options); + }); + } + else if(!txt || txt.match(/Click to enter .+ title/)) { + el.remove(); + } + el.classed('js-placeholder',isplaceholder); +}; diff --git a/src/geo/raw/country-name_to_iso3.json b/src/constants/country-name_to_iso3.json similarity index 100% rename from src/geo/raw/country-name_to_iso3.json rename to src/constants/country-name_to_iso3.json diff --git a/src/geo/lib/params.js b/src/constants/geo_constants.js similarity index 100% rename from src/geo/lib/params.js rename to src/constants/geo_constants.js diff --git a/src/gl2d/lib/dashes.json b/src/constants/gl2d_dashes.json similarity index 100% rename from src/gl2d/lib/dashes.json rename to src/constants/gl2d_dashes.json diff --git a/src/gl3d/lib/dashes.json b/src/constants/gl3d_dashes.json similarity index 100% rename from src/gl3d/lib/dashes.json rename to src/constants/gl3d_dashes.json diff --git a/src/gl3d/lib/markers.json b/src/constants/gl_markers.json similarity index 100% rename from src/gl3d/lib/markers.json rename to src/constants/gl_markers.json diff --git a/src/geo/defaults/choropleth.js b/src/geo/defaults/choropleth.js deleted file mode 100644 index fc33e060793..00000000000 --- a/src/geo/defaults/choropleth.js +++ /dev/null @@ -1,61 +0,0 @@ -'use strict'; - -var Plotly = require('../../plotly'); - -var Choropleth = module.exports = {}; - -Plotly.Plots.register(Choropleth, 'choropleth', ['geo', 'noOpacity'], { - description: [ - 'The data that describes the choropleth value-to-color mapping', - 'is set in `z`.', - 'The geographic locations corresponding to each value in `z`', - 'are set in `locations`.' - ].join(' ') -}); - -Choropleth.attributes = require('../attributes/choropleth'); - -Choropleth.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { - var locations, len, z; - - function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, - Choropleth.attributes, attr, dflt); - } - - locations = coerce('locations'); - if(locations) len = locations.length; - if(!locations || !len) { - traceOut.visible = false; - return; - } - - z = coerce('z'); - if(!Array.isArray(z)) { - traceOut.visible = false; - return; - } - - if(z.length > len) traceOut.z = z.slice(0, len); - - coerce('locationmode'); - - coerce('text'); - - coerce('marker.line.color'); - coerce('marker.line.width'); - - Plotly.Colorscale.handleDefaults( - traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} - ); - - coerce('hoverinfo', (layout._dataLength === 1) ? 'location+z+text' : undefined); -}; - -Choropleth.colorbar = Plotly.Colorbar.traceColorbar; - -Choropleth.calc = function(gd, trace) { - - Plotly.Colorscale.calc(trace, trace.z, '', 'z'); - -}; diff --git a/src/gl3d/defaults/gl3daxes.js b/src/gl3d/defaults/gl3daxes.js deleted file mode 100644 index e4f26fc4932..00000000000 --- a/src/gl3d/defaults/gl3daxes.js +++ /dev/null @@ -1,82 +0,0 @@ -'use strict'; - -var Plotly = require('../../plotly'); - -var Gl3dAxes = module.exports = {}; - -Gl3dAxes.axesNames = ['xaxis', 'yaxis', 'zaxis']; - -Gl3dAxes.layoutAttributes = require('../attributes/gl3daxes'); - -var noop = function () {}; - -Gl3dAxes.supplyLayoutDefaults = function(layoutIn, layoutOut, options) { - - var Axes = Plotly.Axes; - var containerIn, containerOut; - - function coerce(attr, dflt) { - return Plotly.Lib.coerce(containerIn, containerOut, - Gl3dAxes.layoutAttributes, attr, dflt); - } - - for (var j = 0; j < Gl3dAxes.axesNames.length; j++) { - var axName = Gl3dAxes.axesNames[j]; - containerIn = layoutIn[axName] || {}; - - containerOut = { - _id: axName[0] + options.scene, - _name: axName - }; - - layoutOut[axName] = containerOut = Axes.handleAxisDefaults( - containerIn, - containerOut, - coerce, - { - font: options.font, - letter: axName[0], - data: options.data, - showGrid: true - }); - - coerce('gridcolor'); - coerce('title', axName[0]); // shouldn't this be on-par with 2D? - - containerOut.setScale = noop; - - if (coerce('showspikes')) { - coerce('spikesides'); - coerce('spikethickness'); - coerce('spikecolor'); - } - if (coerce('showbackground')) coerce('backgroundcolor'); - - coerce('showaxeslabels'); - } -}; - -Gl3dAxes.setConvert = function (containerOut) { - Plotly.Axes.setConvert(containerOut); - containerOut.setScale = noop; -}; - -Gl3dAxes.initAxes = function (td) { - var fullLayout = td._fullLayout; - - // until they play better together - delete fullLayout.xaxis; - delete fullLayout.yaxis; - - var sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); - - for (var i = 0; i < sceneIds.length; ++i) { - var sceneId = sceneIds[i]; - var sceneLayout = fullLayout[sceneId]; - for (var j = 0; j < 3; ++j) { - var axisName = Gl3dAxes.axesNames[j]; - var ax = sceneLayout[axisName]; - ax._td = td; - } - } -}; diff --git a/src/gl3d/defaults/mesh3d.js b/src/gl3d/defaults/mesh3d.js deleted file mode 100644 index 200633fc1f9..00000000000 --- a/src/gl3d/defaults/mesh3d.js +++ /dev/null @@ -1,94 +0,0 @@ -'use strict'; - -var Plotly = require('../../plotly'); - -var Mesh3D = module.exports = {}; - -Plotly.Plots.register(Mesh3D, 'mesh3d', ['gl3d'], { - description: [ - '' - ].join(' ') -}); - -Mesh3D.attributes = require('../attributes/mesh3d'); - -Mesh3D.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { - var self = this; - function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, self.attributes, attr, dflt); - } - - //Read in face/vertex properties - function readComponents(array) { - var ret = array.map(function(attr) { - var result = coerce(attr); - if(result && Array.isArray(result)) { - return result; - } - return null; - }); - return ret.every(function(x) { - return x && x.length === ret[0].length; - }) && ret; - } - - var coords = readComponents(['x', 'y', 'z']); - var indices = readComponents(['i', 'j', 'k']); - - if(!coords) { - traceOut.visible = false; - return; - } - - if(indices) { - //Otherwise, convert all face indices to ints - indices.forEach(function(index) { - for(var i=0; i= 0) coerce('surfacecolor', lineColor || markerColor); - - var dims = ['x', 'y', 'z']; - for(var i = 0; i < 3; ++i) { - var projection = 'projection.' + dims[i]; - if(coerce(projection + '.show')) { - coerce(projection + '.opacity'); - coerce(projection + '.scale'); - } - } - - if(Plotly.ErrorBars) { - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'z'}); - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y', inherit: 'z'}); - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'z'}); - } -}; - -Scatter3D.colorbar = Plotly.Scatter.colorbar; - -Scatter3D.calc = function(gd, trace) { - // this is a kludge to put the array attributes into - // calcdata the way Scatter.plot does, so that legends and - // popovers know what to do with them. - var cd = [{x: false, y: false, trace: trace, t: {}}]; - Plotly.Scatter.arraysToCalcdata(cd); - - Plotly.Scatter.calcMarkerColorscales(trace); - - return cd; -}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 00000000000..e0a8c258a10 --- /dev/null +++ b/src/index.js @@ -0,0 +1,30 @@ +/* + * Export the plotly.js API methods. + * + * This file is browserify'ed into a standalone 'Plotly' object. + * + */ + +var Plotly = require('./plotly'); + +// plot api +exports.plot = Plotly.plot; +exports.newPlot = Plotly.newPlot; +exports.restyle = Plotly.restyle; +exports.relayout = Plotly.relayout; +exports.redraw = Plotly.redraw; +exports.extendTraces = Plotly.extendTraces; +exports.prependTraces = Plotly.prependTraces; +exports.addTraces = Plotly.addTraces; +exports.deleteTraces = Plotly.deleteTraces; +exports.moveTraces = Plotly.moveTraces; +exports.setPlotConfig = require('./plot_api/set_plot_config'); + +// unofficial 'beta' plot methods, use at your own risk +exports.Plots = Plotly.Plots; +exports.Fx = Plotly.Fx; +exports.Snapshot = Plotly.Snapshot; +exports.PlotSchema = Plotly.PlotSchema; + +// export d3 used in the bundle +exports.d3 = require('d3'); diff --git a/src/geo/lib/array-to-calc-item.js b/src/lib/array_to_calc_item.js similarity index 100% rename from src/geo/lib/array-to-calc-item.js rename to src/lib/array_to_calc_item.js diff --git a/src/lib/coerce.js b/src/lib/coerce.js new file mode 100644 index 00000000000..e62f560cb29 --- /dev/null +++ b/src/lib/coerce.js @@ -0,0 +1,334 @@ +'use strict'; + +var Plotly = require('../plotly'); +var isNumeric = require('fast-isnumeric'); +var tinycolor = require('tinycolor2'); +var nestedProperty = require('./nested_property'); + +var colorscaleNames = Object.keys(require('../components/colorscale/scales')); + + +exports.valObjects = { + data_array: { + // You can use *dflt=[] to force said array to exist though. + description: [ + 'An {array} of data.', + 'The value MUST be an {array}, or we ignore it.' + ].join(' '), + requiredOpts: [], + otherOpts: ['dflt'], + coerceFunction: function(v, propOut, dflt) { + if(Array.isArray(v)) propOut.set(v); + else if(dflt!==undefined) propOut.set(dflt); + } + }, + enumerated: { + description: [ + 'Enumerated value type. The available values are listed', + 'in `values`.' + ].join(' '), + requiredOpts: ['values'], + otherOpts: ['dflt', 'coerceNumber', 'arrayOk'], + coerceFunction: function(v, propOut, dflt, opts) { + if(opts.coerceNumber) v = +v; + if(opts.values.indexOf(v)===-1) propOut.set(dflt); + else propOut.set(v); + } + }, + 'boolean': { + description: 'A boolean (true/false) value.', + requiredOpts: [], + otherOpts: ['dflt'], + coerceFunction: function(v, propOut, dflt) { + if(v===true || v===false) propOut.set(v); + else propOut.set(dflt); + } + }, + number: { + description: [ + 'A number or a numeric value', + '(e.g. a number inside a string).', + 'When applicable, values greater (less) than `max` (`min`)', + 'are coerced to the `dflt`.' + ].join(' '), + requiredOpts: [], + otherOpts: ['dflt', 'min', 'max', 'arrayOk'], + coerceFunction: function(v, propOut, dflt, opts) { + if(!isNumeric(v) || + (opts.min!==undefined && vopts.max)) { + propOut.set(dflt); + } + else propOut.set(+v); + } + }, + integer: { + description: [ + 'An integer or an integer inside a string.', + 'When applicable, values greater (less) than `max` (`min`)', + 'are coerced to the `dflt`.' + ].join(' '), + requiredOpts: [], + otherOpts: ['dflt', 'min', 'max'], + coerceFunction: function(v, propOut, dflt, opts) { + if(v%1 || !isNumeric(v) || + (opts.min!==undefined && vopts.max)) { + propOut.set(dflt); + } + else propOut.set(+v); + } + }, + string: { + description: [ + 'A string value.', + 'Numbers are converted to strings except for attributes with', + '`strict` set to true.' + ].join(' '), + requiredOpts: [], + // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter) + otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'], + coerceFunction: function(v, propOut, dflt, opts) { + if(opts.strict===true && typeof v !== 'string') { + propOut.set(dflt); + return; + } + + var s = String(v); + if(v===undefined || (opts.noBlank===true && !s)) { + propOut.set(dflt); + } + else propOut.set(s); + } + }, + color: { + description: [ + 'A string describing color.', + 'Supported formats:', + '- hex (e.g. \'#d3d3d3\')', + '- rgb (e.g. \'rgb(255, 0, 0)\')', + '- rgba (e.g. \'rgb(255, 0, 0, 0.5)\')', + '- hsl (e.g. \'hsl(0, 100%, 50%)\')', + '- hsv (e.g. \'hsv(0, 100%, 100%)\')', + '- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)' + ].join(' '), + requiredOpts: [], + otherOpts: ['dflt', 'arrayOk'], + coerceFunction: function(v, propOut, dflt) { + if(tinycolor(v).isValid()) propOut.set(v); + else propOut.set(dflt); + } + }, + colorscale: { + description: [ + 'A Plotly colorscale either picked by a name:', + '(any of', colorscaleNames.join(', '), ')', + 'customized as an {array} of 2-element {arrays} where', + 'the first element is the normalized color level value', + '(starting at *0* and ending at *1*),', + 'and the second item is a valid color string.' + ].join(' '), + requiredOpts: [], + otherOpts: ['dflt'], + coerceFunction: function(v, propOut, dflt) { + propOut.set(Plotly.Colorscale.getScale(v, dflt)); + } + }, + angle: { + description: [ + 'A number (in degree) between -180 and 180.' + ].join(' '), + requiredOpts: [], + otherOpts: ['dflt'], + coerceFunction: function(v, propOut, dflt) { + if(v==='auto') propOut.set('auto'); + else if(!isNumeric(v)) propOut.set(dflt); + else { + if(Math.abs(v)>180) v -= Math.round(v/360)*360; + propOut.set(+v); + } + } + }, + axisid: { + description: [ + 'An axis id string (e.g. \'x\', \'x2\', \'x3\', ...).' + ].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; + } + } + propOut.set(dflt); + } + }, + flaglist: { + description: [ + 'A string representing a combination of flags', + '(order does not matter here).', + 'Combine any of the available `flags` with *+*.', + '(e.g. (\'lines+markers\')).', + 'Values in `extras` cannot be combined.' + ].join(' '), + requiredOpts: ['flags'], + otherOpts: ['dflt', 'extras'], + coerceFunction: function(v, propOut, dflt, opts) { + if(typeof v !== 'string') { + propOut.set(dflt); + return; + } + if(opts.extras.indexOf(v)!==-1) { + propOut.set(v); + return; + } + var vParts = v.split('+'), + i = 0; + while(i3 digits, though javascript dates truncate to milliseconds + * returns false if it doesn't find a date + * + * 2-digit to 4-digit year conversion, where to cut off? + * from http://support.microsoft.com/kb/244664: + * 1930-2029 (the most retro of all...) + * but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')): + * 1950-2049 + * by Java, from http://stackoverflow.com/questions/2024273/: + * now-80 - now+20 + * or FileMaker Pro, from + * http://www.filemaker.com/12help/html/add_view_data.4.21.html: + * now-70 - now+30 + * but python strptime etc, via + * http://docs.python.org/py3k/library/time.html: + * 1969-2068 (super forward-looking, but static, not sliding!) + * + * lets go with now-70 to now+30, and if anyone runs into this problem + * they can learn the hard way not to use 2-digit years, as no choice we + * make now will cover all possibilities. mostly this will all be taken + * care of in initial parsing, should only be an issue for hand-entered data + * currently (2012) this range is: + * 1942-2041 + */ + +exports.dateTime2ms = function(s) { + // first check if s is a date object + try { + if (s.getTime) return +s; + } + catch(e) { + return false; + } + + var y, m, d, h; + // split date and time parts + var datetime = String(s).split(' '); + if (datetime.length > 2) return false; + + var p = datetime[0].split('-'); // date part + if (p.length > 3 || (p.length !== 3 && datetime[1])) return false; + + // year + if (p[0].length === 4) y = Number(p[0]); + else if (p[0].length === 2) { + var yNow = new Date().getFullYear(); + y = ((Number(p[0]) - yNow + 70)%100 + 200)%100 + yNow - 70; + } + else return false; + if (!isNumeric(y)) return false; + if (p.length === 1) return new Date(y,0,1).getTime(); // year only + + // month + m = Number(p[1]) - 1; // new Date() uses zero-based months + if (p[1].length > 2 || !(m >= 0 && m <= 11)) return false; + if (p.length === 2) return new Date(y, m, 1).getTime(); // year-month + + // day + d = Number(p[2]); + if (p[2].length > 2 || !(d >= 1 && d <= 31)) return false; + + // now save the date part + d = new Date(y, m, d).getTime(); + if (!datetime[1]) return d; // year-month-day + p = datetime[1].split(':'); + if (p.length > 3) return false; + + // hour + h = Number(p[0]); + if (p[0].length > 2 || !(h >= 0 && h <= 23)) return false; + d += 3600000*h; + if (p.length === 1) return d; + + // minute + m = Number(p[1]); + if (p[1].length > 2 || !(m >= 0 && m <= 59)) return false; + d += 60000*m; + if (p.length === 2) return d; + + // second + s = Number(p[2]); + if (!(s >= 0 && s < 60)) return false; + return d+s*1000; +}; + +// is string s a date? (see above) +exports.isDateTime = function(s) { + return (exports.dateTime2ms(s) !== false); +}; + +// pad a number with zeroes, to given # of digits before the decimal point +function lpad(val, digits){ + return String(val + Math.pow(10, digits)).substr(1); +} + +/** + * Turn ms into string of the form YYYY-mm-dd HH:MM:SS.sss + * Crop any trailing zeros in time, but always leave full date + * (we could choose to crop '-01' from date too)... + * Optional range r is the data range that applies, also in ms. + * If rng is big, the later parts of time will be omitted + */ +exports.ms2DateTime = function(ms, r) { + if(typeof(d3)==='undefined'){ + console.log('d3 is not defined'); + return; + } + + if(!r) r=0; + var d = new Date(ms), + s = d3.time.format('%Y-%m-%d')(d); + if(r<7776000000) { + // <90 days: add hours + s+=' '+lpad(d.getHours(),2); + if(r<432000000) { + // <5 days: add minutes + s+=':'+lpad(d.getMinutes(),2); + if(r<10800000) { + // <3 hours: add seconds + s+=':'+lpad(d.getSeconds(),2); + if(r<300000) { + // <5 minutes: add ms + s+='.'+lpad(d.getMilliseconds(),3); + } + } + } + // strip trailing zeros + return s.replace(/([:\s]00)*\.?[0]*$/,''); + } + return s; +}; + +/** + * parseDate: forgiving attempt to turn any date string + * into a javascript date object + * + * first collate all the date formats we want to support, precompiled + * to d3 format objects see below for the string cleaning that happens + * before this separate out 2-digit (y) and 4-digit-year (Y) formats, + * formats with month names (b), and formats with am/pm (I) or no time (D) + * (also includes hour only, as the test is really for a colon) so we can + * cut down the number of tests we need to run for any given string + * (right now all are between 15 and 32 tests) + */ + +// TODO: this is way out of date vs. the server-side version +var timeFormats = { + // 24 hour + H:['%H:%M:%S~%L', '%H:%M:%S', '%H:%M'], + // with am/pm + I:['%I:%M:%S~%L%p', '%I:%M:%S%p', '%I:%M%p'], + // no colon, ie only date or date with hour (could also support eg 12h34m?) + D:['%H', '%I%p', '%Hh'] +}; + +var dateFormats = { + Y:[ + '%Y~%m~%d', + '%Y%m%d', + '%y%m%d', // YYMMDD, has 6 digits together so will match Y, not y + '%m~%d~%Y', // MM/DD/YYYY has first precedence + '%d~%m~%Y' // then DD/MM/YYYY + ], + Yb:[ + '%b~%d~%Y', // eg nov 21 2013 + '%d~%b~%Y', // eg 21 nov 2013 + '%Y~%d~%b', // eg 2013 21 nov (or 2013 q3, after replacement) + '%Y~%b~%d' // eg 2013 nov 21 + ], + /** + * the two-digit year cases have so many potential ambiguities + * it's not even funny, but we'll try them anyway. + */ + y:[ + '%m~%d~%y', + '%d~%m~%y', + '%y~%m~%d' + ], + yb:[ + '%b~%d~%y', + '%d~%b~%y', + '%y~%d~%b', + '%y~%b~%d' + ] +}; + +// use utc formatter since we're ignoring timezone info +var formatter = d3.time.format.utc; + +/** + * ISO8601 and YYYYMMDDHHMMSS are the only ones where date and time + * are not separated by a space, so they get inserted specially here. + * Also a couple formats with no day (so time makes no sense) + */ +var dateTimeFormats = { + Y: { + H: ['%Y~%m~%dT%H:%M:%S', '%Y~%m~%dT%H:%M:%S~%L'].map(formatter), + I: [], + D: ['%Y%m%d%H%M%S', '%Y~%m', '%m~%Y'].map(formatter) + }, + Yb: {H: [], I: [], D: ['%Y~%b', '%b~%Y'].map(formatter)}, + y: {H: [], I: [], D: []}, + yb: {H: [], I: [], D: []} +}; +// all others get inserted in all possible combinations from dateFormats and timeFormats +['Y', 'Yb', 'y', 'yb'].forEach(function(dateType) { + dateFormats[dateType].forEach(function(dateFormat) { + // just a date (don't do just a time) + dateTimeFormats[dateType].D.push(formatter(dateFormat)); + ['H', 'I', 'D'].forEach(function(timeType) { + timeFormats[timeType].forEach(function(timeFormat) { + var a = dateTimeFormats[dateType][timeType]; + // 'date time', then 'time date' + a.push(formatter(dateFormat+'~'+timeFormat)); + a.push(formatter(timeFormat+'~'+dateFormat)); + }); + }); + }); +}); + +// precompiled regexps for performance +var matchword = /[a-z]*/g, + shortenword = function(m) { return m.substr(0,3); }, + weekdaymatch = /(mon|tue|wed|thu|fri|sat|sun|the|of|st|nd|rd|th)/g, + separatormatch = /[\s,\/\-\.\(\)]+/g, + ampmmatch = /~?([ap])~?m(~|$)/, + replaceampm = function(m,ap) { return ap+'m '; }, + match4Y = /\d\d\d\d/, + matchMonthName = /(^|~)[a-z]{3}/, + matchAMPM = /[ap]m/, + matchcolon = /:/, + matchquarter = /q([1-4])/, + quarters = ['31~mar','30~jun','30~sep','31~dec'], + replacequarter = function(m,n) { return quarters[n-1]; }, + matchTZ = / ?([+\-]\d\d:?\d\d|Z)$/; + +function getDateType(v) { + var dateType; + dateType = (match4Y.test(v) ? 'Y' : 'y'); + dateType = dateType + (matchMonthName.test(v) ? 'b' : ''); + return dateType; +} + +function getTimeType(v) { + var timeType; + timeType = matchcolon.test(v) ? (matchAMPM.test(v) ? 'I' : 'H') : 'D'; + return timeType; +} + +exports.parseDate = function(v) { + // is it already a date? just return it + if (v.getTime) return v; + /** + * otherwise, if it's not a string, return nothing + * the case of numbers that just have years will get + * dealt with elsewhere. + */ + if (typeof v !== 'string') return false; + + // first clean up the string a bit to reduce the number of formats we have to test + v = v.toLowerCase() + /** + * cut all words down to 3 characters - this will result in + * some spurious matches, ie whenever the first three characters + * of a word match a month or weekday but that seems more likely + * to fix typos than to make dates where they shouldn't be... + * and then we can omit the long form of months from our testing + */ + .replace(matchword, shortenword) + /** + * remove weekday names, as they get overridden anyway if they're + * inconsistent also removes a few more words + * (ie "tuesday the 26th of november") + * TODO: language support? + * for months too, but these seem to be built into d3 + */ + .replace(weekdaymatch, '') + /** + * collapse all separators one ~ at a time, except : which seems + * pretty consistent for the time part use ~ instead of space or + * something since d3 can eat a space as padding on 1-digit numbers + */ + .replace(separatormatch, '~') + // in case of a.m. or p.m. (also take off any space before am/pm) + .replace(ampmmatch, replaceampm) + // turn quarters Q1-4 into dates (quarter ends) + .replace(matchquarter, replacequarter) + .trim() + // also try to ignore timezone info, at least for now + .replace(matchTZ, ''); + + // now test against the various formats that might match + var out = null, + dateType = getDateType(v), + timeType = getTimeType(v), + formatList, + len; + + formatList = dateTimeFormats[dateType][timeType]; + len = formatList.length; + + for (var i = 0; i < len; i++) { + out = formatList[i].parse(v); + if (out) break; + } + + // If not an instance of Date at this point, just return it. + if (!(out instanceof Date)) return false; + // parse() method interprets arguments with local time zone. + var tzoff = out.getTimezoneOffset(); + // In general (default) this is not what we want, so force into UTC: + out.setTime(out.getTime() + tzoff * 60 * 1000); + return out; +}; diff --git a/src/events.js b/src/lib/events.js similarity index 100% rename from src/events.js rename to src/lib/events.js diff --git a/src/geo/lib/location-utils.js b/src/lib/geo_location_utils.js similarity index 89% rename from src/geo/lib/location-utils.js rename to src/lib/geo_location_utils.js index 0ae35fd6661..6bb51cc3aa8 100644 --- a/src/geo/lib/location-utils.js +++ b/src/lib/geo_location_utils.js @@ -2,9 +2,10 @@ var locationUtils = module.exports = {}; -var Plotly = require('../../plotly'), - // an hash object iso3 to regex string - countryNameData = require('../raw/country-name_to_iso3.json'); +var Plotly = require('../plotly'); + +// an hash object iso3 to regex string +var countryNameData = require('../constants/country-name_to_iso3.json'); // make list of all country iso3 ids from at runtime var countryIds = Object.keys(countryNameData); diff --git a/src/gl3d/lib/format-color.js b/src/lib/gl_format_color.js similarity index 93% rename from src/gl3d/lib/format-color.js rename to src/lib/gl_format_color.js index 08a7bc7b15c..18ff6897292 100644 --- a/src/gl3d/lib/format-color.js +++ b/src/lib/gl_format_color.js @@ -1,12 +1,12 @@ 'use strict'; -var Plotly = require('../../plotly'); +var Plotly = require('../plotly'); var tinycolor = require('tinycolor2'); var isNumeric = require('fast-isnumeric'); var str2RgbaArray = require('./str2rgbarray'); -var colorDflt = Plotly.Color.defaultLine, - opacityDflt = 1; +var colorDflt = require('../components/color/attributes').defaultLine; +var opacityDflt = 1; function calculateColor(colorIn, opacityIn) { var colorOut = str2RgbaArray(colorIn); diff --git a/src/gl3d/lib/html2unicode.js b/src/lib/html2unicode.js similarity index 100% rename from src/gl3d/lib/html2unicode.js rename to src/lib/html2unicode.js diff --git a/src/lib/index.js b/src/lib/index.js new file mode 100644 index 00000000000..44ed10f12c7 --- /dev/null +++ b/src/lib/index.js @@ -0,0 +1,425 @@ +'use strict'; + +var d3 = require('d3'); + +var lib = module.exports = {}; + +lib.nestedProperty = require('./nested_property'); +lib.isPlainObject = require('./is_plain_object'); + +var coerceModule = require('./coerce'); +lib.valObjects = coerceModule.valObjects; +lib.coerce = coerceModule.coerce; +lib.coerce2 = coerceModule.coerce2; +lib.coerceFont = coerceModule.coerceFont; + +var datesModule = require('./dates'); +lib.dateTime2ms = datesModule.dateTime2ms; +lib.isDateTime = datesModule.isDateTime; +lib.ms2DateTime = datesModule.ms2DateTime; +lib.parseDate = datesModule.parseDate; + +var searchModule = require('./search'); +lib.findBin = searchModule.findBin; +lib.sorterAsc = searchModule.sorterAsc; +lib.distinctVals = searchModule.distinctVals; +lib.roundUp = searchModule.roundUp; + +var statsModule = require('./stats'); +lib.aggNums = statsModule.aggNums; +lib.len = statsModule.len; +lib.mean = statsModule.mean; +lib.variance = statsModule.variance; +lib.stdev = statsModule.stdev; +lib.interp = statsModule.interp; + +var matrixModule = require('./matrix'); +lib.init2dArray = matrixModule.init2dArray; +lib.transposeRagged = matrixModule.transposeRagged; +lib.dot = matrixModule.dot; +lib.translationMatrix = matrixModule.translationMatrix; +lib.rotationMatrix = matrixModule.rotationMatrix; +lib.rotationXYMatrix = matrixModule.rotationXYMatrix; +lib.apply2DTransform = matrixModule.apply2DTransform; +lib.apply2DTransform2 = matrixModule.apply2DTransform2; + +var extendModule = require('./extend'); +lib.extendFlat = extendModule.extendFlat; +lib.extendDeep = extendModule.extendDeep; +lib.extendDeepAll = extendModule.extendDeepAll; + +lib.notifier = require('./notifier'); + +/** + * swap x and y of the same attribute in container cont + * specify attr with a ? in place of x/y + * you can also swap other things than x/y by providing part1 and part2 + */ +lib.swapAttrs = function(cont, attrList, part1, part2) { + if(!part1) part1 = 'x'; + if(!part2) part2 = 'y'; + for(var i = 0; i < attrList.length; i++) { + var attr = attrList[i], + xp = lib.nestedProperty(cont, attr.replace('?', part1)), + yp = lib.nestedProperty(cont, attr.replace('?', part2)), + temp = xp.get(); + xp.set(yp.get()); + yp.set(temp); + } +}; + +/** + * to prevent event bubbling, in particular text selection during drag. + * see http://stackoverflow.com/questions/5429827/ + * how-can-i-prevent-text-element-selection-with-cursor-drag + * for maximum effect use: + * return pauseEvent(e); + */ +lib.pauseEvent = function(e){ + if(e.stopPropagation) e.stopPropagation(); + if(e.preventDefault) e.preventDefault(); + e.cancelBubble = true; + return false; +}; + +/** + * ------------------------------------------ + * debugging tools + * ------------------------------------------ + */ + +// set VERBOSE to true to get a lot more logging and tracing +lib.VERBOSE = false; + +// first markTime call will return time from page load +lib.TIMER = new Date().getTime(); + +// console.log that only runs if VERBOSE is on +lib.log = function(){ + if(lib.VERBOSE) console.log.apply(console, arguments); +}; + +/** + * markTime - for debugging, mark the number of milliseconds + * since the previous call to markTime and log arbitrary info too + */ +lib.markTime = function(v){ + if(!lib.VERBOSE) return; + var t2 = new Date().getTime(); + console.log(v, t2 - lib.TIMER, '(msec)'); + if(lib.VERBOSE === 'trace') console.trace(); + lib.TIMER = t2; +}; + +// constrain - restrict a number v to be between v0 and v1 +lib.constrain = function(v, v0, v1) { + if(v0 > v1) return Math.max(v1, Math.min(v0, v)); + return Math.max(v0, Math.min(v1, v)); +}; + +/** + * do two bounding boxes from getBoundingClientRect, + * ie {left,right,top,bottom,width,height}, overlap? + * takes optional padding pixels + */ +lib.bBoxIntersect = function(a, b, pad){ + pad = pad || 0; + return (a.left <= b.right + pad && + b.left <= a.right + pad && + a.top <= b.bottom + pad && + b.top <= a.bottom + pad); +}; + +// minor convenience/performance booster for d3... +lib.identity = function(d) { return d; }; + +// random string generator +lib.randstr = function randstr(existing, bits, base) { + /* + * Include number of bits, the base of the string you want + * and an optional array of existing strings to avoid. + */ + if (!base) base = 16; + if (bits === undefined) bits = 24; + if (bits <= 0) return '0'; + + var digits = Math.log(Math.pow(2, bits)) / Math.log(base), + res = '', + i, + b, + x; + + for (i = 2; digits === Infinity; i *= 2) { + digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; + } + + var rem = digits - Math.floor(digits); + + for (i = 0; i < Math.floor(digits); i++) { + x = Math.floor(Math.random() * base).toString(base); + res = x + res; + } + + if (rem) { + b = Math.pow(base, rem); + x = Math.floor(Math.random() * b).toString(base); + res = x + res; + } + + var parsed = parseInt(res, base); + if ( (existing && (existing.indexOf(res) > -1)) || + (parsed !== Infinity && parsed >= Math.pow(2, bits)) ) { + return randstr(existing, bits, base); + } + else return res; +}; + +lib.OptionControl = function(opt, optname) { + /* + * An environment to contain all option setters and + * getters that collectively modify opts. + * + * You can call up opts from any function in new object + * as this.optname || this.opt + * + * See FitOpts for example of usage + */ + if (!opt) opt = {}; + if (!optname) optname = 'opt'; + + var self = {}; + self.optionList = []; + + self._newoption = function(optObj) { + optObj[optname] = opt; + self[optObj.name] = optObj; + self.optionList.push(optObj); + }; + + self['_'+optname] = opt; + return self; +}; + +/** + * lib.smooth: smooth arrayIn by convolving with + * a hann window with given full width at half max + * bounce the ends in, so the output has the same length as the input + */ +lib.smooth = function(arrayIn, FWHM) { + FWHM = Math.round(FWHM) || 0; // only makes sense for integers + if(FWHM < 2) return arrayIn; + + var alen = arrayIn.length, + alen2 = 2 * alen, + wlen = 2 * FWHM - 1, + w = new Array(wlen), + arrayOut = new Array(alen), + i, + j, + k, + v; + + // first make the window array + for(i = 0; i < wlen; i++) { + w[i] = (1 - Math.cos(Math.PI * (i + 1) / FWHM)) / (2 * FWHM); + } + + // now do the convolution + for(i = 0; i < alen; i++) { + v = 0; + for(j = 0; j < wlen; j++) { + k = i + j + 1 - FWHM; + + // multibounce + if(k < -alen) k -= alen2 * Math.round(k / alen2); + else if(k >= alen2) k -= alen2 * Math.floor(k / alen2); + + // single bounce + if(k < 0) k = - 1 - k; + else if(k >= alen) k = alen2 - 1 - k; + + v += arrayIn[k] * w[j]; + } + arrayOut[i] = v; + } + + return arrayOut; +}; + +// helpers for promises + +/** + * promiseError: log errors properly inside promises + * use: + * .then(undefined,Plotly.Lib.promiseError) (for IE compatibility) + * or .catch(Plotly.Lib.promiseError) + * TODO: I guess we need another step to send this error to Sentry? + */ +lib.promiseError = function(err) { console.log(err, err.stack); }; + +/** + * syncOrAsync: run a sequence of functions synchronously + * as long as its returns are not promises (ie have no .then) + * includes one argument arg to send to all functions... + * this is mainly just to prevent us having to make wrapper functions + * when the only purpose of the wrapper is to reference gd / td + * and a final step to be executed at the end + * TODO: if there's an error and everything is sync, + * this doesn't happen yet because we want to make sure + * that it gets reported + */ +lib.syncOrAsync = function(sequence, arg, finalStep) { + var ret, fni; + + function continueAsync(){ + lib.markTime('async done ' + fni.name); + return lib.syncOrAsync(sequence, arg, finalStep); + } + while(sequence.length) { + fni = sequence.splice(0, 1)[0]; + ret = fni(arg); + // lib.markTime('done calling '+fni.name) + if(ret && ret.then) { + return ret.then(continueAsync) + .then(undefined, lib.promiseError); + } + lib.markTime('sync done ' + fni.name); + } + + return finalStep && finalStep(arg); +}; + + +/** + * Helper to strip trailing slash, from + * http://stackoverflow.com/questions/6680825/return-string-without-trailing-slash + */ +lib.stripTrailingSlash = function (str) { + if (str.substr(-1) === '/') return str.substr(0, str.length - 1); + return str; +}; + +lib.noneOrAll = function(containerIn, containerOut, attrList) { + /** + * some attributes come together, so if you have one of them + * in the input, you should copy the default values of the others + * to the input as well. + */ + if(!containerIn) return; + + var hasAny = false, + hasAll = true, + i, + val; + + for(i = 0; i < attrList.length; i++) { + val = containerIn[attrList[i]]; + if(val !== undefined && val !== null) hasAny = true; + else hasAll = false; + } + + if(hasAny && !hasAll) { + for(i = 0; i < attrList.length; i++) { + containerIn[attrList[i]] = containerOut[attrList[i]]; + } + } +}; + +lib.mergeArray = function(traceAttr, cd, cdAttr) { + if(Array.isArray(traceAttr)) { + var imax = Math.min(traceAttr.length, cd.length); + for(var i=0; i or 1 elements + * because extend-like algorithms are hella slow + * obj2 is assumed to already be clean of these things (including no arrays) + */ +lib.minExtend = function(obj1, obj2) { + var objOut = {}; + if(typeof obj2 !== 'object') obj2 = {}; + var arrayLen = 3, + keys = Object.keys(obj1), + i, + k, + v; + for(i = 0; i < keys.length; i++) { + k = keys[i]; + v = obj1[k]; + if(k.charAt(0)==='_' || typeof v === 'function') continue; + else if(k==='module') objOut[k] = v; + else if(Array.isArray(v)) objOut[k] = v.slice(0,arrayLen); + else if(v && (typeof v === 'object')) objOut[k] = lib.minExtend(obj1[k], obj2[k]); + else objOut[k] = v; + } + + keys = Object.keys(obj2); + for(i = 0; i < keys.length; i++) { + k = keys[i]; + v = obj2[k]; + if(typeof v !== 'object' || !(k in objOut) || typeof objOut[k] !== 'object') { + objOut[k] = v; + } + } + + return objOut; +}; + +lib.titleCase = function(s) { + return s.charAt(0).toUpperCase() + s.substr(1); +}; + +lib.containsAny = function(s, fragments) { + for(var i = 0; i < fragments.length; i++) { + if(s.indexOf(fragments[i])!== -1) return true; + } + return false; +}; + +// get the parent Plotly plot of any element. Whoo jquery-free tree climbing! +lib.getPlotDiv = function(el) { + for(; el && el.removeAttribute; el = el.parentNode) { + if(lib.isPlotDiv(el)) return el; + } +}; + +lib.isPlotDiv = function(el) { + var el3 = d3.select(el); + return el3.size() && el3.classed('js-plotly-plot'); +}; + +lib.removeElement = function(el) { + var elParent = el && el.parentNode; + if(elParent) elParent.removeChild(el); +}; + +/** + * for dynamically adding style rules + * makes one stylesheet that contains all rules added + * by all calls to this function + */ +lib.addStyleRule = function(selector, styleString) { + if(!lib.styleSheet) { + var style = document.createElement('style'); + // WebKit hack :( + style.appendChild(document.createTextNode('')); + document.head.appendChild(style); + lib.styleSheet = style.sheet; + } + var styleSheet = lib.styleSheet; + + if(styleSheet.insertRule) { + styleSheet.insertRule(selector+'{'+styleString+'}',0); + } + else if(styleSheet.addRule) { + styleSheet.addRule(selector,styleString,0); + } + else console.warn('addStyleRule failed'); +}; + +lib.isIE = function() { + return typeof window.navigator.msSaveBlob !== 'undefined'; +}; diff --git a/src/lib/lib.js b/src/lib/lib.js deleted file mode 100644 index df380b74758..00000000000 --- a/src/lib/lib.js +++ /dev/null @@ -1,1607 +0,0 @@ -'use strict'; - -var Plotly = require('../plotly'); -var d3 = require('d3'); -var tinycolor = require('tinycolor2'); -var isNumeric = require('fast-isnumeric'); - -var lib = module.exports = {}; - -/** - * dateTime2ms - turn a date object or string s of the form - * YYYY-mm-dd HH:MM:SS.sss into milliseconds (relative to 1970-01-01, - * per javascript standard) - * may truncate after any full field, and sss can be any length - * even >3 digits, though javascript dates truncate to milliseconds - * returns false if it doesn't find a date - * - * 2-digit to 4-digit year conversion, where to cut off? - * from http://support.microsoft.com/kb/244664: - * 1930-2029 (the most retro of all...) - * but in my mac chrome from eg. d=new Date(Date.parse('8/19/50')): - * 1950-2049 - * by Java, from http://stackoverflow.com/questions/2024273/: - * now-80 - now+20 - * or FileMaker Pro, from - * http://www.filemaker.com/12help/html/add_view_data.4.21.html: - * now-70 - now+30 - * but python strptime etc, via - * http://docs.python.org/py3k/library/time.html: - * 1969-2068 (super forward-looking, but static, not sliding!) - * - * lets go with now-70 to now+30, and if anyone runs into this problem - * they can learn the hard way not to use 2-digit years, as no choice we - * make now will cover all possibilities. mostly this will all be taken - * care of in initial parsing, should only be an issue for hand-entered data - * currently (2012) this range is: - * 1942-2041 - */ - -lib.dateTime2ms = function(s) { - // first check if s is a date object - try { - if (s.getTime) return +s; - } - catch(e) { - return false; - } - - var y, m, d, h; - // split date and time parts - var datetime = String(s).split(' '); - if (datetime.length > 2) return false; - - var p = datetime[0].split('-'); // date part - if (p.length > 3 || (p.length !== 3 && datetime[1])) return false; - - // year - if (p[0].length === 4) y = Number(p[0]); - else if (p[0].length === 2) { - var yNow = new Date().getFullYear(); - y = ((Number(p[0]) - yNow + 70)%100 + 200)%100 + yNow - 70; - } - else return false; - if (!isNumeric(y)) return false; - if (p.length === 1) return new Date(y,0,1).getTime(); // year only - - // month - m = Number(p[1]) - 1; // new Date() uses zero-based months - if (p[1].length > 2 || !(m >= 0 && m <= 11)) return false; - if (p.length === 2) return new Date(y, m, 1).getTime(); // year-month - - // day - d = Number(p[2]); - if (p[2].length > 2 || !(d >= 1 && d <= 31)) return false; - - // now save the date part - d = new Date(y, m, d).getTime(); - if (!datetime[1]) return d; // year-month-day - p = datetime[1].split(':'); - if (p.length > 3) return false; - - // hour - h = Number(p[0]); - if (p[0].length > 2 || !(h >= 0 && h <= 23)) return false; - d += 3600000*h; - if (p.length === 1) return d; - - // minute - m = Number(p[1]); - if (p[1].length > 2 || !(m >= 0 && m <= 59)) return false; - d += 60000*m; - if (p.length === 2) return d; - - // second - s = Number(p[2]); - if (!(s >= 0 && s < 60)) return false; - return d+s*1000; -}; - -// is string s a date? (see above) -lib.isDateTime = function(s) { - return (lib.dateTime2ms(s) !== false); -}; - -/** - * Turn ms into string of the form YYYY-mm-dd HH:MM:SS.sss - * Crop any trailing zeros in time, but always leave full date - * (we could choose to crop '-01' from date too)... - * Optional range r is the data range that applies, also in ms. - * If rng is big, the later parts of time will be omitted - */ -lib.ms2DateTime = function(ms,r) { - if(typeof(d3)==='undefined'){ - console.log('d3 is not defined'); - return; - } - - if(!r) r=0; - var d = new Date(ms), - s = d3.time.format('%Y-%m-%d')(d); - if(r<7776000000) { - // <90 days: add hours - s+=' '+lib.lpad(d.getHours(),2); - if(r<432000000) { - // <5 days: add minutes - s+=':'+lib.lpad(d.getMinutes(),2); - if(r<10800000) { - // <3 hours: add seconds - s+=':'+lib.lpad(d.getSeconds(),2); - if(r<300000) { - // <5 minutes: add ms - s+='.'+lib.lpad(d.getMilliseconds(),3); - } - } - } - // strip trailing zeros - return s.replace(/([:\s]00)*\.?[0]*$/,''); - } - return s; -}; - -/** - * Plotly.Lib.parseDate: forgiving attempt to turn any date string - * into a javascript date object - * - * first collate all the date formats we want to support, precompiled - * to d3 format objects see below for the string cleaning that happens - * before this separate out 2-digit (y) and 4-digit-year (Y) formats, - * formats with month names (b), and formats with am/pm (I) or no time (D) - * (also includes hour only, as the test is really for a colon) so we can - * cut down the number of tests we need to run for any given string - * (right now all are between 15 and 32 tests) - */ - -// TODO: this is way out of date vs. the server-side version -var timeFormats = { - // 24 hour - H:['%H:%M:%S~%L', '%H:%M:%S', '%H:%M'], - // with am/pm - I:['%I:%M:%S~%L%p', '%I:%M:%S%p', '%I:%M%p'], - // no colon, ie only date or date with hour (could also support eg 12h34m?) - D:['%H', '%I%p', '%Hh'] -}; -var dateFormats = { - Y:[ - '%Y~%m~%d', - '%Y%m%d', - '%y%m%d', // YYMMDD, has 6 digits together so will match Y, not y - '%m~%d~%Y', // MM/DD/YYYY has first precedence - '%d~%m~%Y' // then DD/MM/YYYY - ], - Yb:[ - '%b~%d~%Y', // eg nov 21 2013 - '%d~%b~%Y', // eg 21 nov 2013 - '%Y~%d~%b', // eg 2013 21 nov (or 2013 q3, after replacement) - '%Y~%b~%d' // eg 2013 nov 21 - ], - /** - * the two-digit year cases have so many potential ambiguities - * it's not even funny, but we'll try them anyway. - */ - y:[ - '%m~%d~%y', - '%d~%m~%y', - '%y~%m~%d' - ], - yb:[ - '%b~%d~%y', - '%d~%b~%y', - '%y~%d~%b', - '%y~%b~%d' - ] -}; - -// use utc formatter since we're ignoring timezone info -var formatter = d3.time.format.utc; - -/** - * ISO8601 and YYYYMMDDHHMMSS are the only ones where date and time - * are not separated by a space, so they get inserted specially here. - * Also a couple formats with no day (so time makes no sense) - */ -var dateTimeFormats = { - Y: { - H: ['%Y~%m~%dT%H:%M:%S', '%Y~%m~%dT%H:%M:%S~%L'].map(formatter), - I: [], - D: ['%Y%m%d%H%M%S', '%Y~%m', '%m~%Y'].map(formatter) - }, - Yb: {H: [], I: [], D: ['%Y~%b', '%b~%Y'].map(formatter)}, - y: {H: [], I: [], D: []}, - yb: {H: [], I: [], D: []} -}; -// all others get inserted in all possible combinations from dateFormats and timeFormats -['Y', 'Yb', 'y', 'yb'].forEach(function(dateType) { - dateFormats[dateType].forEach(function(dateFormat) { - // just a date (don't do just a time) - dateTimeFormats[dateType].D.push(formatter(dateFormat)); - ['H', 'I', 'D'].forEach(function(timeType) { - timeFormats[timeType].forEach(function(timeFormat) { - var a = dateTimeFormats[dateType][timeType]; - // 'date time', then 'time date' - a.push(formatter(dateFormat+'~'+timeFormat)); - a.push(formatter(timeFormat+'~'+dateFormat)); - }); - }); - }); -}); - -// precompiled regexps for performance -var matchword = /[a-z]*/g, - shortenword = function(m) { return m.substr(0,3); }, - weekdaymatch = /(mon|tue|wed|thu|fri|sat|sun|the|of|st|nd|rd|th)/g, - separatormatch = /[\s,\/\-\.\(\)]+/g, - ampmmatch = /~?([ap])~?m(~|$)/, - replaceampm = function(m,ap) { return ap+'m '; }, - match4Y = /\d\d\d\d/, - matchMonthName = /(^|~)[a-z]{3}/, - matchAMPM = /[ap]m/, - matchcolon = /:/, - matchquarter = /q([1-4])/, - quarters = ['31~mar','30~jun','30~sep','31~dec'], - replacequarter = function(m,n) { return quarters[n-1]; }, - matchTZ = / ?([+\-]\d\d:?\d\d|Z)$/; - -function getDateType(v) { - var dateType; - dateType = (match4Y.test(v) ? 'Y' : 'y'); - dateType = dateType + (matchMonthName.test(v) ? 'b' : ''); - return dateType; -} - -function getTimeType(v) { - var timeType; - timeType = matchcolon.test(v) ? (matchAMPM.test(v) ? 'I' : 'H') : 'D'; - return timeType; -} - -lib.parseDate = function(v) { - // is it already a date? just return it - if (v.getTime) return v; - /** - * otherwise, if it's not a string, return nothing - * the case of numbers that just have years will get - * dealt with elsewhere. - */ - if (typeof v !== 'string') return false; - - // first clean up the string a bit to reduce the number of formats we have to test - v = v.toLowerCase() - /** - * cut all words down to 3 characters - this will result in - * some spurious matches, ie whenever the first three characters - * of a word match a month or weekday but that seems more likely - * to fix typos than to make dates where they shouldn't be... - * and then we can omit the long form of months from our testing - */ - .replace(matchword, shortenword) - /** - * remove weekday names, as they get overridden anyway if they're - * inconsistent also removes a few more words - * (ie "tuesday the 26th of november") - * TODO: language support? - * for months too, but these seem to be built into d3 - */ - .replace(weekdaymatch, '') - /** - * collapse all separators one ~ at a time, except : which seems - * pretty consistent for the time part use ~ instead of space or - * something since d3 can eat a space as padding on 1-digit numbers - */ - .replace(separatormatch, '~') - // in case of a.m. or p.m. (also take off any space before am/pm) - .replace(ampmmatch, replaceampm) - // turn quarters Q1-4 into dates (quarter ends) - .replace(matchquarter, replacequarter) - .trim() - // also try to ignore timezone info, at least for now - .replace(matchTZ, ''); - - // now test against the various formats that might match - var out = null, - dateType = getDateType(v), - timeType = getTimeType(v), - formatList, - len; - - formatList = dateTimeFormats[dateType][timeType]; - len = formatList.length; - - for (var i = 0; i < len; i++) { - out = formatList[i].parse(v); - if (out) break; - } - - // If not an instance of Date at this point, just return it. - if (!(out instanceof Date)) return false; - // parse() method interprets arguments with local time zone. - var tzoff = out.getTimezoneOffset(); - // In general (default) this is not what we want, so force into UTC: - out.setTime(out.getTime() + tzoff * 60 * 1000); - return out; -}; - -/** - * findBin - find the bin for val - note that it can return outside the - * bin range any pos. or neg. integer for linear bins, or -1 or - * bins.length-1 for explicit. - * bins is either an object {start,size,end} or an array length #bins+1 - * bins can be either increasing or decreasing but must be monotonic - * for linear bins, we can just calculate. For listed bins, run a binary - * search linelow (truthy) says the bin boundary should be attributed to - * the lower bin rather than the default upper bin - */ -lib.findBin = function(val, bins, linelow) { - if(isNumeric(bins.start)) { - return linelow ? - Math.ceil((val - bins.start) / bins.size) - 1 : - Math.floor((val - bins.start) / bins.size); - } - else { - var n1 = 0, - n2 = bins.length, - c = 0, - n, - test; - if(bins[bins.length - 1] > bins[0]) { - test = linelow ? lessThan : lessOrEqual; - } else { - test = linelow ? greaterOrEqual : greaterThan; - } - // c is just to avoid infinite loops if there's an error - while(n1 < n2 && c++ < 100){ - n = Math.floor((n1 + n2) / 2); - if(test(bins[n], val)) n1 = n + 1; - else n2 = n; - } - if(c > 90) console.log('Long binary search...'); - return n1 - 1; - } -}; - -function lessThan(a, b) { return a < b; } -function lessOrEqual(a, b) { return a <= b; } -function greaterThan(a, b) { return a > b; } -function greaterOrEqual(a, b) { return a >= b; } - -lib.sorterAsc = function(a, b) { return a - b; }; - -/** - * find distinct values in an array, lumping together ones that appear to - * just be off by a rounding error - * return the distinct values and the minimum difference between any two - */ -lib.distinctVals = function(valsIn) { - var vals = valsIn.slice(); // otherwise we sort the original array... - vals.sort(lib.sorterAsc); - - var l = vals.length - 1, - minDiff = (vals[l] - vals[0]) || 1, - errDiff = minDiff / (l || 1) / 10000, - v2 = [vals[0]]; - - for(var i = 0; i < l; i++) { - // make sure values aren't just off by a rounding error - if(vals[i + 1] > vals[i] + errDiff) { - minDiff = Math.min(minDiff, vals[i + 1] - vals[i]); - v2.push(vals[i + 1]); - } - } - - return {vals: v2, minDiff: minDiff}; -}; - -/** - * return the smallest element from (sorted) array arrayIn that's bigger than val, - * or (reverse) the largest element smaller than val - * used to find the best tick given the minimum (non-rounded) tick - * particularly useful for date/time where things are not powers of 10 - * binary search is probably overkill here... - */ -lib.roundUp = function(val, arrayIn, reverse){ - var low = 0, - high = arrayIn.length - 1, - mid, - c = 0, - dlow = reverse ? 0 : 1, - dhigh = reverse ? 1 : 0, - rounded = reverse ? Math.ceil : Math.floor; - // c is just to avoid infinite loops if there's an error - while(low < high && c++ < 100){ - mid = rounded((low + high) / 2); - if(arrayIn[mid] <= val) low = mid + dlow; - else high = mid - dhigh; - } - return arrayIn[low]; -}; - -/** - * convert a string s (such as 'xaxis.range[0]') - * representing a property of nested object into set and get methods - * also return the string and object so we don't have to keep track of them - * allows [-1] for an array index, to set a property inside all elements - * of an array - * eg if obj = {arr: [{a: 1}, {a: 2}]} - * you can do p = nestedProperty(obj, 'arr[-1].a') - * but you cannot set the array itself this way, to do that - * just set the whole array. - * eg if obj = {arr: [1, 2, 3]} - * you can't do nestedProperty(obj, 'arr[-1]').set(5) - * but you can do nestedProperty(obj, 'arr').set([5, 5, 5]) - */ -lib.nestedProperty = function(container, propStr) { - if(isNumeric(propStr)) propStr = String(propStr); - else if(typeof propStr !== 'string' || - propStr.substr(propStr.length - 4) === '[-1]') { - throw 'bad property string'; - } - - var j = 0, - propParts = propStr.split('.'), - indexed, - indices, - i; - - // check for parts of the nesting hierarchy that are numbers (ie array elements) - while(j < propParts.length) { - // look for non-bracket chars, then any number of [##] blocks - indexed = String(propParts[j]).match(/^([^\[\]]*)((\[\-?[0-9]*\])+)$/); - if(indexed) { - if(indexed[1]) propParts[j] = indexed[1]; - // allow propStr to start with bracketed array indices - else if(j === 0) propParts.splice(0,1); - else throw 'bad property string'; - - indices = indexed[2] - .substr(1,indexed[2].length-2) - .split(']['); - - for(i=0; i= 0; i--) { - curCont = containerLevels[i]; - remainingKeys = false; - if(Array.isArray(curCont)) { - for(j = curCont.length - 1; j >= 0; j--) { - if(emptyObj(curCont[j])) { - if(remainingKeys) curCont[j] = undefined; - else curCont.pop(); - } - else remainingKeys = true; - } - } - else if(typeof curCont === 'object' && curCont !== null) { - keys = Object.keys(curCont); - remainingKeys = false; - for(j = keys.length - 1; j >= 0; j--) { - if(emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j])) delete curCont[keys[j]]; - else remainingKeys = true; - } - } - if(remainingKeys) return; - } -} - -function emptyObj(obj) { - if(obj===undefined || obj===null) return true; - if(typeof obj !== 'object') return false; // any plain value - if(Array.isArray(obj)) return !obj.length; // [] - return !Object.keys(obj).length; // {} -} - -function badContainer(container, propStr, propParts) { - return { - set: function() { throw 'bad container'; }, - get: function() {}, - astr: propStr, - parts: propParts, - obj: container - }; -} - -/** - * swap x and y of the same attribute in container cont - * specify attr with a ? in place of x/y - * you can also swap other things than x/y by providing part1 and part2 - */ -lib.swapAttrs = function(cont, attrList, part1, part2) { - if(!part1) part1 = 'x'; - if(!part2) part2 = 'y'; - for(var i = 0; i < attrList.length; i++) { - var attr = attrList[i], - xp = lib.nestedProperty(cont, attr.replace('?', part1)), - yp = lib.nestedProperty(cont, attr.replace('?', part2)), - temp = xp.get(); - xp.set(yp.get()); - yp.set(temp); - } -}; - -/** - * to prevent event bubbling, in particular text selection during drag. - * see http://stackoverflow.com/questions/5429827/ - * how-can-i-prevent-text-element-selection-with-cursor-drag - * for maximum effect use: - * return pauseEvent(e); - */ -lib.pauseEvent = function(e){ - if(e.stopPropagation) e.stopPropagation(); - if(e.preventDefault) e.preventDefault(); - e.cancelBubble = true; - return false; -}; - -// pad a number with zeroes, to given # of digits before the decimal point -lib.lpad = function(val, digits){ - return String(val + Math.pow(10, digits)).substr(1); -}; - -// STATISTICS FUNCTIONS - -/** - * aggNums() returns the result of an aggregate function applied to an array of - * values, where non-numerical values have been tossed out. - * - * @param {function} f - aggregation function (e.g., Math.min) - * @param {Number} v - initial value (continuing from previous calls) - * if there's no continuing value, use null for selector-type - * functions (max,min), or 0 for summations - * @param {Array} a - array to aggregate (may be nested, we will recurse, - * but all elements must have the same dimension) - * @param {Number} len - maximum length of a to aggregate - * @return {Number} - result of f applied to a starting from v - */ -lib.aggNums = function(f, v, a, len) { - var i, - b; - if (!len) len = a.length; - if (!isNumeric(v)) v = false; - if (Array.isArray(a[0])) { - b = new Array(len); - for(i = 0; i < len; i++) b[i] = lib.aggNums(f, v, a[i]); - a = b; - } - - for (i = 0; i < len; i++) { - if (!isNumeric(v)) v = a[i]; - else if (isNumeric(a[i])) v = f(+v, +a[i]); - } - return v; -}; - -/** - * mean & std dev functions using aggNums, so it handles non-numerics nicely - * even need to use aggNums instead of .length, to toss out non-numerics - */ -lib.len = function(data) { - return lib.aggNums(function(a){ return a + 1; }, 0, data); -}; - -lib.mean = function(data, len) { - if(!len) len = lib.len(data); - return lib.aggNums(function(a, b){ return a + b; }, 0, data) / len; -}; - -lib.variance = function(data, len, mean) { - if (!len) len = lib.len(data); - if (!isNumeric(mean)) mean = lib.mean(data, len); - - return lib.aggNums(function(a, b) { - return a + Math.pow(b - mean, 2); - }, 0, data)/len; -}; - -lib.stdev = function(data, len, mean) { - return Math.sqrt(lib.variance(data, len, mean)); -}; - -/** - * interp() computes a percentile (quantile) for a given distribution. - * We interpolate the distribution (to compute quantiles, we follow method #10 here: - * http://www.amstat.org/publications/jse/v14n3/langford.html). - * Typically the index or rank (n * arr.length) may be non-integer. - * For reference: ends are clipped to the extreme values in the array; - * For box plots: index you get is half a point too high (see - * http://en.wikipedia.org/wiki/Percentile#Nearest_rank) but note that this definition - * indexes from 1 rather than 0, so we subtract 1/2 (instead of add). - * - * @param {Array} arr - This array contains the values that make up the distribution. - * @param {Number} n - Between 0 and 1, n = p/100 is such that we compute the p^th percentile. - * For example, the 50th percentile (or median) corresponds to n = 0.5 - * @return {Number} - percentile - */ -lib.interp = function(arr, n) { - if (!isNumeric(n)) throw 'n should be a finite number'; - n = n * arr.length - 0.5; - if (n < 0) return arr[0]; - if (n > arr.length - 1) return arr[arr.length - 1]; - var frac = n % 1; - return frac * arr[Math.ceil(n)] + (1 - frac) * arr[Math.floor(n)]; -}; - -/** - * ------------------------------------------ - * debugging tools - * ------------------------------------------ - */ - -// set VERBOSE to true to get a lot more logging and tracing -lib.VERBOSE = false; - -// first markTime call will return time from page load -lib.TIMER = new Date().getTime(); - -// console.log that only runs if VERBOSE is on -lib.log = function(){ - if(lib.VERBOSE) console.log.apply(console, arguments); -}; - -/** - * markTime - for debugging, mark the number of milliseconds - * since the previous call to markTime and log arbitrary info too - */ -lib.markTime = function(v){ - if(!lib.VERBOSE) return; - var t2 = new Date().getTime(); - console.log(v, t2 - lib.TIMER, '(msec)'); - if(lib.VERBOSE === 'trace') console.trace(); - lib.TIMER = t2; -}; - -// constrain - restrict a number v to be between v0 and v1 -lib.constrain = function(v, v0, v1) { - if(v0 > v1) return Math.max(v1, Math.min(v0, v)); - return Math.max(v0, Math.min(v1, v)); -}; - -/** - * notifier - * @param {String} text The person's user name - * @param {Number} [delay=1000] The delay time in milliseconds - * or 'long' which provides 2000 ms delay time. - * @return {undefined} this function does not return a value - */ -var NOTEDATA = []; -lib.notifier = function(text, displayLength) { - - if(NOTEDATA.indexOf(text) !== -1) return; - - NOTEDATA.push(text); - - var ts = 1000; - if (isNumeric(displayLength)) ts = displayLength; - else if (displayLength === 'long') ts = 3000; - - var notifierContainer = d3.select('body') - .selectAll('.plotly-notifier') - .data([0]); - notifierContainer.enter() - .append('div') - .classed('plotly-notifier', true); - - var notes = notifierContainer.selectAll('.notifier-note').data(NOTEDATA); - - function killNote(transition) { - transition - .duration(700) - .style('opacity', 0) - .each('end', function(thisText) { - var thisIndex = NOTEDATA.indexOf(thisText); - if(thisIndex !== -1) NOTEDATA.splice(thisIndex, 1); - d3.select(this).remove(); - }); - } - - notes.enter().append('div') - .classed('notifier-note', true) - .style('opacity', 0) - .each(function(thisText) { - var note = d3.select(this); - - note.append('button') - .classed('notifier-close', true) - .html('×') - .on('click', function() { - note.transition().call(killNote); - }); - - note.append('p').html(thisText); - - note.transition() - .duration(700) - .style('opacity', 1) - .transition() - .delay(ts) - .call(killNote); - }); -}; - -/** - * do two bounding boxes from getBoundingClientRect, - * ie {left,right,top,bottom,width,height}, overlap? - * takes optional padding pixels - */ -lib.bBoxIntersect = function(a, b, pad){ - pad = pad || 0; - return (a.left <= b.right + pad && - b.left <= a.right + pad && - a.top <= b.bottom + pad && - b.top <= a.bottom + pad); -}; - -// minor convenience/performance booster for d3... -lib.identity = function(d) { return d; }; - -// random string generator -lib.randstr = function randstr(existing, bits, base) { - /* - * Include number of bits, the base of the string you want - * and an optional array of existing strings to avoid. - */ - if (!base) base = 16; - if (bits === undefined) bits = 24; - if (bits <= 0) return '0'; - - var digits = Math.log(Math.pow(2, bits)) / Math.log(base), - res = '', - i, - b, - x; - - for (i = 2; digits === Infinity; i *= 2) { - digits = Math.log(Math.pow(2, bits / i)) / Math.log(base) * i; - } - - var rem = digits - Math.floor(digits); - - for (i = 0; i < Math.floor(digits); i++) { - x = Math.floor(Math.random() * base).toString(base); - res = x + res; - } - - if (rem) { - b = Math.pow(base, rem); - x = Math.floor(Math.random() * b).toString(base); - res = x + res; - } - - var parsed = parseInt(res, base); - if ( (existing && (existing.indexOf(res) > -1)) || - (parsed !== Infinity && parsed >= Math.pow(2, bits)) ) { - return randstr(existing, bits, base); - } - else return res; -}; - - -lib.OptionControl = function(opt, optname) { - /* - * An environment to contain all option setters and - * getters that collectively modify opts. - * - * You can call up opts from any function in new object - * as this.optname || this.opt - * - * See FitOpts for example of usage - */ - if (!opt) opt = {}; - if (!optname) optname = 'opt'; - - var self = {}; - self.optionList = []; - - self._newoption = function(optObj) { - optObj[optname] = opt; - self[optObj.name] = optObj; - self.optionList.push(optObj); - }; - - self['_'+optname] = opt; - return self; -}; - -/** - * lib.smooth: smooth arrayIn by convolving with - * a hann window with given full width at half max - * bounce the ends in, so the output has the same length as the input - */ -lib.smooth = function(arrayIn, FWHM) { - FWHM = Math.round(FWHM) || 0; // only makes sense for integers - if(FWHM < 2) return arrayIn; - - var alen = arrayIn.length, - alen2 = 2 * alen, - wlen = 2 * FWHM - 1, - w = new Array(wlen), - arrayOut = new Array(alen), - i, - j, - k, - v; - - // first make the window array - for(i = 0; i < wlen; i++) { - w[i] = (1 - Math.cos(Math.PI * (i + 1) / FWHM)) / (2 * FWHM); - } - - // now do the convolution - for(i = 0; i < alen; i++) { - v = 0; - for(j = 0; j < wlen; j++) { - k = i + j + 1 - FWHM; - - // multibounce - if(k < -alen) k -= alen2 * Math.round(k / alen2); - else if(k >= alen2) k -= alen2 * Math.floor(k / alen2); - - // single bounce - if(k < 0) k = - 1 - k; - else if(k >= alen) k = alen2 - 1 - k; - - v += arrayIn[k] * w[j]; - } - arrayOut[i] = v; - } - - return arrayOut; -}; - -// helpers for promises - -/** - * promiseError: log errors properly inside promises - * use: - * .then(undefined,Plotly.Lib.promiseError) (for IE compatibility) - * or .catch(Plotly.Lib.promiseError) - * TODO: I guess we need another step to send this error to Sentry? - */ -lib.promiseError = function(err) { console.log(err, err.stack); }; - -/** - * syncOrAsync: run a sequence of functions synchronously - * as long as its returns are not promises (ie have no .then) - * includes one argument arg to send to all functions... - * this is mainly just to prevent us having to make wrapper functions - * when the only purpose of the wrapper is to reference gd / td - * and a final step to be executed at the end - * TODO: if there's an error and everything is sync, - * this doesn't happen yet because we want to make sure - * that it gets reported - */ -lib.syncOrAsync = function(sequence, arg, finalStep) { - var ret, fni; - - function continueAsync(){ - lib.markTime('async done ' + fni.name); - return lib.syncOrAsync(sequence, arg, finalStep); - } - while(sequence.length) { - fni = sequence.splice(0, 1)[0]; - ret = fni(arg); - // lib.markTime('done calling '+fni.name) - if(ret && ret.then) { - return ret.then(continueAsync) - .then(undefined, lib.promiseError); - } - lib.markTime('sync done ' + fni.name); - } - - return finalStep && finalStep(arg); -}; - -lib.init2dArray = function(rowLength, colLength) { - var array = new Array(rowLength); - for(var i = 0; i < rowLength; i++) array[i] = new Array(colLength); - return array; -}; - -/** - * transpose a (possibly ragged) 2d array z. inspired by - * http://stackoverflow.com/questions/17428587/ - * transposing-a-2d-array-in-javascript - */ -lib.transposeRagged = function(z) { - var maxlen = 0, - zlen = z.length, - i, - j; - // Maximum row length: - for (i = 0; i < zlen; i++) maxlen = Math.max(maxlen, z[i].length); - - var t = new Array(maxlen); - for (i = 0; i < maxlen; i++) { - t[i] = new Array(zlen); - for (j = 0; j < zlen; j++) t[i][j] = z[j][i]; - } - - return t; -}; - -// our own dot function so that we don't need to include numeric -lib.dot = function(x, y) { - if (!(x.length && y.length) || x.length !== y.length) return null; - - var len = x.length, - out, - i; - - if(x[0].length) { - // mat-vec or mat-mat - out = new Array(len); - for(i = 0; i < len; i++) out[i] = lib.dot(x[i], y); - } - else if(y[0].length) { - // vec-mat - var yTranspose = lib.transposeRagged(y); - out = new Array(yTranspose.length); - for(i = 0; i < yTranspose.length; i++) out[i] = lib.dot(x, yTranspose[i]); - } - else { - // vec-vec - out = 0; - for(i = 0; i < len; i++) out += x[i] * y[i]; - } - - return out; -}; - - -// Functions to manipulate 2D transformation matrices - -// translate by (x,y) -lib.translationMatrix = function (x, y) { - return [[1, 0, x], [0, 1, y], [0, 0, 1]]; -}; - -// rotate by alpha around (0,0) -lib.rotationMatrix = function (alpha) { - var a = alpha*Math.PI/180; - return [[Math.cos(a), -Math.sin(a), 0], - [Math.sin(a), Math.cos(a), 0], - [0, 0, 1]]; -}; - -// rotate by alpha around (x,y) -lib.rotationXYMatrix = function(a, x, y) { - return lib.dot( - lib.dot(lib.translationMatrix(x, y), - lib.rotationMatrix(a)), - lib.translationMatrix(-x, -y)); -}; - -// applies a 2D transformation matrix to either x and y params or an [x,y] array -lib.apply2DTransform = function(transform) { - return function() { - var args = arguments; - if (args.length === 3) { - args = args[0]; - }//from map - var xy = arguments.length === 1 ? args[0] : [args[0], args[1]]; - return lib.dot(transform, [xy[0], xy[1], 1]).slice(0, 2); - }; -}; - -// applies a 2D transformation matrix to an [x1,y1,x2,y2] array (to transform a segment) -lib.apply2DTransform2 = function(transform) { - var at = lib.apply2DTransform(transform); - return function(xys) { - return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4))); - }; -}; - -/** - * Helper to strip trailing slash, from - * http://stackoverflow.com/questions/6680825/return-string-without-trailing-slash - */ -lib.stripTrailingSlash = function (str) { - if (str.substr(-1) === '/') return str.substr(0, str.length - 1); - return str; -}; - -var colorscaleNames = Object.keys(require('../colorscale').scales); - -lib.valObjects = { - data_array: { - // You can use *dflt=[] to force said array to exist though. - description: [ - 'An {array} of data.', - 'The value MUST be an {array}, or we ignore it.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(Array.isArray(v)) propOut.set(v); - else if(dflt!==undefined) propOut.set(dflt); - } - }, - enumerated: { - description: [ - 'Enumerated value type. The available values are listed', - 'in `values`.' - ].join(' '), - requiredOpts: ['values'], - otherOpts: ['dflt', 'coerceNumber', 'arrayOk'], - coerceFunction: function(v, propOut, dflt, opts) { - if(opts.coerceNumber) v = +v; - if(opts.values.indexOf(v)===-1) propOut.set(dflt); - else propOut.set(v); - } - }, - 'boolean': { - description: 'A boolean (true/false) value.', - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(v===true || v===false) propOut.set(v); - else propOut.set(dflt); - } - }, - number: { - description: [ - 'A number or a numeric value', - '(e.g. a number inside a string).', - 'When applicable, values greater (less) than `max` (`min`)', - 'are coerced to the `dflt`.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt', 'min', 'max', 'arrayOk'], - coerceFunction: function(v, propOut, dflt, opts) { - if(!isNumeric(v) || - (opts.min!==undefined && vopts.max)) { - propOut.set(dflt); - } - else propOut.set(+v); - } - }, - integer: { - description: [ - 'An integer or an integer inside a string.', - 'When applicable, values greater (less) than `max` (`min`)', - 'are coerced to the `dflt`.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt', 'min', 'max'], - coerceFunction: function(v, propOut, dflt, opts) { - if(v%1 || !isNumeric(v) || - (opts.min!==undefined && vopts.max)) { - propOut.set(dflt); - } - else propOut.set(+v); - } - }, - string: { - description: [ - 'A string value.', - 'Numbers are converted to strings except for attributes with', - '`strict` set to true.' - ].join(' '), - requiredOpts: [], - // TODO 'values shouldn't be in there (edge case: 'dash' in Scatter) - otherOpts: ['dflt', 'noBlank', 'strict', 'arrayOk', 'values'], - coerceFunction: function(v, propOut, dflt, opts) { - if(opts.strict===true && typeof v !== 'string') { - propOut.set(dflt); - return; - } - - var s = String(v); - if(v===undefined || (opts.noBlank===true && !s)) { - propOut.set(dflt); - } - else propOut.set(s); - } - }, - color: { - description: [ - 'A string describing color.', - 'Supported formats:', - '- hex (e.g. \'#d3d3d3\')', - '- rgb (e.g. \'rgb(255, 0, 0)\')', - '- rgba (e.g. \'rgb(255, 0, 0, 0.5)\')', - '- hsl (e.g. \'hsl(0, 100%, 50%)\')', - '- hsv (e.g. \'hsv(0, 100%, 100%)\')', - '- named colors (full list: http://www.w3.org/TR/css3-color/#svg-color)' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt', 'arrayOk'], - coerceFunction: function(v, propOut, dflt) { - if(tinycolor(v).isValid()) propOut.set(v); - else propOut.set(dflt); - } - }, - colorscale: { - description: [ - 'A Plotly colorscale either picked by a name:', - '(any of', colorscaleNames.join(', '), ')', - 'customized as an {array} of 2-element {arrays} where', - 'the first element is the normalized color level value', - '(starting at *0* and ending at *1*),', - 'and the second item is a valid color string.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - propOut.set(Plotly.Colorscale.getScale(v, dflt)); - } - }, - angle: { - description: [ - 'A number (in degree) between -180 and 180.' - ].join(' '), - requiredOpts: [], - otherOpts: ['dflt'], - coerceFunction: function(v, propOut, dflt) { - if(v==='auto') propOut.set('auto'); - else if(!isNumeric(v)) propOut.set(dflt); - else { - if(Math.abs(v)>180) v -= Math.round(v/360)*360; - propOut.set(+v); - } - } - }, - axisid: { - description: [ - 'An axis id string (e.g. \'x\', \'x2\', \'x3\', ...).' - ].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; - } - } - propOut.set(dflt); - } - }, - flaglist: { - description: [ - 'A string representing a combination of flags', - '(order does not matter here).', - 'Combine any of the available `flags` with *+*.', - '(e.g. (\'lines+markers\')).', - 'Values in `extras` cannot be combined.' - ].join(' '), - requiredOpts: ['flags'], - otherOpts: ['dflt', 'extras'], - coerceFunction: function(v, propOut, dflt, opts) { - if(typeof v !== 'string') { - propOut.set(dflt); - return; - } - if(opts.extras.indexOf(v)!==-1) { - propOut.set(v); - return; - } - var vParts = v.split('+'), - i = 0; - while(i or 1 elements - * because extend-like algorithms are hella slow - * obj2 is assumed to already be clean of these things (including no arrays) - */ -lib.minExtend = function(obj1, obj2) { - var objOut = {}; - if(typeof obj2 !== 'object') obj2 = {}; - var arrayLen = 3, - keys = Object.keys(obj1), - i, - k, - v; - for(i = 0; i < keys.length; i++) { - k = keys[i]; - v = obj1[k]; - if(k.charAt(0)==='_' || typeof v === 'function') continue; - else if(k==='module') objOut[k] = v; - else if(Array.isArray(v)) objOut[k] = v.slice(0,arrayLen); - else if(v && (typeof v === 'object')) objOut[k] = lib.minExtend(obj1[k], obj2[k]); - else objOut[k] = v; - } - - keys = Object.keys(obj2); - for(i = 0; i < keys.length; i++) { - k = keys[i]; - v = obj2[k]; - if(typeof v !== 'object' || !(k in objOut) || typeof objOut[k] !== 'object') { - objOut[k] = v; - } - } - - return objOut; -}; - -lib.titleCase = function(s) { - return s.charAt(0).toUpperCase() + s.substr(1); -}; - -lib.containsAny = function(s, fragments) { - for(var i = 0; i < fragments.length; i++) { - if(s.indexOf(fragments[i])!== -1) return true; - } - return false; -}; - -// get the parent Plotly plot of any element. Whoo jquery-free tree climbing! -lib.getPlotDiv = function(el) { - for(; el && el.removeAttribute; el = el.parentNode) { - if(lib.isPlotDiv(el)) return el; - } -}; - -lib.isPlotDiv = function(el) { - var el3 = d3.select(el); - return el3.size() && el3.classed('js-plotly-plot'); -}; - -lib.removeElement = function(el) { - var elParent = el && el.parentNode; - if(elParent) elParent.removeChild(el); -}; - -/** - * for dynamically adding style rules - * makes one stylesheet that contains all rules added - * by all calls to this function - */ -lib.addStyleRule = function(selector, styleString) { - if(!lib.styleSheet) { - var style = document.createElement('style'); - // WebKit hack :( - style.appendChild(document.createTextNode('')); - document.head.appendChild(style); - lib.styleSheet = style.sheet; - } - var styleSheet = lib.styleSheet; - - if(styleSheet.insertRule) { - styleSheet.insertRule(selector+'{'+styleString+'}',0); - } - else if(styleSheet.addRule) { - styleSheet.addRule(selector,styleString,0); - } - else console.warn('addStyleRule failed'); -}; - -lib.isIE = function() { - return typeof window.navigator.msSaveBlob !== 'undefined'; -}; - -lib.isPlainObject = require('./is_plain_object'); - -var extendModule = require('./extend'); -lib.extendFlat = extendModule.extendFlat; -lib.extendDeep = extendModule.extendDeep; -lib.extendDeepAll = extendModule.extendDeepAll; diff --git a/src/lib/matrix.js b/src/lib/matrix.js new file mode 100644 index 00000000000..4fbc4087ad6 --- /dev/null +++ b/src/lib/matrix.js @@ -0,0 +1,99 @@ +'use strict'; + + +exports.init2dArray = function(rowLength, colLength) { + var array = new Array(rowLength); + for(var i = 0; i < rowLength; i++) array[i] = new Array(colLength); + return array; +}; + +/** + * transpose a (possibly ragged) 2d array z. inspired by + * http://stackoverflow.com/questions/17428587/ + * transposing-a-2d-array-in-javascript + */ +exports.transposeRagged = function(z) { + var maxlen = 0, + zlen = z.length, + i, + j; + // Maximum row length: + for (i = 0; i < zlen; i++) maxlen = Math.max(maxlen, z[i].length); + + var t = new Array(maxlen); + for (i = 0; i < maxlen; i++) { + t[i] = new Array(zlen); + for (j = 0; j < zlen; j++) t[i][j] = z[j][i]; + } + + return t; +}; + +// our own dot function so that we don't need to include numeric +exports.dot = function(x, y) { + if (!(x.length && y.length) || x.length !== y.length) return null; + + var len = x.length, + out, + i; + + if(x[0].length) { + // mat-vec or mat-mat + out = new Array(len); + for(i = 0; i < len; i++) out[i] = exports.dot(x[i], y); + } + else if(y[0].length) { + // vec-mat + var yTranspose = exports.transposeRagged(y); + out = new Array(yTranspose.length); + for(i = 0; i < yTranspose.length; i++) out[i] = exports.dot(x, yTranspose[i]); + } + else { + // vec-vec + out = 0; + for(i = 0; i < len; i++) out += x[i] * y[i]; + } + + return out; +}; + +// translate by (x,y) +exports.translationMatrix = function (x, y) { + return [[1, 0, x], [0, 1, y], [0, 0, 1]]; +}; + +// rotate by alpha around (0,0) +exports.rotationMatrix = function (alpha) { + var a = alpha*Math.PI/180; + return [[Math.cos(a), -Math.sin(a), 0], + [Math.sin(a), Math.cos(a), 0], + [0, 0, 1]]; +}; + +// rotate by alpha around (x,y) +exports.rotationXYMatrix = function(a, x, y) { + return exports.dot( + exports.dot(exports.translationMatrix(x, y), + exports.rotationMatrix(a)), + exports.translationMatrix(-x, -y)); +}; + +// applies a 2D transformation matrix to either x and y params or an [x,y] array +exports.apply2DTransform = function(transform) { + return function() { + var args = arguments; + if (args.length === 3) { + args = args[0]; + }//from map + var xy = arguments.length === 1 ? args[0] : [args[0], args[1]]; + return exports.dot(transform, [xy[0], xy[1], 1]).slice(0, 2); + }; +}; + +// applies a 2D transformation matrix to an [x1,y1,x2,y2] array (to transform a segment) +exports.apply2DTransform2 = function(transform) { + var at = exports.apply2DTransform(transform); + return function(xys) { + return at(xys.slice(0, 2)).concat(at(xys.slice(2, 4))); + }; +}; diff --git a/src/lib/nested_property.js b/src/lib/nested_property.js new file mode 100644 index 00000000000..19c4a346fd9 --- /dev/null +++ b/src/lib/nested_property.js @@ -0,0 +1,245 @@ +'use strict'; + +var isNumeric = require('fast-isnumeric'); + +/** + * convert a string s (such as 'xaxis.range[0]') + * representing a property of nested object into set and get methods + * also return the string and object so we don't have to keep track of them + * allows [-1] for an array index, to set a property inside all elements + * of an array + * eg if obj = {arr: [{a: 1}, {a: 2}]} + * you can do p = nestedProperty(obj, 'arr[-1].a') + * but you cannot set the array itself this way, to do that + * just set the whole array. + * eg if obj = {arr: [1, 2, 3]} + * you can't do nestedProperty(obj, 'arr[-1]').set(5) + * but you can do nestedProperty(obj, 'arr').set([5, 5, 5]) + */ +module.exports = function nestedProperty(container, propStr) { + if(isNumeric(propStr)) propStr = String(propStr); + else if(typeof propStr !== 'string' || + propStr.substr(propStr.length - 4) === '[-1]') { + throw 'bad property string'; + } + + var j = 0, + propParts = propStr.split('.'), + indexed, + indices, + i; + + // check for parts of the nesting hierarchy that are numbers (ie array elements) + while(j < propParts.length) { + // look for non-bracket chars, then any number of [##] blocks + indexed = String(propParts[j]).match(/^([^\[\]]*)((\[\-?[0-9]*\])+)$/); + if(indexed) { + if(indexed[1]) propParts[j] = indexed[1]; + // allow propStr to start with bracketed array indices + else if(j === 0) propParts.splice(0,1); + else throw 'bad property string'; + + indices = indexed[2] + .substr(1,indexed[2].length-2) + .split(']['); + + for(i=0; i= 0; i--) { + curCont = containerLevels[i]; + remainingKeys = false; + if(Array.isArray(curCont)) { + for(j = curCont.length - 1; j >= 0; j--) { + if(emptyObj(curCont[j])) { + if(remainingKeys) curCont[j] = undefined; + else curCont.pop(); + } + else remainingKeys = true; + } + } + else if(typeof curCont === 'object' && curCont !== null) { + keys = Object.keys(curCont); + remainingKeys = false; + for(j = keys.length - 1; j >= 0; j--) { + if(emptyObj(curCont[keys[j]]) && !isDataArray(curCont[keys[j]], keys[j])) delete curCont[keys[j]]; + else remainingKeys = true; + } + } + if(remainingKeys) return; + } +} + +function emptyObj(obj) { + if(obj===undefined || obj===null) return true; + if(typeof obj !== 'object') return false; // any plain value + if(Array.isArray(obj)) return !obj.length; // [] + return !Object.keys(obj).length; // {} +} + +function badContainer(container, propStr, propParts) { + return { + set: function() { throw 'bad container'; }, + get: function() {}, + astr: propStr, + parts: propParts, + obj: container + }; +} diff --git a/src/lib/notifier.js b/src/lib/notifier.js new file mode 100644 index 00000000000..a1742717f80 --- /dev/null +++ b/src/lib/notifier.js @@ -0,0 +1,66 @@ +'use strict'; + +var d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var NOTEDATA = []; + +/** + * notifier + * @param {String} text The person's user name + * @param {Number} [delay=1000] The delay time in milliseconds + * or 'long' which provides 2000 ms delay time. + * @return {undefined} this function does not return a value + */ +module.exports = function(text, displayLength) { + if(NOTEDATA.indexOf(text) !== -1) return; + + NOTEDATA.push(text); + + var ts = 1000; + if (isNumeric(displayLength)) ts = displayLength; + else if (displayLength === 'long') ts = 3000; + + var notifierContainer = d3.select('body') + .selectAll('.plotly-notifier') + .data([0]); + notifierContainer.enter() + .append('div') + .classed('plotly-notifier', true); + + var notes = notifierContainer.selectAll('.notifier-note').data(NOTEDATA); + + function killNote(transition) { + transition + .duration(700) + .style('opacity', 0) + .each('end', function(thisText) { + var thisIndex = NOTEDATA.indexOf(thisText); + if(thisIndex !== -1) NOTEDATA.splice(thisIndex, 1); + d3.select(this).remove(); + }); + } + + notes.enter().append('div') + .classed('notifier-note', true) + .style('opacity', 0) + .each(function(thisText) { + var note = d3.select(this); + + note.append('button') + .classed('notifier-close', true) + .html('×') + .on('click', function() { + note.transition().call(killNote); + }); + + note.append('p').html(thisText); + + note.transition() + .duration(700) + .style('opacity', 1) + .transition() + .delay(ts) + .call(killNote); + }); +}; diff --git a/src/queue.js b/src/lib/queue.js similarity index 99% rename from src/queue.js rename to src/lib/queue.js index 240b3a6cf4d..f8852f41ff9 100644 --- a/src/queue.js +++ b/src/lib/queue.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../plotly'); /** * Copy arg array *without* removing `undefined` values from objects. diff --git a/src/lib/search.js b/src/lib/search.js new file mode 100644 index 00000000000..45297c5d394 --- /dev/null +++ b/src/lib/search.js @@ -0,0 +1,98 @@ +'use strict'; + +var isNumeric = require('fast-isnumeric'); + + +/** + * findBin - find the bin for val - note that it can return outside the + * bin range any pos. or neg. integer for linear bins, or -1 or + * bins.length-1 for explicit. + * bins is either an object {start,size,end} or an array length #bins+1 + * bins can be either increasing or decreasing but must be monotonic + * for linear bins, we can just calculate. For listed bins, run a binary + * search linelow (truthy) says the bin boundary should be attributed to + * the lower bin rather than the default upper bin + */ +exports.findBin = function(val, bins, linelow) { + if(isNumeric(bins.start)) { + return linelow ? + Math.ceil((val - bins.start) / bins.size) - 1 : + Math.floor((val - bins.start) / bins.size); + } + else { + var n1 = 0, + n2 = bins.length, + c = 0, + n, + test; + if(bins[bins.length - 1] > bins[0]) { + test = linelow ? lessThan : lessOrEqual; + } else { + test = linelow ? greaterOrEqual : greaterThan; + } + // c is just to avoid infinite loops if there's an error + while(n1 < n2 && c++ < 100){ + n = Math.floor((n1 + n2) / 2); + if(test(bins[n], val)) n1 = n + 1; + else n2 = n; + } + if(c > 90) console.log('Long binary search...'); + return n1 - 1; + } +}; + +function lessThan(a, b) { return a < b; } +function lessOrEqual(a, b) { return a <= b; } +function greaterThan(a, b) { return a > b; } +function greaterOrEqual(a, b) { return a >= b; } + +exports.sorterAsc = function(a, b) { return a - b; }; + +/** + * find distinct values in an array, lumping together ones that appear to + * just be off by a rounding error + * return the distinct values and the minimum difference between any two + */ +exports.distinctVals = function(valsIn) { + var vals = valsIn.slice(); // otherwise we sort the original array... + vals.sort(exports.sorterAsc); + + var l = vals.length - 1, + minDiff = (vals[l] - vals[0]) || 1, + errDiff = minDiff / (l || 1) / 10000, + v2 = [vals[0]]; + + for(var i = 0; i < l; i++) { + // make sure values aren't just off by a rounding error + if(vals[i + 1] > vals[i] + errDiff) { + minDiff = Math.min(minDiff, vals[i + 1] - vals[i]); + v2.push(vals[i + 1]); + } + } + + return {vals: v2, minDiff: minDiff}; +}; + +/** + * return the smallest element from (sorted) array arrayIn that's bigger than val, + * or (reverse) the largest element smaller than val + * used to find the best tick given the minimum (non-rounded) tick + * particularly useful for date/time where things are not powers of 10 + * binary search is probably overkill here... + */ +exports.roundUp = function(val, arrayIn, reverse){ + var low = 0, + high = arrayIn.length - 1, + mid, + c = 0, + dlow = reverse ? 0 : 1, + dhigh = reverse ? 1 : 0, + rounded = reverse ? Math.ceil : Math.floor; + // c is just to avoid infinite loops if there's an error + while(low < high && c++ < 100){ + mid = rounded((low + high) / 2); + if(arrayIn[mid] <= val) low = mid + dlow; + else high = mid - dhigh; + } + return arrayIn[low]; +}; diff --git a/src/gl3d/lib/show_no_webgl_msg.js b/src/lib/show_no_webgl_msg.js similarity index 96% rename from src/gl3d/lib/show_no_webgl_msg.js rename to src/lib/show_no_webgl_msg.js index 5e382318ea2..0118e1e0d9b 100644 --- a/src/gl3d/lib/show_no_webgl_msg.js +++ b/src/lib/show_no_webgl_msg.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('../../plotly'); +var Plotly = require('../plotly'); /** * Prints a no webgl error message into the scene container diff --git a/src/lib/stats.js b/src/lib/stats.js new file mode 100644 index 00000000000..962f9d8c569 --- /dev/null +++ b/src/lib/stats.js @@ -0,0 +1,85 @@ +'use strict'; + +var isNumeric = require('fast-isnumeric'); + + +/** + * aggNums() returns the result of an aggregate function applied to an array of + * values, where non-numerical values have been tossed out. + * + * @param {function} f - aggregation function (e.g., Math.min) + * @param {Number} v - initial value (continuing from previous calls) + * if there's no continuing value, use null for selector-type + * functions (max,min), or 0 for summations + * @param {Array} a - array to aggregate (may be nested, we will recurse, + * but all elements must have the same dimension) + * @param {Number} len - maximum length of a to aggregate + * @return {Number} - result of f applied to a starting from v + */ +exports.aggNums = function(f, v, a, len) { + var i, + b; + if (!len) len = a.length; + if (!isNumeric(v)) v = false; + if (Array.isArray(a[0])) { + b = new Array(len); + for(i = 0; i < len; i++) b[i] = exports.aggNums(f, v, a[i]); + a = b; + } + + for (i = 0; i < len; i++) { + if (!isNumeric(v)) v = a[i]; + else if (isNumeric(a[i])) v = f(+v, +a[i]); + } + return v; +}; + +/** + * mean & std dev functions using aggNums, so it handles non-numerics nicely + * even need to use aggNums instead of .length, to toss out non-numerics + */ +exports.len = function(data) { + return exports.aggNums(function(a){ return a + 1; }, 0, data); +}; + +exports.mean = function(data, len) { + if(!len) len = exports.len(data); + return exports.aggNums(function(a, b){ return a + b; }, 0, data) / len; +}; + +exports.variance = function(data, len, mean) { + if (!len) len = exports.len(data); + if (!isNumeric(mean)) mean = exports.mean(data, len); + + return exports.aggNums(function(a, b) { + return a + Math.pow(b - mean, 2); + }, 0, data)/len; +}; + +exports.stdev = function(data, len, mean) { + return Math.sqrt(exports.variance(data, len, mean)); +}; + +/** + * interp() computes a percentile (quantile) for a given distribution. + * We interpolate the distribution (to compute quantiles, we follow method #10 here: + * http://www.amstat.org/publications/jse/v14n3/langford.html). + * Typically the index or rank (n * arr.length) may be non-integer. + * For reference: ends are clipped to the extreme values in the array; + * For box plots: index you get is half a point too high (see + * http://en.wikipedia.org/wiki/Percentile#Nearest_rank) but note that this definition + * indexes from 1 rather than 0, so we subtract 1/2 (instead of add). + * + * @param {Array} arr - This array contains the values that make up the distribution. + * @param {Number} n - Between 0 and 1, n = p/100 is such that we compute the p^th percentile. + * For example, the 50th percentile (or median) corresponds to n = 0.5 + * @return {Number} - percentile + */ +exports.interp = function(arr, n) { + if (!isNumeric(n)) throw 'n should be a finite number'; + n = n * arr.length - 0.5; + if (n < 0) return arr[0]; + if (n > arr.length - 1) return arr[arr.length - 1]; + var frac = n % 1; + return frac * arr[Math.ceil(n)] + (1 - frac) * arr[Math.floor(n)]; +}; diff --git a/src/gl3d/lib/str2rgbarray.js b/src/lib/str2rgbarray.js similarity index 68% rename from src/gl3d/lib/str2rgbarray.js rename to src/lib/str2rgbarray.js index 5593ab75e5a..2dfcd0bb3f3 100644 --- a/src/gl3d/lib/str2rgbarray.js +++ b/src/lib/str2rgbarray.js @@ -1,7 +1,7 @@ 'use strict'; -var tinycolor = require('tinycolor2'), - arrtools = require('arraytools'); +var tinycolor = require('tinycolor2'); +var arrtools = require('arraytools'); function str2RgbaArray(color) { color = tinycolor(color); diff --git a/src/lib/plotly_util.js b/src/lib/svg_text_utils.js similarity index 98% rename from src/lib/plotly_util.js rename to src/lib/svg_text_utils.js index a299113d549..40ca14d3a93 100644 --- a/src/lib/plotly_util.js +++ b/src/lib/svg_text_utils.js @@ -1,14 +1,13 @@ 'use strict'; -/* global MathJax:false, Promise:false */ +/* global MathJax:false */ -var Plotly = require('../plotly'), - d3 = require('d3'); +var Plotly = require('../plotly'); +var d3 = require('d3'); var util = module.exports = {}; // Append SVG -///////////////////////////// d3.selection.prototype.appendSVG = function(_svgString) { var skeleton = '= (fullLayout.width - 20)) { - // Align the text at the left - attrs['text-anchor'] = 'start'; - attrs.x = 5; - } - else { - // Align the text at the right - attrs['text-anchor'] = 'end'; - attrs.x = fullLayout._paper.attr('width') - 7; - } - - linkContainer.attr(attrs); - - var toolspan = linkContainer.select('.js-link-to-tool'), - spacespan = linkContainer.select('.js-link-spacer'), - sourcespan = linkContainer.select('.js-sourcelinks'); - - if(gd._context.showSources) gd._context.showSources(gd); - - // 'view in plotly' link for embedded plots - if(gd._context.showLink) positionPlayWithData(gd, toolspan); - - // separator if we have both sources and tool link - spacespan.text((toolspan.text() && sourcespan.text()) ? ' - ' : ''); -}; - -// note that now this function is only adding the brand in -// iframes and 3rd-party apps -function positionPlayWithData(gd, container){ - container.text(''); - var link = container.append('a') - .attr({ - 'xlink:xlink:href': '#', - 'class': 'link--impt link--embedview', - 'font-weight':'bold' - }) - .text(gd._context.linkText + ' ' + String.fromCharCode(187)); - - if(gd._context.sendData) { - link.on('click',function(){ - gd.emit('plotly_beforeexport'); - - var baseUrl = (window.PLOTLYENV && window.PLOTLYENV.BASE_URL) || 'https://plot.ly'; - - var hiddenformDiv = d3.select(gd) - .append('div') - .attr('id', 'hiddenform') - .style('display', 'none'); - - var hiddenform = hiddenformDiv - .append('form') - .attr({ - action: baseUrl + '/external', - method: 'post', - target: '_blank' - }); - - var hiddenformInput = hiddenform - .append('input') - .attr({ - type: 'text', - name: 'data' - }); - - hiddenformInput.node().value = plots.graphJson(gd, false, 'keepdata'); - hiddenform.node().submit(); - hiddenformDiv.remove(); - - gd.emit('plotly_afterexport'); - return false; - }); - } - else { - var path = window.location.pathname.split('/'); - var query = window.location.search; - link.attr({ - 'xlink:xlink:show': 'new', - 'xlink:xlink:href': '/' + path[2].split('.')[0] + '/' + path[1] + query - }); - } -} - -// ---------------------------------------------------- -// Main plot-creation function. Note: will call makePlotFramework -// if necessary to create the framework -// ---------------------------------------------------- -// inputs: -// gd - the id or DOM element of the graph container div -// data - array of traces, containing the data and display -// information for each trace -// layout - object describing the overall display of the plot, -// all the stuff that doesn't pertain to any individual trace Plotly.plot = function(gd, data, layout, config) { Plotly.Lib.markTime('in plot'); @@ -548,7 +164,7 @@ Plotly.plot = function(gd, data, layout, config) { else trace._module.colorbar(gd, cd); } - doAutoMargin(gd); + plots.doAutoMargin(gd); return plots.previousPromises(gd); } @@ -583,10 +199,8 @@ Plotly.plot = function(gd, data, layout, config) { Plotly.Lib.markTime('done with bar/box adjustments'); // calc and autorange for errorbars - if(Plotly.ErrorBars) { - Plotly.ErrorBars.calc(gd); - Plotly.Lib.markTime('done Plotly.ErrorBars.calc'); - } + Plotly.ErrorBars.calc(gd); + Plotly.Lib.markTime('done Plotly.ErrorBars.calc'); // TODO: autosize extra for text markers return Plotly.Lib.syncOrAsync([ @@ -686,7 +300,7 @@ Plotly.plot = function(gd, data, layout, config) { } // finally do all error bars at once - if(gd._fullLayout._hasCartesian && Plotly.ErrorBars) { + if(gd._fullLayout._hasCartesian) { Plotly.ErrorBars.plot(gd, subplotInfo, cdError); Plotly.Lib.markTime('done ErrorBars'); } @@ -735,6 +349,70 @@ Plotly.plot = function(gd, data, layout, config) { donePlotting : Promise.resolve(); }; +// Get the container div: we store all variables for this plot as +// properties of this div +// some callers send this in by DOM element, others by id (string) +function getGraphDiv(gd) { + var gdElement; + + if(typeof gd === 'string') { + gdElement = document.getElementById(gd); + + if(gdElement === null) { + throw new Error('No DOM element with id \'' + gd + '\' exits on the page.'); + } + + return gdElement; + } + else if(gd===null || gd===undefined) { + throw new Error('DOM element provided is null or undefined'); + } + + return gd; // otherwise assume that gd is a DOM element +} + +function opaqueSetBackground(gd, bgColor) { + gd._fullLayout._paperdiv.style('background', 'white'); + Plotly.defaultConfig.setBackground(gd, bgColor); +} + +function setPlotContext(gd, config) { + if(!gd._context) gd._context = Plotly.Lib.extendFlat({}, Plotly.defaultConfig); + var context = gd._context; + + if(config) { + Object.keys(config).forEach(function(key) { + if(key in context) { + if(key === 'setBackground' && config[key] === 'opaque') { + context[key] = opaqueSetBackground; + } + else context[key] = config[key]; + } + }); + + // cause a remake of the modebar any time we change context + if(gd._fullLayout && gd._fullLayout._modebar) { + delete gd._fullLayout._modebar; + } + + // map plot3dPixelRatio to plotGlPixelRatio for backward compatibility + if(config.plot3dPixelRatio && !context.plotGlPixelRatio) { + context.plotGlPixelRatio = context.plot3dPixelRatio; + } + } + + //staticPlot forces a bunch of others: + if(context.staticPlot) { + context.editable = false; + context.autosizable = false; + context.scrollZoom = false; + context.doubleClick = false; + context.showTips = false; + context.showLink = false; + context.displayModeBar = false; + } +} + function plotGl3d(gd) { var fullLayout = gd._fullLayout, fullData = gd._fullData, @@ -1230,43 +908,6 @@ function emptyContainer(outer, innerStr) { (Object.keys(outer[innerStr]).length === 0); } -function sanitizeMargins(fullLayout) { - // polar doesn't do margins... - if(!fullLayout || !fullLayout.margin) return; - - var width = fullLayout.width, - height = fullLayout.height, - margin = fullLayout.margin, - plotWidth = width - (margin.l + margin.r), - plotHeight = height - (margin.t + margin.b), - correction; - - // if margin.l + margin.r = 0 then plotWidth > 0 - // as width >= 10 by supplyDefaults - // similarly for margin.t + margin.b - - if (plotWidth < 0) { - correction = (width - 1) / (margin.l + margin.r); - margin.l = Math.floor(correction * margin.l); - margin.r = Math.floor(correction * margin.r); - } - - if (plotHeight < 0) { - correction = (height - 1) / (margin.t + margin.b); - margin.t = Math.floor(correction * margin.t); - margin.b = Math.floor(correction * margin.b); - } -} - -// for use in Plotly.Lib.syncOrAsync, check if there are any -// pending promises in this plot and wait for them -plots.previousPromises = function(gd){ - if((gd._promises||[]).length) { - return Promise.all(gd._promises) - .then(function(){ gd._promises=[]; }); - } -}; - // convenience function to force a full redraw, mostly for use by plotly.js Plotly.redraw = function(gd) { gd = getGraphDiv(gd); @@ -1275,6 +916,7 @@ Plotly.redraw = function(gd) { console.log('This element is not a Plotly Plot', gd); return; } + gd.calcdata = undefined; return Plotly.plot(gd).then(function () { gd.emit('plotly_redraw'); @@ -1291,639 +933,10 @@ Plotly.redraw = function(gd) { */ Plotly.newPlot = function (gd, data, layout, config) { gd = getGraphDiv(gd); - Plotly.Plots.purge(gd); + plots.purge(gd); Plotly.plot(gd, data, layout, config); }; -plots.attributes = { - type: { - valType: 'enumerated', - role: 'info', - values: allTypes, - dflt: 'scatter' - }, - visible: { - valType: 'enumerated', - values: [true, false, 'legendonly'], - role: 'info', - dflt: true, - description: [ - 'Determines whether or not this trace is visible.', - 'If *legendonly*, the trace is not drawn,', - 'but can appear as a legend item', - '(provided that the legend itself is visible).' - ].join(' ') - }, - showlegend: { - valType: 'boolean', - role: 'info', - dflt: true, - description: [ - 'Determines whether or not an item corresponding to this', - 'trace is shown in the legend.' - ].join(' ') - }, - legendgroup: { - valType: 'string', - role: 'info', - dflt: '', - description: [ - 'Sets the legend group for this trace.', - 'Traces part of the same legend group hide/show at the same time', - 'when toggling legend items.' - ].join(' ') - }, - opacity: { - valType: 'number', - role: 'style', - min: 0, - max: 1, - dflt: 1, - description: 'Sets the opacity of the trace.' - }, - name: { - valType: 'string', - role: 'info', - description: [ - 'Sets the trace name.', - 'The trace name appear as the legend item and on hover.' - ].join(' ') - }, - uid: { - valType: 'string', - role: 'info', - dflt: '' - }, - hoverinfo: { - valType: 'flaglist', - role: 'info', - flags: ['x', 'y', 'z', 'text', 'name'], - extras: ['all', 'none'], - dflt: 'all', - description: 'Determines which trace information appear on hover.' - }, - stream: { - token: { - valType: 'string', - noBlank: true, - strict: true, - role: 'info', - description: [ - 'The stream id number links a data trace on a plot with a stream.', - 'See https://plot.ly/settings for more details.' - ].join(' ') - }, - maxpoints: { - valType: 'number', - min: 0, - role: 'info', - description: [ - 'Sets the maximum number of points to keep on the plots from an', - 'incoming stream.', - 'If `maxpoints` is set to *50*, only the newest 50 points will', - 'be displayed on the plot.' - ].join(' ') - } - } -}; - -plots.supplyDefaults = function(gd) { - // fill in default values: - // gd.data, gd.layout: - // are precisely what the user specified - // gd._fullData, gd._fullLayout: - // are complete descriptions of how to draw the plot - var oldFullLayout = gd._fullLayout || {}, - newFullLayout = gd._fullLayout = {}, - newLayout = gd.layout || {}, - oldFullData = gd._fullData || [], - newFullData = gd._fullData = [], - newData = gd.data || [], - modules = gd._modules = []; - - var i, trace, fullTrace, module, axList, ax; - - - // first fill in what we can of layout without looking at data - // because fullData needs a few things from layout - plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); - - // keep track of how many traces are inputted - newFullLayout._dataLength = newData.length; - - // then do the data - for (i = 0; i < newData.length; i++) { - trace = newData[i]; - - fullTrace = plots.supplyDataDefaults(trace, i, newFullLayout); - newFullData.push(fullTrace); - - // DETECT 3D, Cartesian, and Polar - if(plots.traceIs(fullTrace, 'cartesian')) newFullLayout._hasCartesian = true; - else if(plots.traceIs(fullTrace, 'gl3d')) newFullLayout._hasGL3D = true; - 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('r' in fullTrace) newFullLayout._hasPolar = true; - - module = fullTrace._module; - if (module && modules.indexOf(module)===-1) modules.push(module); - } - - // special cases that introduce interactions between traces - for (i = 0; i < modules.length; i++) { - module = modules[i]; - if (module.cleanData) module.cleanData(newFullData); - } - - if (oldFullData.length === newData.length) { - for (i = 0; i < newFullData.length; i++) { - relinkPrivateKeys(newFullData[i], oldFullData[i]); - } - } - - // finally, fill in the pieces of layout that may need to look at data - plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData); - - cleanScenes(newFullLayout, oldFullLayout); - - /* - * Relink functions and underscore attributes to promote consistency between - * plots. - */ - relinkPrivateKeys(newFullLayout, oldFullLayout); - - doAutoMargin(gd); - - // can't quite figure out how to get rid of this... each axis needs - // a reference back to the DOM object for just a few purposes - axList = Plotly.Axes.list(gd); - for (i = 0; i < axList.length; i++) { - ax = axList[i]; - ax._td = gd; - ax.setScale(); - } - - // update object references in calcdata - if ((gd.calcdata || []).length === newFullData.length) { - for (i = 0; i < newFullData.length; i++) { - trace = newFullData[i]; - (gd.calcdata[i][0] || {}).trace = trace; - } - } -}; - -function cleanScenes(newFullLayout, oldFullLayout) { - var oldSceneKey, - oldSceneKeys = plots.getSubplotIds(oldFullLayout, 'gl3d'); - - for (var i = 0; i < oldSceneKeys.length; i++) { - oldSceneKey = oldSceneKeys[i]; - if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { - oldFullLayout[oldSceneKey]._scene.destroy(); - } - } - -} - -// relink private _keys and keys with a function value from one layout -// (usually cached) to the new fullLayout. -// relink means copying if object is pass-by-value and adding a reference -// if object is pass-by-ref. This prevents deepCopying massive structures like -// a webgl context. -function relinkPrivateKeys(toLayout, fromLayout) { - - var keys = Object.keys(fromLayout), - j; - - for (var i = 0; i < keys.length; ++i) { - var k = keys[i]; - if(k.charAt(0)==='_' || typeof fromLayout[k]==='function') { - // if it already exists at this point, it's something - // that we recreate each time around, so ignore it - if(k in toLayout) continue; - - toLayout[k] = fromLayout[k]; - } - else if (Array.isArray(fromLayout[k]) && - Array.isArray(toLayout[k]) && - fromLayout[k].length && - Plotly.Lib.isPlainObject(fromLayout[k][0])) { - if(fromLayout[k].length !== toLayout[k].length) { - // this should be handled elsewhere, it causes - // ambiguity if we try to deal with it here. - throw new Error('relinkPrivateKeys needs equal ' + - 'length arrays'); - } - - for(j = 0; j < fromLayout[k].length; j++) { - relinkPrivateKeys(toLayout[k][j], fromLayout[k][j]); - } - } - else if (Plotly.Lib.isPlainObject(fromLayout[k]) && - Plotly.Lib.isPlainObject(toLayout[k])) { - // recurse into objects, but only if they still exist - relinkPrivateKeys(toLayout[k], fromLayout[k]); - if (!Object.keys(toLayout[k]).length) delete toLayout[k]; - } - } -} - -plots.supplyDataDefaults = function(traceIn, i, layout) { - var traceOut = {}, - defaultColor = Plotly.Color.defaults[i % Plotly.Color.defaults.length]; - - function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt); - } - - function coerceSubplotAttr(subplotType, subplotAttr) { - if(!plots.traceIs(traceOut, subplotType)) return; - return Plotly.Lib.coerce(traceIn, traceOut, - plots.subplotsRegistry[subplotType].attributes, subplotAttr); - } - - // module-independent attributes - traceOut.index = i; - var visible = coerce('visible'), - scene, - module; - - coerce('type'); - coerce('uid'); - - // this is necessary otherwise we lose references to scene objects when - // the traces of a scene are invisible. Also we handle visible/unvisible - // differently for 3D cases. - coerceSubplotAttr('gl3d', 'scene'); - coerceSubplotAttr('geo', 'geo'); - - // module-specific attributes --- note: we need to send a trace into - // the 3D modules to have it removed from the webgl context. - if(visible || scene) { - module = plots.getModule(traceOut); - traceOut._module = module; - } - - // gets overwritten in pie and geo - if(visible) coerce('hoverinfo', (layout._dataLength === 1) ? 'x+y+z+text' : undefined); - - if(module && visible) module.supplyDefaults(traceIn, traceOut, defaultColor, layout); - - if(visible) { - coerce('name', 'trace ' + i); - - if(!plots.traceIs(traceOut, 'noOpacity')) coerce('opacity'); - - coerceSubplotAttr('cartesian', 'xaxis'); - coerceSubplotAttr('cartesian', 'yaxis'); - - coerceSubplotAttr('gl2d', 'xaxis'); - coerceSubplotAttr('gl2d', 'yaxis'); - - if(plots.traceIs(traceOut, 'showLegend')) { - coerce('showlegend'); - coerce('legendgroup'); - } - } - - // NOTE: I didn't include fit info at all... for now I think it can stay - // just in gd.data, as this info isn't involved in creating plots at all, - // only in pulling back up the fit popover - - // reference back to the input object for convenience - traceOut._input = traceIn; - - return traceOut; -}; - -plots.fontAttrs = { - family: { - valType: 'string', - role: 'style', - noBlank: true, - strict: true, - description: [ - 'HTML font family - the typeface that will be applied by the web browser.', - 'The web browser will only be able to apply a font if it is available on the system', - 'which it operates. Provide multiple font families, separated by commas, to indicate', - 'the preference in which to apply fonts if they aren\'t available on the system.', - 'The plotly service (at https://plot.ly or on-premise) generates images on a server,', - 'where only a select number of', - 'fonts are installed and supported.', - 'These include *Arial*, *Balto*, *Courier New*, *Droid Sans*,, *Droid Serif*,', - '*Droid Sans Mono*, *Gravitas One*, *Old Standard TT*, *Open Sans*, *Overpass*,', - '*PT Sans Narrow*, *Raleway*, *Times New Roman*.' - ].join(' ') - }, - size: { - valType: 'number', - role: 'style', - min: 1 - }, - color: { - valType: 'color', - role: 'style' - } -}; - -// TODO make this a plot attribute? -plots.fontWeight = 'normal'; - -var extendFlat = Plotly.Lib.extendFlat; - -plots.layoutAttributes = { - font: { - family: extendFlat({}, plots.fontAttrs.family, { - dflt: '"Open sans", verdana, arial, sans-serif' - }), - size: extendFlat({}, plots.fontAttrs.size, { - dflt: 12 - }), - color: extendFlat({}, plots.fontAttrs.color, { - dflt: Plotly.Color.defaultLine - }), - description: [ - 'Sets the global font.', - 'Note that fonts used in traces and other', - 'layout components inherit from the global font.' - ].join(' ') - }, - title: { - valType: 'string', - role: 'info', - dflt: 'Click to enter Plot title', - description: [ - 'Sets the plot\'s title.' - ].join(' ') - }, - titlefont: extendFlat({}, plots.fontAttrs, { - description: 'Sets the title font.' - }), - autosize: { - valType: 'enumerated', - role: 'info', - // TODO: better handling of 'initial' - values: [true, false, 'initial'], - description: [ - 'Determines whether or not the dimensions of the figure are', - 'computed as a function of the display size.' - ].join(' ') - }, - width: { - valType: 'number', - role: 'info', - min: 10, - dflt: 700, - description: [ - 'Sets the plot\'s width (in px).' - ].join(' ') - }, - height: { - valType: 'number', - role: 'info', - min: 10, - dflt: 450, - description: [ - 'Sets the plot\'s height (in px).' - ].join(' ') - }, - margin: { - l: { - valType: 'number', - role: 'info', - min: 0, - dflt: 80, - description: 'Sets the left margin (in px).' - }, - r: { - valType: 'number', - role: 'info', - min: 0, - dflt: 80, - description: 'Sets the right margin (in px).' - }, - t: { - valType: 'number', - role: 'info', - min: 0, - dflt: 100, - description: 'Sets the top margin (in px).' - }, - b: { - valType: 'number', - role: 'info', - min: 0, - dflt: 80, - description: 'Sets the bottom margin (in px).' - }, - pad: { - valType: 'number', - role: 'info', - min: 0, - dflt: 0, - description: [ - 'Sets the amount of padding (in px)', - 'between the plotting area and the axis lines' - ].join(' ') - }, - autoexpand: { - valType: 'boolean', - role: 'info', - dflt: true - } - }, - paper_bgcolor: { - valType: 'color', - role: 'style', - dflt: Plotly.Color.background, - description: 'Sets the color of paper where the graph is drawn.' - }, - plot_bgcolor: { - // defined here, but set in Axes.supplyLayoutDefaults - // because it needs to know if there are (2D) axes or not - valType: 'color', - role: 'style', - dflt: Plotly.Color.background, - description: [ - 'Sets the color of plotting area in-between x and y axes.' - ].join(' ') - }, - separators: { - valType: 'string', - role: 'style', - dflt: '.,', - description: [ - 'Sets the decimal and thousand separators.', - 'For example, *. * puts a \'.\' before decimals and', - 'a space between thousands.' - ].join(' ') - }, - hidesources: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Determines whether or not a text link citing the data source is', - 'placed at the bottom-right cored of the figure.', - 'Has only an effect only on graphs that have been generated via', - 'forked graphs from the plotly service (at https://plot.ly or on-premise).' - ].join(' ') - }, - smith: { - // will become a boolean if/when we implement this - valType: 'enumerated', - role: 'info', - values: [false], - dflt: false - }, - showlegend: { - // handled in legend.supplyLayoutDefaults - // but included here because it's not in the legend object - valType: 'boolean', - role: 'info', - description: 'Determines whether or not a legend is drawn.' - }, - _hasCartesian: { - valType: 'boolean', - dflt: false - }, - _hasGL3D: { - valType: 'boolean', - dflt: false - }, - _hasGeo: { - valType: 'boolean', - dflt: false - }, - _hasPie: { - valType: 'boolean', - dflt: false - }, - _hasGL2D: { - valType: 'boolean', - dflt: false - }, - _composedModules: { - '*': 'Fx' - }, - _nestedModules: { - 'xaxis': 'Axes', - 'yaxis': 'Axes', - 'scene': 'Gl3dLayout', - 'geo': 'GeoLayout', - 'legend': 'Legend', - 'annotations': 'Annotations', - 'shapes': 'Shapes' - } -}; - -plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { - function coerce(attr, dflt) { - return Plotly.Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); - } - - var globalFont = Plotly.Lib.coerceFont(coerce, 'font'); - - coerce('title'); - - Plotly.Lib.coerceFont(coerce, 'titlefont', { - family: globalFont.family, - size: Math.round(globalFont.size * 1.4), - color: globalFont.color - }); - - var autosize = coerce('autosize', - (layoutIn.width && layoutIn.height) ? false : 'initial'); - coerce('width'); - coerce('height'); - - coerce('margin.l'); - coerce('margin.r'); - coerce('margin.t'); - coerce('margin.b'); - coerce('margin.pad'); - coerce('margin.autoexpand'); - - // called in plotAutoSize otherwise - if (autosize!=='initial') sanitizeMargins(layoutOut); - - coerce('paper_bgcolor'); - - coerce('separators'); - coerce('hidesources'); - coerce('smith'); - coerce('_hasCartesian'); - coerce('_hasGL3D'); - coerce('_hasGeo'); - coerce('_hasPie'); - coerce('_hasGL2D'); -}; - -plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { - var moduleLayoutDefaults = [ - 'Axes', 'Annotations', 'Shapes', 'Fx', - 'Bars', 'Boxes', 'Gl3dLayout', 'GeoLayout', 'Pie', 'Legend' - ]; - - var i, module; - - // don't add a check for 'function in module' as it is better to error out and - // secure the module API then not apply the default function. - for(i = 0; i < moduleLayoutDefaults.length; i++) { - module = moduleLayoutDefaults[i]; - if(Plotly[module]) { - Plotly[module].supplyLayoutDefaults(layoutIn, layoutOut, fullData); - } - } -}; - -plots.purge = function(gd) { - // remove all plotly attributes from a div so it can be replotted fresh - // TODO: these really need to be encapsulated into a much smaller set... - - // note: we DO NOT remove _context because it doesn't change when we insert - // a new plot, and may have been set outside of our scope. - - // clean up the gl and geo containers - // TODO unify subplot creation/update with d3.selection.order - // and/or subplot ids - var fullLayout = gd._fullLayout || {}; - if(fullLayout._glcontainer !== undefined) fullLayout._glcontainer.remove(); - if(fullLayout._geocontainer !== undefined) fullLayout._geocontainer.remove(); - - // data and layout - delete gd.data; - delete gd.layout; - delete gd._fullData; - delete gd._fullLayout; - delete gd.calcdata; - delete gd.framework; - delete gd.empty; - - delete gd.fid; - - delete gd.undoqueue; // action queue - delete gd.undonum; - delete gd.autoplay; // are we doing an action that doesn't go in undo queue? - delete gd.changed; - - // these get recreated on Plotly.plot anyway, but just to be safe - // (and to have a record of them...) - delete gd._modules; - delete gd._tester; - delete gd._testref; - delete gd._promises; - delete gd._redrawTimer; - delete gd._replotting; - delete gd.firstscatter; - delete gd.hmlumcount; - delete gd.hmpixcount; - delete gd.numboxes; - delete gd._hoverTimer; - delete gd._lastHoverTime; -}; - function doCalcdata(gd) { var axList = Plotly.Axes.list(gd), fullData = gd._fullData, @@ -1979,18 +992,6 @@ function doCalcdata(gd) { } } -plots.style = function(gd) { - var modulesWithErrorBars = Plotly.ErrorBars ? - gd._modules.concat(Plotly.ErrorBars) : gd._modules, - i, - module; - - for (i = 0; i < modulesWithErrorBars.length; i++) { - module = modulesWithErrorBars[i]; - if (module.style) module.style(gd); - } -}; - /** * Wrap negative indicies to their positive counterparts. * @@ -2357,9 +1358,7 @@ Plotly.extendTraces = function extendTraces (gd, update, indices, maxPoints) { Plotly.redraw(gd); var undoArgs = [gd, undo.update, indices, undo.maxPoints]; - if (Plotly.Queue) { - Plotly.Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments); - } + Plotly.Queue.add(gd, Plotly.prependTraces, undoArgs, extendTraces, arguments); }; Plotly.prependTraces = function prependTraces (gd, update, indices, maxPoints) { @@ -2384,9 +1383,7 @@ Plotly.prependTraces = function prependTraces (gd, update, indices, maxPoints) Plotly.redraw(gd); var undoArgs = [gd, undo.update, indices, undo.maxPoints]; - if (Plotly.Queue) { - Plotly.Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments); - } + Plotly.Queue.add(gd, Plotly.extendTraces, undoArgs, prependTraces, arguments); }; /** @@ -2430,7 +1427,7 @@ Plotly.addTraces = function addTraces (gd, traces, newIndices) { // i.e., we can simply redraw and be done if (typeof newIndices === 'undefined') { Plotly.redraw(gd); - if (Plotly.Queue) Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); return; } @@ -2453,10 +1450,10 @@ Plotly.addTraces = function addTraces (gd, traces, newIndices) { // if we're here, the user has defined specific places to place the new traces // this requires some extra work that moveTraces will do - if (Plotly.Queue) Plotly.Queue.startSequence(gd); - if (Plotly.Queue) Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + Plotly.Queue.startSequence(gd); + Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); Plotly.moveTraces(gd, currentIndices, newIndices); - if (Plotly.Queue) Plotly.Queue.stopSequence(gd); + Plotly.Queue.stopSequence(gd); }; /** @@ -2496,8 +1493,7 @@ Plotly.deleteTraces = function deleteTraces (gd, indices) { } Plotly.redraw(gd); - - if (Plotly.Queue) Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); }; /** @@ -2591,10 +1587,8 @@ Plotly.moveTraces = function moveTraces (gd, currentIndices, newIndices) { } gd.data = newData; - Plotly.redraw(gd); - - if (Plotly.Queue) Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); + Plotly.Queue.add(gd, undoFunc, undoArgs, redoFunc, redoArgs); }; // ----------------------------------------------------- @@ -3031,9 +2025,7 @@ Plotly.restyle = function restyle(gd, astr, val, traces) { // now all attribute mods are done, as are redo and undo // so we can save them - if(Plotly.Queue) { - Plotly.Queue.add(gd, restyle, [gd, undoit, traces], restyle, [gd, redoit, traces]); - } + Plotly.Queue.add(gd, restyle, [gd, undoit, traces], restyle, [gd, redoit, traces]); // do we need to force a recalc? var autorangeOn = false; @@ -3411,9 +2403,7 @@ Plotly.relayout = function relayout(gd, astr, val) { } // now all attribute mods are done, as are // redo and undo so we can save them - if(Plotly.Queue) { - Plotly.Queue.add(gd, relayout, [gd, undoit], relayout, [gd, redoit]); - } + Plotly.Queue.add(gd, relayout, [gd, undoit], relayout, [gd, redoit]); // calculate autosizing - if size hasn't changed, // will remove h&w so we don't need to redraw @@ -3453,7 +2443,7 @@ Plotly.relayout = function relayout(gd, astr, val) { if(doticks) { seq.push(function(){ Plotly.Axes.doTicks(gd,'redraw'); - plots.titles(gd,'gtitle'); + Plotly.Titles.draw(gd, 'gtitle'); return plots.previousPromises(gd); }); } @@ -3559,53 +2549,11 @@ function plotAutoSize(gd, aobj) { fullLayout.autosize = gd.layout.autosize = true; } - sanitizeMargins(fullLayout); + plots.sanitizeMargins(fullLayout); return aobj; } -// check whether to resize a tab (if it's a plot) to the container -plots.resize = function(gd) { - if(!gd || d3.select(gd).style('display') === 'none') return; - - if(gd._redrawTimer) clearTimeout(gd._redrawTimer); - - gd._redrawTimer = setTimeout(function() { - if((gd._fullLayout || {}).autosize) { - // autosizing doesn't count as a change that needs saving - var oldchanged = gd.changed; - - // nor should it be included in the undo queue - gd.autoplay = true; - - Plotly.relayout(gd, {autosize: true}); - gd.changed = oldchanged; - } - }, 100); -}; - -// Get the container div: we store all variables for this plot as -// properties of this div -// some callers send this in by DOM element, others by id (string) -function getGraphDiv(gd) { - var gdElement; - - if(typeof gd === 'string') { - gdElement = document.getElementById(gd); - - if(gdElement === null) { - throw new Error('No DOM element with id \'' + gd + '\' exits on the page.'); - } - - return gdElement; - } - else if(gd===null || gd===undefined) { - throw new Error('DOM element provided is null or undefined'); - } - - return gd; // otherwise assume that gd is a DOM element -} - // ------------------------------------------------------- // makePlotFramework: Create the plot container and axes // ------------------------------------------------------- @@ -3616,7 +2564,7 @@ function makePlotFramework(gd) { /* * TODO - find a better place for 3D to initialize axes */ - if(fullLayout._hasGL3D) Plotly.Gl3dAxes.initAxes(gd); + if(fullLayout._hasGL3D) Plotly.Gl3dLayout.initAxes(gd); // Plot container fullLayout._container = gd3.selectAll('.plot-container').data([0]); @@ -3870,119 +2818,9 @@ function makeCartesianPlotFramwork(gd, subplots) { }); } -// called by legend and colorbar routines to see if we need to -// expand the margins to show them -// o is {x,l,r,y,t,b} where x and y are plot fractions, -// the rest are pixels in each direction -// or leave o out to delete this entry (like if it's hidden) -plots.autoMargin = function(gd,id,o) { - var fullLayout = gd._fullLayout; - if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; - if(fullLayout.margin.autoexpand!==false) { - if(!o) delete fullLayout._pushmargin[id]; - else { - var pad = o.pad||12; - - // if the item is too big, just give it enough automargin to - // make sure you can still grab it and bring it back - if(o.l+o.r > fullLayout.width*0.5) o.l = o.r = 0; - if(o.b+o.t > fullLayout.height*0.5) o.b = o.t = 0; - - fullLayout._pushmargin[id] = { - l: {val:o.x, size: o.l+pad}, - r: {val:o.x, size: o.r+pad}, - b: {val:o.y, size: o.b+pad}, - t: {val:o.y, size: o.t+pad} - }; - } - - if(!gd._replotting) doAutoMargin(gd); - } -}; - -function doAutoMargin(gd) { - var fullLayout = gd._fullLayout; - if(!fullLayout._size) fullLayout._size = {}; - if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; - var gs = fullLayout._size, - oldmargins = JSON.stringify(gs); - - // adjust margins for outside legends and colorbars - // fullLayout.margin is the requested margin, - // fullLayout._size has margins and plotsize after adjustment - var ml = Math.max(fullLayout.margin.l||0,0), - mr = Math.max(fullLayout.margin.r||0,0), - mt = Math.max(fullLayout.margin.t||0,0), - mb = Math.max(fullLayout.margin.b||0,0), - pm = fullLayout._pushmargin; - if(fullLayout.margin.autoexpand!==false) { - // fill in the requested margins - pm.base = { - l:{val:0, size:ml}, - r:{val:1, size:mr}, - t:{val:1, size:mt}, - b:{val:0, size:mb} - }; - // now cycle through all the combinations of l and r - // (and t and b) to find the required margins - Object.keys(pm).forEach(function(k1) { - var pushleft = pm[k1].l||{}, - pushbottom = pm[k1].b||{}, - fl = pushleft.val, - pl = pushleft.size, - fb = pushbottom.val, - pb = pushbottom.size; - Object.keys(pm).forEach(function(k2) { - if(isNumeric(pl) && pm[k2].r) { - var fr = pm[k2].r.val, - pr = pm[k2].r.size; - if(fr>fl) { - var newl = (pl*fr + - (pr-fullLayout.width)*fl) / (fr-fl), - newr = (pr*(1-fl) + - (pl-fullLayout.width)*(1-fr)) / (fr-fl); - if(newl>=0 && newr>=0 && newl+newr>ml+mr) { - ml = newl; - mr = newr; - } - } - } - if(isNumeric(pb) && pm[k2].t) { - var ft = pm[k2].t.val, - pt = pm[k2].t.size; - if(ft>fb) { - var newb = (pb*ft + - (pt-fullLayout.height)*fb) / (ft-fb), - newt = (pt*(1-fb) + - (pb-fullLayout.height)*(1-ft)) / (ft-fb); - if(newb>=0 && newt>=0 && newb+newt>mb+mt) { - mb = newb; - mt = newt; - } - } - } - }); - }); - } - - gs.l = Math.round(ml); - gs.r = Math.round(mr); - gs.t = Math.round(mt); - gs.b = Math.round(mb); - gs.p = Math.round(fullLayout.margin.pad); - gs.w = Math.round(fullLayout.width)-gs.l-gs.r; - gs.h = Math.round(fullLayout.height)-gs.t-gs.b; - - // if things changed and we're not already redrawing, trigger a redraw - if(!gd._replotting && oldmargins!=='{}' && - oldmargins!==JSON.stringify(fullLayout._size)) { - return Plotly.plot(gd); - } -} - // layoutStyles: styling for plot layout elements function layoutStyles(gd) { - return Plotly.Lib.syncOrAsync([doAutoMargin, lsInner], gd); + return Plotly.Lib.syncOrAsync([plots.doAutoMargin, lsInner], gd); } function lsInner(gd) { @@ -4110,7 +2948,6 @@ function lsInner(gd) { rightpos += xa._offset - gs.l; } - plotinfo.xlines .attr('transform', originx) .attr('d',( @@ -4146,420 +2983,9 @@ function lsInner(gd) { Plotly.Axes.makeClipPaths(gd); - plots.titles(gd,'gtitle'); + Plotly.Titles.draw(gd, 'gtitle'); Plotly.Fx.modeBar(gd); return gd._promises.length && Promise.all(gd._promises); } - -// titles - (re)draw titles on the axes and plot -// title can be 'xtitle', 'ytitle', 'gtitle', -// or empty to draw all -plots.titles = function(gd, title) { - gd = getGraphDiv(gd); - - if(!title) { - Plotly.Axes.listIds(gd).forEach(function(axId) { - plots.titles(gd, axId+'title'); - }); - plots.titles(gd,'gtitle'); - return; - } - - 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[Plotly.Axes.id2name(title.replace('title',''))] || fullLayout; - - var prop = cont===fullLayout ? 'title' : cont._name+'.title', - name = colorbar ? 'colorscale' : - ((cont._id||axletter).toUpperCase()+' axis'), - font = cont.titlefont.family, - fontSize = cont.titlefont.size, - fontColor = cont.titlefont.color, - x, - y, - transform='', - attr = {}, - 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 Plotly.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} : - Plotly.Axes.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))); - 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} : - Plotly.Axes.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))); - attr = {center: 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, - isplaceholder = false, - txt = cont.title.trim(); - if(txt === '') { opacity = 0; } - if(txt.match(/Click to enter .+ title/)) { - opacity = 0.2; - 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) - .data([0]); - group.enter().append('g') - .classed('g-'+title, true); - } - - var el = group.selectAll('text') - .data([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', title); - - function titleLayout(titleEl){ - Plotly.Lib.syncOrAsync([drawTitle,scootTitle], titleEl); - } - - function drawTitle(titleEl) { - titleEl.attr('transform', transform ? - 'rotate(' + [transform.rotate, options.x, options.y] + - ') translate(0, '+transform.offset+')' : - null); - titleEl.style({ - 'font-family': font, - 'font-size': d3.round(fontSize,2)+'px', - fill: Plotly.Color.rgb(fontColor), - opacity: opacity*Plotly.Color.opacity(fontColor), - 'font-weight': plots.fontWeight - }) - .attr(options) - .call(Plotly.util.convertToTspans) - .attr(options); - titleEl.selectAll('tspan.line') - .attr(options); - 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 shift = 0, - backside = { - left: 'right', - right: 'left', - top: 'bottom', - bottom: 'top' - }[avoid.side], - shiftSign = (['left','top'].indexOf(avoid.side)!==-1) ? - -1 : 1, - pad = isNumeric(avoid.pad) ? avoid.pad : 2, - titlebb = Plotly.Drawing.bBox(titleGroup.node()), - paperbb = { - left: 0, - top: 0, - right: fullLayout.width, - bottom: fullLayout.height - }, - maxshift = colorbar ? fullLayout.width: - (paperbb[avoid.side]-titlebb[avoid.side]) * - ((avoid.side==='left' || avoid.side==='top') ? -1 : 1); - // 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 - titlebb.left -= avoid.offsetLeft; - titlebb.right -= avoid.offsetLeft; - titlebb.top -= avoid.offsetTop; - titlebb.bottom -= avoid.offsetTop; - - // iterate over a set of elements (avoid.selection) - // to avoid collisions with - avoid.selection.each(function(){ - var avoidbb = Plotly.Drawing.bBox(this); - - if(Plotly.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', - 'translate(' + shiftTemplate + ')'); - } - } - } - - el.attr({'data-unformatted': txt}) - .call(titleLayout); - - var placeholderText = 'Click to enter '+name.replace(/\d+/,'')+' title'; - - function setPlaceholder(){ - opacity = 0; - isplaceholder = true; - txt = placeholderText; - fullLayout._infolayer.select('.'+title) - .attr({'data-unformatted': txt}) - .text(txt) - .on('mouseover.opacity',function(){ - d3.select(this).transition() - .duration(100).style('opacity',1); - }) - .on('mouseout.opacity',function(){ - d3.select(this).transition() - .duration(1000).style('opacity',0); - }); - } - - if(gd._context.editable){ - if(!txt) setPlaceholder(); - - el.call(Plotly.util.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); - }) - .on('cancel', function(){ - this.text(this.attr('data-unformatted')) - .call(titleLayout); - }) - .on('input', function(d){ - this.text(d || ' ').attr(options) - .selectAll('tspan.line') - .attr(options); - }); - } - else if(!txt || txt.match(/Click to enter .+ title/)) { - el.remove(); - } - el.classed('js-placeholder',isplaceholder); -}; - -// ---------------------------------------------------- -// Utility functions -// ---------------------------------------------------- - -/** - * JSONify the graph data and layout - * - * This function needs to recurse because some src can be inside - * sub-objects. - * - * It also strips out functions and private (starts with _) elements. - * Therefore, we can add temporary things to data and layout that don't - * get saved. - * - * @param gd The graphDiv - * @param {Boolean} dataonly If true, don't return layout. - * @param {'keepref'|'keepdata'|'keepall'} [mode='keepref'] Filter what's kept - * keepref: remove data for which there's a src present - * eg if there's xsrc present (and xsrc is well-formed, - * ie has : and some chars before it), strip out x - * keepdata: remove all src tags, don't remove the data itself - * keepall: keep data and src - * @param {String} output If you specify 'object', the result will not be stringified - * @param {Boolean} useDefaults If truthy, use _fullLayout and _fullData - * @returns {Object|String} - */ -plots.graphJson = function(gd, dataonly, mode, output, useDefaults){ - gd = getGraphDiv(gd); - - // if the defaults aren't supplied yet, we need to do that... - if ((useDefaults && dataonly && !gd._fullData) || - (useDefaults && !dataonly && !gd._fullLayout)) { - plots.supplyDefaults(gd); - } - - var data = (useDefaults) ? gd._fullData : gd.data, - layout = (useDefaults) ? gd._fullLayout : gd.layout; - - function stripObj(d) { - if(typeof d === 'function') { - return null; - } - if(Plotly.Lib.isPlainObject(d)) { - var o={}, v, src; - for(v in d) { - // remove private elements and functions - // _ is for private, [ is a mistake ie [object Object] - if(typeof d[v]==='function' || - ['_','['].indexOf(v.charAt(0))!==-1) { - continue; - } - - // look for src/data matches and remove the appropriate one - if(mode==='keepdata') { - // keepdata: remove all ...src tags - if(v.substr(v.length-3)==='src') { - continue; - } - } - else if(mode==='keepstream') { - // keep sourced data if it's being streamed. - // similar to keepref, but if the 'stream' object exists - // in a trace, we will keep the data array. - src = d[v+'src']; - if(typeof src==='string' && src.indexOf(':')>0) { - if(!Plotly.Lib.isPlainObject(d.stream)) { - continue; - } - } - } - else if(mode!=='keepall') { - // keepref: remove sourced data but only - // if the source tag is well-formed - src = d[v+'src']; - if(typeof src==='string' && src.indexOf(':')>0) { - continue; - } - } - - // OK, we're including this... recurse into it - o[v] = stripObj(d[v]); - } - return o; - } - - if(Array.isArray(d)) { - return d.map(stripObj); - } - - // convert native dates to date strings... - // mostly for external users exporting to plotly - if(d && d.getTime) { - return Plotly.Lib.ms2DateTime(d); - } - - return d; - } - - var obj = { - data:(data||[]).map(function(v){ - var d = stripObj(v); - // fit has some little arrays in it that don't contain data, - // just fit params and meta - if(dataonly) { delete d.fit; } - return d; - }) - }; - if(!dataonly) { obj.layout = stripObj(layout); } - - if(gd.framework && gd.framework.isPolar) obj = gd.framework.getConfig(); - - return (output==='object') ? obj : JSON.stringify(obj); -}; diff --git a/src/plot_config.js b/src/plot_api/plot_config.js similarity index 99% rename from src/plot_config.js rename to src/plot_api/plot_config.js index ffdc41f8727..a5820ec0a78 100644 --- a/src/plot_config.js +++ b/src/plot_api/plot_config.js @@ -1,6 +1,6 @@ 'use strict'; -/* +/** * This will be transfered over to gd and overridden by * config args to Plotly.plot. * diff --git a/src/plotschema.js b/src/plot_api/plot_schema.js similarity index 98% rename from src/plotschema.js rename to src/plot_api/plot_schema.js index aa655e795b2..b6606d7bb40 100644 --- a/src/plotschema.js +++ b/src/plot_api/plot_schema.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../plotly'); var extendFlat = Plotly.Lib.extendFlat; var extendDeep = Plotly.Lib.extendDeep; @@ -22,8 +22,8 @@ var plotSchema = { }; // FIXME polar attribute are not part of Plotly yet -var polarAreaAttrs = require('./polar/attributes/area'), - polarAxisAttrs = require('./polar/attributes/polaraxes'); +var polarAreaAttrs = require('../plots/polar/area_attributes'), + polarAxisAttrs = require('../plots/polar/axis_attributes'); var PlotSchema = module.exports = {}; diff --git a/src/plot_api/set_plot_config.js b/src/plot_api/set_plot_config.js new file mode 100644 index 00000000000..d3a4b8c739f --- /dev/null +++ b/src/plot_api/set_plot_config.js @@ -0,0 +1,14 @@ +'use strict'; + +var Plotly = require('../plotly'); + +/** + * Extends the plot config + * + * @param {object} configObj partial plot configuration object + * to extend the current plot configuration. + * + */ +module.exports = function setPlotConfig(configObj) { + return Plotly.Lib.extendFlat(Plotly.defaultConfig, configObj); +}; diff --git a/src/plotly.js b/src/plotly.js index 01aab2b1290..99e5756838c 100644 --- a/src/plotly.js +++ b/src/plotly.js @@ -1,72 +1,70 @@ -require('es6-promise').polyfill(); +/* + * Pack internal modules unto an object. + * + * This object is require'ed in as 'Plotly' in numerous src and test files. + * Require'ing 'Plotly' bypasses circular dependencies. + * + * Future development should move away from this pattern. + * + */ -// order of requires should matter only for interdependencies -// in attributes definitions. put the common modules first +// promise polyfill +require('es6-promise').polyfill(); -exports.Lib = require('./lib/lib'); -exports.util = require('./lib/plotly_util'); +// lib functions +exports.Lib = require('./lib'); +exports.util = require('./lib/svg_text_utils'); +exports.Queue = require('./lib/queue'); -// icons, css and configuration +// plot icons svg and plot css exports.Icons = require('../build/ploticon'); require('../build/plotcss'); +// configuration exports.MathJaxConfig = require('./fonts/mathjax_config'); -exports.defaultConfig = require('./plot_config'); - -exports.Color = require('./color'); -exports.Colorscale = require('./colorscale'); -exports.Drawing = require('./drawing'); -// then the plot structure -exports.Plots = require('./graph_obj'); -exports.Axes = require('./axes'); -exports.Colorbar = require('./colorbar'); -exports.Fx = require('./graph_interact'); -// then trace modules - scatter has to come first -exports.Scatter = require('./scatter'); -exports.Bars = require('./bars'); -exports.Boxes = require('./boxes'); -exports.ErrorBars = require('./errorbars'); -exports.Heatmap = require('./heatmap'); -exports.Histogram = require('./histogram'); -exports.Pie = require('./pie'); -exports.Contour = require('./contour'); -// and extra plot components -exports.Annotations = require('./annotations'); -exports.Shapes = require('./shapes'); -exports.Legend = require('./legend'); -exports.ModeBar = require('./modebar'); - -// polar -exports.micropolar = require('./polar/micropolar'); -exports.micropolar.manager = require('./polar/micropolar_manager'); - -// GL3D -exports.Gl3dLayout = require('./gl3d/defaults/gl3dlayout'); -exports.Gl3dAxes = require('./gl3d/defaults/gl3daxes'); -exports.Scatter3D = require('./gl3d/defaults/scatter3d'); -exports.Surface = require('./gl3d/defaults/surface'); -exports.Mesh3D = require('./gl3d/defaults/mesh3d'); -exports.Scene = require('./gl3d/scene'); - -// Geo -exports.GeoLayout = require('./geo/defaults/geolayout'); -exports.GeoAxes = require('./geo/defaults/geoaxes'); -exports.ScatterGeo = require('./geo/defaults/scattergeo'); -exports.Choropleth = require('./geo/defaults/choropleth'); -exports.Geo = require('./geo/geo'); +exports.defaultConfig = require('./plot_api/plot_config'); -// GL2D -exports.ScatterGl = require('./gl2d/scattergl/scattergl'); -exports.Scene2D = require('./gl2d/scene2d'); +// plots +exports.Plots = require('./plots/plots'); +exports.Axes = require('./plots/cartesian/axes'); +exports.Fx = require('./plots/cartesian/graph_interact'); +exports.Scene = require('./plots/gl3d/scene'); +exports.Gl3dLayout = require('./plots/gl3d/layout'); +exports.Geo = require('./plots/geo/geo'); +exports.GeoLayout = require('./plots/geo/layout'); +exports.Scene2D = require('./plots/gl2d/scene2d'); +exports.micropolar = require('./plots/polar/micropolar'); -// plot schema -exports.PlotSchema = require('./plotschema'); +// components +exports.Color = require('./components/color'); +exports.Drawing = require('./components/drawing'); +exports.Colorscale = require('./components/colorscale'); +exports.Colorbar = require('./components/colorbar'); +exports.ErrorBars = require('./components/errorbars'); +exports.Annotations = require('./components/annotations'); +exports.Shapes = require('./components/shapes'); +exports.Titles = require('./components/titles'); +exports.Legend = require('./components/legend'); +exports.ModeBar = require('./components/modebar'); -// imaging Routines -exports.Snapshot = require('./snapshot/snapshot'); +// traces +exports.Scatter = require('./traces/scatter'); +exports.Bars = require('./traces/bars'); +exports.Boxes = require('./traces/boxes'); +exports.Heatmap = require('./traces/heatmap'); +exports.Histogram = require('./traces/histogram'); +exports.Pie = require('./traces/pie'); +exports.Contour = require('./traces/contour'); +exports.Scatter3D = require('./traces/scatter3d'); +exports.Surface = require('./traces/surface'); +exports.Mesh3D = require('./traces/mesh3d'); +exports.ScatterGeo = require('./traces/scattergeo'); +exports.Choropleth = require('./traces/choropleth'); +exports.ScatterGl = require('./traces/scattergl'); -// Queue for undo/redo -exports.Queue = require('./queue'); +// plot api +require('./plot_api/plot_api'); +exports.PlotSchema = require('./plot_api/plot_schema'); -// exports d3 used in the bundle -exports.d3 = require('d3'); +// imaging routines +exports.Snapshot = require('./snapshot'); diff --git a/src/plots/attributes.js b/src/plots/attributes.js new file mode 100644 index 00000000000..cc17f271eb0 --- /dev/null +++ b/src/plots/attributes.js @@ -0,0 +1,91 @@ +module.exports = { + type: { + valType: 'enumerated', + role: 'info', + values: [], // listed dynamically + dflt: 'scatter' + }, + visible: { + valType: 'enumerated', + values: [true, false, 'legendonly'], + role: 'info', + dflt: true, + description: [ + 'Determines whether or not this trace is visible.', + 'If *legendonly*, the trace is not drawn,', + 'but can appear as a legend item', + '(provided that the legend itself is visible).' + ].join(' ') + }, + showlegend: { + valType: 'boolean', + role: 'info', + dflt: true, + description: [ + 'Determines whether or not an item corresponding to this', + 'trace is shown in the legend.' + ].join(' ') + }, + legendgroup: { + valType: 'string', + role: 'info', + dflt: '', + description: [ + 'Sets the legend group for this trace.', + 'Traces part of the same legend group hide/show at the same time', + 'when toggling legend items.' + ].join(' ') + }, + opacity: { + valType: 'number', + role: 'style', + min: 0, + max: 1, + dflt: 1, + description: 'Sets the opacity of the trace.' + }, + name: { + valType: 'string', + role: 'info', + description: [ + 'Sets the trace name.', + 'The trace name appear as the legend item and on hover.' + ].join(' ') + }, + uid: { + valType: 'string', + role: 'info', + dflt: '' + }, + hoverinfo: { + valType: 'flaglist', + role: 'info', + flags: ['x', 'y', 'z', 'text', 'name'], + extras: ['all', 'none'], + dflt: 'all', + description: 'Determines which trace information appear on hover.' + }, + stream: { + token: { + valType: 'string', + noBlank: true, + strict: true, + role: 'info', + description: [ + 'The stream id number links a data trace on a plot with a stream.', + 'See https://plot.ly/settings for more details.' + ].join(' ') + }, + maxpoints: { + valType: 'number', + min: 0, + role: 'info', + description: [ + 'Sets the maximum number of points to keep on the plots from an', + 'incoming stream.', + 'If `maxpoints` is set to *50*, only the newest 50 points will', + 'be displayed on the plot.' + ].join(' ') + } + } +}; diff --git a/src/plots/cartesian/attributes.js b/src/plots/cartesian/attributes.js new file mode 100644 index 00000000000..a7c0127c1b8 --- /dev/null +++ b/src/plots/cartesian/attributes.js @@ -0,0 +1,26 @@ +module.exports = { + xaxis: { + valType: 'axisid', + role: 'info', + dflt: 'x', + description: [ + 'Sets a reference between this trace\'s x coordinates and', + 'a 2D cartesian x axis.', + 'If *x* (the default value), the x coordinates refer to', + '`layout.xaxis`.', + 'If *x2*, the x coordinates refer to `layout.xaxis2`, and so on.' + ].join(' ') + }, + yaxis: { + valType: 'axisid', + role: 'info', + dflt: 'y', + description: [ + 'Sets a reference between this trace\'s y coordinates and', + 'a 2D cartesian y axis.', + 'If *y* (the default value), the y coordinates refer to', + '`layout.yaxis`.', + 'If *y2*, the y coordinates refer to `layout.xaxis2`, and so on.' + ].join(' ') + } +}; diff --git a/src/axes.js b/src/plots/cartesian/axes.js similarity index 84% rename from src/axes.js rename to src/plots/cartesian/axes.js index 345e3ce0f80..fae0224ddc0 100644 --- a/src/axes.js +++ b/src/plots/cartesian/axes.js @@ -1,490 +1,16 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); var axes = module.exports = {}; -axes.traceAttributes = { - xaxis: { - valType: 'axisid', - role: 'info', - dflt: 'x', - description: [ - 'Sets a reference between this trace\'s x coordinates and', - 'a 2D cartesian x axis.', - 'If *x* (the default value), the x coordinates refer to', - '`layout.xaxis`.', - 'If *x2*, the x coordinates refer to `layout.xaxis2`, and so on.' - ].join(' ') - }, - yaxis: { - valType: 'axisid', - role: 'info', - dflt: 'y', - description: [ - 'Sets a reference between this trace\'s y coordinates and', - 'a 2D cartesian y axis.', - 'If *y* (the default value), the y coordinates refer to', - '`layout.yaxis`.', - 'If *y2*, the y coordinates refer to `layout.xaxis2`, and so on.' - ].join(' ') - } -}; +axes.attributes = require('./attributes'); -Plotly.Plots.registerSubplot('cartesian', ['xaxis', 'yaxis'], ['x', 'y'], - axes.traceAttributes); - -var extendFlat = Plotly.Lib.extendFlat; - -axes.layoutAttributes = { - title: { - valType: 'string', - role: 'info', - description: 'Sets the title of this axis.' - }, - titlefont: extendFlat({}, Plotly.Plots.fontAttrs, { - description: [ - 'Sets this axis\' title font.' - ].join(' ') - }), - type: { - valType: 'enumerated', - // '-' means we haven't yet run autotype or couldn't find any data - // it gets turned into linear in td._fullLayout but not copied back - // to td.data like the others are. - values: ['-', 'linear', 'log', 'date', 'category'], - dflt: '-', - role: 'info', - description: [ - 'Sets the axis type.', - 'By default, plotly attempts to determined the axis type', - 'by looking into the data of the traces that referenced', - 'the axis in question.' - ].join(' ') - }, - autorange: { - valType: 'enumerated', - values: [true, false, 'reversed'], - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the range of this axis is', - 'computed in relation to the input data.', - 'See `rangemode` for more info.', - 'If `range` is provided, then `autorange` is set to *false*.' - ].join(' ') - }, - rangemode: { - valType: 'enumerated', - values: ['normal', 'tozero', 'nonnegative'], - dflt: 'normal', - role: 'style', - description: [ - 'If *normal*, the range is computed in relation to the extrema', - 'of the input data.', - 'If *tozero*`, the range extends to 0,', - 'regardless of the input data', - 'If *nonnegative*, the range is non-negative,', - 'regardless of the input data.' - ].join(' ') - }, - range: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number'}, - {valType: 'number'} - ], - description: [ - 'Sets the range of this axis.', - 'If the axis `type` is *log*, then you must take the log of your desired range', - '(e.g. to set the range from 1 to 100, set the range from 0 to 2).', - 'If the axis `type` is *date*, then you must convert the date to unix time in milliseconds', - '(the number of milliseconds since January 1st, 1970). For example, to set the date range from', - 'January 1st 1970 to November 4th, 2013, set the range from 0 to 1380844800000.0' - ].join(' ') - }, - fixedrange: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'Determines whether or not this axis is zoom-able.', - 'If true, then zoom is disabled.' - ].join(' ') - }, - // ticks - tickmode: { - valType: 'enumerated', - values: ['auto', 'linear', 'array'], - role: 'info', - description: [ - 'Sets the tick mode for this axis.', - 'If *auto*, the number of ticks is set via `nticks`.', - 'If *linear*, the placement of the ticks is determined by', - 'a starting position `tick0` and a tick step `dtick`', - '(*linear* is the default value if `tick0` and `dtick` are provided).', - 'If *array*, the placement of the ticks is set via `tickvals`', - 'and the tick text is `ticktext`.', - '(*array* is the default value if `tickvals` is provided).' - ].join(' ') - }, - nticks: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Sets the number of ticks.', - 'Has an effect only if `tickmode` is set to *auto*.' - ].join(' ') - }, - tick0: { - valType: 'number', - dflt: 0, - role: 'style', - description: [ - 'Sets the placement of the first tick on this axis.', - 'Use with `dtick`.', - 'If the axis `type` is *log*, then you must take the log of your starting tick', - '(e.g. to set the starting tick to 100, set the `tick0` to 2).', - 'If the axis `type` is *date*, then you must convert the date to unix time in milliseconds', - '(the number of milliseconds since January 1st, 1970).', - 'For example, to set the starting tick to', - 'November 4th, 2013, set the range to 1380844800000.0.' - ].join(' ') - }, - dtick: { - valType: 'any', - dflt: 1, - role: 'style', - description: [ - 'Sets the step in-between ticks on this axis', - 'Use with `tick0`.', - 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', - 'is the tick number. For example,', - 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', - 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', - 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', - 'If the axis `type` is *date*, then you must convert the time to milliseconds.', - 'For example, to set the interval between ticks to one day,', - 'set `dtick` to 86400000.0.' - ].join(' ') - }, - tickvals: { - valType: 'data_array', - description: [ - 'Sets the values at which ticks on this axis appear.', - 'Only has an effect if `tickmode` is set to *array*.', - 'Used with `ticktext`.' - ].join(' ') - }, - ticktext: { - valType: 'data_array', - description: [ - 'Sets the text displayed at the ticks position via `tickvals`.', - 'Only has an effect if `tickmode` is set to *array*.', - 'Used with `ticktext`.' - ].join(' ') - }, - ticks: { - valType: 'enumerated', - values: ['outside', 'inside', ''], - role: 'style', - description: [ - 'Determines whether ticks are drawn or not.', - 'If **, this axis\' ticks are not drawn.', - 'If *outside* (*inside*), this axis\' are drawn outside (inside)', - 'the axis lines.' - ].join(' ') - }, - mirror: { - valType: 'enumerated', - values: [true, 'ticks', false, 'all', 'allticks'], - dflt: false, - role: 'style', - description: [ - 'Determines if the axis lines or/and ticks are mirrored to', - 'the opposite side of the plotting area.', - 'If *true*, the axis lines are mirrored.', - 'If *ticks*, the axis lines and ticks are mirrored.', - 'If *false*, mirroring is disable.', - 'If *all*, axis lines are mirrored on all shared-axes subplots.', - 'If *allticks*, axis lines and ticks are mirrored', - 'on all shared-axes subplots.' - ].join(' ') - }, - ticklen: { - valType: 'number', - min: 0, - dflt: 5, - role: 'style', - description: 'Sets the tick length (in px).' - }, - tickwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the tick width (in px).' - }, - tickcolor: { - valType: 'color', - dflt: Plotly.Color.defaultLine, - role: 'style', - description: 'Sets the tick color.' - }, - showticklabels: { - valType: 'boolean', - dflt: true, - role: 'style', - description: 'Determines whether or not the tick labels are drawn.' - }, - tickfont: extendFlat({}, Plotly.Plots.fontAttrs, { - description: 'Sets the tick font.' - }), - tickangle: { - valType: 'angle', - dflt: 'auto', - role: 'style', - description: [ - 'Sets the angle of the tick labels with respect to the horizontal.', - 'For example, a `tickangle` of -90 draws the tick labels', - 'vertically.' - ].join(' ') - }, - tickprefix: { - valType: 'string', - dflt: '', - role: 'style', - description: 'Sets a tick label prefix.' - }, - showtickprefix: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: [ - 'If *all*, all tick labels are displayed with a prefix.', - 'If *first*, only the first tick is displayed with a prefix.', - 'If *last*, only the last tick is displayed with a suffix.', - 'If *none*, tick prefixes are hidden.' - ].join(' ') - }, - ticksuffix: { - valType: 'string', - dflt: '', - role: 'style', - description: 'Sets a tick label suffix.' - }, - showticksuffix: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: 'Same as `showtickprefix` but for tick suffixes.' - }, - showexponent: { - valType: 'enumerated', - values: ['all', 'first', 'last', 'none'], - dflt: 'all', - role: 'style', - description: [ - 'If *all*, all exponents are shown besides their significands.', - 'If *first*, only the exponent of the first tick is shown.', - 'If *last*, only the exponent of the last tick is shown.', - 'If *none*, no exponents appear.' - ].join(' ') - }, - exponentformat: { - valType: 'enumerated', - values: ['none', 'e', 'E', 'power', 'SI', 'B'], - dflt: 'B', - role: 'style', - description: [ - 'Determines a formatting rule for the tick exponents.', - 'For example, consider the number 1,000,000,000.', - 'If *none*, it appears as 1,000,000,000.', - 'If *e*, 1e+9.', - 'If *E*, 1E+9.', - 'If *power*, 1x10^9 (with 9 in a super script).', - 'If *SI*, 1G.', - 'If *B*, 1B.' - ].join(' ') - }, - tickformat: { - valType: 'string', - dflt: '', - role: 'style', - description: [ - 'Sets the tick label formatting rule using the', - 'python/d3 number formatting language.', - 'See https://github.com/mbostock/d3/wiki/Formatting#numbers', - 'or https://docs.python.org/release/3.1.3/library/string.html#formatspec', - 'for more info.' - ].join(' ') - }, - hoverformat: { - valType: 'string', - dflt: '', - role: 'style', - description: [ - 'Sets the hover text formatting rule for data values on this axis,', - 'using the python/d3 number formatting language.', - 'See https://github.com/mbostock/d3/wiki/Formatting#numbers', - 'or https://docs.python.org/release/3.1.3/library/string.html#formatspec', - 'for more info.' - ].join(' ') - }, - // lines and grids - showline: { - valType: 'boolean', - dflt: false, - role: 'style', - description: [ - 'Determines whether or not a line bounding this axis is drawn.' - ].join(' ') - }, - linecolor: { - valType: 'color', - dflt: Plotly.Color.defaultLine, - role: 'style', - description: 'Sets the axis line color.' - }, - linewidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the axis line.' - }, - showgrid: { - valType: 'boolean', - role: 'style', - description: [ - 'Determines whether or not grid lines are drawn.', - 'If *true*, the grid lines are drawn at every tick mark.' - ].join(' ') - }, - gridcolor: { - valType: 'color', - dflt: Plotly.Color.lightLine, - role: 'style', - description: 'Sets the color of the grid lines.' - }, - gridwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the grid lines.' - }, - zeroline: { - valType: 'boolean', - role: 'style', - description: [ - 'Determines whether or not a line is drawn at along the 0 value', - 'of this axis.', - 'If *true*, the zero line is drawn on top of the grid lines.' - ].join(' ') - }, - zerolinecolor: { - valType: 'color', - dflt: Plotly.Color.defaultLine, - role: 'style', - description: 'Sets the line color of the zero line.' - }, - zerolinewidth: { - valType: 'number', - dflt: 1, - role: 'style', - description: 'Sets the width (in px) of the zero line.' - }, - // positioning attributes - // anchor: not used directly, just put here for reference - // values are any opposite-letter axis id - anchor: { - valType: 'enumerated', - values: [ - 'free', - Plotly.Plots.subplotsRegistry.cartesian.idRegex.x.toString(), - Plotly.Plots.subplotsRegistry.cartesian.idRegex.y.toString() - ], - role: 'info', - description: [ - 'If set to an opposite-letter axis id (e.g. `xaxis2`, `yaxis`), this axis is bound to', - 'the corresponding opposite-letter axis.', - 'If set to *free*, this axis\' position is determined by `position`.' - ].join(' ') - }, - // side: not used directly, as values depend on direction - // values are top, bottom for x axes, and left, right for y - side: { - valType: 'enumerated', - values: ['top', 'bottom', 'left', 'right'], - role: 'info', - description: [ - 'Determines whether a x (y) axis is positioned', - 'at the *bottom* (*left*) or *top* (*right*)', - 'of the plotting area.' - ].join(' ') - }, - // overlaying: not used directly, just put here for reference - // values are false and any other same-letter axis id that's not - // itself overlaying anything - overlaying: { - valType: 'enumerated', - values: [ - 'free', - Plotly.Plots.subplotsRegistry.cartesian.idRegex.x.toString(), - Plotly.Plots.subplotsRegistry.cartesian.idRegex.y.toString() - ], - role: 'info', - description: [ - 'If set a same-letter axis id, this axis is overlaid on top of', - 'the corresponding same-letter axis.', - 'If *false*, this axis does not overlay any same-letter axes.' - ].join(' ') - }, - domain: { - valType: 'info_array', - role: 'info', - items: [ - {valType: 'number', min: 0, max: 1}, - {valType: 'number', min: 0, max: 1} - ], - dflt: [0, 1], - description: [ - 'Sets the domain of this axis (in plot fraction).' - ].join(' ') - }, - position: { - valType: 'number', - min: 0, - max: 1, - dflt: 0, - role: 'style', - description: [ - 'Sets the position of this axis in the plotting space', - '(in normalized coordinates).', - 'Only has an effect if `anchor` is set to *free*.' - ].join(' ') - }, - - _deprecated: { - autotick: { - valType: 'boolean', - role: 'info', - description: [ - 'Obsolete.', - 'Set `tickmode` to *auto* for old `autotick` *true* behavior.', - 'Set `tickmode` to *linear* for `autotick` *false*.' - ].join(' ') - } - } -}; +Plotly.Plots.registerSubplot('cartesian', ['xaxis', 'yaxis'], ['x', 'y'], axes.attributes); + +axes.layoutAttributes = require('./layout_attributes'); var xAxisMatch = /^xaxis[0-9]*$/, yAxisMatch = /^yaxis[0-9]*$/; @@ -2718,7 +2244,7 @@ axes.doTicks = function(td, axid, skipTitle) { var tickLabels=container.selectAll('g.'+tcls).data(vals, datafn); if(!ax.showticklabels || !isNumeric(position)) { tickLabels.remove(); - Plotly.Plots.titles(td, axid+'title'); + Plotly.Titles.draw(td, axid + 'title'); return; } @@ -2886,7 +2412,7 @@ axes.doTicks = function(td, axid, skipTitle) { // (so it can move out of the way if needed) // TODO: separate out scoot so we don't need to do // a full redraw of the title (modtly relevant for MathJax) - if(!skipTitle) Plotly.Plots.titles(td,axid+'title'); + if(!skipTitle) Plotly.Titles.draw(td, axid + 'title'); return axid+' done'; } diff --git a/src/graph_interact.js b/src/plots/cartesian/graph_interact.js similarity index 99% rename from src/graph_interact.js rename to src/plots/cartesian/graph_interact.js index deeb6f37a15..ce068313722 100644 --- a/src/graph_interact.js +++ b/src/plots/cartesian/graph_interact.js @@ -1,10 +1,10 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var tinycolor = require('tinycolor2'); var isNumeric = require('fast-isnumeric'); -var Events = require('./events'); +var Events = require('../../lib/events'); var fx = module.exports = {}; diff --git a/src/plots/cartesian/layout_attributes.js b/src/plots/cartesian/layout_attributes.js new file mode 100644 index 00000000000..00ce79c2cde --- /dev/null +++ b/src/plots/cartesian/layout_attributes.js @@ -0,0 +1,453 @@ +var Plotly = require('../../plotly'); +var fontAttrs = require('../font_attributes'); +var colorAttrs = require('../../components/color/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + + +module.exports = { + title: { + valType: 'string', + role: 'info', + description: 'Sets the title of this axis.' + }, + titlefont: extendFlat({}, fontAttrs, { + description: [ + 'Sets this axis\' title font.' + ].join(' ') + }), + type: { + valType: 'enumerated', + // '-' means we haven't yet run autotype or couldn't find any data + // it gets turned into linear in td._fullLayout but not copied back + // to td.data like the others are. + values: ['-', 'linear', 'log', 'date', 'category'], + dflt: '-', + role: 'info', + description: [ + 'Sets the axis type.', + 'By default, plotly attempts to determined the axis type', + 'by looking into the data of the traces that referenced', + 'the axis in question.' + ].join(' ') + }, + autorange: { + valType: 'enumerated', + values: [true, false, 'reversed'], + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the range of this axis is', + 'computed in relation to the input data.', + 'See `rangemode` for more info.', + 'If `range` is provided, then `autorange` is set to *false*.' + ].join(' ') + }, + rangemode: { + valType: 'enumerated', + values: ['normal', 'tozero', 'nonnegative'], + dflt: 'normal', + role: 'style', + description: [ + 'If *normal*, the range is computed in relation to the extrema', + 'of the input data.', + 'If *tozero*`, the range extends to 0,', + 'regardless of the input data', + 'If *nonnegative*, the range is non-negative,', + 'regardless of the input data.' + ].join(' ') + }, + range: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number'}, + {valType: 'number'} + ], + description: [ + 'Sets the range of this axis.', + 'If the axis `type` is *log*, then you must take the log of your desired range', + '(e.g. to set the range from 1 to 100, set the range from 0 to 2).', + 'If the axis `type` is *date*, then you must convert the date to unix time in milliseconds', + '(the number of milliseconds since January 1st, 1970). For example, to set the date range from', + 'January 1st 1970 to November 4th, 2013, set the range from 0 to 1380844800000.0' + ].join(' ') + }, + fixedrange: { + valType: 'boolean', + dflt: false, + role: 'info', + description: [ + 'Determines whether or not this axis is zoom-able.', + 'If true, then zoom is disabled.' + ].join(' ') + }, + // ticks + tickmode: { + valType: 'enumerated', + values: ['auto', 'linear', 'array'], + role: 'info', + description: [ + 'Sets the tick mode for this axis.', + 'If *auto*, the number of ticks is set via `nticks`.', + 'If *linear*, the placement of the ticks is determined by', + 'a starting position `tick0` and a tick step `dtick`', + '(*linear* is the default value if `tick0` and `dtick` are provided).', + 'If *array*, the placement of the ticks is set via `tickvals`', + 'and the tick text is `ticktext`.', + '(*array* is the default value if `tickvals` is provided).' + ].join(' ') + }, + nticks: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'style', + description: [ + 'Sets the number of ticks.', + 'Has an effect only if `tickmode` is set to *auto*.' + ].join(' ') + }, + tick0: { + valType: 'number', + dflt: 0, + role: 'style', + description: [ + 'Sets the placement of the first tick on this axis.', + 'Use with `dtick`.', + 'If the axis `type` is *log*, then you must take the log of your starting tick', + '(e.g. to set the starting tick to 100, set the `tick0` to 2).', + 'If the axis `type` is *date*, then you must convert the date to unix time in milliseconds', + '(the number of milliseconds since January 1st, 1970).', + 'For example, to set the starting tick to', + 'November 4th, 2013, set the range to 1380844800000.0.' + ].join(' ') + }, + dtick: { + valType: 'any', + dflt: 1, + role: 'style', + description: [ + 'Sets the step in-between ticks on this axis', + 'Use with `tick0`.', + 'If the axis `type` is *log*, then ticks are set every 10^(n*dtick) where n', + 'is the tick number. For example,', + 'to set a tick mark at 1, 10, 100, 1000, ... set dtick to 1.', + 'To set tick marks at 1, 100, 10000, ... set dtick to 2.', + 'To set tick marks at 1, 5, 25, 125, 625, 3125, ... set dtick to log_10(5), or 0.69897000433.', + 'If the axis `type` is *date*, then you must convert the time to milliseconds.', + 'For example, to set the interval between ticks to one day,', + 'set `dtick` to 86400000.0.' + ].join(' ') + }, + tickvals: { + valType: 'data_array', + description: [ + 'Sets the values at which ticks on this axis appear.', + 'Only has an effect if `tickmode` is set to *array*.', + 'Used with `ticktext`.' + ].join(' ') + }, + ticktext: { + valType: 'data_array', + description: [ + 'Sets the text displayed at the ticks position via `tickvals`.', + 'Only has an effect if `tickmode` is set to *array*.', + 'Used with `ticktext`.' + ].join(' ') + }, + ticks: { + valType: 'enumerated', + values: ['outside', 'inside', ''], + role: 'style', + description: [ + 'Determines whether ticks are drawn or not.', + 'If **, this axis\' ticks are not drawn.', + 'If *outside* (*inside*), this axis\' are drawn outside (inside)', + 'the axis lines.' + ].join(' ') + }, + mirror: { + valType: 'enumerated', + values: [true, 'ticks', false, 'all', 'allticks'], + dflt: false, + role: 'style', + description: [ + 'Determines if the axis lines or/and ticks are mirrored to', + 'the opposite side of the plotting area.', + 'If *true*, the axis lines are mirrored.', + 'If *ticks*, the axis lines and ticks are mirrored.', + 'If *false*, mirroring is disable.', + 'If *all*, axis lines are mirrored on all shared-axes subplots.', + 'If *allticks*, axis lines and ticks are mirrored', + 'on all shared-axes subplots.' + ].join(' ') + }, + ticklen: { + valType: 'number', + min: 0, + dflt: 5, + role: 'style', + description: 'Sets the tick length (in px).' + }, + tickwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the tick width (in px).' + }, + tickcolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the tick color.' + }, + showticklabels: { + valType: 'boolean', + dflt: true, + role: 'style', + description: 'Determines whether or not the tick labels are drawn.' + }, + tickfont: extendFlat({}, fontAttrs, { + description: 'Sets the tick font.' + }), + tickangle: { + valType: 'angle', + dflt: 'auto', + role: 'style', + description: [ + 'Sets the angle of the tick labels with respect to the horizontal.', + 'For example, a `tickangle` of -90 draws the tick labels', + 'vertically.' + ].join(' ') + }, + tickprefix: { + valType: 'string', + dflt: '', + role: 'style', + description: 'Sets a tick label prefix.' + }, + showtickprefix: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: [ + 'If *all*, all tick labels are displayed with a prefix.', + 'If *first*, only the first tick is displayed with a prefix.', + 'If *last*, only the last tick is displayed with a suffix.', + 'If *none*, tick prefixes are hidden.' + ].join(' ') + }, + ticksuffix: { + valType: 'string', + dflt: '', + role: 'style', + description: 'Sets a tick label suffix.' + }, + showticksuffix: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: 'Same as `showtickprefix` but for tick suffixes.' + }, + showexponent: { + valType: 'enumerated', + values: ['all', 'first', 'last', 'none'], + dflt: 'all', + role: 'style', + description: [ + 'If *all*, all exponents are shown besides their significands.', + 'If *first*, only the exponent of the first tick is shown.', + 'If *last*, only the exponent of the last tick is shown.', + 'If *none*, no exponents appear.' + ].join(' ') + }, + exponentformat: { + valType: 'enumerated', + values: ['none', 'e', 'E', 'power', 'SI', 'B'], + dflt: 'B', + role: 'style', + description: [ + 'Determines a formatting rule for the tick exponents.', + 'For example, consider the number 1,000,000,000.', + 'If *none*, it appears as 1,000,000,000.', + 'If *e*, 1e+9.', + 'If *E*, 1E+9.', + 'If *power*, 1x10^9 (with 9 in a super script).', + 'If *SI*, 1G.', + 'If *B*, 1B.' + ].join(' ') + }, + tickformat: { + valType: 'string', + dflt: '', + role: 'style', + description: [ + 'Sets the tick label formatting rule using the', + 'python/d3 number formatting language.', + 'See https://github.com/mbostock/d3/wiki/Formatting#numbers', + 'or https://docs.python.org/release/3.1.3/library/string.html#formatspec', + 'for more info.' + ].join(' ') + }, + hoverformat: { + valType: 'string', + dflt: '', + role: 'style', + description: [ + 'Sets the hover text formatting rule for data values on this axis,', + 'using the python/d3 number formatting language.', + 'See https://github.com/mbostock/d3/wiki/Formatting#numbers', + 'or https://docs.python.org/release/3.1.3/library/string.html#formatspec', + 'for more info.' + ].join(' ') + }, + // lines and grids + showline: { + valType: 'boolean', + dflt: false, + role: 'style', + description: [ + 'Determines whether or not a line bounding this axis is drawn.' + ].join(' ') + }, + linecolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the axis line color.' + }, + linewidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the axis line.' + }, + showgrid: { + valType: 'boolean', + role: 'style', + description: [ + 'Determines whether or not grid lines are drawn.', + 'If *true*, the grid lines are drawn at every tick mark.' + ].join(' ') + }, + gridcolor: { + valType: 'color', + dflt: colorAttrs.lightLine, + role: 'style', + description: 'Sets the color of the grid lines.' + }, + gridwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the grid lines.' + }, + zeroline: { + valType: 'boolean', + role: 'style', + description: [ + 'Determines whether or not a line is drawn at along the 0 value', + 'of this axis.', + 'If *true*, the zero line is drawn on top of the grid lines.' + ].join(' ') + }, + zerolinecolor: { + valType: 'color', + dflt: colorAttrs.defaultLine, + role: 'style', + description: 'Sets the line color of the zero line.' + }, + zerolinewidth: { + valType: 'number', + dflt: 1, + role: 'style', + description: 'Sets the width (in px) of the zero line.' + }, + // positioning attributes + // anchor: not used directly, just put here for reference + // values are any opposite-letter axis id + anchor: { + valType: 'enumerated', + values: [ + 'free', + Plotly.Plots.subplotsRegistry.cartesian.idRegex.x.toString(), + Plotly.Plots.subplotsRegistry.cartesian.idRegex.y.toString() + ], + role: 'info', + description: [ + 'If set to an opposite-letter axis id (e.g. `xaxis2`, `yaxis`), this axis is bound to', + 'the corresponding opposite-letter axis.', + 'If set to *free*, this axis\' position is determined by `position`.' + ].join(' ') + }, + // side: not used directly, as values depend on direction + // values are top, bottom for x axes, and left, right for y + side: { + valType: 'enumerated', + values: ['top', 'bottom', 'left', 'right'], + role: 'info', + description: [ + 'Determines whether a x (y) axis is positioned', + 'at the *bottom* (*left*) or *top* (*right*)', + 'of the plotting area.' + ].join(' ') + }, + // overlaying: not used directly, just put here for reference + // values are false and any other same-letter axis id that's not + // itself overlaying anything + overlaying: { + valType: 'enumerated', + values: [ + 'free', + Plotly.Plots.subplotsRegistry.cartesian.idRegex.x.toString(), + Plotly.Plots.subplotsRegistry.cartesian.idRegex.y.toString() + ], + role: 'info', + description: [ + 'If set a same-letter axis id, this axis is overlaid on top of', + 'the corresponding same-letter axis.', + 'If *false*, this axis does not overlay any same-letter axes.' + ].join(' ') + }, + domain: { + valType: 'info_array', + role: 'info', + items: [ + {valType: 'number', min: 0, max: 1}, + {valType: 'number', min: 0, max: 1} + ], + dflt: [0, 1], + description: [ + 'Sets the domain of this axis (in plot fraction).' + ].join(' ') + }, + position: { + valType: 'number', + min: 0, + max: 1, + dflt: 0, + role: 'style', + description: [ + 'Sets the position of this axis in the plotting space', + '(in normalized coordinates).', + 'Only has an effect if `anchor` is set to *free*.' + ].join(' ') + }, + + _deprecated: { + autotick: { + valType: 'boolean', + role: 'info', + description: [ + 'Obsolete.', + 'Set `tickmode` to *auto* for old `autotick` *true* behavior.', + 'Set `tickmode` to *linear* for `autotick` *false*.' + ].join(' ') + } + } +}; diff --git a/src/plots/font_attributes.js b/src/plots/font_attributes.js new file mode 100644 index 00000000000..742bc509c81 --- /dev/null +++ b/src/plots/font_attributes.js @@ -0,0 +1,29 @@ +module.exports = { + family: { + valType: 'string', + role: 'style', + noBlank: true, + strict: true, + description: [ + 'HTML font family - the typeface that will be applied by the web browser.', + 'The web browser will only be able to apply a font if it is available on the system', + 'which it operates. Provide multiple font families, separated by commas, to indicate', + 'the preference in which to apply fonts if they aren\'t available on the system.', + 'The plotly service (at https://plot.ly or on-premise) generates images on a server,', + 'where only a select number of', + 'fonts are installed and supported.', + 'These include *Arial*, *Balto*, *Courier New*, *Droid Sans*,, *Droid Serif*,', + '*Droid Sans Mono*, *Gravitas One*, *Old Standard TT*, *Open Sans*, *Overpass*,', + '*PT Sans Narrow*, *Raleway*, *Times New Roman*.' + ].join(' ') + }, + size: { + valType: 'number', + role: 'style', + min: 1 + }, + color: { + valType: 'color', + role: 'style' + } +}; diff --git a/src/geo/geo.js b/src/plots/geo/geo.js similarity index 90% rename from src/geo/geo.js rename to src/plots/geo/geo.js index 6574b00da22..0408074eb95 100644 --- a/src/geo/geo.js +++ b/src/plots/geo/geo.js @@ -2,17 +2,21 @@ /* global PlotlyGeoAssets:false */ -var Plotly = require('../plotly'), - d3 = require('d3'), - params = require('./lib/params'), - addProjectionsToD3 = require('./lib/projections'), - createGeoScale = require('./lib/set-scale'), - createGeoZoom = require('./lib/zoom'), - createGeoZoomReset = require('./lib/zoom-reset'), - plotScatterGeo = require('./plot/scattergeo'), - plotChoropleth = require('./plot/choropleth'), - topojsonUtils = require('./lib/topojson-utils'), - topojsonFeature = require('topojson').feature; +var Plotly = require('../../plotly'); +var d3 = require('d3'); + +var addProjectionsToD3 = require('./projections'); +var createGeoScale = require('./set_scale'); +var createGeoZoom = require('./zoom'); +var createGeoZoomReset = require('./zoom_reset'); + +var plotScatterGeo = require('../../traces/scattergeo/plot'); +var plotChoropleth = require('../../traces/choropleth/plot'); + +var constants = require('../../constants/geo_constants'); +var topojsonUtils = require('../../lib/topojson_utils'); +var topojsonFeature = require('topojson').feature; + function Geo(options, fullLayout) { @@ -129,13 +133,13 @@ proto.makeProjection = function(geoLayout) { if(isNew) { this.projectionType = projType; - projection = this.projection = d3.geo[params.projNames[projType]](); + projection = this.projection = d3.geo[constants.projNames[projType]](); } else projection = this.projection; projection .translate(projLayout._translate0) - .precision(params.precision); + .precision(constants.precision); if(!geoLayout._isAlbersUsa) { projection @@ -146,7 +150,7 @@ proto.makeProjection = function(geoLayout) { if(geoLayout._clipAngle) { this.clipAngle = geoLayout._clipAngle; // needed in proto.render projection - .clipAngle(geoLayout._clipAngle - params.clipPad); + .clipAngle(geoLayout._clipAngle - constants.clipPad); } else this.clipAngle = null; // for graph edits @@ -249,7 +253,7 @@ proto.drawTopo = function(selection, layerName, geoLayout) { var topojson = this.topojson, datum = layerName==='frame' ? - params.sphereSVG : + constants.sphereSVG : topojsonFeature(topojson, topojson.objects[layerName]); selection.append('g') @@ -273,7 +277,7 @@ proto.drawGraticule = function(selection, axisName, geoLayout) { if(axisLayout.showgrid !== true) return; - var scopeDefaults = params.scopeDefaults[geoLayout.scope], + var scopeDefaults = constants.scopeDefaults[geoLayout.scope], lonaxisRange = scopeDefaults.lonaxisRange, lataxisRange = scopeDefaults.lataxisRange, step = axisName==='lonaxis' ? @@ -290,8 +294,8 @@ proto.drawGraticule = function(selection, axisName, geoLayout) { proto.drawLayout = function(geoLayout) { var gBaseLayer = this.framework.select('g.baselayer'), - baseLayers = params.baseLayers, - axesNames = params.axesNames, + baseLayers = constants.baseLayers, + axesNames = constants.axesNames, layerName; // TODO move to more d3-idiomatic pattern (that's work on replot) @@ -311,7 +315,7 @@ proto.drawLayout = function(geoLayout) { }; function styleFillLayer(selection, layerName, geoLayout) { - var layerAdj = params.layerNameToAdjective[layerName]; + var layerAdj = constants.layerNameToAdjective[layerName]; selection.select('.' + layerName) .selectAll('path') @@ -320,7 +324,7 @@ function styleFillLayer(selection, layerName, geoLayout) { } function styleLineLayer(selection, layerName, geoLayout) { - var layerAdj = params.layerNameToAdjective[layerName]; + var layerAdj = constants.layerNameToAdjective[layerName]; selection.select('.' + layerName) .selectAll('path') @@ -338,8 +342,8 @@ function styleGraticule(selection, axisName, geoLayout) { } proto.styleLayer = function(selection, layerName, geoLayout) { - var fillLayers = params.fillLayers, - lineLayers = params.lineLayers; + var fillLayers = constants.fillLayers, + lineLayers = constants.lineLayers; if(fillLayers.indexOf(layerName)!==-1) { styleFillLayer(selection, layerName, geoLayout); @@ -351,8 +355,8 @@ proto.styleLayer = function(selection, layerName, geoLayout) { proto.styleLayout = function(geoLayout) { var gBaseLayer = this.framework.select('g.baselayer'), - baseLayers = params.baseLayers, - axesNames = params.axesNames, + baseLayers = constants.baseLayers, + axesNames = constants.axesNames, layerName; for(var i = 0; i < baseLayers.length; i++) { diff --git a/src/plots/geo/layout/attributes.js b/src/plots/geo/layout/attributes.js new file mode 100644 index 00000000000..9ad8341f0f9 --- /dev/null +++ b/src/plots/geo/layout/attributes.js @@ -0,0 +1,15 @@ +module.exports = { + geo: { + valType: 'geoid', + role: 'info', + dflt: 'geo', + description: [ + 'Sets a reference between this trace\'s geospatial coordinates and', + 'a geographic map.', + 'If *geo* (the default value), the geospatial coordinates refer to', + '`layout.geo`.', + 'If *geo2*, the geospatial coordinates refer to `layout.geo2`,', + 'and so on.' + ].join(' ') + } +}; diff --git a/src/geo/attributes/geoaxes.js b/src/plots/geo/layout/axis_attributes.js similarity index 91% rename from src/geo/attributes/geoaxes.js rename to src/plots/geo/layout/axis_attributes.js index 0018f4b198c..99cb42dfa9a 100644 --- a/src/geo/attributes/geoaxes.js +++ b/src/plots/geo/layout/axis_attributes.js @@ -1,4 +1,4 @@ -var Plotly = require('../../plotly'); +var colorAttrs = require('../../../components/color/attributes'); module.exports = { range: { @@ -33,7 +33,7 @@ module.exports = { gridcolor: { valType: 'color', role: 'style', - dflt: Plotly.Color.lightLine, + dflt: colorAttrs.lightLine, description: [ 'Sets the graticule\'s stroke color.' ].join(' ') diff --git a/src/geo/defaults/geoaxes.js b/src/plots/geo/layout/axis_defaults.js similarity index 58% rename from src/geo/defaults/geoaxes.js rename to src/plots/geo/layout/axis_defaults.js index 1a028b241d7..7261c9d185c 100644 --- a/src/geo/defaults/geoaxes.js +++ b/src/plots/geo/layout/axis_defaults.js @@ -1,20 +1,15 @@ 'use strict'; -var Plotly = require('../../plotly'), - params = require('../lib/params'); +var Plotly = require('../../../plotly'); +var constants = require('../../../constants/geo_constants'); +var axisAttributes = require('./axis_attributes'); -var GeoAxes = module.exports = {}; -GeoAxes.layoutAttributes = require('../attributes/geoaxes'); - -GeoAxes.supplyLayoutDefaults = function(geoLayoutIn, geoLayoutOut) { - var axesNames = params.axesNames; - - var axisIn, axisOut, axisName, rangeDflt, range, show; +module.exports = function supplyGeoAxisLayoutDefaults(geoLayoutIn, geoLayoutOut) { + var axesNames = constants.axesNames; function coerce(attr, dflt) { - return Plotly.Lib.coerce(axisIn, axisOut, - GeoAxes.layoutAttributes, attr, dflt); + return Plotly.Lib.coerce(axisIn, axisOut, axisAttributes, attr, dflt); } function getRangeDflt(axisName) { @@ -26,7 +21,7 @@ GeoAxes.supplyLayoutDefaults = function(geoLayoutIn, geoLayoutOut) { projLayout = geoLayoutOut.projection; projType = projLayout.type; projRotation = projLayout.rotation; - dfltSpans = params[axisName + 'Span']; + dfltSpans = constants[axisName + 'Span']; halfSpan = dfltSpans[projType]!==undefined ? dfltSpans[projType] / 2 : @@ -37,24 +32,24 @@ GeoAxes.supplyLayoutDefaults = function(geoLayoutIn, geoLayoutOut) { return [rotateAngle - halfSpan, rotateAngle + halfSpan]; } - else return params.scopeDefaults[scope][axisName + 'Range']; + else return constants.scopeDefaults[scope][axisName + 'Range']; } for(var i = 0; i < axesNames.length; i++) { - axisName = axesNames[i]; - axisIn = geoLayoutIn[axisName] || {}; - axisOut = {}; + var axisName = axesNames[i]; + var axisIn = geoLayoutIn[axisName] || {}; + var axisOut = {}; - rangeDflt = getRangeDflt(axisName); + var rangeDflt = getRangeDflt(axisName); - range = coerce('range', rangeDflt); + var range = coerce('range', rangeDflt); Plotly.Lib.noneOrAll(axisIn.range, axisOut.range, [0, 1]); coerce('tick0', range[0]); coerce('dtick', axisName==='lonaxis' ? 30 : 10); - show = coerce('showgrid'); + var show = coerce('showgrid'); if(show) { coerce('gridcolor'); coerce('gridwidth'); diff --git a/src/geo/defaults/geolayout.js b/src/plots/geo/layout/defaults.js similarity index 54% rename from src/geo/defaults/geolayout.js rename to src/plots/geo/layout/defaults.js index 1d40136d402..bf8bd1a8b32 100644 --- a/src/geo/defaults/geolayout.js +++ b/src/plots/geo/layout/defaults.js @@ -1,74 +1,52 @@ 'use strict'; -var Plotly = require('../../plotly'), - params = require('../lib/params'); - -var GeoLayout = module.exports = {}; - -Plotly.Plots.registerSubplot('geo', 'geo', 'geo', { - geo: { - valType: 'geoid', - role: 'info', - dflt: 'geo', - description: [ - 'Sets a reference between this trace\'s geospatial coordinates and', - 'a geographic map.', - 'If *geo* (the default value), the geospatial coordinates refer to', - '`layout.geo`.', - 'If *geo2*, the geospatial coordinates refer to `layout.geo2`,', - 'and so on.' - ].join(' ') - } -}); +var Plotly = require('../../../plotly'); +var constants = require('../../../constants/geo_constants'); +var layoutAttributes = require('./layout_attributes'); +var supplyGeoAxisLayoutDefaults = require('./axis_defaults'); -GeoLayout.layoutAttributes = require('../attributes/geolayout'); -GeoLayout.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) { var geos = Plotly.Plots.getSubplotIdsInData(fullData, 'geo'), geosLength = geos.length; - var geo, geoLayoutIn, geoLayoutOut; - function coerce(attr, dflt) { - return Plotly.Lib.coerce(geoLayoutIn, geoLayoutOut, - GeoLayout.layoutAttributes, attr, dflt); + return Plotly.Lib.coerce(geoLayoutIn, geoLayoutOut, layoutAttributes, attr, dflt); } for(var i = 0; i < geosLength; i++) { - geo = geos[i]; - geoLayoutIn = layoutIn[geo] || {}; - geoLayoutOut = {}; + var geo = geos[i]; + var geoLayoutIn = layoutIn[geo] || {}; + var geoLayoutOut = {}; coerce('domain.x'); coerce('domain.y', [i / geosLength, (i + 1) / geosLength]); - GeoLayout.handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce); + handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce); layoutOut[geo] = geoLayoutOut; } }; -GeoLayout.handleGeoDefaults = function(geoLayoutIn, geoLayoutOut, coerce) { - var scope, resolution, projType, - scopeParams, dfltProjRotate, dfltProjParallels, - isScoped, isAlbersUsa, isConic, show; +function handleGeoDefaults(geoLayoutIn, geoLayoutOut, coerce) { + var show; - scope = coerce('scope'); - isScoped = scope!=='world'; - scopeParams = params.scopeDefaults[scope]; + var scope = coerce('scope'); + var isScoped = (scope !== 'world'); + var scopeParams = constants.scopeDefaults[scope]; - resolution = coerce('resolution'); + var resolution = coerce('resolution'); - projType = coerce('projection.type', scopeParams.projType); - isAlbersUsa = projType==='albers usa'; - isConic = projType.indexOf('conic')!==-1; + var projType = coerce('projection.type', scopeParams.projType); + var isAlbersUsa = projType==='albers usa'; + var isConic = projType.indexOf('conic')!==-1; if(isConic) { - dfltProjParallels = scopeParams.projParallels || [0, 60]; + var dfltProjParallels = scopeParams.projParallels || [0, 60]; coerce('projection.parallels', dfltProjParallels); } if(!isAlbersUsa) { - dfltProjRotate = scopeParams.projRotate || [0, 0, 0]; + var dfltProjRotate = scopeParams.projRotate || [0, 0, 0]; coerce('projection.rotation.lon', dfltProjRotate[0]); coerce('projection.rotation.lat', dfltProjRotate[1]); coerce('projection.rotation.roll', dfltProjRotate[2]); @@ -124,11 +102,11 @@ GeoLayout.handleGeoDefaults = function(geoLayoutIn, geoLayoutOut, coerce) { coerce('bgcolor'); - Plotly.GeoAxes.supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + supplyGeoAxisLayoutDefaults(geoLayoutIn, geoLayoutOut); // bind a few helper variables geoLayoutOut._isHighRes = resolution===50; - geoLayoutOut._clipAngle = params.lonaxisSpan[projType] / 2; + geoLayoutOut._clipAngle = constants.lonaxisSpan[projType] / 2; geoLayoutOut._isAlbersUsa = isAlbersUsa; geoLayoutOut._isConic = isConic; geoLayoutOut._isScoped = isScoped; @@ -139,4 +117,4 @@ GeoLayout.handleGeoDefaults = function(geoLayoutIn, geoLayoutOut, coerce) { -rotation.lat || 0, rotation.roll || 0 ]; -}; +} diff --git a/src/plots/geo/layout/index.js b/src/plots/geo/layout/index.js new file mode 100644 index 00000000000..91616991e53 --- /dev/null +++ b/src/plots/geo/layout/index.js @@ -0,0 +1,9 @@ +'use strict'; + +var Plotly = require('../../../plotly'); +var attributes = require('./attributes'); + +Plotly.Plots.registerSubplot('geo', 'geo', 'geo', attributes); + +exports.layoutAttributes = require('./layout_attributes'); +exports.supplyLayoutDefaults = require('./defaults'); diff --git a/src/geo/attributes/geolayout.js b/src/plots/geo/layout/layout_attributes.js similarity index 90% rename from src/geo/attributes/geolayout.js rename to src/plots/geo/layout/layout_attributes.js index be775750f96..24e87e052fd 100644 --- a/src/geo/attributes/geolayout.js +++ b/src/plots/geo/layout/layout_attributes.js @@ -1,5 +1,7 @@ -var Plotly = require('../../plotly'), - params = require('../lib/params'); +var colorAttrs = require('../../../components/color/attributes'); +var constants = require('../../../constants/geo_constants'); +var geoAxesAttrs = require('./axis_attributes'); + module.exports = { domain: { @@ -45,7 +47,7 @@ module.exports = { scope: { valType: 'enumerated', role: 'info', - values: Object.keys(params.scopeDefaults), + values: Object.keys(constants.scopeDefaults), dflt: 'world', description: 'Set the scope of the map.' }, @@ -53,7 +55,7 @@ module.exports = { type: { valType: 'enumerated', role: 'info', - values: Object.keys(params.projNames), + values: Object.keys(constants.projNames), description: 'Sets the projection type.' }, rotation: { @@ -112,7 +114,7 @@ module.exports = { coastlinecolor: { valType: 'color', role: 'style', - dflt: Plotly.Color.defaultLine, + dflt: colorAttrs.defaultLine, description: 'Sets the coastline color.' }, coastlinewidth: { @@ -131,7 +133,7 @@ module.exports = { landcolor: { valType: 'color', role: 'style', - dflt: params.landColor, + dflt: constants.landColor, description: 'Sets the land mass color.' }, showocean: { @@ -143,7 +145,7 @@ module.exports = { oceancolor: { valType: 'color', role: 'style', - dflt: params.waterColor, + dflt: constants.waterColor, description: 'Sets the ocean color' }, showlakes: { @@ -155,7 +157,7 @@ module.exports = { lakecolor: { valType: 'color', role: 'style', - dflt: params.waterColor, + dflt: constants.waterColor, description: 'Sets the color of the lakes.' }, showrivers: { @@ -167,7 +169,7 @@ module.exports = { rivercolor: { valType: 'color', role: 'style', - dflt: params.waterColor, + dflt: constants.waterColor, description: 'Sets color of the rivers.' }, riverwidth: { @@ -185,7 +187,7 @@ module.exports = { countrycolor: { valType: 'color', role: 'style', - dflt: Plotly.Color.defaultLine, + dflt: colorAttrs.defaultLine, description: 'Sets line color of the country boundaries.' }, countrywidth: { @@ -206,7 +208,7 @@ module.exports = { subunitcolor: { valType: 'color', role: 'style', - dflt: Plotly.Color.defaultLine, + dflt: colorAttrs.defaultLine, description: 'Sets the color of the subunits boundaries.' }, subunitwidth: { @@ -224,7 +226,7 @@ module.exports = { framecolor: { valType: 'color', role: 'style', - dflt: Plotly.Color.defaultLine, + dflt: colorAttrs.defaultLine, description: 'Sets the color the frame.' }, framewidth: { @@ -237,11 +239,9 @@ module.exports = { bgcolor: { valType: 'color', role: 'style', - dflt: Plotly.Color.background, + dflt: colorAttrs.background, description: 'Set the background color of the map' }, - _nestedModules: { - 'lonaxis': 'GeoAxes', - 'lataxis': 'GeoAxes' - } + lonaxis: geoAxesAttrs, + lataxis: geoAxesAttrs }; diff --git a/src/geo/lib/projections.js b/src/plots/geo/projections.js similarity index 100% rename from src/geo/lib/projections.js rename to src/plots/geo/projections.js diff --git a/src/geo/lib/set-scale.js b/src/plots/geo/set_scale.js similarity index 98% rename from src/geo/lib/set-scale.js rename to src/plots/geo/set_scale.js index 8169e00b329..0ba1703b7dd 100644 --- a/src/geo/lib/set-scale.js +++ b/src/plots/geo/set_scale.js @@ -2,7 +2,7 @@ var d3 = require('d3'); -var clipPad = require('./params').clipPad; +var clipPad = require('../../constants/geo_constants').clipPad; function createGeoScale(geoLayout, graphSize) { var projLayout = geoLayout.projection, diff --git a/src/geo/lib/zoom.js b/src/plots/geo/zoom.js similarity index 99% rename from src/geo/lib/zoom.js rename to src/plots/geo/zoom.js index c4cad650f2c..527122f5965 100644 --- a/src/geo/lib/zoom.js +++ b/src/plots/geo/zoom.js @@ -4,12 +4,8 @@ var d3 = require('d3'); var radians = Math.PI / 180, degrees = 180 / Math.PI, - zoomstartStyle = { - cursor: 'pointer' - }, - zoomendStyle = { - cursor: 'auto' - }; + zoomstartStyle = { cursor: 'pointer' }, + zoomendStyle = { cursor: 'auto' }; function createGeoZoom(geo, geoLayout) { diff --git a/src/geo/lib/zoom-reset.js b/src/plots/geo/zoom_reset.js similarity index 100% rename from src/geo/lib/zoom-reset.js rename to src/plots/geo/zoom_reset.js diff --git a/src/gl2d/lib/camera.js b/src/plots/gl2d/camera.js similarity index 100% rename from src/gl2d/lib/camera.js rename to src/plots/gl2d/camera.js diff --git a/src/gl2d/lib/gl2daxes.js b/src/plots/gl2d/convert.js similarity index 98% rename from src/gl2d/lib/gl2daxes.js rename to src/plots/gl2d/convert.js index 33cb8089afd..4b0701751e8 100644 --- a/src/gl2d/lib/gl2daxes.js +++ b/src/plots/gl2d/convert.js @@ -1,8 +1,9 @@ 'use strict'; var Plotly = require('../../plotly'); -var htmlToUnicode = require('../../gl3d/lib/html2unicode'); -var str2RGBArray = require('../../gl3d/lib/str2rgbarray'); + +var htmlToUnicode = require('../../lib/html2unicode'); +var str2RGBArray = require('../../lib/str2rgbarray'); function Axes2DOptions(scene) { this.scene = scene; diff --git a/src/gl2d/scene2d.js b/src/plots/gl2d/scene2d.js similarity index 97% rename from src/gl2d/scene2d.js rename to src/plots/gl2d/scene2d.js index b9ab2f3cad8..a63683471db 100644 --- a/src/gl2d/scene2d.js +++ b/src/plots/gl2d/scene2d.js @@ -1,21 +1,23 @@ 'use strict'; -var Plotly = require('../plotly'); +var Plotly = require('../../plotly'); var createPlot2D = require('gl-plot2d'); var createSpikes = require('gl-spikes2d'); var createSelectBox = require('gl-select-box'); -var createLineWithMarkers = require('./scattergl/convert/'); -var createOptions = require('./lib/gl2daxes'); -var createCamera = require('./lib/camera'); -var htmlToUnicode = require('../gl3d/lib/html2unicode'); -var showNoWebGlMsg = require('../gl3d/lib/show_no_webgl_msg'); + +var createOptions = require('./convert'); +var createCamera = require('./camera'); + +var createLineWithMarkers = require('../../traces/scattergl/convert'); + +var htmlToUnicode = require('../../lib/html2unicode'); +var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); var AXES = ['xaxis', 'yaxis']; var STATIC_CANVAS, STATIC_CONTEXT; - Plotly.Plots.registerSubplot('gl2d', ['xaxis', 'yaxis'], ['x', 'y'], - Plotly.Axes.traceAttributes); + Plotly.Axes.attributes); function Scene2D(options, fullLayout) { this.container = options.container; diff --git a/src/gl3d/lib/camera.js b/src/plots/gl3d/camera.js similarity index 100% rename from src/gl3d/lib/camera.js rename to src/plots/gl3d/camera.js diff --git a/src/plots/gl3d/layout/attributes.js b/src/plots/gl3d/layout/attributes.js new file mode 100644 index 00000000000..13f119f6646 --- /dev/null +++ b/src/plots/gl3d/layout/attributes.js @@ -0,0 +1,15 @@ +module.exports = { + scene: { + valType: 'sceneid', + role: 'info', + dflt: 'scene', + description: [ + 'Sets a reference between this trace\'s 3D coordinate system and', + 'a 3D scene.', + 'If *scene* (the default value), the (x,y,z) coordinates refer to', + '`layout.scene`.', + 'If *scene2*, the (x,y,z) coordinates refer to `layout.scene2`,', + 'and so on.' + ].join(' ') + } +}; diff --git a/src/gl3d/attributes/gl3daxes.js b/src/plots/gl3d/layout/axis_attributes.js similarity index 94% rename from src/gl3d/attributes/gl3daxes.js rename to src/plots/gl3d/layout/axis_attributes.js index dad9635c28c..31bcb9a0ece 100644 --- a/src/gl3d/attributes/gl3daxes.js +++ b/src/plots/gl3d/layout/axis_attributes.js @@ -1,7 +1,6 @@ -var Plotly = require('../../plotly'); +var axesAttrs = require('../../cartesian/layout_attributes'); +var extendFlat = require('../../../lib/extend').extendFlat; -var axesAttrs = Plotly.Axes.layoutAttributes; -var extendFlat = Plotly.Lib.extendFlat; module.exports = { showspikes: { @@ -54,7 +53,6 @@ module.exports = { showaxeslabels: { valType: 'boolean', role: 'info', - dflt: 'rgba(204, 204, 204, 0.5)', dflt: true, description: 'Sets whether or not this axis is labeled' }, diff --git a/src/plots/gl3d/layout/axis_defaults.js b/src/plots/gl3d/layout/axis_defaults.js new file mode 100644 index 00000000000..fdb547b27be --- /dev/null +++ b/src/plots/gl3d/layout/axis_defaults.js @@ -0,0 +1,52 @@ +'use strict'; + +var Plotly = require('../../../plotly'); +var layoutAttributes = require('./axis_attributes'); + +var axesNames = ['xaxis', 'yaxis', 'zaxis']; +var noop = function() {}; + + +module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, options) { + var Axes = Plotly.Axes; + var containerIn, containerOut; + + function coerce(attr, dflt) { + return Plotly.Lib.coerce(containerIn, containerOut, layoutAttributes, attr, dflt); + } + + for (var j = 0; j < axesNames.length; j++) { + var axName = axesNames[j]; + containerIn = layoutIn[axName] || {}; + + containerOut = { + _id: axName[0] + options.scene, + _name: axName + }; + + layoutOut[axName] = containerOut = Axes.handleAxisDefaults( + containerIn, + containerOut, + coerce, + { + font: options.font, + letter: axName[0], + data: options.data, + showGrid: true + }); + + coerce('gridcolor'); + coerce('title', axName[0]); // shouldn't this be on-par with 2D? + + containerOut.setScale = noop; + + if (coerce('showspikes')) { + coerce('spikesides'); + coerce('spikethickness'); + coerce('spikecolor'); + } + if (coerce('showbackground')) coerce('backgroundcolor'); + + coerce('showaxeslabels'); + } +}; diff --git a/src/gl3d/convert/axes.js b/src/plots/gl3d/layout/convert.js similarity index 96% rename from src/gl3d/convert/axes.js rename to src/plots/gl3d/layout/convert.js index 3ba79fb3fbf..c1553f49aa2 100644 --- a/src/gl3d/convert/axes.js +++ b/src/plots/gl3d/layout/convert.js @@ -1,9 +1,10 @@ 'use strict'; -var arrtools = require('arraytools'), - convertHTML = require('../lib/html2unicode'), - arrayCopy1D = arrtools.copy1D, - str2RgbaArray = require('../lib/str2rgbarray'); +var arrtools = require('arraytools'); +var convertHTML = require('../../../lib/html2unicode'); +var str2RgbaArray = require('../../../lib/str2rgbarray'); + +var arrayCopy1D = arrtools.copy1D; var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; diff --git a/src/gl3d/defaults/gl3dlayout.js b/src/plots/gl3d/layout/defaults.js similarity index 68% rename from src/gl3d/defaults/gl3dlayout.js rename to src/plots/gl3d/layout/defaults.js index 70ca6eba11d..c5826c27b0a 100644 --- a/src/gl3d/defaults/gl3dlayout.js +++ b/src/plots/gl3d/layout/defaults.js @@ -1,33 +1,14 @@ 'use strict'; -var Plotly = require('../../plotly'); - -var Gl3dLayout = module.exports = {}; - -Plotly.Plots.registerSubplot('gl3d', 'scene', 'scene', { - scene: { - valType: 'sceneid', - role: 'info', - dflt: 'scene', - description: [ - 'Sets a reference between this trace\'s 3D coordinate system and', - 'a 3D scene.', - 'If *scene* (the default value), the (x,y,z) coordinates refer to', - '`layout.scene`.', - 'If *scene2*, the (x,y,z) coordinates refer to `layout.scene2`,', - 'and so on.' - ].join(' ') - } -}); - -Gl3dLayout.layoutAttributes = require('../attributes/gl3dlayout'); +var Plotly = require('../../../plotly'); +var layoutAttributes = require('./layout_attributes'); +var supplyGl3dAxisLayoutDefaults = require('./axis_defaults'); -Gl3dLayout.supplyLayoutDefaults = function (layoutIn, layoutOut, fullData) { +module.exports = function supplyLayoutDefaults (layoutIn, layoutOut, fullData) { if (!layoutOut._hasGL3D) return; var scenes = Plotly.Plots.getSubplotIdsInData(fullData, 'gl3d'); - var attributes = Gl3dLayout.layoutAttributes; var i; // until they play better together @@ -38,8 +19,7 @@ Gl3dLayout.supplyLayoutDefaults = function (layoutIn, layoutOut, fullData) { var scenesLength = scenes.length; function coerce(attr, dflt) { - return Plotly.Lib.coerce(sceneLayoutIn, sceneLayoutOut, - attributes, attr, dflt); + return Plotly.Lib.coerce(sceneLayoutIn, sceneLayoutOut, layoutAttributes, attr, dflt); } for (i = 0; i < scenesLength; ++i) { @@ -63,7 +43,7 @@ Gl3dLayout.supplyLayoutDefaults = function (layoutIn, layoutOut, fullData) { coerce('bgcolor'); - var cameraKeys = Object.keys(attributes.camera); + var cameraKeys = Object.keys(layoutAttributes.camera); for(var j = 0; j < cameraKeys.length; j++) { coerce('camera.' + cameraKeys[j] + '.x'); @@ -104,7 +84,7 @@ Gl3dLayout.supplyLayoutDefaults = function (layoutIn, layoutOut, fullData) { * x:[0,1] -> x:[0,0.5], x:[0.5,1] -> * x:[0, 0.333] x:[0.333,0.666] x:[0.666, 1] */ - Plotly.Gl3dAxes.supplyLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { + supplyGl3dAxisLayoutDefaults(sceneLayoutIn, sceneLayoutOut, { font: layoutOut.font, scene: scene, data: fullData @@ -113,13 +93,3 @@ Gl3dLayout.supplyLayoutDefaults = function (layoutIn, layoutOut, fullData) { layoutOut[scene] = sceneLayoutOut; } }; - -// Clean scene ids, 'scene1' -> 'scene' -Gl3dLayout.cleanId = function cleanId(id) { - if (!id.match(/^scene[0-9]*$/)) return; - - var sceneNum = id.substr(5); - if (sceneNum === '1') sceneNum = ''; - - return 'scene' + sceneNum; -}; diff --git a/src/plots/gl3d/layout/index.js b/src/plots/gl3d/layout/index.js new file mode 100644 index 00000000000..42a5cb4be7b --- /dev/null +++ b/src/plots/gl3d/layout/index.js @@ -0,0 +1,48 @@ +'use strict'; + +var Plotly = require('../../../plotly'); +var attributes = require('./attributes'); + +var axesNames = ['xaxis', 'yaxis', 'zaxis']; +var noop = function () {}; + +Plotly.Plots.registerSubplot('gl3d', 'scene', 'scene', attributes); + +exports.layoutAttributes = require('./layout_attributes'); + +exports.supplyLayoutDefaults = require('./defaults'); + +// clean scene ids, 'scene1' -> 'scene' +exports.cleanId = function cleanId(id) { + if (!id.match(/^scene[0-9]*$/)) return; + + var sceneNum = id.substr(5); + if (sceneNum === '1') sceneNum = ''; + + return 'scene' + sceneNum; +}; + +exports.setConvert = function(containerOut) { + Plotly.Axes.setConvert(containerOut); + containerOut.setScale = noop; +}; + +exports.initAxes = function (td) { + var fullLayout = td._fullLayout; + + // until they play better together + delete fullLayout.xaxis; + delete fullLayout.yaxis; + + var sceneIds = Plotly.Plots.getSubplotIds(fullLayout, 'gl3d'); + + for (var i = 0; i < sceneIds.length; ++i) { + var sceneId = sceneIds[i]; + var sceneLayout = fullLayout[sceneId]; + for (var j = 0; j < 3; ++j) { + var axisName = axesNames[j]; + var ax = sceneLayout[axisName]; + ax._td = td; + } + } +}; diff --git a/src/gl3d/attributes/gl3dlayout.js b/src/plots/gl3d/layout/layout_attributes.js similarity index 95% rename from src/gl3d/attributes/gl3dlayout.js rename to src/plots/gl3d/layout/layout_attributes.js index a73ff04fa01..7b0234bbf5d 100644 --- a/src/gl3d/attributes/gl3dlayout.js +++ b/src/plots/gl3d/layout/layout_attributes.js @@ -1,8 +1,7 @@ 'use strict'; -var Plotly = require('../../plotly'); - -var extendFlat = Plotly.Lib.extendFlat; +var gl3dAxisAttrs = require('./axis_attributes'); +var extendFlat = require('../../../lib/extend').extendFlat; function makeVector(x, y, z) { return { @@ -127,11 +126,9 @@ module.exports = { ].join(' ') }, - _nestedModules: { - 'xaxis': 'Gl3dAxes', - 'yaxis': 'Gl3dAxes', - 'zaxis': 'Gl3dAxes' - }, + xaxis: gl3dAxisAttrs, + yaxis: gl3dAxisAttrs, + zaxis: gl3dAxisAttrs, _deprecated: { cameraposition: { diff --git a/src/gl3d/convert/spikes.js b/src/plots/gl3d/layout/spikes.js similarity index 93% rename from src/gl3d/convert/spikes.js rename to src/plots/gl3d/layout/spikes.js index 98da3b78f93..f1b4cf5bbbb 100644 --- a/src/gl3d/convert/spikes.js +++ b/src/plots/gl3d/layout/spikes.js @@ -1,6 +1,6 @@ 'use strict'; -var str2RGBArray = require('../lib/str2rgbarray'); +var str2RGBArray = require('../../../lib/str2rgbarray'); var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; diff --git a/src/gl3d/lib/tick-marks.js b/src/plots/gl3d/layout/tick_marks.js similarity index 96% rename from src/gl3d/lib/tick-marks.js rename to src/plots/gl3d/layout/tick_marks.js index 7d067007b67..42f3212b3b4 100644 --- a/src/gl3d/lib/tick-marks.js +++ b/src/plots/gl3d/layout/tick_marks.js @@ -4,8 +4,8 @@ module.exports = computeTickMarks; -var Plotly = require('../../plotly'); -var convertHTML = require('./html2unicode'); +var Plotly = require('../../../plotly'); +var convertHTML = require('../../../lib/html2unicode'); var AXES_NAMES = ['xaxis', 'yaxis', 'zaxis']; diff --git a/src/gl3d/lib/project.js b/src/plots/gl3d/project.js similarity index 100% rename from src/gl3d/lib/project.js rename to src/plots/gl3d/project.js diff --git a/src/gl3d/scene.js b/src/plots/gl3d/scene.js similarity index 96% rename from src/gl3d/scene.js rename to src/plots/gl3d/scene.js index acbdb2f5343..4825bec0ec3 100644 --- a/src/gl3d/scene.js +++ b/src/plots/gl3d/scene.js @@ -1,21 +1,26 @@ /* jshint shadow: true */ 'use strict'; -var createPlot = require('gl-plot3d'), - createAxesOptions = require('./convert/axes'), - createSpikeOptions = require('./convert/spikes'), - createScatterTrace = require('./convert/scatter'), - createSurfaceTrace = require('./convert/surface'), - createMeshTrace = require('./convert/mesh'), - computeTickMarks = require('./lib/tick-marks'), - createCamera = require('./lib/camera'), - str2RGBAarray = require('./lib/str2rgbarray'), - project = require('./lib/project'), - showNoWebGlMsg = require('./lib/show_no_webgl_msg'), - Plotly = require('../plotly'); +var Plotly = require('../../plotly'); +var createPlot = require('gl-plot3d'); + +var createAxesOptions = require('./layout/convert'); +var createSpikeOptions = require('./layout/spikes'); +var computeTickMarks = require('./layout/tick_marks'); + +var createScatterTrace = require('../../traces/scatter3d/convert'); +var createSurfaceTrace = require('../../traces/surface/convert'); +var createMeshTrace = require('../../traces/mesh3d/convert'); + +var createCamera = require('./camera'); +var project = require('./project'); + +var str2RGBAarray = require('../../lib/str2rgbarray'); +var showNoWebGlMsg = require('../../lib/show_no_webgl_msg'); var STATIC_CANVAS, STATIC_CONTEXT; + function render(scene) { //Update size of svg container @@ -297,7 +302,7 @@ proto.plot = function(sceneData, fullLayout, layout) { // Update axes functions BEFORE updating traces for (i = 0; i < 3; ++i) { var axis = fullSceneLayout[axisProperties[i]]; - Plotly.Gl3dAxes.setConvert(axis); + Plotly.Gl3dLayout.setConvert(axis); } //Convert scene data diff --git a/src/plots/layout_attributes.js b/src/plots/layout_attributes.js new file mode 100644 index 00000000000..fb4a13af9f2 --- /dev/null +++ b/src/plots/layout_attributes.js @@ -0,0 +1,195 @@ +var Plotly = require('../plotly'); + +var fontAttrs = require('./font_attributes'); +var colorAttrs = require('../components/color/attributes'); + +var extendFlat = Plotly.Lib.extendFlat; + + +module.exports = { + font: { + family: extendFlat({}, fontAttrs.family, { + dflt: '"Open sans", verdana, arial, sans-serif' + }), + size: extendFlat({}, fontAttrs.size, { + dflt: 12 + }), + color: extendFlat({}, fontAttrs.color, { + dflt: colorAttrs.defaultLine + }), + description: [ + 'Sets the global font.', + 'Note that fonts used in traces and other', + 'layout components inherit from the global font.' + ].join(' ') + }, + title: { + valType: 'string', + role: 'info', + dflt: 'Click to enter Plot title', + description: [ + 'Sets the plot\'s title.' + ].join(' ') + }, + titlefont: extendFlat({}, fontAttrs, { + description: 'Sets the title font.' + }), + autosize: { + valType: 'enumerated', + role: 'info', + // TODO: better handling of 'initial' + values: [true, false, 'initial'], + description: [ + 'Determines whether or not the dimensions of the figure are', + 'computed as a function of the display size.' + ].join(' ') + }, + width: { + valType: 'number', + role: 'info', + min: 10, + dflt: 700, + description: [ + 'Sets the plot\'s width (in px).' + ].join(' ') + }, + height: { + valType: 'number', + role: 'info', + min: 10, + dflt: 450, + description: [ + 'Sets the plot\'s height (in px).' + ].join(' ') + }, + margin: { + l: { + valType: 'number', + role: 'info', + min: 0, + dflt: 80, + description: 'Sets the left margin (in px).' + }, + r: { + valType: 'number', + role: 'info', + min: 0, + dflt: 80, + description: 'Sets the right margin (in px).' + }, + t: { + valType: 'number', + role: 'info', + min: 0, + dflt: 100, + description: 'Sets the top margin (in px).' + }, + b: { + valType: 'number', + role: 'info', + min: 0, + dflt: 80, + description: 'Sets the bottom margin (in px).' + }, + pad: { + valType: 'number', + role: 'info', + min: 0, + dflt: 0, + description: [ + 'Sets the amount of padding (in px)', + 'between the plotting area and the axis lines' + ].join(' ') + }, + autoexpand: { + valType: 'boolean', + role: 'info', + dflt: true + } + }, + paper_bgcolor: { + valType: 'color', + role: 'style', + dflt: colorAttrs.background, + description: 'Sets the color of paper where the graph is drawn.' + }, + plot_bgcolor: { + // defined here, but set in Axes.supplyLayoutDefaults + // because it needs to know if there are (2D) axes or not + valType: 'color', + role: 'style', + dflt: colorAttrs.background, + description: [ + 'Sets the color of plotting area in-between x and y axes.' + ].join(' ') + }, + separators: { + valType: 'string', + role: 'style', + dflt: '.,', + description: [ + 'Sets the decimal and thousand separators.', + 'For example, *. * puts a \'.\' before decimals and', + 'a space between thousands.' + ].join(' ') + }, + hidesources: { + valType: 'boolean', + role: 'info', + dflt: false, + description: [ + 'Determines whether or not a text link citing the data source is', + 'placed at the bottom-right cored of the figure.', + 'Has only an effect only on graphs that have been generated via', + 'forked graphs from the plotly service (at https://plot.ly or on-premise).' + ].join(' ') + }, + smith: { + // will become a boolean if/when we implement this + valType: 'enumerated', + role: 'info', + values: [false], + dflt: false + }, + showlegend: { + // handled in legend.supplyLayoutDefaults + // but included here because it's not in the legend object + valType: 'boolean', + role: 'info', + description: 'Determines whether or not a legend is drawn.' + }, + _hasCartesian: { + valType: 'boolean', + dflt: false + }, + _hasGL3D: { + valType: 'boolean', + dflt: false + }, + _hasGeo: { + valType: 'boolean', + dflt: false + }, + _hasPie: { + valType: 'boolean', + dflt: false + }, + _hasGL2D: { + valType: 'boolean', + dflt: false + }, + _composedModules: { + '*': 'Fx' + }, + + // TODO merge with moduleLayoutDefaults in plots.js + _nestedModules: { + 'xaxis': 'Axes', + 'yaxis': 'Axes', + 'scene': 'Gl3dLayout', + 'geo': 'GeoLayout', + 'legend': 'Legend', + 'annotations': 'Annotations', + 'shapes': 'Shapes' + } +}; diff --git a/src/plots/plots.js b/src/plots/plots.js new file mode 100644 index 00000000000..e61d1778436 --- /dev/null +++ b/src/plots/plots.js @@ -0,0 +1,957 @@ +'use strict'; + +var Plotly = require('../plotly'); +var d3 = require('d3'); +var isNumeric = require('fast-isnumeric'); + +var plots = module.exports = {}; + +var modules = plots.modules = {}, + allTypes = plots.allTypes = [], + allCategories = plots.allCategories = {}, + subplotsRegistry = plots.subplotsRegistry = {}; + +plots.attributes = require('./attributes'); +plots.attributes.type.values = allTypes; +plots.fontAttrs = require('./font_attributes'); +plots.layoutAttributes = require('./layout_attributes'); + +// TODO make this a plot attribute? +plots.fontWeight = 'normal'; + +/** + * plots.register: register a module as the handler for a trace type + * + * @param {object} module the module that will handle plotting this trace type + * @param {string} thisType + * @param {array of strings} categoriesIn all the categories this type is in, + * tested by calls: Plotly.Plots.traceIs(trace, oneCategory) + * @param {object} meta meta information about the trace type + */ +plots.register = function(_module, thisType, categoriesIn, meta) { + if(modules[thisType]) { + throw new Error('type ' + thisType + ' already registered'); + } + + var categoryObj = {}; + for(var i = 0; i < categoriesIn.length; i++) { + categoryObj[categoriesIn[i]] = true; + allCategories[categoriesIn[i]] = true; + } + + modules[thisType] = { + module: _module, + categories: categoryObj + }; + + if(meta && Object.keys(meta).length) { + modules[thisType].meta = meta; + } + + allTypes.push(thisType); +}; + +function getTraceType(traceType) { + if(typeof traceType === 'object') traceType = traceType.type; + return traceType; +} + +plots.getModule = function(trace) { + if(trace.r !== undefined) { + console.log('Oops, tried to put a polar trace ' + + 'on an incompatible graph of cartesian ' + + 'data. Ignoring this dataset.', trace + ); + return false; + } + + var _module = modules[getTraceType(trace)]; + if(!_module) return false; + return _module.module; +}; + + +/** + * plots.traceIs: is this trace type in this category? + * + * traceType: a trace (object) or trace type (string) + * category: a category (string) + */ +plots.traceIs = function traceIs(traceType, category) { + traceType = getTraceType(traceType); + + if(traceType === 'various') return false; // FIXME + + var _module = modules[traceType]; + + if(!_module) { + if(traceType !== undefined) { + console.warn('unrecognized trace type ' + traceType); + } + _module = modules[plots.attributes.type.dflt]; + } + + return !!_module.categories[category]; +}; + + +/** + * plots.registerSubplot: register a subplot type + * + * @param {string} subplotType subplot type + * (these must also be categories defined with plots.register) + * @param {string or array of strings} attr attribute name in traces and layout + * @param {string or array of strings} idRoot root of id + * (setting the possible value for attrName) + * @param {object} attributes attribute(s) for traces of this subplot type + * + * In trace objects `attr` is the object key taking a valid `id` as value + * (the set of all valid ids is generated below and stored in idRegex). + * + * In the layout object, a or several valid `attr` name(s) can be keys linked + * to a nested attribute objects + * (the set of all valid attr names is generated below and stored in attrRegex). + * + * TODO use these in Lib.coerce + */ +plots.registerSubplot = function(subplotType, attr, idRoot, attributes) { + if(subplotsRegistry[subplotType]) { + throw new Error('subplot ' + subplotType + ' already registered'); + } + + var regexStart = '^', + regexEnd = '([2-9]|[1-9][0-9]+)?$', + hasXY = (subplotType === 'cartesian' || subplotsRegistry === 'gl2d'); + + function makeRegex(mid) { + return new RegExp(regexStart + mid + regexEnd); + } + + // not sure what's best for the 'cartesian' type at this point + subplotsRegistry[subplotType] = { + attr: attr, + idRoot: idRoot, + attributes: attributes, + // register the regex representing the set of all valid attribute names + attrRegex: hasXY ? + { x: makeRegex(attr[0]), y: makeRegex(attr[1]) } : + makeRegex(attr), + // register the regex representing the set of all valid attribute ids + idRegex: hasXY ? + { x: makeRegex(idRoot[0]), y: makeRegex(idRoot[1]) } : + makeRegex(idRoot) + }; +}; + +// TODO separate the 'find subplot' step (which looks in layout) +// from the 'get subplot ids' step (which looks in fullLayout._plots) +plots.getSubplotIds = function getSubplotIds(layout, type) { + if(plots.subplotsRegistry[type] === undefined) return []; + + // layout must be 'fullLayout' here + if(type === 'cartesian' && !layout._hasCartesian) return []; + if(type === 'gl2d' && !layout._hasGL2D) return []; + if(type === 'cartesian' || type === 'gl2d') { + return Object.keys(layout._plots); + } + + var idRegex = plots.subplotsRegistry[type].idRegex, + layoutKeys = Object.keys(layout), + subplotIds = [], + layoutKey; + + for(var i = 0; i < layoutKeys.length; i++) { + layoutKey = layoutKeys[i]; + if(idRegex.test(layoutKey)) subplotIds.push(layoutKey); + } + + return subplotIds; +}; + +plots.getSubplotIdsInData = function getSubplotsInData(data, type) { + if(plots.subplotsRegistry[type] === undefined) return []; + + var attr = plots.subplotsRegistry[type].attr, + subplotIds = [], + trace; + + for (var i = 0; i < data.length; i++) { + trace = data[i]; + if(Plotly.Plots.traceIs(trace, type) && subplotIds.indexOf(trace[attr])===-1) { + subplotIds.push(trace[attr]); + } + } + + return subplotIds; +}; + +plots.getSubplotData = function getSubplotData(data, type, subplotId) { + if(plots.subplotsRegistry[type] === undefined) return []; + + var attr = plots.subplotsRegistry[type].attr, + subplotData = [], + trace; + + for(var i = 0; i < data.length; i++) { + trace = data[i]; + + if(type === 'gl2d' && plots.traceIs(trace, 'gl2d')) { + var spmatch = Plotly.Axes.subplotMatch, + subplotX = 'x' + subplotId.match(spmatch)[1], + subplotY = 'y' + subplotId.match(spmatch)[2]; + + if(trace[attr[0]]===subplotX && trace[attr[1]]===subplotY) { + subplotData.push(trace); + } + } + else { + if(trace[attr] === subplotId) subplotData.push(trace); + } + } + + return subplotData; +}; + +// in some cases the browser doesn't seem to know how big +// the text is at first, so it needs to draw it, +// then wait a little, then draw it again +plots.redrawText = function(gd) { + + // doesn't work presently (and not needed) for polar or 3d + if(gd._fullLayout._hasGL3D || (gd.data && gd.data[0] && gd.data[0].r)) { + return; + } + + return new Promise(function(resolve) { + setTimeout(function(){ + Plotly.Annotations.drawAll(gd); + Plotly.Legend.draw(gd); + (gd.calcdata||[]).forEach(function(d){ + if(d[0]&&d[0].t&&d[0].t.cb) d[0].t.cb(); + }); + resolve(plots.previousPromises(gd)); + },300); + }); +}; + +// resize plot about the container size +plots.resize = function(gd) { + if(!gd || d3.select(gd).style('display') === 'none') return; + + if(gd._redrawTimer) clearTimeout(gd._redrawTimer); + + gd._redrawTimer = setTimeout(function() { + if((gd._fullLayout || {}).autosize) { + // autosizing doesn't count as a change that needs saving + var oldchanged = gd.changed; + + // nor should it be included in the undo queue + gd.autoplay = true; + + Plotly.relayout(gd, {autosize: true}); + gd.changed = oldchanged; + } + }, 100); +}; + + +// for use in Plotly.Lib.syncOrAsync, check if there are any +// pending promises in this plot and wait for them +plots.previousPromises = function(gd) { + if((gd._promises || []).length) { + return Promise.all(gd._promises) + .then(function(){ gd._promises=[]; }); + } +}; + +/** + * Adds the 'Edit chart' link. + * Note that now Plotly.plot() calls this so it can regenerate whenever it replots + * + * Add source links to your graph inside the 'showSources' config argument. + */ +plots.addLinks = function(gd) { + var fullLayout = gd._fullLayout; + + var linkContainer = fullLayout._paper + .selectAll('text.js-plot-link-container').data([0]); + + linkContainer.enter().append('text') + .classed('js-plot-link-container', true) + .style({ + 'font-family':'"Open Sans",Arial,sans-serif', + 'font-size':'12px', + 'fill': Plotly.Color.defaultLine, + 'pointer-events': 'all' + }) + .each(function(){ + var links = d3.select(this); + links.append('tspan').classed('js-link-to-tool', true); + links.append('tspan').classed('js-link-spacer', true); + links.append('tspan').classed('js-sourcelinks', true); + }); + + // The text node inside svg + var text = linkContainer.node(), + attrs = { + y: fullLayout._paper.attr('height') - 9 + }; + + // If text's width is bigger than the layout + // IE doesn't like getComputedTextLength if an element + // isn't visible, which it (sometimes?) isn't + // apparently offsetParent is null for invisibles. + // http://stackoverflow.com/questions/19669786/check-if-element-is-visible-in-dom + if (text && text.offsetParent && + text.getComputedTextLength() >= (fullLayout.width - 20)) { + // Align the text at the left + attrs['text-anchor'] = 'start'; + attrs.x = 5; + } + else { + // Align the text at the right + attrs['text-anchor'] = 'end'; + attrs.x = fullLayout._paper.attr('width') - 7; + } + + linkContainer.attr(attrs); + + var toolspan = linkContainer.select('.js-link-to-tool'), + spacespan = linkContainer.select('.js-link-spacer'), + sourcespan = linkContainer.select('.js-sourcelinks'); + + if(gd._context.showSources) gd._context.showSources(gd); + + // 'view in plotly' link for embedded plots + if(gd._context.showLink) positionPlayWithData(gd, toolspan); + + // separator if we have both sources and tool link + spacespan.text((toolspan.text() && sourcespan.text()) ? ' - ' : ''); +}; + +// note that now this function is only adding the brand in +// iframes and 3rd-party apps +function positionPlayWithData(gd, container){ + container.text(''); + var link = container.append('a') + .attr({ + 'xlink:xlink:href': '#', + 'class': 'link--impt link--embedview', + 'font-weight':'bold' + }) + .text(gd._context.linkText + ' ' + String.fromCharCode(187)); + + if(gd._context.sendData) { + link.on('click',function(){ + gd.emit('plotly_beforeexport'); + + var baseUrl = (window.PLOTLYENV && window.PLOTLYENV.BASE_URL) || 'https://plot.ly'; + + var hiddenformDiv = d3.select(gd) + .append('div') + .attr('id', 'hiddenform') + .style('display', 'none'); + + var hiddenform = hiddenformDiv + .append('form') + .attr({ + action: baseUrl + '/external', + method: 'post', + target: '_blank' + }); + + var hiddenformInput = hiddenform + .append('input') + .attr({ + type: 'text', + name: 'data' + }); + + hiddenformInput.node().value = plots.graphJson(gd, false, 'keepdata'); + hiddenform.node().submit(); + hiddenformDiv.remove(); + + gd.emit('plotly_afterexport'); + return false; + }); + } + else { + var path = window.location.pathname.split('/'); + var query = window.location.search; + link.attr({ + 'xlink:xlink:show': 'new', + 'xlink:xlink:href': '/' + path[2].split('.')[0] + '/' + path[1] + query + }); + } +} + +plots.supplyDefaults = function(gd) { + // fill in default values: + // gd.data, gd.layout: + // are precisely what the user specified + // gd._fullData, gd._fullLayout: + // are complete descriptions of how to draw the plot + var oldFullLayout = gd._fullLayout || {}, + newFullLayout = gd._fullLayout = {}, + newLayout = gd.layout || {}, + oldFullData = gd._fullData || [], + newFullData = gd._fullData = [], + newData = gd.data || [], + modules = gd._modules = []; + + var i, trace, fullTrace, module, axList, ax; + + + // first fill in what we can of layout without looking at data + // because fullData needs a few things from layout + plots.supplyLayoutGlobalDefaults(newLayout, newFullLayout); + + // keep track of how many traces are inputted + newFullLayout._dataLength = newData.length; + + // then do the data + for (i = 0; i < newData.length; i++) { + trace = newData[i]; + + fullTrace = plots.supplyDataDefaults(trace, i, newFullLayout); + newFullData.push(fullTrace); + + // DETECT 3D, Cartesian, and Polar + if(plots.traceIs(fullTrace, 'cartesian')) newFullLayout._hasCartesian = true; + else if(plots.traceIs(fullTrace, 'gl3d')) newFullLayout._hasGL3D = true; + 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('r' in fullTrace) newFullLayout._hasPolar = true; + + module = fullTrace._module; + if (module && modules.indexOf(module)===-1) modules.push(module); + } + + // special cases that introduce interactions between traces + for (i = 0; i < modules.length; i++) { + module = modules[i]; + if (module.cleanData) module.cleanData(newFullData); + } + + if (oldFullData.length === newData.length) { + for (i = 0; i < newFullData.length; i++) { + relinkPrivateKeys(newFullData[i], oldFullData[i]); + } + } + + // finally, fill in the pieces of layout that may need to look at data + plots.supplyLayoutModuleDefaults(newLayout, newFullLayout, newFullData); + + cleanScenes(newFullLayout, oldFullLayout); + + /* + * Relink functions and underscore attributes to promote consistency between + * plots. + */ + relinkPrivateKeys(newFullLayout, oldFullLayout); + + plots.doAutoMargin(gd); + + // can't quite figure out how to get rid of this... each axis needs + // a reference back to the DOM object for just a few purposes + axList = Plotly.Axes.list(gd); + for (i = 0; i < axList.length; i++) { + ax = axList[i]; + ax._td = gd; + ax.setScale(); + } + + // update object references in calcdata + if ((gd.calcdata || []).length === newFullData.length) { + for (i = 0; i < newFullData.length; i++) { + trace = newFullData[i]; + (gd.calcdata[i][0] || {}).trace = trace; + } + } +}; + +function cleanScenes(newFullLayout, oldFullLayout) { + var oldSceneKey, + oldSceneKeys = plots.getSubplotIds(oldFullLayout, 'gl3d'); + + for (var i = 0; i < oldSceneKeys.length; i++) { + oldSceneKey = oldSceneKeys[i]; + if(!newFullLayout[oldSceneKey] && !!oldFullLayout[oldSceneKey]._scene) { + oldFullLayout[oldSceneKey]._scene.destroy(); + } + } + +} + +/** + * Relink private _keys and keys with a function value from one layout + * (usually cached) to the new fullLayout. + * relink means copying if object is pass-by-value and adding a reference + * if object is pass-by-ref. This prevents deepCopying massive structures like + * a webgl context. + */ +function relinkPrivateKeys(toLayout, fromLayout) { + var keys = Object.keys(fromLayout); + var j; + + for (var i = 0; i < keys.length; ++i) { + var k = keys[i]; + if(k.charAt(0)==='_' || typeof fromLayout[k]==='function') { + // if it already exists at this point, it's something + // that we recreate each time around, so ignore it + if(k in toLayout) continue; + + toLayout[k] = fromLayout[k]; + } + else if (Array.isArray(fromLayout[k]) && + Array.isArray(toLayout[k]) && + fromLayout[k].length && + Plotly.Lib.isPlainObject(fromLayout[k][0])) { + if(fromLayout[k].length !== toLayout[k].length) { + // this should be handled elsewhere, it causes + // ambiguity if we try to deal with it here. + throw new Error('relinkPrivateKeys needs equal ' + + 'length arrays'); + } + + for(j = 0; j < fromLayout[k].length; j++) { + relinkPrivateKeys(toLayout[k][j], fromLayout[k][j]); + } + } + else if (Plotly.Lib.isPlainObject(fromLayout[k]) && + Plotly.Lib.isPlainObject(toLayout[k])) { + // recurse into objects, but only if they still exist + relinkPrivateKeys(toLayout[k], fromLayout[k]); + if (!Object.keys(toLayout[k]).length) delete toLayout[k]; + } + } +} + +plots.supplyDataDefaults = function(traceIn, i, layout) { + var traceOut = {}, + defaultColor = Plotly.Color.defaults[i % Plotly.Color.defaults.length]; + + function coerce(attr, dflt) { + return Plotly.Lib.coerce(traceIn, traceOut, plots.attributes, attr, dflt); + } + + function coerceSubplotAttr(subplotType, subplotAttr) { + if(!plots.traceIs(traceOut, subplotType)) return; + return Plotly.Lib.coerce(traceIn, traceOut, + plots.subplotsRegistry[subplotType].attributes, subplotAttr); + } + + // module-independent attributes + traceOut.index = i; + var visible = coerce('visible'), + scene, + module; + + coerce('type'); + coerce('uid'); + + // this is necessary otherwise we lose references to scene objects when + // the traces of a scene are invisible. Also we handle visible/unvisible + // differently for 3D cases. + coerceSubplotAttr('gl3d', 'scene'); + coerceSubplotAttr('geo', 'geo'); + + // module-specific attributes --- note: we need to send a trace into + // the 3D modules to have it removed from the webgl context. + if(visible || scene) { + module = plots.getModule(traceOut); + traceOut._module = module; + } + + // gets overwritten in pie and geo + if(visible) coerce('hoverinfo', (layout._dataLength === 1) ? 'x+y+z+text' : undefined); + + if(module && visible) module.supplyDefaults(traceIn, traceOut, defaultColor, layout); + + if(visible) { + coerce('name', 'trace ' + i); + + if(!plots.traceIs(traceOut, 'noOpacity')) coerce('opacity'); + + coerceSubplotAttr('cartesian', 'xaxis'); + coerceSubplotAttr('cartesian', 'yaxis'); + + coerceSubplotAttr('gl2d', 'xaxis'); + coerceSubplotAttr('gl2d', 'yaxis'); + + if(plots.traceIs(traceOut, 'showLegend')) { + coerce('showlegend'); + coerce('legendgroup'); + } + } + + // NOTE: I didn't include fit info at all... for now I think it can stay + // just in gd.data, as this info isn't involved in creating plots at all, + // only in pulling back up the fit popover + + // reference back to the input object for convenience + traceOut._input = traceIn; + + return traceOut; +}; + +plots.supplyLayoutGlobalDefaults = function(layoutIn, layoutOut) { + function coerce(attr, dflt) { + return Plotly.Lib.coerce(layoutIn, layoutOut, plots.layoutAttributes, attr, dflt); + } + + var globalFont = Plotly.Lib.coerceFont(coerce, 'font'); + + coerce('title'); + + Plotly.Lib.coerceFont(coerce, 'titlefont', { + family: globalFont.family, + size: Math.round(globalFont.size * 1.4), + color: globalFont.color + }); + + var autosize = coerce('autosize', + (layoutIn.width && layoutIn.height) ? false : 'initial'); + coerce('width'); + coerce('height'); + + coerce('margin.l'); + coerce('margin.r'); + coerce('margin.t'); + coerce('margin.b'); + coerce('margin.pad'); + coerce('margin.autoexpand'); + + // called in plotAutoSize otherwise + if(autosize!=='initial') plots.sanitizeMargins(layoutOut); + + coerce('paper_bgcolor'); + + coerce('separators'); + coerce('hidesources'); + coerce('smith'); + coerce('_hasCartesian'); + coerce('_hasGL3D'); + coerce('_hasGeo'); + coerce('_hasPie'); + coerce('_hasGL2D'); +}; + +plots.supplyLayoutModuleDefaults = function(layoutIn, layoutOut, fullData) { + var moduleLayoutDefaults = [ + 'Axes', 'Annotations', 'Shapes', 'Fx', + 'Bars', 'Boxes', 'Gl3dLayout', 'GeoLayout', 'Pie', 'Legend' + ]; + + var i, module; + + // don't add a check for 'function in module' as it is better to error out and + // secure the module API then not apply the default function. + for(i = 0; i < moduleLayoutDefaults.length; i++) { + module = moduleLayoutDefaults[i]; + if(Plotly[module]) { + Plotly[module].supplyLayoutDefaults(layoutIn, layoutOut, fullData); + } + } +}; + +plots.purge = function(gd) { + // remove all plotly attributes from a div so it can be replotted fresh + // TODO: these really need to be encapsulated into a much smaller set... + + // note: we DO NOT remove _context because it doesn't change when we insert + // a new plot, and may have been set outside of our scope. + + // clean up the gl and geo containers + // TODO unify subplot creation/update with d3.selection.order + // and/or subplot ids + var fullLayout = gd._fullLayout || {}; + if(fullLayout._glcontainer !== undefined) fullLayout._glcontainer.remove(); + if(fullLayout._geocontainer !== undefined) fullLayout._geocontainer.remove(); + + // data and layout + delete gd.data; + delete gd.layout; + delete gd._fullData; + delete gd._fullLayout; + delete gd.calcdata; + delete gd.framework; + delete gd.empty; + + delete gd.fid; + + delete gd.undoqueue; // action queue + delete gd.undonum; + delete gd.autoplay; // are we doing an action that doesn't go in undo queue? + delete gd.changed; + + // these get recreated on Plotly.plot anyway, but just to be safe + // (and to have a record of them...) + delete gd._modules; + delete gd._tester; + delete gd._testref; + delete gd._promises; + delete gd._redrawTimer; + delete gd._replotting; + delete gd.firstscatter; + delete gd.hmlumcount; + delete gd.hmpixcount; + delete gd.numboxes; + delete gd._hoverTimer; + delete gd._lastHoverTime; +}; + +plots.style = function(gd) { + var modulesWithErrorBars = gd._modules.concat(Plotly.ErrorBars); + + for(var i = 0; i < modulesWithErrorBars.length; i++) { + var _module = modulesWithErrorBars[i]; + if(_module.style) _module.style(gd); + } +}; + +plots.sanitizeMargins = function(fullLayout) { + // polar doesn't do margins... + if(!fullLayout || !fullLayout.margin) return; + + var width = fullLayout.width, + height = fullLayout.height, + margin = fullLayout.margin, + plotWidth = width - (margin.l + margin.r), + plotHeight = height - (margin.t + margin.b), + correction; + + // if margin.l + margin.r = 0 then plotWidth > 0 + // as width >= 10 by supplyDefaults + // similarly for margin.t + margin.b + + if (plotWidth < 0) { + correction = (width - 1) / (margin.l + margin.r); + margin.l = Math.floor(correction * margin.l); + margin.r = Math.floor(correction * margin.r); + } + + if (plotHeight < 0) { + correction = (height - 1) / (margin.t + margin.b); + margin.t = Math.floor(correction * margin.t); + margin.b = Math.floor(correction * margin.b); + } +}; +// called by legend and colorbar routines to see if we need to +// expand the margins to show them +// o is {x,l,r,y,t,b} where x and y are plot fractions, +// the rest are pixels in each direction +// or leave o out to delete this entry (like if it's hidden) +plots.autoMargin = function(gd,id,o) { + var fullLayout = gd._fullLayout; + if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; + if(fullLayout.margin.autoexpand!==false) { + if(!o) delete fullLayout._pushmargin[id]; + else { + var pad = o.pad||12; + + // if the item is too big, just give it enough automargin to + // make sure you can still grab it and bring it back + if(o.l+o.r > fullLayout.width*0.5) o.l = o.r = 0; + if(o.b+o.t > fullLayout.height*0.5) o.b = o.t = 0; + + fullLayout._pushmargin[id] = { + l: {val:o.x, size: o.l+pad}, + r: {val:o.x, size: o.r+pad}, + b: {val:o.y, size: o.b+pad}, + t: {val:o.y, size: o.t+pad} + }; + } + + if(!gd._replotting) plots.doAutoMargin(gd); + } +}; + +plots.doAutoMargin = function(gd) { + var fullLayout = gd._fullLayout; + if(!fullLayout._size) fullLayout._size = {}; + if(!fullLayout._pushmargin) fullLayout._pushmargin = {}; + var gs = fullLayout._size, + oldmargins = JSON.stringify(gs); + + // adjust margins for outside legends and colorbars + // fullLayout.margin is the requested margin, + // fullLayout._size has margins and plotsize after adjustment + var ml = Math.max(fullLayout.margin.l||0,0), + mr = Math.max(fullLayout.margin.r||0,0), + mt = Math.max(fullLayout.margin.t||0,0), + mb = Math.max(fullLayout.margin.b||0,0), + pm = fullLayout._pushmargin; + if(fullLayout.margin.autoexpand!==false) { + // fill in the requested margins + pm.base = { + l:{val:0, size:ml}, + r:{val:1, size:mr}, + t:{val:1, size:mt}, + b:{val:0, size:mb} + }; + // now cycle through all the combinations of l and r + // (and t and b) to find the required margins + Object.keys(pm).forEach(function(k1) { + var pushleft = pm[k1].l||{}, + pushbottom = pm[k1].b||{}, + fl = pushleft.val, + pl = pushleft.size, + fb = pushbottom.val, + pb = pushbottom.size; + Object.keys(pm).forEach(function(k2) { + if(isNumeric(pl) && pm[k2].r) { + var fr = pm[k2].r.val, + pr = pm[k2].r.size; + if(fr>fl) { + var newl = (pl*fr + + (pr-fullLayout.width)*fl) / (fr-fl), + newr = (pr*(1-fl) + + (pl-fullLayout.width)*(1-fr)) / (fr-fl); + if(newl>=0 && newr>=0 && newl+newr>ml+mr) { + ml = newl; + mr = newr; + } + } + } + if(isNumeric(pb) && pm[k2].t) { + var ft = pm[k2].t.val, + pt = pm[k2].t.size; + if(ft>fb) { + var newb = (pb*ft + + (pt-fullLayout.height)*fb) / (ft-fb), + newt = (pt*(1-fb) + + (pb-fullLayout.height)*(1-ft)) / (ft-fb); + if(newb>=0 && newt>=0 && newb+newt>mb+mt) { + mb = newb; + mt = newt; + } + } + } + }); + }); + } + + gs.l = Math.round(ml); + gs.r = Math.round(mr); + gs.t = Math.round(mt); + gs.b = Math.round(mb); + gs.p = Math.round(fullLayout.margin.pad); + gs.w = Math.round(fullLayout.width)-gs.l-gs.r; + gs.h = Math.round(fullLayout.height)-gs.t-gs.b; + + // if things changed and we're not already redrawing, trigger a redraw + if(!gd._replotting && oldmargins!=='{}' && + oldmargins!==JSON.stringify(fullLayout._size)) { + return Plotly.plot(gd); + } +}; + +/** + * JSONify the graph data and layout + * + * This function needs to recurse because some src can be inside + * sub-objects. + * + * It also strips out functions and private (starts with _) elements. + * Therefore, we can add temporary things to data and layout that don't + * get saved. + * + * @param gd The graphDiv + * @param {Boolean} dataonly If true, don't return layout. + * @param {'keepref'|'keepdata'|'keepall'} [mode='keepref'] Filter what's kept + * keepref: remove data for which there's a src present + * eg if there's xsrc present (and xsrc is well-formed, + * ie has : and some chars before it), strip out x + * keepdata: remove all src tags, don't remove the data itself + * keepall: keep data and src + * @param {String} output If you specify 'object', the result will not be stringified + * @param {Boolean} useDefaults If truthy, use _fullLayout and _fullData + * @returns {Object|String} + */ +plots.graphJson = function(gd, dataonly, mode, output, useDefaults){ + // if the defaults aren't supplied yet, we need to do that... + if ((useDefaults && dataonly && !gd._fullData) || + (useDefaults && !dataonly && !gd._fullLayout)) { + plots.supplyDefaults(gd); + } + + var data = (useDefaults) ? gd._fullData : gd.data, + layout = (useDefaults) ? gd._fullLayout : gd.layout; + + function stripObj(d) { + if(typeof d === 'function') { + return null; + } + if(Plotly.Lib.isPlainObject(d)) { + var o={}, v, src; + for(v in d) { + // remove private elements and functions + // _ is for private, [ is a mistake ie [object Object] + if(typeof d[v]==='function' || + ['_','['].indexOf(v.charAt(0))!==-1) { + continue; + } + + // look for src/data matches and remove the appropriate one + if(mode==='keepdata') { + // keepdata: remove all ...src tags + if(v.substr(v.length-3)==='src') { + continue; + } + } + else if(mode==='keepstream') { + // keep sourced data if it's being streamed. + // similar to keepref, but if the 'stream' object exists + // in a trace, we will keep the data array. + src = d[v+'src']; + if(typeof src==='string' && src.indexOf(':')>0) { + if(!Plotly.Lib.isPlainObject(d.stream)) { + continue; + } + } + } + else if(mode!=='keepall') { + // keepref: remove sourced data but only + // if the source tag is well-formed + src = d[v+'src']; + if(typeof src==='string' && src.indexOf(':')>0) { + continue; + } + } + + // OK, we're including this... recurse into it + o[v] = stripObj(d[v]); + } + return o; + } + + if(Array.isArray(d)) { + return d.map(stripObj); + } + + // convert native dates to date strings... + // mostly for external users exporting to plotly + if(d && d.getTime) { + return Plotly.Lib.ms2DateTime(d); + } + + return d; + } + + var obj = { + data:(data||[]).map(function(v){ + var d = stripObj(v); + // fit has some little arrays in it that don't contain data, + // just fit params and meta + if(dataonly) { delete d.fit; } + return d; + }) + }; + if(!dataonly) { obj.layout = stripObj(layout); } + + if(gd.framework && gd.framework.isPolar) obj = gd.framework.getConfig(); + + return (output==='object') ? obj : JSON.stringify(obj); +}; diff --git a/src/polar/attributes/area.js b/src/plots/polar/area_attributes.js similarity index 66% rename from src/polar/attributes/area.js rename to src/plots/polar/area_attributes.js index a9b6963ef06..47cc4307e8d 100644 --- a/src/polar/attributes/area.js +++ b/src/plots/polar/area_attributes.js @@ -1,7 +1,5 @@ -var Plotly = require('../../plotly'); - -var scatterAttrs = Plotly.Scatter.attributes, - scatterMarkerAttrs = scatterAttrs.marker; +var scatterAttrs = require('../../traces/scatter/attributes'); +var scatterMarkerAttrs = scatterAttrs.marker; module.exports = { r: scatterAttrs.r, diff --git a/src/polar/attributes/polaraxes.js b/src/plots/polar/axis_attributes.js similarity index 95% rename from src/polar/attributes/polaraxes.js rename to src/plots/polar/axis_attributes.js index 3b63271a493..07a50f04e5f 100644 --- a/src/polar/attributes/polaraxes.js +++ b/src/plots/polar/axis_attributes.js @@ -1,10 +1,9 @@ 'use strict'; -var Plotly = require('../../plotly'); +var axesAttrs = require('../cartesian/layout_attributes'); +var extendFlat = require('../../lib/extend').extendFlat; -var extendFlat = Plotly.Lib.extendFlat; - -var domainAttr = extendFlat({}, Plotly.Axes.layoutAttributes.domain, { +var domainAttr = extendFlat({}, axesAttrs.domain, { description: [ 'Polar chart subplots are not supported yet.', 'This key has currently no effect.' diff --git a/src/polar/micropolar.js b/src/plots/polar/micropolar.js similarity index 99% rename from src/polar/micropolar.js rename to src/plots/polar/micropolar.js index 2f3871be934..97e5f850d32 100644 --- a/src/polar/micropolar.js +++ b/src/plots/polar/micropolar.js @@ -1,13 +1,13 @@ -var Plotly = require('../plotly'), - d3 = require('d3'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); var µ = module.exports = { - version: '0.2.2' + version: '0.2.2', + manager: require('./micropolar_manager') }; var extendDeepAll = Plotly.Lib.extendDeepAll; - µ.Axis = function module() { var config = { data: [], diff --git a/src/polar/micropolar_manager.js b/src/plots/polar/micropolar_manager.js similarity index 95% rename from src/polar/micropolar_manager.js rename to src/plots/polar/micropolar_manager.js index e622c9acbad..2b7da576136 100644 --- a/src/polar/micropolar_manager.js +++ b/src/plots/polar/micropolar_manager.js @@ -1,8 +1,8 @@ 'use strict'; -var Plotly = require('../plotly'), - d3 = require('d3'), - UndoManager = require('./utils/undo_manager'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); +var UndoManager = require('./undo_manager'); var manager = module.exports = {}; diff --git a/src/polar/utils/undo_manager.js b/src/plots/polar/undo_manager.js similarity index 100% rename from src/polar/utils/undo_manager.js rename to src/plots/polar/undo_manager.js diff --git a/src/snapshot/snapshot.js b/src/snapshot/index.js similarity index 100% rename from src/snapshot/snapshot.js rename to src/snapshot/index.js diff --git a/src/traces/bars/attributes.js b/src/traces/bars/attributes.js new file mode 100644 index 00000000000..1ed2800c5ac --- /dev/null +++ b/src/traces/bars/attributes.js @@ -0,0 +1,65 @@ +var scatterAttrs = require('../scatter/attributes'), + scatterMarkerAttrs = scatterAttrs.marker, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; + + +module.exports = { + x: scatterAttrs.x, + x0: scatterAttrs.x0, + dx: scatterAttrs.dx, + y: scatterAttrs.y, + y0: scatterAttrs.y0, + dy: scatterAttrs.dy, + text: scatterAttrs.text, + orientation: { + valType: 'enumerated', + role: 'info', + values: ['v', 'h'], + description: [ + 'Sets the orientation of the bars.', + 'With *v* (*h*), the value of the each bar spans', + 'along the vertical (horizontal).' + ].join(' ') + }, + marker: { + 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, + colorscale: scatterMarkerLineAttrs.colorscale, + cauto: scatterMarkerLineAttrs.cauto, + cmax: scatterMarkerLineAttrs.cmax, + cmin: scatterMarkerLineAttrs.cmin, + width: scatterMarkerLineAttrs.width, + autocolorscale: scatterMarkerLineAttrs.autocolorscale, + reversescale: scatterMarkerLineAttrs.reversescale + } + }, + + r: scatterAttrs.r, // FIXME this shouldn't get included in 'histogram' + t: scatterAttrs.t, + + _composedModules: { // composed module coupling + 'histogram': 'Histogram' + }, + _nestedModules: { // nested module coupling + 'error_y': 'ErrorBars', + 'error_x': 'ErrorBars', + 'marker.colorbar': 'Colorbar' + }, + + _deprecated: { + bardir: { + valType: 'enumerated', + role: 'info', + values: ['v', 'h'], + description: 'Renamed to `orientation`.' + } + } +}; diff --git a/src/bars.js b/src/traces/bars/index.js similarity index 83% rename from src/bars.js rename to src/traces/bars/index.js index 7acce3d1534..65dd651541a 100644 --- a/src/bars.js +++ b/src/traces/bars/index.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); @@ -17,122 +17,10 @@ Plotly.Plots.register(bars, 'bar', ].join(' ') }); -// For coerce-level coupling -var scatterAttrs = Plotly.Scatter.attributes, - scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line; - -bars.attributes = { - x: scatterAttrs.x, - x0: scatterAttrs.x0, - dx: scatterAttrs.dx, - y: scatterAttrs.y, - y0: scatterAttrs.y0, - dy: scatterAttrs.dy, - text: scatterAttrs.text, - orientation: { - valType: 'enumerated', - role: 'info', - values: ['v', 'h'], - description: [ - 'Sets the orientation of the bars.', - 'With *v* (*h*), the value of the each bar spans', - 'along the vertical (horizontal).' - ].join(' ') - }, - marker: { - 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, - colorscale: scatterMarkerLineAttrs.colorscale, - cauto: scatterMarkerLineAttrs.cauto, - cmax: scatterMarkerLineAttrs.cmax, - cmin: scatterMarkerLineAttrs.cmin, - width: scatterMarkerLineAttrs.width, - autocolorscale: scatterMarkerLineAttrs.autocolorscale, - reversescale: scatterMarkerLineAttrs.reversescale - } - }, - - r: scatterAttrs.r, // FIXME this shouldn't get included in 'histogram' - t: scatterAttrs.t, - - _composedModules: { // composed module coupling - 'histogram': 'Histogram' - }, - _nestedModules: { // nested module coupling - 'error_y': 'ErrorBars', - 'error_x': 'ErrorBars', - 'marker.colorbar': 'Colorbar' - }, - - _deprecated: { - bardir: { - valType: 'enumerated', - role: 'info', - values: ['v', 'h'], - description: 'Renamed to `orientation`.' - } - } -}; -bars.layoutAttributes = { - barmode: { - valType: 'enumerated', - values: ['stack', 'group', 'overlay'], - dflt: 'group', - role: 'info', - description: [ - 'Determines how bars at the same location coordinate', - 'are displayed on the graph.', - 'With *stack*, the bars are stacked on top of one another', - 'With *group*, the bars are plotted next to one another', - 'centered around the shared location.', - 'With *overlay*, the bars are plotted over one another,', - 'you might need to an *opacity* to see multiple bars.' - ].join(' ') - }, - barnorm: { - valType: 'enumerated', - values: ['', 'fraction', 'percent'], - dflt: '', - role: 'info', - description: [ - 'Sets the normalization for bar traces on the graph.', - 'With *fraction*, the value of each bar is divide by the sum of the', - 'values at the location coordinate.', - 'With *percent*, the results form *fraction* are presented in percents.' - ].join(' ') - }, - bargap: { - valType: 'number', - min: 0, - max: 1, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between bars of', - 'adjacent location coordinates.' - ].join(' ') - }, - bargroupgap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between bars of', - 'the same location coordinate.' - ].join(' ') - } -}; +bars.attributes = require('./attributes'); + +bars.layoutAttributes = require('./layout_attributes'); bars.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { @@ -173,10 +61,8 @@ bars.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { coerce('text'); // override defaultColor for error bars with defaultLine - if(Plotly.ErrorBars) { - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, Plotly.Color.defaultLine, {axis: 'y'}); - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, Plotly.Color.defaultLine, {axis: 'x', inherit: 'y'}); - } + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, Plotly.Color.defaultLine, {axis: 'y'}); + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, Plotly.Color.defaultLine, {axis: 'x', inherit: 'y'}); }; bars.supplyLayoutDefaults = function(layoutIn, layoutOut, fullData) { @@ -670,7 +556,7 @@ bars.hoverPoints = function(pointData, xval, yval, hovermode) { if(di.tx) pointData.text = di.tx; - if(Plotly.ErrorBars) Plotly.ErrorBars.hoverInfo(di, trace, pointData); + Plotly.ErrorBars.hoverInfo(di, trace, pointData); return [pointData]; }; diff --git a/src/traces/bars/layout_attributes.js b/src/traces/bars/layout_attributes.js new file mode 100644 index 00000000000..4030d8e34a4 --- /dev/null +++ b/src/traces/bars/layout_attributes.js @@ -0,0 +1,50 @@ +module.exports = { + barmode: { + valType: 'enumerated', + values: ['stack', 'group', 'overlay'], + dflt: 'group', + role: 'info', + description: [ + 'Determines how bars at the same location coordinate', + 'are displayed on the graph.', + 'With *stack*, the bars are stacked on top of one another', + 'With *group*, the bars are plotted next to one another', + 'centered around the shared location.', + 'With *overlay*, the bars are plotted over one another,', + 'you might need to an *opacity* to see multiple bars.' + ].join(' ') + }, + barnorm: { + valType: 'enumerated', + values: ['', 'fraction', 'percent'], + dflt: '', + role: 'info', + description: [ + 'Sets the normalization for bar traces on the graph.', + 'With *fraction*, the value of each bar is divide by the sum of the', + 'values at the location coordinate.', + 'With *percent*, the results form *fraction* are presented in percents.' + ].join(' ') + }, + bargap: { + valType: 'number', + min: 0, + max: 1, + role: 'style', + description: [ + 'Sets the gap (in plot fraction) between bars of', + 'adjacent location coordinates.' + ].join(' ') + }, + bargroupgap: { + valType: 'number', + min: 0, + max: 1, + dflt: 0, + role: 'style', + description: [ + 'Sets the gap (in plot fraction) between bars of', + 'the same location coordinate.' + ].join(' ') + } +}; diff --git a/src/traces/boxes/attributes.js b/src/traces/boxes/attributes.js new file mode 100644 index 00000000000..4a8c9419b59 --- /dev/null +++ b/src/traces/boxes/attributes.js @@ -0,0 +1,166 @@ +var scatterAttrs = require('../scatter/attributes'); +var colorAttrs = require('../../components/color/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterMarkerAttrs = scatterAttrs.marker, + scatterMarkerLineAttrs = scatterMarkerAttrs.line; + + +module.exports = { + y: { + valType: 'data_array', + description: [ + 'Sets the y sample data or coordinates.', + 'See overview for more info.' + ].join(' ') + }, + x: { + valType: 'data_array', + description: [ + 'Sets the x sample data or coordinates.', + 'See overview for more info.' + ].join(' ') + }, + x0: { + valType: 'any', + role: 'info', + description: [ + 'Sets the x coordinate of the box.', + 'See overview for more info.' + ].join(' ') + }, + y0: { + valType: 'any', + role: 'info', + description: [ + 'Sets the y coordinate of the box.', + 'See overview for more info.' + ].join(' ') + }, + whiskerwidth: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.5, + role: 'style', + description: [ + 'Sets the width of the whiskers relative to', + 'the box\' width.', + 'For example, with 1, the whiskers are as wide as the box(es).' + ].join(' ') + }, + boxpoints: { + valType: 'enumerated', + values: ['all', 'outliers', 'suspectedoutliers', false], + dflt: 'outliers', + role: 'style', + description: [ + 'If *outliers*, only the sample points lying outside the whiskers', + 'are shown', + 'If *suspectedoutliers*, the outlier points are shown and', + 'points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1', + 'are highlighted (see `outliercolor`)', + 'If *all*, all sample points are shown', + 'If *false*, only the box(es) are shown with no sample points' + ].join(' ') + }, + boxmean: { + valType: 'enumerated', + values: [true, 'sd', false], + dflt: false, + role: 'style', + description: [ + 'If *true*, the mean of the box(es)\' underlying distribution is', + 'drawn as a dashed line inside the box(es).', + 'If *sd* the standard deviation is also drawn.' + ].join(' ') + }, + jitter: { + valType: 'number', + min: 0, + max: 1, + role: 'style', + description: [ + 'Sets the amount of jitter in the sample points drawn.', + 'If *0*, the sample points align along the distribution axis.', + 'If *1*, the sample points are drawn in a random jitter of width', + 'equal to the width of the box(es).' + ].join(' ') + }, + pointpos: { + valType: 'number', + min: -2, + max: 2, + role: 'style', + description: [ + 'Sets the position of the sample points in relation to the box(es).', + 'If *0*, the sample points are places over the center of the box(es).', + 'Positive (negative) values correspond to positions to the', + 'right (left) for vertical boxes and above (below) for horizontal boxes' + ].join(' ') + }, + orientation: { + valType: 'enumerated', + values: ['v', 'h'], + role: 'style', + description: [ + 'Sets the orientation of the box(es).', + 'If *v* (*h*), the distribution is visualized along', + 'the vertical (horizontal).' + ].join(' ') + }, + marker: { + outliercolor: { + valType: 'color', + dflt: 'rgba(0, 0, 0, 0)', + role: 'style', + description: 'Sets the color of the outlier sample points.' + }, + symbol: extendFlat({}, scatterMarkerAttrs.symbol, + {arrayOk: false}), + opacity: extendFlat({}, scatterMarkerAttrs.opacity, + {arrayOk: false, dflt: 1}), + size: extendFlat({}, scatterMarkerAttrs.size, + {arrayOk: false}), + color: extendFlat({}, scatterMarkerAttrs.color, + {arrayOk: false}), + line: { + color: extendFlat({}, scatterMarkerLineAttrs.color, + {arrayOk: false, dflt: colorAttrs.defaultLine}), + width: extendFlat({}, scatterMarkerLineAttrs.width, + {arrayOk: false, dflt: 0}), + outliercolor: { + valType: 'color', + role: 'style', + description: [ + 'Sets the border line color of the outlier sample points.', + 'Defaults to marker.color' + ].join(' ') + }, + outlierwidth: { + valType: 'number', + min: 0, + dflt: 1, + role: 'style', + description: [ + 'Sets the border line width (in px) of the outlier sample points.' + ].join(' ') + } + } + }, + line: { + color: { + valType: 'color', + role: 'style', + description: 'Sets the color of line bounding the box(es).' + }, + width: { + valType: 'number', + role: 'style', + min: 0, + dflt: 2, + description: 'Sets the width (in px) of line bounding the box(es).' + } + }, + fillcolor: scatterAttrs.fillcolor +}; diff --git a/src/boxes.js b/src/traces/boxes/index.js similarity index 77% rename from src/boxes.js rename to src/traces/boxes/index.js index 9a18983c11d..1b37368efc7 100644 --- a/src/boxes.js +++ b/src/traces/boxes/index.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var isNumeric = require('fast-isnumeric'); @@ -24,208 +24,9 @@ Plotly.Plots.register(boxes, 'box', ].join(' ') }); -var scatterAttrs = Plotly.Scatter.attributes, - scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line, - extendFlat = Plotly.Lib.extendFlat; - -boxes.attributes = { - y: { - valType: 'data_array', - description: [ - 'Sets the y sample data or coordinates.', - 'See overview for more info.' - ].join(' ') - }, - x: { - valType: 'data_array', - description: [ - 'Sets the x sample data or coordinates.', - 'See overview for more info.' - ].join(' ') - }, - x0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the x coordinate of the box.', - 'See overview for more info.' - ].join(' ') - }, - y0: { - valType: 'any', - role: 'info', - description: [ - 'Sets the y coordinate of the box.', - 'See overview for more info.' - ].join(' ') - }, - whiskerwidth: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.5, - role: 'style', - description: [ - 'Sets the width of the whiskers relative to', - 'the box\' width.', - 'For example, with 1, the whiskers are as wide as the box(es).' - ].join(' ') - }, - boxpoints: { - valType: 'enumerated', - values: ['all', 'outliers', 'suspectedoutliers', false], - dflt: 'outliers', - role: 'style', - description: [ - 'If *outliers*, only the sample points lying outside the whiskers', - 'are shown', - 'If *suspectedoutliers*, the outlier points are shown and', - 'points either less than 4*Q1-3*Q3 or greater than 4*Q3-3*Q1', - 'are highlighted (see `outliercolor`)', - 'If *all*, all sample points are shown', - 'If *false*, only the box(es) are shown with no sample points' - ].join(' ') - }, - boxmean: { - valType: 'enumerated', - values: [true, 'sd', false], - dflt: false, - role: 'style', - description: [ - 'If *true*, the mean of the box(es)\' underlying distribution is', - 'drawn as a dashed line inside the box(es).', - 'If *sd* the standard deviation is also drawn.' - ].join(' ') - }, - jitter: { - valType: 'number', - min: 0, - max: 1, - role: 'style', - description: [ - 'Sets the amount of jitter in the sample points drawn.', - 'If *0*, the sample points align along the distribution axis.', - 'If *1*, the sample points are drawn in a random jitter of width', - 'equal to the width of the box(es).' - ].join(' ') - }, - pointpos: { - valType: 'number', - min: -2, - max: 2, - role: 'style', - description: [ - 'Sets the position of the sample points in relation to the box(es).', - 'If *0*, the sample points are places over the center of the box(es).', - 'Positive (negative) values correspond to positions to the', - 'right (left) for vertical boxes and above (below) for horizontal boxes' - ].join(' ') - }, - orientation: { - valType: 'enumerated', - values: ['v', 'h'], - role: 'style', - description: [ - 'Sets the orientation of the box(es).', - 'If *v* (*h*), the distribution is visualized along', - 'the vertical (horizontal).' - ].join(' ') - }, - marker: { - outliercolor: { - valType: 'color', - dflt: 'rgba(0, 0, 0, 0)', - role: 'style', - description: 'Sets the color of the outlier sample points.' - }, - symbol: extendFlat({}, scatterMarkerAttrs.symbol, - {arrayOk: false}), - opacity: extendFlat({}, scatterMarkerAttrs.opacity, - {arrayOk: false, dflt: 1}), - size: extendFlat({}, scatterMarkerAttrs.size, - {arrayOk: false}), - color: extendFlat({}, scatterMarkerAttrs.color, - {arrayOk: false}), - line: { - color: extendFlat({}, scatterMarkerLineAttrs.color, - {arrayOk: false, dflt: Plotly.Color.defaultLine}), - width: extendFlat({}, scatterMarkerLineAttrs.width, - {arrayOk: false, dflt: 0}), - outliercolor: { - valType: 'color', - role: 'style', - description: [ - 'Sets the border line color of the outlier sample points.', - 'Defaults to marker.color' - ].join(' ') - }, - outlierwidth: { - valType: 'number', - min: 0, - dflt: 1, - role: 'style', - description: [ - 'Sets the border line width (in px) of the outlier sample points.' - ].join(' ') - } - } - }, - line: { - color: { - valType: 'color', - role: 'style', - description: 'Sets the color of line bounding the box(es).' - }, - width: { - valType: 'number', - role: 'style', - min: 0, - dflt: 2, - description: 'Sets the width (in px) of line bounding the box(es).' - } - }, - fillcolor: scatterAttrs.fillcolor -}; +boxes.attributes = require('./attributes'); -boxes.layoutAttributes = { - boxmode: { - valType: 'enumerated', - values: ['group', 'overlay'], - dflt: 'overlay', - role: 'info', - description: [ - 'Determines how boxes at the same location coordinate', - 'are displayed on the graph.', - 'If *group*, the boxes are plotted next to one another', - 'centered around the shared location.', - 'If *overlay*, the boxes are plotted over one another,', - 'you might need to set *opacity* to see them multiple boxes.' - ].join(' ') - }, - boxgap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.3, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between boxes of', - 'adjacent location coordinates.' - ].join(' ') - }, - boxgroupgap: { - valType: 'number', - min: 0, - max: 1, - dflt: 0.3, - role: 'style', - description: [ - 'Sets the gap (in plot fraction) between boxes of', - 'the same location coordinate.' - ].join(' ') - } -}; +boxes.layoutAttributes = require('./layout_attributes'); boxes.supplyDefaults = function(traceIn, traceOut, defaultColor) { function coerce(attr, dflt) { diff --git a/src/traces/boxes/layout_attributes.js b/src/traces/boxes/layout_attributes.js new file mode 100644 index 00000000000..a1ad6a1058e --- /dev/null +++ b/src/traces/boxes/layout_attributes.js @@ -0,0 +1,38 @@ +module.exports = { + boxmode: { + valType: 'enumerated', + values: ['group', 'overlay'], + dflt: 'overlay', + role: 'info', + description: [ + 'Determines how boxes at the same location coordinate', + 'are displayed on the graph.', + 'If *group*, the boxes are plotted next to one another', + 'centered around the shared location.', + 'If *overlay*, the boxes are plotted over one another,', + 'you might need to set *opacity* to see them multiple boxes.' + ].join(' ') + }, + boxgap: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.3, + role: 'style', + description: [ + 'Sets the gap (in plot fraction) between boxes of', + 'adjacent location coordinates.' + ].join(' ') + }, + boxgroupgap: { + valType: 'number', + min: 0, + max: 1, + dflt: 0.3, + role: 'style', + description: [ + 'Sets the gap (in plot fraction) between boxes of', + 'the same location coordinate.' + ].join(' ') + } +}; diff --git a/src/geo/attributes/choropleth.js b/src/traces/choropleth/attributes.js similarity index 54% rename from src/geo/attributes/choropleth.js rename to src/traces/choropleth/attributes.js index 3e2b134516c..d71d293f263 100644 --- a/src/geo/attributes/choropleth.js +++ b/src/traces/choropleth/attributes.js @@ -1,8 +1,9 @@ -var Plotly = require('../../plotly'); +var ScatterGeoAttrs = require('../scattergeo/attributes'); +var colorscaleAttrs = require('../../components/colorscale/attributes'); +var plotAttrs = require('../../plots/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; -var ScatterGeoAttrs = Plotly.ScatterGeo.attributes, - ScatterGeoMarkerLineAttrs = ScatterGeoAttrs.marker.line, - traceColorbarAttrs = Plotly.Colorbar.traceColorbarAttributes; +var ScatterGeoMarkerLineAttrs = ScatterGeoAttrs.marker.line; module.exports = { locations: { @@ -27,14 +28,14 @@ module.exports = { width: ScatterGeoMarkerLineAttrs.width } }, - zauto: traceColorbarAttrs.zauto, - zmin: traceColorbarAttrs.zmin, - zmax: traceColorbarAttrs.zmax, - colorscale: traceColorbarAttrs.colorscale, - autocolorscale: traceColorbarAttrs.autocolorscale, - reversescale: traceColorbarAttrs.reversescale, - showscale: traceColorbarAttrs.showscale, - hoverinfo: Plotly.Lib.extendFlat({}, Plotly.Plots.attributes.hoverinfo, { + zauto: colorscaleAttrs.zauto, + zmin: colorscaleAttrs.zmin, + zmax: colorscaleAttrs.zmax, + colorscale: colorscaleAttrs.colorscale, + autocolorscale: colorscaleAttrs.autocolorscale, + reversescale: colorscaleAttrs.reversescale, + showscale: colorscaleAttrs.showscale, + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['location', 'z', 'text', 'name'] }), _nestedModules: { diff --git a/src/traces/choropleth/defaults.js b/src/traces/choropleth/defaults.js new file mode 100644 index 00000000000..a731d93dfa6 --- /dev/null +++ b/src/traces/choropleth/defaults.js @@ -0,0 +1,40 @@ +'use strict'; + +var Plotly = require('../../plotly'); +var Choropleth = require('./'); + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + var locations, len, z; + + function coerce(attr, dflt) { + return Plotly.Lib.coerce(traceIn, traceOut, Choropleth.attributes, attr, dflt); + } + + locations = coerce('locations'); + if(locations) len = locations.length; + if(!locations || !len) { + traceOut.visible = false; + return; + } + + z = coerce('z'); + if(!Array.isArray(z)) { + traceOut.visible = false; + return; + } + + if(z.length > len) traceOut.z = z.slice(0, len); + + coerce('locationmode'); + + coerce('text'); + + coerce('marker.line.color'); + coerce('marker.line.width'); + + Plotly.Colorscale.handleDefaults( + traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} + ); + + coerce('hoverinfo', (layout._dataLength === 1) ? 'location+z+text' : undefined); +}; diff --git a/src/traces/choropleth/index.js b/src/traces/choropleth/index.js new file mode 100644 index 00000000000..600656763eb --- /dev/null +++ b/src/traces/choropleth/index.js @@ -0,0 +1,26 @@ +'use strict'; + +var Plotly = require('../../plotly'); + +var Choropleth = module.exports = {}; + +Plotly.Plots.register(Choropleth, 'choropleth', ['geo', 'noOpacity'], { + description: [ + 'The data that describes the choropleth value-to-color mapping', + 'is set in `z`.', + 'The geographic locations corresponding to each value in `z`', + 'are set in `locations`.' + ].join(' ') +}); + +Choropleth.attributes = require('./attributes'); + +Choropleth.supplyDefaults = require('./defaults'); + +Choropleth.colorbar = Plotly.Colorbar.traceColorbar; + +Choropleth.calc = function(gd, trace) { + + Plotly.Colorscale.calc(trace, trace.z, '', 'z'); + +}; diff --git a/src/geo/plot/choropleth.js b/src/traces/choropleth/plot.js similarity index 93% rename from src/geo/plot/choropleth.js rename to src/traces/choropleth/plot.js index 25ab0384f6f..b69a78590f8 100644 --- a/src/geo/plot/choropleth.js +++ b/src/traces/choropleth/plot.js @@ -1,11 +1,13 @@ 'use strict'; -var Plotly = require('../../plotly'), - d3 = require('d3'), - params = require('../lib/params'), - getTopojsonFeatures = require('../lib/topojson-utils').getTopojsonFeatures, - locationToFeature = require('../lib/location-utils').locationToFeature, - arrayToCalcItem = require('../lib/array-to-calc-item'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); + +var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures; +var locationToFeature = require('../../lib/geo_location_utils').locationToFeature; +var arrayToCalcItem = require('../../lib/array_to_calc_item'); + +var constants = require('../../constants/geo_constants'); var plotChoropleth = module.exports = {}; @@ -46,7 +48,7 @@ plotChoropleth.plot = function(geo, choroplethData, geoLayout) { gChoropleth = framework.select('g.choroplethlayer'), gBaseLayer = framework.select('g.baselayer'), gBaseLayerOverChoropleth = framework.select('g.baselayeroverchoropleth'), - baseLayersOverChoropleth = params.baseLayersOverChoropleth, + baseLayersOverChoropleth = constants.baseLayersOverChoropleth, layerName; // TODO move to more d3-idiomatic pattern (that's work on replot) diff --git a/src/traces/contour/attributes.js b/src/traces/contour/attributes.js new file mode 100644 index 00000000000..3e064c13f5b --- /dev/null +++ b/src/traces/contour/attributes.js @@ -0,0 +1,88 @@ +var scatterAttrs = require('../scatter/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; + +var scatterLineAttrs = scatterAttrs.line; + +module.exports = { + _composedModules: { // composed module coupling + 'contour': 'Heatmap', + 'histogram2dcontour': 'Heatmap' + }, + autocontour: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether of not the contour level attributes at', + 'picked by an algorithm.', + 'If *true*, the number of contour levels can be set in `ncontours`.', + 'If *false*, set the contour level attributes in `contours`.' + ].join(' ') + }, + ncontours: { + valType: 'integer', + dflt: 0, + role: 'style', + description: 'Sets the number of contour levels.' + }, + contours: { + start: { + valType: 'number', + dflt: null, + role: 'style', + description: 'Sets the starting contour level value.' + }, + end: { + valType: 'number', + dflt: null, + role: 'style', + description: 'Sets the end contour level value.' + }, + size: { + valType: 'number', + dflt: null, + role: 'style', + description: 'Sets the step between each contour level.' + }, + coloring: { + valType: 'enumerated', + values: ['fill', 'heatmap', 'lines', 'none'], + dflt: 'fill', + role: 'style', + description: [ + 'Determines the coloring method showing the contour values.', + 'If *fill*, coloring is done evenly between each contour level', + 'If *heatmap*, a heatmap gradient is coloring is applied', + 'between each contour level.', + 'If *lines*, coloring is done on the contour lines.', + 'If *none*, no coloring is applied on this trace.' + ].join(' ') + }, + showlines: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the contour lines are drawn.', + 'Has only an effect if `contours.coloring` is set to *fill*.' + ].join(' ') + } + }, + line: { + color: extendFlat({}, scatterLineAttrs.color, { + description: [ + 'Sets the color of the contour level.', + 'Has no if `contours.coloring` is set to *lines*.' + ].join(' ') + }), + width: scatterLineAttrs.width, + dash: scatterLineAttrs.dash, + smoothing: extendFlat({}, scatterLineAttrs.smoothing, { + description: [ + 'Sets the amount of smoothing for the contour lines,', + 'where *0* corresponds to no smoothing.' + ].join(' ') + }) + } +}; + diff --git a/src/contour.js b/src/traces/contour/index.js similarity index 91% rename from src/contour.js rename to src/traces/contour/index.js index ec132684f24..d5681aecf6c 100644 --- a/src/contour.js +++ b/src/traces/contour/index.js @@ -1,7 +1,7 @@ 'use strict'; -var Plotly = require('./plotly'), - d3 = require('d3'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); var contour = module.exports = {}; @@ -33,91 +33,7 @@ Plotly.Plots.register(contour, 'histogram2dcontour', ].join(' ') }); -var scatterLineAttrs = Plotly.Scatter.attributes.line, - extendFlat = Plotly.Lib.extendFlat; - -contour.attributes = { - _composedModules: { // composed module coupling - 'contour': 'Heatmap', - 'histogram2dcontour': 'Heatmap' - }, - autocontour: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether of not the contour level attributes at', - 'picked by an algorithm.', - 'If *true*, the number of contour levels can be set in `ncontours`.', - 'If *false*, set the contour level attributes in `contours`.' - ].join(' ') - }, - ncontours: { - valType: 'integer', - dflt: 0, - role: 'style', - description: 'Sets the number of contour levels.' - }, - contours: { - start: { - valType: 'number', - dflt: null, - role: 'style', - description: 'Sets the starting contour level value.' - }, - end: { - valType: 'number', - dflt: null, - role: 'style', - description: 'Sets the end contour level value.' - }, - size: { - valType: 'number', - dflt: null, - role: 'style', - description: 'Sets the step between each contour level.' - }, - coloring: { - valType: 'enumerated', - values: ['fill', 'heatmap', 'lines', 'none'], - dflt: 'fill', - role: 'style', - description: [ - 'Determines the coloring method showing the contour values.', - 'If *fill*, coloring is done evenly between each contour level', - 'If *heatmap*, a heatmap gradient is coloring is applied', - 'between each contour level.', - 'If *lines*, coloring is done on the contour lines.', - 'If *none*, no coloring is applied on this trace.' - ].join(' ') - }, - showlines: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the contour lines are drawn.', - 'Has only an effect if `contours.coloring` is set to *fill*.' - ].join(' ') - } - }, - line: { - color: extendFlat({}, scatterLineAttrs.color, { - description: [ - 'Sets the color of the contour level.', - 'Has no if `contours.coloring` is set to *lines*.' - ].join(' ') - }), - width: scatterLineAttrs.width, - dash: scatterLineAttrs.dash, - smoothing: extendFlat({}, scatterLineAttrs.smoothing, { - description: [ - 'Sets the amount of smoothing for the contour lines,', - 'where *0* corresponds to no smoothing.' - ].join(' ') - }) - } -}; +contour.attributes = require('./attributes'); contour.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { function coerce(attr, dflt) { diff --git a/src/traces/heatmap/attributes.js b/src/traces/heatmap/attributes.js new file mode 100644 index 00000000000..f95004bd596 --- /dev/null +++ b/src/traces/heatmap/attributes.js @@ -0,0 +1,84 @@ +var scatterAttrs = require('../scatter/attributes'); +var colorscaleAttrs = require('../../components/colorscale/attributes'); + +var extendFlat = require('../../lib/extend').extendFlat; + + +module.exports = { + z: { + valType: 'data_array', + description: 'Sets the z data.' + }, + x: scatterAttrs.x, + x0: scatterAttrs.x0, + dx: scatterAttrs.dx, + y: scatterAttrs.y, + y0: scatterAttrs.y0, + dy: scatterAttrs.dy, + text: { + valType: 'data_array', + description: 'Sets the text elements associated with each z value.' + }, + transpose: { + valType: 'boolean', + dflt: false, + role: 'info', + description: 'Transposes the z data.' + }, + xtype: { + valType: 'enumerated', + values: ['array', 'scaled'], + role: 'info', + description: [ + 'If *array*, the heatmap\'s x coordinates are given by *x*', + '(the default behavior when `x` is provided).', + 'If *scaled*, the heatmap\'s x coordinates are given by *x0* and *dx*', + '(the default behavior when `x` is not provided).' + ].join(' ') + }, + ytype: { + valType: 'enumerated', + values: ['array', 'scaled'], + role: 'info', + description: [ + 'If *array*, the heatmap\'s y coordinates are given by *y*', + '(the default behavior when `y` is provided)', + 'If *scaled*, the heatmap\'s y coordinates are given by *y0* and *dy*', + '(the default behavior when `y` is not provided)' + ].join(' ') + }, + zauto: colorscaleAttrs.zauto, + zmin: colorscaleAttrs.zmin, + zmax: colorscaleAttrs.zmax, + colorscale: colorscaleAttrs.colorscale, + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, + {dflt: false}), + reversescale: colorscaleAttrs.reversescale, + showscale: colorscaleAttrs.showscale, + zsmooth: { + valType: 'enumerated', + values: ['fast', 'best', false], + dflt: false, + role: 'style', + description: [ + 'Picks a smoothing algorithm use to smooth `z` data.' + ].join(' ') + }, + connectgaps: { + valType: 'boolean', + dflt: false, + role: 'info', + description: [ + 'Determines whether or not gaps', + '(i.e. {nan} or missing values)', + 'in the `z` data are filled in.' + ].join(' ') + }, + _nestedModules: { // nested module coupling + 'colorbar': 'Colorbar' + }, + _composedModules: { // composed module coupling + 'histogram2d': 'Histogram', + 'histogram2dcontour': 'Histogram' + } +}; diff --git a/src/heatmap.js b/src/traces/heatmap/index.js similarity index 92% rename from src/heatmap.js rename to src/traces/heatmap/index.js index fe70cfd216d..01645956c68 100644 --- a/src/heatmap.js +++ b/src/traces/heatmap/index.js @@ -1,6 +1,6 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var d3 = require('d3'); var tinycolor = require('tinycolor2'); var isNumeric = require('fast-isnumeric'); @@ -34,87 +34,7 @@ Plotly.Plots.register(heatmap, 'heatmap', ['cartesian', '2dMap'], { ].join(' ') }); -var scatterAttrs = Plotly.Scatter.attributes, - traceColorbarAttrs = Plotly.Colorbar.traceColorbarAttributes; - -heatmap.attributes = { - z: { - valType: 'data_array', - description: 'Sets the z data.' - }, - x: scatterAttrs.x, - x0: scatterAttrs.x0, - dx: scatterAttrs.dx, - y: scatterAttrs.y, - y0: scatterAttrs.y0, - dy: scatterAttrs.dy, - text: { - valType: 'data_array', - description: 'Sets the text elements associated with each z value.' - }, - transpose: { - valType: 'boolean', - dflt: false, - role: 'info', - description: 'Transposes the z data.' - }, - xtype: { - valType: 'enumerated', - values: ['array', 'scaled'], - role: 'info', - description: [ - 'If *array*, the heatmap\'s x coordinates are given by *x*', - '(the default behavior when `x` is provided).', - 'If *scaled*, the heatmap\'s x coordinates are given by *x0* and *dx*', - '(the default behavior when `x` is not provided).' - ].join(' ') - }, - ytype: { - valType: 'enumerated', - values: ['array', 'scaled'], - role: 'info', - description: [ - 'If *array*, the heatmap\'s y coordinates are given by *y*', - '(the default behavior when `y` is provided)', - 'If *scaled*, the heatmap\'s y coordinates are given by *y0* and *dy*', - '(the default behavior when `y` is not provided)' - ].join(' ') - }, - zauto: traceColorbarAttrs.zauto, - zmin: traceColorbarAttrs.zmin, - zmax: traceColorbarAttrs.zmax, - colorscale: traceColorbarAttrs.colorscale, - autocolorscale: Plotly.Lib.extendFlat({}, traceColorbarAttrs.autocolorscale, - {dflt: false}), - reversescale: traceColorbarAttrs.reversescale, - showscale: traceColorbarAttrs.showscale, - zsmooth: { - valType: 'enumerated', - values: ['fast', 'best', false], - dflt: false, - role: 'style', - description: [ - 'Picks a smoothing algorithm use to smooth `z` data.' - ].join(' ') - }, - connectgaps: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'Determines whether or not gaps', - '(i.e. {nan} or missing values)', - 'in the `z` data are filled in.' - ].join(' ') - }, - _nestedModules: { // nested module coupling - 'colorbar': 'Colorbar' - }, - _composedModules: { // composed module coupling - 'histogram2d': 'Histogram', - 'histogram2dcontour': 'Histogram' - } -}; +heatmap.attributes = require('./attributes'); heatmap.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { var isContour = Plotly.Plots.traceIs(traceOut, 'contour'); diff --git a/src/traces/histogram/attributes.js b/src/traces/histogram/attributes.js new file mode 100644 index 00000000000..5a17956cb83 --- /dev/null +++ b/src/traces/histogram/attributes.js @@ -0,0 +1,136 @@ +var barAttrs = require('../bars/attributes'); + + +module.exports = { + x: { + valType: 'data_array', + description: [ + 'Sets the sample data to be binned on the x axis.' + ].join(' ') + }, + y: { + valType: 'data_array', + description: [ + 'Sets the sample data to be binned on the y axis.' + ].join(' ') + }, + z: { + valType: 'data_array', + description: 'Sets the aggregation data.' + }, + marker: { + color: { // FIXME this overrides 'bar' + valType: 'data_array', + arrayOk: undefined + } + }, + orientation: barAttrs.orientation, + histfunc: { + valType: 'enumerated', + values: ['count', 'sum', 'avg', 'min', 'max'], + role: 'style', + dflt: 'count', + description: [ + 'Specifies the binning function used for this histogram trace.', + + 'If *count*, the histogram values are computed by counting the', + 'number of values lying inside each bin.', + + 'If *sum*, *avg*, *min*, *max*,', + 'the histogram values are computed using', + 'the sum, the average, the minimum or the maximum', + 'of the values lying inside each bin respectively.' + ].join(' ') + }, + histnorm: { + valType: 'enumerated', + values: ['', 'percent', 'probability', 'density', 'probability density'], + dflt: '', + role: 'style', + description: [ + 'Specifies the type of normalization used for this histogram trace.', + + 'If **, the span of each bar corresponds to the number of', + 'occurrences (i.e. the number of data points lying inside the bins).', + + 'If *percent*, the span of each bar corresponds to the percentage', + 'of occurrences with respect to the total number of sample points', + '(here, the sum of all bin area equals 100%).', + + 'If *density*, the span of each bar corresponds to the number of', + 'occurrences in a bin divided by the size of the bin interval', + '(here, the sum of all bin area equals the', + 'total number of sample points).', + + 'If *probability density*, the span of each bar corresponds to the', + 'probability that an event will fall into the corresponding bin', + '(here, the sum of all bin area equals 1).' + ].join(' ') + }, + autobinx: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the x axis bin attributes are picked', + 'by an algorithm.' + ].join(' ') + }, + nbinsx: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'style', + description: 'Sets the number of x axis bins.' + }, + xbins: makeBinsAttr('x'), + autobiny: { + valType: 'boolean', + dflt: true, + role: 'style', + description: [ + 'Determines whether or not the y axis bin attributes are picked', + 'by an algorithm.' + ].join(' ') + }, + nbinsy: { + valType: 'integer', + min: 0, + dflt: 0, + role: 'style', + description: 'Sets the number of y axis bins.' + }, + ybins: makeBinsAttr('y') +}; + +function makeBinsAttr(axLetter) { + return { + start: { + valType: 'number', + dflt: null, + role: 'style', + description: [ + 'Sets the starting value for the', axLetter, + 'axis bins.' + ].join(' ') + }, + end: { + valType: 'number', + dflt: null, + role: 'style', + description: [ + 'Sets the end value for the', axLetter, + 'axis bins.' + ].join(' ') + }, + size: { + valType: 'any', // for date axes + dflt: 1, + role: 'style', + description: [ + 'Sets the step in-between value each', axLetter, + 'axis bin.' + ].join(' ') + } + }; +} diff --git a/src/histogram.js b/src/traces/histogram/index.js similarity index 76% rename from src/histogram.js rename to src/traces/histogram/index.js index d24d687388e..7dfcf1c4a72 100644 --- a/src/histogram.js +++ b/src/traces/histogram/index.js @@ -1,9 +1,13 @@ 'use strict'; -var Plotly = require('./plotly'); +var Plotly = require('../../plotly'); var isNumeric = require('fast-isnumeric'); -var barAttrs = Plotly.Bars.attributes; +/** + * Histogram has its own calc function, + * but uses Bars.plot to display + * and Bars.setPositions for stacking and grouping + */ var histogram = module.exports = {}; /** @@ -37,142 +41,7 @@ Plotly.Plots.register(Plotly.Heatmap, 'histogram2d', ].join(' ') }); -// histogram has its own calc function, -// but uses Bars.plot to display -// and Bars.setPositions for stacking and grouping -function makeBinsAttr(axLetter) { - return { - start: { - valType: 'number', - dflt: null, - role: 'style', - description: [ - 'Sets the starting value for the', axLetter, - 'axis bins.' - ].join(' ') - }, - end: { - valType: 'number', - dflt: null, - role: 'style', - description: [ - 'Sets the end value for the', axLetter, - 'axis bins.' - ].join(' ') - }, - size: { - valType: 'any', // for date axes - dflt: 1, - role: 'style', - description: [ - 'Sets the step in-between value each', axLetter, - 'axis bin.' - ].join(' ') - } - }; -} - -histogram.attributes = { - x: { - valType: 'data_array', - description: [ - 'Sets the sample data to be binned on the x axis.' - ].join(' ') - }, - y: { - valType: 'data_array', - description: [ - 'Sets the sample data to be binned on the y axis.' - ].join(' ') - }, - z: { - valType: 'data_array', - description: 'Sets the aggregation data.' - }, - marker: { - color: { // FIXME this overrides 'bar' - valType: 'data_array', - arrayOk: undefined - } - }, - orientation: barAttrs.orientation, - histfunc: { - valType: 'enumerated', - values: ['count', 'sum', 'avg', 'min', 'max'], - role: 'style', - dflt: 'count', - description: [ - 'Specifies the binning function used for this histogram trace.', - - 'If *count*, the histogram values are computed by counting the', - 'number of values lying inside each bin.', - - 'If *sum*, *avg*, *min*, *max*,', - 'the histogram values are computed using', - 'the sum, the average, the minimum or the maximum', - 'of the values lying inside each bin respectively.' - ].join(' ') - }, - histnorm: { - valType: 'enumerated', - values: ['', 'percent', 'probability', 'density', 'probability density'], - dflt: '', - role: 'style', - description: [ - 'Specifies the type of normalization used for this histogram trace.', - - 'If **, the span of each bar corresponds to the number of', - 'occurrences (i.e. the number of data points lying inside the bins).', - - 'If *percent*, the span of each bar corresponds to the percentage', - 'of occurrences with respect to the total number of sample points', - '(here, the sum of all bin area equals 100%).', - - 'If *density*, the span of each bar corresponds to the number of', - 'occurrences in a bin divided by the size of the bin interval', - '(here, the sum of all bin area equals the', - 'total number of sample points).', - - 'If *probability density*, the span of each bar corresponds to the', - 'probability that an event will fall into the corresponding bin', - '(here, the sum of all bin area equals 1).' - ].join(' ') - }, - autobinx: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the x axis bin attributes are picked', - 'by an algorithm.' - ].join(' ') - }, - nbinsx: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'style', - description: 'Sets the number of x axis bins.' - }, - xbins: makeBinsAttr('x'), - autobiny: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Determines whether or not the y axis bin attributes are picked', - 'by an algorithm.' - ].join(' ') - }, - nbinsy: { - valType: 'integer', - min: 0, - dflt: 0, - role: 'style', - description: 'Sets the number of y axis bins.' - }, - ybins: makeBinsAttr('y') -}; +histogram.attributes = require('./attributes'); histogram.supplyDefaults = function(traceIn, traceOut) { function coerce(attr, dflt) { diff --git a/src/gl3d/attributes/mesh3d.js b/src/traces/mesh3d/attributes.js similarity index 90% rename from src/gl3d/attributes/mesh3d.js rename to src/traces/mesh3d/attributes.js index 83852ed6fd1..31d3e153691 100644 --- a/src/gl3d/attributes/mesh3d.js +++ b/src/traces/mesh3d/attributes.js @@ -1,6 +1,4 @@ -var Plotly = require('../../plotly'); - -var traceColorbarAttrs = Plotly.Colorbar.traceColorbarAttributes; +var colorscaleAttrs = require('../../components/colorscale/attributes'); module.exports = { x: {valType: 'data_array'}, @@ -76,9 +74,9 @@ module.exports = { } }, - colorscale: traceColorbarAttrs.colorscale, - reversescale: traceColorbarAttrs.reversescale, - showscale: traceColorbarAttrs.showscale, + colorscale: colorscaleAttrs.colorscale, + reversescale: colorscaleAttrs.reversescale, + showscale: colorscaleAttrs.showscale, lighting: { ambient: { diff --git a/src/gl3d/convert/mesh.js b/src/traces/mesh3d/convert.js similarity index 93% rename from src/gl3d/convert/mesh.js rename to src/traces/mesh3d/convert.js index 9335a303d8f..9e5ec078782 100644 --- a/src/gl3d/convert/mesh.js +++ b/src/traces/mesh3d/convert.js @@ -1,11 +1,12 @@ 'use strict'; -var createMesh = require('gl-mesh3d'), - str2RgbaArray = require('../lib/str2rgbarray'), - tinycolor = require('tinycolor2'), - triangulate = require('delaunay-triangulate'), - alphaShape = require('alpha-shape'), - convexHull = require('convex-hull'); +var createMesh = require('gl-mesh3d'); +var tinycolor = require('tinycolor2'); +var triangulate = require('delaunay-triangulate'); +var alphaShape = require('alpha-shape'); +var convexHull = require('convex-hull'); + +var str2RgbaArray = require('../../lib/str2rgbarray'); function Mesh3DTrace(scene, mesh, uid) { diff --git a/src/traces/mesh3d/defaults.js b/src/traces/mesh3d/defaults.js new file mode 100644 index 00000000000..d667a55485c --- /dev/null +++ b/src/traces/mesh3d/defaults.js @@ -0,0 +1,81 @@ +'use strict'; + +var Plotly = require('../../plotly'); +var Mesh3D = require('./'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + function coerce(attr, dflt) { + return Plotly.Lib.coerce(traceIn, traceOut, Mesh3D.attributes, attr, dflt); + } + + // read in face/vertex properties + function readComponents(array) { + var ret = array.map(function(attr) { + var result = coerce(attr); + + if(result && Array.isArray(result)) return result; + return null; + }); + + return ret.every(function(x) { + return x && x.length === ret[0].length; + }) && ret; + } + + var coords = readComponents(['x', 'y', 'z']); + var indices = readComponents(['i', 'j', 'k']); + + if(!coords) { + traceOut.visible = false; + return; + } + + if(indices) { + // otherwise, convert all face indices to ints + indices.forEach(function(index) { + for(var i=0; i just get lines scatter.PTS_LINESONLY = 20; -scatter.attributes = { - x: { - valType: 'data_array', - description: 'Sets the x coordinates.' - }, - x0: { - valType: 'any', - dflt: 0, - role: 'info', - description: [ - 'Alternate to `x`.', - 'Builds a linear space of x coordinates.', - 'Use with `dx`', - 'where `x0` is the starting coordinate and `dx` the step.' - ].join(' ') - }, - dx: { - valType: 'number', - dflt: 1, - role: 'info', - description: [ - 'Sets the x coordinate step.', - 'See `x0` for more info.' - ].join(' ') - }, - y: { - valType: 'data_array', - description: 'Sets the y coordinates.' - }, - y0: { - valType: 'any', - dflt: 0, - role: 'info', - description: [ - 'Alternate to `y`.', - 'Builds a linear space of y coordinates.', - 'Use with `dy`', - 'where `y0` is the starting coordinate and `dy` the step.' - ].join(' ') - }, - dy: { - valType: 'number', - dflt: 1, - role: 'info', - description: [ - 'Sets the y coordinate step.', - 'See `y0` for more info.' - ].join(' ') - }, - text: { - valType: 'string', - role: 'info', - dflt: '', - arrayOk: true, - description: [ - 'Sets text elements associated with each (x,y) pair.', - 'If a single string, the same string appears over', - 'all the data points.', - 'If an array of string, the items are mapped in order to the', - 'this trace\'s (x,y) coordinates.' - ].join(' ') - }, - mode: { - valType: 'flaglist', - flags: ['lines','markers','text'], - extras: ['none'], - role: 'info', - description: [ - 'Determines the drawing mode for this scatter trace.', - 'If the provided `mode` includes *text* then the `text` elements', - 'appear at the coordinates. Otherwise, the `text` elements', - 'appear on hover.', - 'If there are less than ' + scatter.PTS_LINESONLY + ' points,', - 'then the default is *lines+markers*. Otherwise, *lines*.' - ].join(' ') - }, - line: { - color: { - valType: 'color', - role: 'style', - description: 'Sets the line color.' - }, - width: { - valType: 'number', - min: 0, - dflt: 2, - role: 'style', - description: 'Sets the line width (in px).' - }, - shape: { - valType: 'enumerated', - values: ['linear', 'spline', 'hv', 'vh', 'hvh', 'vhv'], - dflt: 'linear', - role: 'style', - description: [ - 'Determines the line shape.', - 'With *spline* the lines are drawn using spline interpolation.', - 'The other available values correspond to step-wise line shapes.' - ].join(' ') - }, - smoothing: { - valType: 'number', - min: 0, - max: 1.3, - dflt: 1, - role: 'style', - description: [ - 'Has only an effect if `shape` is set to *spline*', - 'Sets the amount of smoothing.', - '*0* corresponds to no smoothing (equivalent to a *linear* shape).' - ].join(' ') - }, - dash: { - valType: 'string', - // string type usually doesn't take values... this one should really be - // a special type or at least a special coercion function, from the GUI - // you only get these values but elsewhere the user can supply a list of - // dash lengths in px, and it will be honored - values: ['solid', 'dot', 'dash', 'longdash', 'dashdot', 'longdashdot'], - dflt: 'solid', - role: 'style', - description: [ - 'Sets the style of the lines. Set to a dash string type', - 'or a dash length in px.' - ].join(' ') - } - }, - connectgaps: { - valType: 'boolean', - dflt: false, - role: 'info', - description: [ - 'Determines whether or not gaps', - '(i.e. {nan} or missing values)', - 'in the provided data arrays are connected.' - ].join(' ') - }, - fill: { - valType: 'enumerated', - values: ['none', 'tozeroy', 'tozerox', 'tonexty', 'tonextx'], - dflt: 'none', - role: 'style', - description: [ - 'Sets the area to fill with a solid color.', - 'Use with `fillcolor`.' - ].join(' ') - }, - fillcolor: { - valType: 'color', - role: 'style', - description: 'Sets the fill color.' - }, - marker: { - symbol: { - valType: 'enumerated', - values: Plotly.Drawing.symbolList, - dflt: 'circle', - arrayOk: true, - role: 'style', - description: [ - 'Sets the marker symbol type.', - 'Adding 100 is equivalent to appending *-open* to a symbol name.', - 'Adding 200 is equivalent to appending *-dot* to a symbol name.', - 'Adding 300 is equivalent to appending *-open-dot*', - 'or *dot-open* to a symbol name.' - ].join(' ') - }, - opacity: { - valType: 'number', - min: 0, - max: 1, - arrayOk: true, - role: 'style', - description: 'Sets the marker opacity.' - }, - size: { - valType: 'number', - min: 0, - dflt: 6, - arrayOk: true, - role: 'style', - description: 'Sets the marker size (in px).' - }, - color: { - valType: 'color', - arrayOk: true, - role: 'style', - description: 'Sets the marker color.' - }, - maxdisplayed: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Sets a maximum number of points to be drawn on the graph.', - '*0* corresponds to no limit.' - ].join(' ') - }, - sizeref: { - valType: 'number', - dflt: 1, - role: 'style', - description: [ - 'Has only an effect if `marker.size` is set to a numerical array.', - 'Sets the scale factor used to determine the rendered size of', - 'marker points. Use with `sizemin` and `sizemode`.' - ].join(' ') - }, - sizemin: { - valType: 'number', - min: 0, - dflt: 0, - role: 'style', - description: [ - 'Has only an effect if `marker.size` is set to a numerical array.', - 'Sets the minimum size (in px) of the rendered marker points.' - ].join(' ') - }, - sizemode: { - valType: 'enumerated', - values: ['diameter', 'area'], - dflt: 'diameter', - role: 'info', - description: [ - 'Has only an effect if `marker.size` is set to a numerical array.', - 'Sets the rule for which the data in `size` is converted', - 'to pixels.' - ].join(' ') - }, - colorscale: { - valType: 'colorscale', - role: 'style', - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Sets the colorscale.' - ].join(' ') - }, - cauto: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Determines the whether or not the color domain is computed', - 'automatically.' - ].join(' ') - }, - cmax: { - valType: 'number', - dflt: null, - role: 'info', - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Sets the upper bound of the color domain.' - ].join(' ') - }, - cmin: { - valType: 'number', - dflt: null, - role: 'info', - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Sets the lower bound of the color domain.' - ].join(' ') - }, - autocolorscale: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Determines whether or not the colorscale is picked using', - 'values inside `marker.color`.' - ].join(' ') - }, - reversescale: { - valType: 'boolean', - role: 'style', - dflt: false, - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Reverses the colorscale.' - ].join(' ') - }, - showscale: { - valType: 'boolean', - role: 'info', - dflt: false, - description: [ - 'Has only an effect if `marker.color` is set to a numerical array.', - 'Determines whether or not a colorbar is displayed.' - ].join(' ') - }, - line: { - color: { - valType: 'color', - arrayOk: true, - role: 'style', - description: 'Sets the color of the lines bounding the marker points.' - }, - width: { - valType: 'number', - min: 0, - arrayOk: true, - role: 'style', - description: 'Sets the width (in px) of the lines bounding the marker points.' - }, - colorscale: { - valType: 'colorscale', - role: 'style', - description: [ - 'Has only an effect if `marker.line.color` is set to a numerical array.', - 'Sets the colorscale.' - ].join(' ') - }, - cauto: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Has only an effect if `marker.line.color` is set to a numerical array.', - 'Determines the whether or not the color domain is computed', - 'with respect to the input data.' - ].join(' ') - }, - cmax: { - valType: 'number', - dflt: null, - role: 'info', - description: [ - 'Has only an effect if `marker.line.color` is set to a numerical array.', - 'Sets the upper bound of the color domain.' - ].join(' ') - }, - cmin: { - valType: 'number', - dflt: null, - role: 'info', - description: [ - 'Has only an effect if `marker.line.color` is set to a numerical array.', - 'Sets the lower bound of the color domain.' - ].join(' ') - }, - autocolorscale: { - valType: 'boolean', - dflt: true, - role: 'style', - description: [ - 'Has only an effect if `marker.line.color` is set to a numerical array.', - 'Determines whether or not the colorscale is picked using', - 'the sign of values inside `marker.line.color`.' - ].join(' ') - }, - reversescale: { - valType: 'boolean', - dflt: false, - role: 'style', - description: [ - 'Has only an effect if `marker.line.color` is set to a numerical array.', - 'Reverses the colorscale.' - ].join(' ') - } - } - }, - textposition: { - valType: 'enumerated', - values: [ - 'top left', 'top center', 'top right', - 'middle left', 'middle center', 'middle right', - 'bottom left', 'bottom center', 'bottom right' - ], - dflt: 'middle center', - arrayOk: true, - role: 'style', - description: [ - 'Sets the positions of the `text` elements', - 'with respects to the (x,y) coordinates.' - ].join(' ') - }, - textfont: { - family: { - valType: 'string', - role: 'style', - noBlank: true, - strict: true, - arrayOk: true - }, - size: { - valType: 'number', - role: 'style', - min: 1, - arrayOk: true - }, - color: { - valType: 'color', - role: 'style', - arrayOk: true - }, - description: 'Sets the text font.' - }, - r: { - valType: 'data_array', - description: [ - 'For polar chart only.', - 'Sets the radial coordinates.' - ].join('') - }, - t: { - valType: 'data_array', - description: [ - 'For polar chart only.', - 'Sets the angular coordinates.' - ].join('') - }, - _nestedModules: { // nested module coupling - 'error_y': 'ErrorBars', - 'error_x': 'ErrorBars', - 'marker.colorbar': 'Colorbar' - } -}; +scatter.attributes = require('./attributes'); scatter.handleXYDefaults = function(traceIn, traceOut, coerce) { var len, @@ -517,10 +95,8 @@ scatter.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { if(!scatter.hasLines(traceOut)) lineShapeDefaults(traceIn, traceOut, coerce); } - if(Plotly.ErrorBars) { - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); - } + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); }; // common to 'scatter', 'scatter3d', 'scattergeo' and 'scattergl' @@ -789,13 +365,10 @@ scatter.calc = function(gd, trace) { } // if no error bars, markers or text, or fill to y=0 remove x padding - else if( - (Plotly.ErrorBars===undefined || !trace.error_y.visible) && - ( - ['tonexty', 'tozeroy'].indexOf(trace.fill)!==-1 || - (!scatter.hasMarkers(trace) && !scatter.hasText(trace)) - ) - ) { + else if(!trace.error_y.visible && ( + ['tonexty', 'tozeroy'].indexOf(trace.fill)!==-1 || + (!scatter.hasMarkers(trace) && !scatter.hasText(trace)) + )) { xOptions.padded = false; xOptions.ppad = 0; } @@ -1280,7 +853,7 @@ scatter.getTraceColor = function(trace, di) { lc : trace.fillcolor; } } -} +}; scatter.hoverPoints = function(pointData, xval, yval, hovermode) { var cd = pointData.cd, @@ -1331,7 +904,7 @@ scatter.hoverPoints = function(pointData, xval, yval, hovermode) { if(di.tx) pointData.text = di.tx; else if(trace.text) pointData.text = trace.text; - if(Plotly.ErrorBars) Plotly.ErrorBars.hoverInfo(di, trace, pointData); + Plotly.ErrorBars.hoverInfo(di, trace, pointData); return [pointData]; }; diff --git a/src/gl3d/attributes/scatter3d.js b/src/traces/scatter3d/attributes.js similarity index 93% rename from src/gl3d/attributes/scatter3d.js rename to src/traces/scatter3d/attributes.js index 9f22ce111b7..469cfecae43 100644 --- a/src/gl3d/attributes/scatter3d.js +++ b/src/traces/scatter3d/attributes.js @@ -1,13 +1,12 @@ 'use strict'; -var Plotly = require('../../plotly'); -var MARKER_SYMBOLS = require('../lib/markers.json'); +var scatterAttrs = require('../scatter/attributes'); +var MARKER_SYMBOLS = require('../../constants/gl_markers.json'); +var extendFlat = require('../../lib/extend').extendFlat; -var scatterAttrs = Plotly.Scatter.attributes, - scatterLineAttrs = scatterAttrs.line, +var scatterLineAttrs = scatterAttrs.line, scatterMarkerAttrs = scatterAttrs.marker, - scatterMarkerLineAttrs = scatterMarkerAttrs.line, - extendFlat = Plotly.Lib.extendFlat; + scatterMarkerLineAttrs = scatterMarkerAttrs.line; function makeProjectionAttr(axLetter) { return { diff --git a/src/gl3d/lib/calc-errors.js b/src/traces/scatter3d/calc_errors.js similarity index 100% rename from src/gl3d/lib/calc-errors.js rename to src/traces/scatter3d/calc_errors.js diff --git a/src/gl3d/convert/scatter.js b/src/traces/scatter3d/convert.js similarity index 95% rename from src/gl3d/convert/scatter.js rename to src/traces/scatter3d/convert.js index c65ebf4bfb4..8d98e54e573 100644 --- a/src/gl3d/convert/scatter.js +++ b/src/traces/scatter3d/convert.js @@ -1,16 +1,21 @@ 'use strict'; -var createLinePlot = require('gl-line3d'), - createScatterPlot = require('gl-scatter3d'), - createErrorBars = require('gl-error3d'), - createMesh = require('gl-mesh3d'), - triangulate = require('delaunay-triangulate'), - str2RgbaArray = require('../lib/str2rgbarray'), - formatColor = require('../lib/format-color'), - calculateError = require('../lib/calc-errors'), - DASH_PATTERNS = require('../lib/dashes.json'), - MARKER_SYMBOLS = require('../lib/markers.json'), - Plotly = require('../../plotly'); +var Plotly = require('../../plotly'); + +var createLinePlot = require('gl-line3d'); +var createScatterPlot = require('gl-scatter3d'); +var createErrorBars = require('gl-error3d'); +var createMesh = require('gl-mesh3d'); +var triangulate = require('delaunay-triangulate'); + +var str2RgbaArray = require('../../lib/str2rgbarray'); +var formatColor = require('../../lib/gl_format_color'); + +var DASH_PATTERNS = require('../../constants/gl3d_dashes.json'); +var MARKER_SYMBOLS = require('../../constants/gl_markers.json'); + +// TODO use ErrorBars.calcFromTrace instead +var calculateError = require('./calc_errors'); function LineWithMarkers(scene, uid) { this.scene = scene; diff --git a/src/traces/scatter3d/defaults.js b/src/traces/scatter3d/defaults.js new file mode 100644 index 00000000000..44373a6e238 --- /dev/null +++ b/src/traces/scatter3d/defaults.js @@ -0,0 +1,67 @@ +'use strict'; + +var Plotly = require('../../plotly'); +var Scatter3D = require('./'); + + +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + var Scatter = Plotly.Scatter; + + function coerce(attr, dflt) { + return Plotly.Lib.coerce(traceIn, traceOut, Scatter3D.attributes, attr, dflt); + } + + var len = handleXYZDefaults(traceIn, traceOut, coerce); + if(!len) { + traceOut.visible = false; + return; + } + + coerce('text'); + coerce('mode'); + + if(Scatter.hasLines(traceOut)) { + Scatter.lineDefaults(traceIn, traceOut, defaultColor, coerce); + } + + if(Scatter.hasMarkers(traceOut)) { + Scatter.markerDefaults(traceIn, traceOut, defaultColor, layout, coerce); + } + + if(Scatter.hasText(traceOut)) { + Scatter.textDefaults(traceIn, traceOut, layout, coerce); + } + + var lineColor = (traceOut.line || {}).color , + markerColor = (traceOut.marker || {}).color; + if(coerce('surfaceaxis') >= 0) coerce('surfacecolor', lineColor || markerColor); + + var dims = ['x', 'y', 'z']; + for(var i = 0; i < 3; ++i) { + var projection = 'projection.' + dims[i]; + if(coerce(projection + '.show')) { + coerce(projection + '.opacity'); + coerce(projection + '.scale'); + } + } + + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'z'}); + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y', inherit: 'z'}); + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'z'}); +}; + +function handleXYZDefaults(traceIn, traceOut, coerce) { + var len = 0, + x = coerce('x'), + y = coerce('y'), + z = coerce('z'); + + if(x && y && z) { + len = Math.min(x.length, y.length, z.length); + if(len < x.length) traceOut.x = x.slice(0, len); + if(len < y.length) traceOut.y = y.slice(0, len); + if(len < z.length) traceOut.z = z.slice(0, len); + } + + return len; +} diff --git a/src/traces/scatter3d/index.js b/src/traces/scatter3d/index.js new file mode 100644 index 00000000000..b48e88fccb9 --- /dev/null +++ b/src/traces/scatter3d/index.js @@ -0,0 +1,38 @@ +'use strict'; + +var Plotly = require('../../plotly'); + +var Scatter3D = module.exports = {}; + +Plotly.Plots.register(Scatter3D, + 'scatter3d', ['gl3d', 'symbols', 'markerColorscale', 'showLegend'], { + hrName: 'scatter_3d', + description: [ + 'The data visualized as scatter point or lines in 3D dimension', + 'is set in `x`, `y`, `z`.', + '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`', + 'Projections are achieved via `projection`.', + 'Surface fills are achieved via `surfaceaxis`.' + ].join(' ') +}); + +Scatter3D.attributes = require('./attributes'); + +Scatter3D.markerSymbols = require('../../constants/gl_markers.json'); + +Scatter3D.supplyDefaults = require('./defaults'); + +Scatter3D.colorbar = Plotly.Scatter.colorbar; + +Scatter3D.calc = function(gd, trace) { + // this is a kludge to put the array attributes into + // calcdata the way Scatter.plot does, so that legends and + // popovers know what to do with them. + var cd = [{x: false, y: false, trace: trace, t: {}}]; + Plotly.Scatter.arraysToCalcdata(cd); + + Plotly.Scatter.calcMarkerColorscales(trace); + + return cd; +}; diff --git a/src/geo/attributes/scattergeo.js b/src/traces/scattergeo/attributes.js similarity index 91% rename from src/geo/attributes/scattergeo.js rename to src/traces/scattergeo/attributes.js index 6c80e7f6e50..9a9f18bedbb 100644 --- a/src/geo/attributes/scattergeo.js +++ b/src/traces/scattergeo/attributes.js @@ -1,11 +1,11 @@ -var Plotly = require('../../plotly'); +var scatterAttrs = require('../scatter/attributes'); +var plotAttrs = require('../../plots/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; -var scatterAttrs = Plotly.Scatter.attributes, - scatterMarkerAttrs = scatterAttrs.marker, +var scatterMarkerAttrs = scatterAttrs.marker, scatterLineAttrs = scatterAttrs.line, scatterMarkerLineAttrs = scatterMarkerAttrs.line; -var extendFlat = Plotly.Lib.extendFlat; module.exports = { lon: { @@ -78,7 +78,7 @@ module.exports = { }, textfont: scatterAttrs.textfont, textposition: scatterAttrs.textposition, - hoverinfo: Plotly.Lib.extendFlat({}, Plotly.Plots.attributes.hoverinfo, { + hoverinfo: extendFlat({}, plotAttrs.hoverinfo, { flags: ['lon', 'lat', 'location', 'text', 'name'] }), _nestedModules: { diff --git a/src/geo/defaults/scattergeo.js b/src/traces/scattergeo/defaults.js similarity index 52% rename from src/geo/defaults/scattergeo.js rename to src/traces/scattergeo/defaults.js index e2516842a67..81ed36996df 100644 --- a/src/geo/defaults/scattergeo.js +++ b/src/traces/scattergeo/defaults.js @@ -1,30 +1,17 @@ 'use strict'; var Plotly = require('../../plotly'); +var ScatterGeo = require('./'); -var ScatterGeo = module.exports = {}; -Plotly.Plots.register(ScatterGeo, 'scattergeo', - ['geo', 'symbols', 'markerColorscale', 'showLegend'], { - hrName: 'scatter_geo', - description: [ - 'The data visualized as scatter point or lines on a geographic map', - 'is provided either by longitude/latitude pairs in `lon` and `lat`', - 'respectively or by geographic location IDs or names in `locations`.' - ].join(' ') -}); - -ScatterGeo.attributes = require('../attributes/scattergeo'); - -ScatterGeo.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { var Scatter = Plotly.Scatter; function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, - ScatterGeo.attributes, attr, dflt); + return Plotly.Lib.coerce(traceIn, traceOut, ScatterGeo.attributes, attr, dflt); } - var len = ScatterGeo.handleLonLatLocDefaults(traceIn, traceOut, coerce); + var len = handleLonLatLocDefaults(traceIn, traceOut, coerce); if(!len) { traceOut.visible = false; return; @@ -48,7 +35,7 @@ ScatterGeo.supplyDefaults = function(traceIn, traceOut, defaultColor, layout) { coerce('hoverinfo', (layout._dataLength === 1) ? 'lon+lat+location+text' : undefined); }; -ScatterGeo.handleLonLatLocDefaults = function(traceIn, traceOut, coerce) { +function handleLonLatLocDefaults(traceIn, traceOut, coerce) { var len = 0, locations = coerce('locations'); @@ -68,12 +55,4 @@ ScatterGeo.handleLonLatLocDefaults = function(traceIn, traceOut, coerce) { if(len < lat.length) traceOut.lat = lat.slice(0, len); return len; -}; - -ScatterGeo.colorbar = Plotly.Scatter.colorbar; - -ScatterGeo.calc = function(gd, trace) { - - Plotly.Scatter.calcMarkerColorscales(trace); - -}; +} diff --git a/src/traces/scattergeo/index.js b/src/traces/scattergeo/index.js new file mode 100644 index 00000000000..001a49937c8 --- /dev/null +++ b/src/traces/scattergeo/index.js @@ -0,0 +1,27 @@ +'use strict'; + +var Plotly = require('../../plotly'); + +var ScatterGeo = module.exports = {}; + +Plotly.Plots.register(ScatterGeo, 'scattergeo', + ['geo', 'symbols', 'markerColorscale', 'showLegend'], { + hrName: 'scatter_geo', + description: [ + 'The data visualized as scatter point or lines on a geographic map', + 'is provided either by longitude/latitude pairs in `lon` and `lat`', + 'respectively or by geographic location IDs or names in `locations`.' + ].join(' ') +}); + +ScatterGeo.attributes = require('./attributes'); + +ScatterGeo.supplyDefaults = require('./defaults'); + +ScatterGeo.colorbar = Plotly.Scatter.colorbar; + +ScatterGeo.calc = function(gd, trace) { + + Plotly.Scatter.calcMarkerColorscales(trace); + +}; diff --git a/src/geo/plot/scattergeo.js b/src/traces/scattergeo/plot.js similarity index 96% rename from src/geo/plot/scattergeo.js rename to src/traces/scattergeo/plot.js index 6c0c3dc9a8a..0c192770024 100644 --- a/src/geo/plot/scattergeo.js +++ b/src/traces/scattergeo/plot.js @@ -1,10 +1,11 @@ 'use strict'; -var Plotly = require('../../plotly'), - d3 = require('d3'), - getTopojsonFeatures = require('../lib/topojson-utils').getTopojsonFeatures, - locationToFeature = require('../lib/location-utils').locationToFeature, - arrayToCalcItem = require('../lib/array-to-calc-item'); +var Plotly = require('../../plotly'); +var d3 = require('d3'); + +var getTopojsonFeatures = require('../../lib/topojson_utils').getTopojsonFeatures; +var locationToFeature = require('../../lib/geo_location_utils').locationToFeature; +var arrayToCalcItem = require('../../lib/array_to_calc_item'); var plotScatterGeo = module.exports = {}; diff --git a/src/gl2d/scattergl/attributes.js b/src/traces/scattergl/attributes.js similarity index 88% rename from src/gl2d/scattergl/attributes.js rename to src/traces/scattergl/attributes.js index 78482f76dbc..eecd58ad9df 100644 --- a/src/gl2d/scattergl/attributes.js +++ b/src/traces/scattergl/attributes.js @@ -1,10 +1,9 @@ -'use strict'; +var scatterAttrs = require('../scatter/attributes'); +var DASHES = require('../../constants/gl2d_dashes.json'); +var MARKERS = require('../../constants/gl_markers.json'); +var extendFlat = require('../../lib/extend').extendFlat; -var Plotly = require('../../plotly'), - extendFlat = Plotly.Lib.extendFlat; - -var scatterAttrs = Plotly.Scatter.attributes, - scatterLineAttrs = scatterAttrs.line, +var scatterLineAttrs = scatterAttrs.line, scatterMarkerAttrs = scatterAttrs.marker, scatterMarkerLineAttrs = scatterMarkerAttrs.line; @@ -38,7 +37,7 @@ module.exports = { width: scatterLineAttrs.width, dash: { valType: 'enumerated', - values: Object.keys(require('../lib/dashes.json')), + values: Object.keys(DASHES), dflt: 'solid', role: 'style', description: 'Sets the style of the lines.' @@ -48,7 +47,7 @@ module.exports = { color: scatterMarkerAttrs.color, symbol: { valType: 'enumerated', - values: Object.keys(require('../../gl3d/lib/markers.json')), + values: Object.keys(MARKERS), dflt: 'circle', arrayOk: true, role: 'style', diff --git a/src/gl2d/scattergl/convert.js b/src/traces/scattergl/convert.js similarity index 98% rename from src/gl2d/scattergl/convert.js rename to src/traces/scattergl/convert.js index 203fa363d96..d4144892d7c 100644 --- a/src/gl2d/scattergl/convert.js +++ b/src/traces/scattergl/convert.js @@ -6,13 +6,13 @@ var createScatter = require('gl-scatter2d'); var createFancyScatter = require('gl-scatter2d-fancy'); var createLine = require('gl-line2d'); var createError = require('gl-error2d'); - -var str2RGBArray = require('../../gl3d/lib/str2rgbarray'); -var formatColor = require('../../gl3d/lib/format-color'); var isNumeric = require('fast-isnumeric'); -var MARKER_SYMBOLS = require('../../gl3d/lib/markers.json'); -var DASHES = require('../lib/dashes.json'); +var str2RGBArray = require('../../lib/str2rgbarray'); +var formatColor = require('../../lib/gl_format_color'); + +var MARKER_SYMBOLS = require('../../constants/gl_markers.json'); +var DASHES = require('../../constants/gl2d_dashes.json'); var AXES = ['xaxis', 'yaxis']; diff --git a/src/gl2d/scattergl/defaults.js b/src/traces/scattergl/defaults.js similarity index 63% rename from src/gl2d/scattergl/defaults.js rename to src/traces/scattergl/defaults.js index 194ac2bd8d6..be7b51a03a7 100644 --- a/src/gl2d/scattergl/defaults.js +++ b/src/traces/scattergl/defaults.js @@ -1,20 +1,20 @@ 'use strict'; -var Plotly = require('../../plotly'), - ScatterGl = require('./scattergl'); +var Plotly = require('../../plotly'); +var ScatterGl = require('./'); module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { var Scatter = Plotly.Scatter; function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, ScatterGl.attributes, attr, dflt); + return Plotly.Lib.coerce(traceIn, traceOut, ScatterGl.attributes, attr, dflt); } var len = Scatter.handleXYDefaults(traceIn, traceOut, coerce); if(!len) { - traceOut.visible = false; - return; + traceOut.visible = false; + return; } coerce('text'); @@ -33,8 +33,6 @@ module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout Scatter.fillColorDefaults(traceIn, traceOut, defaultColor, coerce); } - if(Plotly.ErrorBars) { - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); - Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); - } + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'y'}); + Plotly.ErrorBars.supplyDefaults(traceIn, traceOut, defaultColor, {axis: 'x', inherit: 'y'}); }; diff --git a/src/gl2d/scattergl/scattergl.js b/src/traces/scattergl/index.js similarity index 100% rename from src/gl2d/scattergl/scattergl.js rename to src/traces/scattergl/index.js diff --git a/src/gl3d/attributes/surface.js b/src/traces/surface/attributes.js similarity index 88% rename from src/gl3d/attributes/surface.js rename to src/traces/surface/attributes.js index aa21f652bb8..eb8256c0047 100644 --- a/src/gl3d/attributes/surface.js +++ b/src/traces/surface/attributes.js @@ -1,8 +1,8 @@ 'use strict'; -var Plotly = require('../../plotly'); +var colorscaleAttrs = require('../../components/colorscale/attributes'); +var extendFlat = require('../../lib/extend').extendFlat; -var traceColorbarAttrs = Plotly.Colorbar.traceColorbarAttributes; function makeContourProjAttr(axLetter) { return { @@ -86,14 +86,14 @@ module.exports = { valType: 'data_array', description: 'Sets the text elements associated with each z value.' }, - zauto: traceColorbarAttrs.zauto, - zmin: traceColorbarAttrs.zmin, - zmax: traceColorbarAttrs.zmax, - colorscale: traceColorbarAttrs.colorscale, - autocolorscale: Plotly.Lib.extendFlat({}, traceColorbarAttrs.autocolorscale, + zauto: colorscaleAttrs.zauto, + zmin: colorscaleAttrs.zmin, + zmax: colorscaleAttrs.zmax, + colorscale: colorscaleAttrs.colorscale, + autocolorscale: extendFlat({}, colorscaleAttrs.autocolorscale, {dflt: false}), - reversescale: traceColorbarAttrs.reversescale, - showscale: traceColorbarAttrs.showscale, + reversescale: colorscaleAttrs.reversescale, + showscale: colorscaleAttrs.showscale, contours: { x: makeContourAttr('x'), y: makeContourAttr('y'), diff --git a/src/gl3d/convert/surface.js b/src/traces/surface/convert.js similarity index 97% rename from src/gl3d/convert/surface.js rename to src/traces/surface/convert.js index 3ade2ff8a2c..8e23fbe011d 100644 --- a/src/gl3d/convert/surface.js +++ b/src/traces/surface/convert.js @@ -1,12 +1,13 @@ 'use strict'; -var createSurface = require('gl-surface3d'), - ndarray = require('ndarray'), - homography = require('ndarray-homography'), - fill = require('ndarray-fill'), - ops = require('ndarray-ops'), - str2RgbaArray = require('../lib/str2rgbarray'), - tinycolor = require('tinycolor2'); +var createSurface = require('gl-surface3d'); +var ndarray = require('ndarray'); +var homography = require('ndarray-homography'); +var fill = require('ndarray-fill'); +var ops = require('ndarray-ops'); +var tinycolor = require('tinycolor2'); + +var str2RgbaArray = require('../../lib/str2rgbarray'); var MIN_RESOLUTION = 128; diff --git a/src/gl3d/defaults/surface.js b/src/traces/surface/defaults.js similarity index 63% rename from src/gl3d/defaults/surface.js rename to src/traces/surface/defaults.js index c8894b3eda7..c92afde9e81 100644 --- a/src/gl3d/defaults/surface.js +++ b/src/traces/surface/defaults.js @@ -1,29 +1,14 @@ 'use strict'; var Plotly = require('../../plotly'); +var Surface = require('./'); -var Surface = module.exports = {}; -Plotly.Plots.register(Surface, 'surface', ['gl3d', 'noOpacity'], { - description: [ - 'The data the describes the coordinates of the surface is set in `z`.', - 'Data in `z` should be a {2D array}.', - - 'Coordinates in `x` and `y` can either be 1D {arrays}', - 'or {2D arrays} (e.g. to graph parametric surfaces).', - - 'If not provided in `x` and `y`, the x and y coordinates are assumed', - 'to be linear starting at 0 with a unit step.' - ].join(' ') -}); - -Surface.attributes = require('../attributes/surface'); - -Surface.supplyDefaults = function (traceIn, traceOut, defaultColor, layout) { - var i, j, _this = this; +module.exports = function supplyDefaults(traceIn, traceOut, defaultColor, layout) { + var i, j; function coerce(attr, dflt) { - return Plotly.Lib.coerce(traceIn, traceOut, _this.attributes, attr, dflt); + return Plotly.Lib.coerce(traceIn, traceOut, Surface.attributes, attr, dflt); } var z = coerce('z'); @@ -93,12 +78,3 @@ Surface.supplyDefaults = function (traceIn, traceOut, defaultColor, layout) { traceIn, traceOut, layout, coerce, {prefix: '', cLetter: 'z'} ); }; - -Surface.colorbar = Plotly.Colorbar.traceColorbar; - -Surface.calc = function(gd, trace) { - - // auto-z and autocolorscale if applicable - Plotly.Colorscale.calc(trace, trace.z, '', 'z'); - -}; diff --git a/src/traces/surface/index.js b/src/traces/surface/index.js new file mode 100644 index 00000000000..32d3fe2676b --- /dev/null +++ b/src/traces/surface/index.js @@ -0,0 +1,31 @@ +'use strict'; + +var Plotly = require('../../plotly'); + +var Surface = module.exports = {}; + +Plotly.Plots.register(Surface, 'surface', ['gl3d', 'noOpacity'], { + description: [ + 'The data the describes the coordinates of the surface is set in `z`.', + 'Data in `z` should be a {2D array}.', + + 'Coordinates in `x` and `y` can either be 1D {arrays}', + 'or {2D arrays} (e.g. to graph parametric surfaces).', + + 'If not provided in `x` and `y`, the x and y coordinates are assumed', + 'to be linear starting at 0 with a unit step.' + ].join(' ') +}); + +Surface.attributes = require('./attributes'); + +Surface.supplyDefaults = require('./defaults'); + +Surface.colorbar = Plotly.Colorbar.traceColorbar; + +Surface.calc = function(gd, trace) { + + // auto-z and autocolorscale if applicable + Plotly.Colorscale.calc(trace, trace.z, '', 'z'); + +}; diff --git a/tasks/util/constants.js b/tasks/util/constants.js index ed3a1b54aac..349c5b20480 100644 --- a/tasks/util/constants.js +++ b/tasks/util/constants.js @@ -10,12 +10,12 @@ module.exports = { pathToSrc: pathToSrc, pathToMocks: path.join(pathToRoot, 'test/image/mocks'), - pathToPlotlySrc: path.join(pathToSrc, 'plotly.js'), + pathToPlotlySrc: path.join(pathToSrc, 'index.js'), pathToPlotlyDist: path.join(pathToDist, 'plotly.js'), pathToPlotlyDistMin: path.join(pathToDist, 'plotly.min.js'), pathToPlotlyDistWithMeta: path.join(pathToDist, 'plotly-with-meta.js'), - pathToPlotlyGeoAssetsSrc: path.join(pathToSrc, 'geo/geo-assets.js'), + pathToPlotlyGeoAssetsSrc: path.join(pathToSrc, 'assets/geo_assets.js'), pathToPlotlyGeoAssetsDist: path.join(pathToDist, 'plotly-geo-assets.js'), pathToFontSVG: path.join(pathToSrc, 'fonts/ploticon/ploticon.svg'), diff --git a/test/jasmine/tests/axes_test.js b/test/jasmine/tests/axes_test.js index 336618e2a31..bd336907956 100644 --- a/test/jasmine/tests/axes_test.js +++ b/test/jasmine/tests/axes_test.js @@ -27,7 +27,7 @@ describe('Test axes', function () { } } }; - var expectedYaxis = Plotly.Lib.extendDeep({} ,gd.layout.xaxis), + var expectedYaxis = Plotly.Lib.extendDeep({}, gd.layout.xaxis), expectedXaxis = { title: 'Click to enter X axis title', type: 'date' diff --git a/test/jasmine/tests/events_test.js b/test/jasmine/tests/events_test.js index 93b5a9b6fe7..35c76b6dcdd 100644 --- a/test/jasmine/tests/events_test.js +++ b/test/jasmine/tests/events_test.js @@ -5,7 +5,7 @@ */ -var Events = require('@src/events'); +var Events = require('@src/lib/events'); describe('Events', function () { 'use strict'; diff --git a/test/jasmine/tests/geoaxes_test.js b/test/jasmine/tests/geoaxes_test.js index e0472ae2d88..c043bc764b0 100644 --- a/test/jasmine/tests/geoaxes_test.js +++ b/test/jasmine/tests/geoaxes_test.js @@ -1,11 +1,9 @@ -var Plotly = require('@src/plotly'), - params = require('@src/geo/lib/params'); +var params = require('@src/constants/geo_constants'); +var supplyLayoutDefaults = require('@src/plots/geo/layout/axis_defaults'); describe('Test geoaxes', function () { 'use strict'; - var GeoAxes = Plotly.GeoAxes; - describe('supplyLayoutDefaults', function() { var geoLayoutIn, geoLayoutOut; @@ -31,7 +29,7 @@ describe('Test geoaxes', function () { geoLayoutIn = {}; geoLayoutOut = {scope: scope}; - GeoAxes.supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); expect(geoLayoutOut.lonaxis.range).toEqual(dfltLonaxisRange); expect(geoLayoutOut.lataxis.range).toEqual(dfltLataxisRange); expect(geoLayoutOut.lonaxis.tick0).toEqual(dfltLonaxisRange[0]); @@ -43,7 +41,7 @@ describe('Test geoaxes', function () { }; geoLayoutOut = {scope: scope}; - GeoAxes.supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); expect(geoLayoutOut.lonaxis.range).toEqual(customLonaxisRange); expect(geoLayoutOut.lataxis.range).toEqual(customLataxisRange); expect(geoLayoutOut.lonaxis.tick0).toEqual(customLonaxisRange[0]); @@ -55,7 +53,7 @@ describe('Test geoaxes', function () { var expectedLonaxisRange, expectedLataxisRange; function testOne() { - GeoAxes.supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); + supplyLayoutDefaults(geoLayoutIn, geoLayoutOut); expect(geoLayoutOut.lonaxis.range).toEqual(expectedLonaxisRange); expect(geoLayoutOut.lataxis.range).toEqual(expectedLataxisRange); } diff --git a/test/jasmine/tests/gl3daxes_test.js b/test/jasmine/tests/gl3daxes_test.js index 8489b84347a..acfec12c285 100644 --- a/test/jasmine/tests/gl3daxes_test.js +++ b/test/jasmine/tests/gl3daxes_test.js @@ -1,4 +1,5 @@ -var Plotly = require('@src/plotly'); +var supplyLayoutDefaults = require('@src/plots/gl3d/layout/axis_defaults'); + describe('Test Gl3dAxes', function () { 'use strict'; @@ -7,7 +8,6 @@ describe('Test Gl3dAxes', function () { var layoutIn, layoutOut; - var supplyLayoutDefaults = Plotly.Gl3dAxes.supplyLayoutDefaults; var options = { font: 'Open Sans', scene: {id: 'scene'}, @@ -22,41 +22,41 @@ describe('Test Gl3dAxes', function () { layoutIn = {}; var expected = { - "xaxis": { - "showline": false, - "showgrid": true, - "gridcolor": "rgb(204, 204, 204)", - "gridwidth": 1, - "showspikes": true, - "spikesides": true, - "spikethickness": 2, - "spikecolor": "rgb(0,0,0)", - "showbackground": false, - "showaxeslabels": true + 'xaxis': { + 'showline': false, + 'showgrid': true, + 'gridcolor': 'rgb(204, 204, 204)', + 'gridwidth': 1, + 'showspikes': true, + 'spikesides': true, + 'spikethickness': 2, + 'spikecolor': 'rgb(0,0,0)', + 'showbackground': false, + 'showaxeslabels': true }, - "yaxis": { - "showline": false, - "showgrid": true, - "gridcolor": "rgb(204, 204, 204)", - "gridwidth": 1, - "showspikes": true, - "spikesides": true, - "spikethickness": 2, - "spikecolor": "rgb(0,0,0)", - "showbackground": false, - "showaxeslabels": true + 'yaxis': { + 'showline': false, + 'showgrid': true, + 'gridcolor': 'rgb(204, 204, 204)', + 'gridwidth': 1, + 'showspikes': true, + 'spikesides': true, + 'spikethickness': 2, + 'spikecolor': 'rgb(0,0,0)', + 'showbackground': false, + 'showaxeslabels': true }, - "zaxis": { - "showline": false, - "showgrid": true, - "gridcolor": "rgb(204, 204, 204)", - "gridwidth": 1, - "showspikes": true, - "spikesides": true, - "spikethickness": 2, - "spikecolor": "rgb(0,0,0)", - "showbackground": false, - "showaxeslabels": true + 'zaxis': { + 'showline': false, + 'showgrid': true, + 'gridcolor': 'rgb(204, 204, 204)', + 'gridwidth': 1, + 'showspikes': true, + 'spikesides': true, + 'spikethickness': 2, + 'spikecolor': 'rgb(0,0,0)', + 'showbackground': false, + 'showaxeslabels': true } };