Skip to content

Commit 432f0d0

Browse files
committed
Merge branch 'master' into axis-breaks
2 parents 28c328d + c5173e1 commit 432f0d0

File tree

108 files changed

+2275
-134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

108 files changed

+2275
-134
lines changed

CONTRIBUTING.md

+75-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ We use the following [labels](https://github.com/plotly/plotly.js/labels) to tra
1717
| `type: bug` | bug report confirmed by a plotly team member |
1818
| `type: regression` | bug that introduced a change in behavior from one version to the next |
1919
| `type: feature` | planned feature additions |
20+
| `type: new trace type` | subset of `type: feature` reserved for planned new trace types |
2021
| `type: translation` | localization-related tasks |
2122
| `type: performance` | performance related tasks |
2223
| `type: maintenance` | source code cleanup resulting in no enhancement for users |
@@ -96,6 +97,13 @@ Three additional helpers exist that are refreshed every second:
9697
There is also a search bar in the top right of the dashboard. This fuzzy-searches
9798
image mocks based on their file name and trace type.
9899

100+
#### Alternative to test dashboard
101+
102+
Use the [`plotly-mock-viewer`](https://github.com/rreusser/plotly-mock-viewer)
103+
which has live-reloading and a bunch of other cool features.
104+
An online version of `plotly-mock-viewer` is available at https://rreusser.github.io/plotly-mock-viewer/
105+
which uses https://cdn.plot.ly/plotly-latest.min.js
106+
99107
#### Other npm scripts
100108

101109
- `npm run preprocess`: pre-processes the css and svg source file in js. This
@@ -175,11 +183,23 @@ which shows the baseline image, the generated image, the diff and the json mocks
175183

176184
To view the results of a run on CircleCI, download the `build/test_images/` and `build/test_images_diff/` artifacts into your local repo and then run `npm run start-image_viewer`.
177185

178-
### Writing interaction tests
186+
### Using the developer console in karma to write/debug jasmine tests
187+
188+
- Click on the `DEBUG` button
189+
- In the `DEBUG RUNNER` window, open the console (e.g. with `<ctrl-shift-j>`)
190+
- Find test file (e.g. with `<ctrl-o>` + typing the name of the file), look out
191+
for "bundled" files with the same name.
192+
- Set `debugger` on relevant line(s)
193+
- Rerun the test suite by refreshing the page (e.g. with `<crtl-r>`)
194+
195+
![Peek 2020-03-11 10-45](https://user-images.githubusercontent.com/6675409/76438118-f2502300-6390-11ea-88d2-17a553c3b4e8.gif)
196+
197+
### Writing jasmine interaction tests
198+
179199
Keep in mind that the interaction coordinates are relative to the top-left corner of the plot, including the margins. To produce a reliable interaction test,
180200
it may be necessary to fix the width, height, margins, X axis range and Y axis range of the plot. For example:
181201

182-
```
202+
```js
183203
Plotly.newPlot(gd, [{
184204
x: [1, 1, 1, 2, 2, 2, 3, 3, 3],
185205
y: [1, 2, 3, 1, 2, 3, 1, 2, 3],
@@ -196,18 +216,67 @@ This will produce the following plot, and say you want to simulate a selection p
196216

197217
<img src="https://user-images.githubusercontent.com/31989842/38890553-0bc6190c-4282-11e8-8efc-077bf05ca565.png">
198218

199-
200219
## Repo organization
201220

202221
- Distributed files are in `dist/`
203222
- CommonJS require-able modules are in `lib/`
204-
- Sources files are in `src/`, including the index
223+
- Sources files are in `src/`
205224
- Build and repo management scripts are in `tasks/`
206225
- All tasks can be run using [`npm run-script`](https://docs.npmjs.com/cli/run-script)
207226
- Tests are `test/`, they are partitioned into `image` and `jasmine` tests
208227
- Test dashboard and image viewer code is in `devtools/`
209-
- Built files are in `build/` (most files in here are git-ignored, the css and font built files are exceptions)
210-
228+
- Built files are in `build/` (the files in here are git-ignored, except for `plotcss.js`)
229+
230+
## Trace module design
231+
232+
The trace modules (found in [`src/traces`](https://github.com/plotly/plotly.js/tree/master/src/traces))
233+
are defined as plain objects with functions and constants attached to them in an index file
234+
(e.g. `src/traces/scatter/index.js`). The trace modules are "registered" undo the `Registry` object
235+
(found in [`src/registry.js`](https://github.com/plotly/plotly.js/blob/master/src/registry.js)) using
236+
`Plotly.register` (as done in the index files in `dist/`).
237+
238+
The trace module methods are meant to be called as part of loops during subplot-specific
239+
(e.g. in `plots/cartesian/index.js`) and figure-wide (e.g. in `plots/plots.js`) subroutines.
240+
That way, the subroutines work no matter which trace modules got registered.
241+
242+
All traces modules set:
243+
244+
- `_module.name`: name of the trace module as used by the trace `type` attribute.
245+
- `_module.basePlotModule`: base plot (or subplot) module corresponding to the
246+
trace type (e.g. `scatter` links to the `Cartesian` base plot module, `scatter3d` links to `gl3d`).
247+
- `_module.attributes`: JSON-serializable object of attribute declarations.
248+
This object is used to generate the plot-schema JSON.
249+
- `_module.supplyDefaults`: Takes in input trace settings and coerces them into "full" settings
250+
under `gd._fullData`. This one is called during the figure-wide `Plots.supplyDefaults` routine.
251+
Note that the `supplyDefaults` method performance should scale with the number of attributes (**not** the
252+
number of data points - so it should not loop over any data arrays).
253+
- `_module.calc`: Converts inputs data into "calculated" (or sanitized) data. This one is called during
254+
the figure-wide `Plots.doCalcdata` routine. The `calc` method is allowed to
255+
scale with the number of data points and is in general more costly than `supplyDefaults`.
256+
Please note that some edit pathways skip `Plots.doCalcdata` (as determined by the
257+
`editType` flags in the attributes files).
258+
- `_module.plot`: Draws the trace on screen. This one is called by the defined `basePlotModule`.
259+
260+
Other methods used by some trace modules:
261+
262+
- `_module.categories`: list of string identifiers used to group traces by behavior. Traces that
263+
have a given category can then be detected using [`Registry.traceIs`](https://github.com/plotly/plotly.js/blob/8f049fddbac0ca0382816984b8526857e9714fe6/src/registry.js#L129-L155)
264+
- `_module.layoutAttributes`: JSON-serializable object of attribute declarations
265+
coerced in the layout (e.g. `barmode` for `bar` traces)
266+
- `_module.supplyLayoutDefaults`: Defaults logic for layout attributes.
267+
- `_module.crossTraceDefaults`: Defaults logic that depends on input setting of multiple traces.
268+
- `_module.crossTraceCalc`: Computations that depend on the data of multiple traces.
269+
- `_module.colorbar`: Defines the colorbar appearance for traces that support it.
270+
- `_module.hoverPoints`: Point-picking logic called during hover.
271+
- `_module.selectPoints`: Polygon-containing logic called during selections.
272+
- `_module.style`: Sometimes split from `_module.plot` where `_module.plot` only
273+
draws the elements and `_module.style` styles them.
274+
- `_module.styleOnSelect`: Optimization of `_module.style` called during
275+
selections.
276+
- `_module.convert`: Sometimes separated from `_module.plot` or `_module.calc` to convert the
277+
plotly.js settings to another framework e.g. to `gl-plot3d` for `gl3d` traces, to
278+
`mapbox-gl` from `mapbox` traces. This split can make the logic easier to test.
279+
If you make a `convert`, you should call it from either `calc` or `plot`.
211280

212281
## Coding style
213282

package-lock.json

+3-3
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@
8888
"gl-select-box": "^1.0.3",
8989
"gl-spikes2d": "^1.0.2",
9090
"gl-streamtube3d": "^1.4.0",
91-
"gl-surface3d": "^1.4.6",
91+
"gl-surface3d": "^1.5.0",
9292
"gl-text": "^1.1.8",
9393
"glslify": "^7.0.0",
9494
"has-hover": "^1.0.1",

src/assets/geo_assets.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
var saneTopojson = require('sane-topojson');
1212

1313

14-
// package version injected by `npm run preprocess`
15-
exports.version = '1.52.3';
14+
exports.version = require('../version').version;
1615

1716
exports.topojson = saneTopojson;

src/core.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
'use strict';
1010

11-
// package version injected by `npm run preprocess`
12-
exports.version = '1.52.3';
11+
exports.version = require('./version').version;
1312

1413
// inject promise polyfill
1514
require('es6-promise').polyfill();

src/plot_api/to_image.js

+21-3
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,18 @@
1111
var isNumeric = require('fast-isnumeric');
1212

1313
var plotApi = require('./plot_api');
14+
var plots = require('../plots/plots');
1415
var Lib = require('../lib');
1516

1617
var helpers = require('../snapshot/helpers');
1718
var toSVG = require('../snapshot/tosvg');
1819
var svgToImg = require('../snapshot/svgtoimg');
20+
var version = require('../version').version;
1921

2022
var attrs = {
2123
format: {
2224
valType: 'enumerated',
23-
values: ['png', 'jpeg', 'webp', 'svg'],
25+
values: ['png', 'jpeg', 'webp', 'svg', 'full-json'],
2426
dflt: 'png',
2527
description: 'Sets the format of exported image.'
2628
},
@@ -170,8 +172,24 @@ function toImage(gd, opts) {
170172
var width = clonedGd._fullLayout.width;
171173
var height = clonedGd._fullLayout.height;
172174

173-
plotApi.purge(clonedGd);
174-
document.body.removeChild(clonedGd);
175+
function cleanup() {
176+
plotApi.purge(clonedGd);
177+
document.body.removeChild(clonedGd);
178+
}
179+
180+
if(format === 'full-json') {
181+
var json = plots.graphJson(clonedGd, false, 'keepdata', 'object', true, true);
182+
json.version = version;
183+
json = JSON.stringify(json);
184+
cleanup();
185+
if(imageDataOnly) {
186+
return resolve(json);
187+
} else {
188+
return resolve(helpers.encodeJSON(json));
189+
}
190+
}
191+
192+
cleanup();
175193

176194
if(format === 'svg') {
177195
if(imageDataOnly) {

src/plots/gl3d/scene.js

+33-11
Original file line numberDiff line numberDiff line change
@@ -1018,6 +1018,37 @@ proto.updateFx = function(dragmode, hovermode) {
10181018
scene.fullSceneLayout.hovermode = hovermode;
10191019
};
10201020

1021+
function flipPixels(pixels, w, h) {
1022+
for(var i = 0, q = h - 1; i < q; ++i, --q) {
1023+
for(var j = 0; j < w; ++j) {
1024+
for(var k = 0; k < 4; ++k) {
1025+
var a = 4 * (w * i + j) + k;
1026+
var b = 4 * (w * q + j) + k;
1027+
var tmp = pixels[a];
1028+
pixels[a] = pixels[b];
1029+
pixels[b] = tmp;
1030+
}
1031+
}
1032+
}
1033+
}
1034+
1035+
function correctRGB(pixels, w, h) {
1036+
for(var i = 0; i < h; ++i) {
1037+
for(var j = 0; j < w; ++j) {
1038+
var k = 4 * (w * i + j);
1039+
1040+
var a = pixels[k + 3]; // alpha
1041+
if(a > 0) {
1042+
var q = 255 / a;
1043+
1044+
for(var l = 0; l < 3; ++l) { // RGB
1045+
pixels[k + l] = Math.min(q * pixels[k + l], 255);
1046+
}
1047+
}
1048+
}
1049+
}
1050+
}
1051+
10211052
proto.toImage = function(format) {
10221053
var scene = this;
10231054

@@ -1036,17 +1067,8 @@ proto.toImage = function(format) {
10361067

10371068
var pixels = new Uint8Array(w * h * 4);
10381069
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
1039-
1040-
// Flip pixels
1041-
for(var j = 0, k = h - 1; j < k; ++j, --k) {
1042-
for(var i = 0; i < w; ++i) {
1043-
for(var l = 0; l < 4; ++l) {
1044-
var tmp = pixels[4 * (w * j + i) + l];
1045-
pixels[4 * (w * j + i) + l] = pixels[4 * (w * k + i) + l];
1046-
pixels[4 * (w * k + i) + l] = tmp;
1047-
}
1048-
}
1049-
}
1070+
flipPixels(pixels, w, h);
1071+
correctRGB(pixels, w, h);
10501072

10511073
var canvas = document.createElement('canvas');
10521074
canvas.width = w;

src/plots/plots.js

+20-14
Original file line numberDiff line numberDiff line change
@@ -2037,9 +2037,10 @@ plots.didMarginChange = function(margin0, margin1) {
20372037
* keepall: keep data and src
20382038
* @param {String} output If you specify 'object', the result will not be stringified
20392039
* @param {Boolean} useDefaults If truthy, use _fullLayout and _fullData
2040+
* @param {Boolean} includeConfig If truthy, include _context
20402041
* @returns {Object|String}
20412042
*/
2042-
plots.graphJson = function(gd, dataonly, mode, output, useDefaults) {
2043+
plots.graphJson = function(gd, dataonly, mode, output, useDefaults, includeConfig) {
20432044
// if the defaults aren't supplied yet, we need to do that...
20442045
if((useDefaults && dataonly && !gd._fullData) ||
20452046
(useDefaults && !dataonly && !gd._fullLayout)) {
@@ -2050,26 +2051,29 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) {
20502051
var layout = (useDefaults) ? gd._fullLayout : gd.layout;
20512052
var frames = (gd._transitionData || {})._frames;
20522053

2053-
function stripObj(d) {
2054+
function stripObj(d, keepFunction) {
20542055
if(typeof d === 'function') {
2055-
return null;
2056+
return keepFunction ? '_function_' : null;
20562057
}
20572058
if(Lib.isPlainObject(d)) {
20582059
var o = {};
2059-
var v, src;
2060-
for(v in d) {
2060+
var src;
2061+
Object.keys(d).sort().forEach(function(v) {
20612062
// remove private elements and functions
20622063
// _ is for private, [ is a mistake ie [object Object]
2063-
if(typeof d[v] === 'function' ||
2064-
['_', '['].indexOf(v.charAt(0)) !== -1) {
2065-
continue;
2064+
if(['_', '['].indexOf(v.charAt(0)) !== -1) return;
2065+
2066+
// if a function, add if necessary then move on
2067+
if(typeof d[v] === 'function') {
2068+
if(keepFunction) o[v] = '_function';
2069+
return;
20662070
}
20672071

20682072
// look for src/data matches and remove the appropriate one
20692073
if(mode === 'keepdata') {
20702074
// keepdata: remove all ...src tags
20712075
if(v.substr(v.length - 3) === 'src') {
2072-
continue;
2076+
return;
20732077
}
20742078
} else if(mode === 'keepstream') {
20752079
// keep sourced data if it's being streamed.
@@ -2078,26 +2082,26 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) {
20782082
src = d[v + 'src'];
20792083
if(typeof src === 'string' && src.indexOf(':') > 0) {
20802084
if(!Lib.isPlainObject(d.stream)) {
2081-
continue;
2085+
return;
20822086
}
20832087
}
20842088
} else if(mode !== 'keepall') {
20852089
// keepref: remove sourced data but only
20862090
// if the source tag is well-formed
20872091
src = d[v + 'src'];
20882092
if(typeof src === 'string' && src.indexOf(':') > 0) {
2089-
continue;
2093+
return;
20902094
}
20912095
}
20922096

20932097
// OK, we're including this... recurse into it
2094-
o[v] = stripObj(d[v]);
2095-
}
2098+
o[v] = stripObj(d[v], keepFunction);
2099+
});
20962100
return o;
20972101
}
20982102

20992103
if(Array.isArray(d)) {
2100-
return d.map(stripObj);
2104+
return d.map(function(x) {return stripObj(x, keepFunction);});
21012105
}
21022106

21032107
if(Lib.isTypedArray(d)) {
@@ -2126,6 +2130,8 @@ plots.graphJson = function(gd, dataonly, mode, output, useDefaults) {
21262130

21272131
if(frames) obj.frames = stripObj(frames);
21282132

2133+
if(includeConfig) obj.config = stripObj(gd._context, true);
2134+
21292135
return (output === 'object') ? obj : JSON.stringify(obj);
21302136
};
21312137

src/snapshot/download.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ function downloadImage(gd, opts) {
5151
var promise = toImage(gd, opts);
5252

5353
var filename = opts.filename || gd.fn || 'newplot';
54-
filename += '.' + opts.format;
54+
filename += '.' + opts.format.replace('-', '.');
5555

5656
promise.then(function(result) {
5757
if(_gd) _gd._snapshotInProgress = false;

src/snapshot/helpers.js

+6
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ exports.encodeSVG = function(svg) {
3636
return 'data:image/svg+xml,' + encodeURIComponent(svg);
3737
};
3838

39+
exports.encodeJSON = function(json) {
40+
return 'data:application/json,' + encodeURIComponent(json);
41+
};
42+
3943
var DOM_URL = window.URL || window.webkitURL;
4044

4145
exports.createObjectURL = function(blob) {
@@ -49,6 +53,8 @@ exports.revokeObjectURL = function(url) {
4953
exports.createBlob = function(url, format) {
5054
if(format === 'svg') {
5155
return new window.Blob([url], {type: 'image/svg+xml;charset=utf-8'});
56+
} else if(format === 'full-json') {
57+
return new window.Blob([url], {type: 'application/json;charset=utf-8'});
5258
} else {
5359
var binary = fixBinary(window.atob(url));
5460
return new window.Blob([binary], {type: 'image/' + format});

src/traces/bar/plot.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,7 @@ function plot(gd, plotinfo, cdModule, traceLayer, opts, makeOnCompleteCallback)
242242
var sel = transition(Lib.ensureSingle(bar, 'path'), fullLayout, opts, makeOnCompleteCallback);
243243
sel
244244
.style('vector-effect', 'non-scaling-stroke')
245-
.attr('d', 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z')
245+
.attr('d', isNaN((x1 - x0) * (y1 - y0)) ? 'M0,0Z' : 'M' + x0 + ',' + y0 + 'V' + y1 + 'H' + x1 + 'V' + y0 + 'Z')
246246
.call(Drawing.setClipUrl, plotinfo.layerClipId, gd);
247247

248248
if(!fullLayout.uniformtext.mode && withTransition) {

0 commit comments

Comments
 (0)