Skip to content

Commit 5bb341e

Browse files
committedApr 24, 2019
implement crossTraceDefaults, calc and Colobar.draw for color axes
- use min of min (and max of max) of all traces linked to same color axis to compute min/max when auto:true - call Colorscale.crossTraceDefaults after having relinked layout, to use coloraxis._min, coloraxis._max -
1 parent 8f1fde1 commit 5bb341e

File tree

8 files changed

+297
-95
lines changed

8 files changed

+297
-95
lines changed
 

‎src/components/colorbar/draw.js

+66-37
Original file line numberDiff line numberDiff line change
@@ -72,67 +72,96 @@ function draw(gd) {
7272
}
7373

7474
function makeColorBarData(gd) {
75+
var fullLayout = gd._fullLayout;
7576
var calcdata = gd.calcdata;
7677
var out = [];
7778

79+
// single out item
80+
var opts;
81+
// colorbar attr parent container
82+
var cont;
83+
// trace attr container
84+
var trace;
85+
// colorbar options
86+
var cbOpt;
87+
88+
function initOpts(opts) {
89+
return extendFlat(opts, {
90+
// fillcolor can be a d3 scale, domain is z values, range is colors
91+
// or leave it out for no fill,
92+
// or set to a string constant for single-color fill
93+
_fillcolor: null,
94+
// line.color has the same options as fillcolor
95+
_line: {color: null, width: null, dash: null},
96+
// levels of lines to draw.
97+
// note that this DOES NOT determine the extent of the bar
98+
// that's given by the domain of fillcolor
99+
// (or line.color if no fillcolor domain)
100+
_levels: {start: null, end: null, size: null},
101+
// separate fill levels (for example, heatmap coloring of a
102+
// contour map) if this is omitted, fillcolors will be
103+
// evaluated halfway between levels
104+
_filllevels: null,
105+
// for continuous colorscales: fill with a gradient instead of explicit levels
106+
// value should be the colorscale [[0, c0], [v1, c1], ..., [1, cEnd]]
107+
_fillgradient: null,
108+
// when using a gradient, we need the data range specified separately
109+
_zrange: null
110+
});
111+
}
112+
113+
function calcOpts() {
114+
if(typeof cbOpt.calc === 'function') {
115+
cbOpt.calc(gd, trace, opts);
116+
} else {
117+
opts._fillgradient = cont.reversescale ?
118+
flipScale(cont.colorscale) :
119+
cont.colorscale;
120+
opts._zrange = [cont[cbOpt.min], cont[cbOpt.max]];
121+
}
122+
}
123+
78124
for(var i = 0; i < calcdata.length; i++) {
79125
var cd = calcdata[i];
80-
var trace = cd[0].trace;
126+
trace = cd[0].trace;
81127
var moduleOpts = trace._module.colorbar;
82128

83129
if(trace.visible === true && moduleOpts) {
84130
var allowsMultiplotCbs = Array.isArray(moduleOpts);
85131
var cbOpts = allowsMultiplotCbs ? moduleOpts : [moduleOpts];
86132

87133
for(var j = 0; j < cbOpts.length; j++) {
88-
var cbOpt = cbOpts[j];
134+
cbOpt = cbOpts[j];
89135
var contName = cbOpt.container;
90-
var cont = contName ? trace[contName] : trace;
136+
cont = contName ? trace[contName] : trace;
91137

92138
if(cont && cont.showscale) {
93-
var opts = cont.colorbar;
139+
opts = initOpts(cont.colorbar);
94140
opts._id = 'cb' + trace.uid + (allowsMultiplotCbs && contName ? '-' + contName : '');
95141
opts._traceIndex = trace.index;
96142
opts._propPrefix = (contName ? contName + '.' : '') + 'colorbar.';
97-
98-
extendFlat(opts, {
99-
// fillcolor can be a d3 scale, domain is z values, range is colors
100-
// or leave it out for no fill,
101-
// or set to a string constant for single-color fill
102-
_fillcolor: null,
103-
// line.color has the same options as fillcolor
104-
_line: {color: null, width: null, dash: null},
105-
// levels of lines to draw.
106-
// note that this DOES NOT determine the extent of the bar
107-
// that's given by the domain of fillcolor
108-
// (or line.color if no fillcolor domain)
109-
_levels: {start: null, end: null, size: null},
110-
// separate fill levels (for example, heatmap coloring of a
111-
// contour map) if this is omitted, fillcolors will be
112-
// evaluated halfway between levels
113-
_filllevels: null,
114-
// for continuous colorscales: fill with a gradient instead of explicit levels
115-
// value should be the colorscale [[0, c0], [v1, c1], ..., [1, cEnd]]
116-
_fillgradient: null,
117-
// when using a gradient, we need the data range specified separately
118-
_zrange: null
119-
});
120-
121-
if(typeof cbOpt.calc === 'function') {
122-
cbOpt.calc(gd, cd, opts);
123-
} else {
124-
opts._fillgradient = cont.reversescale ?
125-
flipScale(cont.colorscale) :
126-
cont.colorscale;
127-
opts._zrange = [cont[cbOpt.min], cont[cbOpt.max]];
128-
}
129-
143+
calcOpts();
130144
out.push(opts);
131145
}
132146
}
133147
}
134148
}
135149

150+
for(var k in fullLayout._colorAxes) {
151+
cont = fullLayout[k];
152+
153+
if(cont.showscale) {
154+
var colorAxOpts = fullLayout._colorAxes[k];
155+
156+
opts = initOpts(cont.colorbar);
157+
opts._id = 'cb' + k;
158+
159+
cbOpt = {min: 'cmin', max: 'cmax'};
160+
calcOpts();
161+
out.push(opts);
162+
}
163+
}
164+
136165
return out;
137166
}
138167

‎src/components/colorscale/calc.js

+33-20
Original file line numberDiff line numberDiff line change
@@ -8,37 +8,50 @@
88

99
'use strict';
1010

11+
var isNumeric = require('fast-isnumeric');
12+
1113
var Lib = require('../../lib');
14+
var extractOpts = require('./helpers').extractOpts;
1215

1316
module.exports = function calc(gd, trace, opts) {
1417
var fullLayout = gd._fullLayout;
1518
var vals = opts.vals;
1619
var containerStr = opts.containerStr;
17-
var cLetter = opts.cLetter;
1820

1921
var container = containerStr ?
2022
Lib.nestedProperty(trace, containerStr).get() :
2123
trace;
2224

23-
var autoAttr = cLetter + 'auto';
24-
var minAttr = cLetter + 'min';
25-
var maxAttr = cLetter + 'max';
26-
var midAttr = cLetter + 'mid';
27-
var auto = container[autoAttr];
28-
var min = container[minAttr];
29-
var max = container[maxAttr];
30-
var mid = container[midAttr];
31-
var scl = container.colorscale;
25+
var cOpts = extractOpts(container);
26+
var auto = cOpts.auto !== false;
27+
var min = cOpts.min;
28+
var max = cOpts.max;
29+
var mid = cOpts.mid;
30+
31+
var minVal = function() { return Lib.aggNums(Math.min, null, vals); };
32+
var maxVal = function() { return Lib.aggNums(Math.max, null, vals); };
3233

33-
if(auto !== false || min === undefined) {
34-
min = Lib.aggNums(Math.min, null, vals);
34+
if(min === undefined) {
35+
min = minVal();
36+
} else if(auto) {
37+
if(container._colorAx && isNumeric(min)) {
38+
min = Math.min(min, minVal());
39+
} else {
40+
min = minVal();
41+
}
3542
}
3643

37-
if(auto !== false || max === undefined) {
38-
max = Lib.aggNums(Math.max, null, vals);
44+
if(max === undefined) {
45+
max = maxVal();
46+
} else if(auto) {
47+
if(container._colorAx && isNumeric(max)) {
48+
max = Math.max(max, maxVal());
49+
} else {
50+
max = maxVal();
51+
}
3952
}
4053

41-
if(auto !== false && mid !== undefined) {
54+
if(auto && mid !== undefined) {
4255
if(max - mid > mid - min) {
4356
min = mid - (max - mid);
4457
} else if(max - mid < mid - min) {
@@ -51,14 +64,14 @@ module.exports = function calc(gd, trace, opts) {
5164
max += 0.5;
5265
}
5366

54-
container['_' + minAttr] = container[minAttr] = min;
55-
container['_' + maxAttr] = container[maxAttr] = max;
67+
cOpts._sync('min', min);
68+
cOpts._sync('max', max);
5669

57-
if(container.autocolorscale) {
70+
if(cOpts.autocolorscale) {
71+
var scl;
5872
if(min * max < 0) scl = fullLayout.colorscale.diverging;
5973
else if(min >= 0) scl = fullLayout.colorscale.sequential;
6074
else scl = fullLayout.colorscale.sequentialminus;
61-
62-
container._colorscale = container.colorscale = scl;
75+
cOpts._sync('colorscale', scl);
6376
}
6477
};

‎src/components/colorscale/cross_trace_defaults.js

+30-24
Original file line numberDiff line numberDiff line change
@@ -10,60 +10,66 @@
1010

1111
var Lib = require('../../lib');
1212
var hasColorscale = require('./helpers').hasColorscale;
13+
var extractOpts = require('./helpers').extractOpts;
1314

14-
module.exports = function crossTraceDefaults(fullData) {
15+
module.exports = function crossTraceDefaults(fullData, fullLayout) {
1516
function replace(cont, k) {
1617
var val = cont['_' + k];
1718
if(val !== undefined) {
1819
cont[k] = val;
1920
}
2021
}
2122

22-
function relinkColorAtts(trace, cAttrs) {
23-
var cont = cAttrs.container ?
24-
Lib.nestedProperty(trace, cAttrs.container).get() :
25-
trace;
23+
function relinkColorAtts(outerCont, cbOpt) {
24+
var cont = cbOpt.container ?
25+
Lib.nestedProperty(outerCont, cbOpt.container).get() :
26+
outerCont;
2627

2728
if(cont) {
28-
var isAuto = cont.zauto || cont.cauto;
29-
var minAttr = cAttrs.min;
30-
var maxAttr = cAttrs.max;
29+
if(cont.coloraxis) {
30+
// stash ref to color axis
31+
cont._colorAx = fullLayout[cont.coloraxis];
32+
} else {
33+
var cOpts = extractOpts(cont);
34+
var isAuto = cOpts.auto;
3135

32-
if(isAuto || cont[minAttr] === undefined) {
33-
replace(cont, minAttr);
34-
}
35-
if(isAuto || cont[maxAttr] === undefined) {
36-
replace(cont, maxAttr);
37-
}
38-
if(cont.autocolorscale) {
39-
replace(cont, 'colorscale');
36+
if(isAuto || cOpts.min === undefined) {
37+
replace(cont, cbOpt.min);
38+
}
39+
if(isAuto || cOpts.max === undefined) {
40+
replace(cont, cbOpt.max);
41+
}
42+
if(cOpts.autocolorscale) {
43+
replace(cont, 'colorscale');
44+
}
4045
}
4146
}
4247
}
4348

4449
for(var i = 0; i < fullData.length; i++) {
4550
var trace = fullData[i];
46-
var colorbar = trace._module.colorbar;
51+
var cbOpts = trace._module.colorbar;
4752

48-
if(colorbar) {
49-
if(Array.isArray(colorbar)) {
50-
for(var j = 0; j < colorbar.length; j++) {
51-
relinkColorAtts(trace, colorbar[j]);
53+
if(cbOpts) {
54+
if(Array.isArray(cbOpts)) {
55+
for(var j = 0; j < cbOpts.length; j++) {
56+
relinkColorAtts(trace, cbOpts[j]);
5257
}
5358
} else {
54-
relinkColorAtts(trace, colorbar);
59+
relinkColorAtts(trace, cbOpts);
5560
}
5661
}
5762

58-
// TODO could generalize _module.colorscale and use it here?
59-
6063
if(hasColorscale(trace, 'marker.line')) {
6164
relinkColorAtts(trace, {
6265
container: 'marker.line',
6366
min: 'cmin',
6467
max: 'cmax'
6568
});
6669
}
70+
}
6771

72+
for(var k in fullLayout._colorAxes) {
73+
relinkColorAtts(fullLayout[k], {min: 'cmin', max: 'cmax'});
6874
}
6975
};

‎src/components/drawing/index.js

+2-6
Original file line numberDiff line numberDiff line change
@@ -638,13 +638,9 @@ drawing.tryColorscale = function(marker, prefix) {
638638
var cont = prefix ? Lib.nestedProperty(marker, prefix).get() : marker;
639639

640640
if(cont) {
641-
var scl = cont.colorscale;
642641
var colorArray = cont.color;
643-
644-
if(scl && Lib.isArrayOrTypedArray(colorArray)) {
645-
return Colorscale.makeColorScaleFunc(
646-
Colorscale.extractScale(cont, {cLetter: 'c'})
647-
);
642+
if((cont.colorscale || cont._colorAx) && Lib.isArrayOrTypedArray(colorArray)) {
643+
return Colorscale.makeColorScaleFuncFromTrace(cont);
648644
}
649645
}
650646
return Lib.identity;

‎src/plots/plots.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,6 @@ plots.supplyDefaults = function(gd, opts) {
447447
for(i = 0; i < crossTraceDefaultsFuncs.length; i++) {
448448
crossTraceDefaultsFuncs[i](newFullData, newFullLayout);
449449
}
450-
Registry.getComponentMethod('colorscale', 'crossTraceDefaults')(newFullData, newFullLayout);
451450

452451
// turn on flag to optimize large splom-only graphs
453452
// mostly by omitting SVG layers during Cartesian.drawFramework
@@ -487,6 +486,9 @@ plots.supplyDefaults = function(gd, opts) {
487486
// relink functions and _ attributes to promote consistency between plots
488487
relinkPrivateKeys(newFullLayout, oldFullLayout);
489488

489+
// colorscale crossTraceDefaults needs newFullLayout with relinked keys
490+
Registry.getComponentMethod('colorscale', 'crossTraceDefaults')(newFullData, newFullLayout);
491+
490492
// For persisting GUI-driven changes in layout
491493
// _preGUI and _tracePreGUI were already copied over in relinkPrivateKeys
492494
if(!newFullLayout._preGUI) newFullLayout._preGUI = {};

‎src/traces/heatmap/hover.js

+4-6
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9-
109
'use strict';
1110

1211
var Fx = require('../../components/fx');
1312
var Lib = require('../../lib');
1413
var Axes = require('../../plots/cartesian/axes');
14+
var extractOpts = require('../../components/colorscale').extractOpts;
1515

1616
module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLayer, contour) {
1717
var cd0 = pointData.cd[0];
@@ -24,7 +24,6 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
2424
var xc = cd0.xCenter;
2525
var yc = cd0.yCenter;
2626
var zmask = cd0.zmask;
27-
var range = [trace.zmin, trace.zmax];
2827
var zhoverformat = trace.zhoverformat;
2928
var x2 = x;
3029
var y2 = y;
@@ -95,17 +94,16 @@ module.exports = function hoverPoints(pointData, xval, yval, hovermode, hoverLay
9594
text = cd0.text[ny][nx];
9695
}
9796

98-
var zLabel;
9997
// dummy axis for formatting the z value
98+
var cOpts = extractOpts(trace);
10099
var dummyAx = {
101100
type: 'linear',
102-
range: range,
101+
range: [cOpts.min, cOpts.max],
103102
hoverformat: zhoverformat,
104103
_separators: xa._separators,
105104
_numFormat: xa._numFormat
106105
};
107-
var zLabelObj = Axes.tickText(dummyAx, zVal, 'hover');
108-
zLabel = zLabelObj.text;
106+
var zLabel = Axes.tickText(dummyAx, zVal, 'hover').text;
109107

110108
return [Lib.extendFlat(pointData, {
111109
index: [ny, nx],

‎test/jasmine/tests/colorbar_test.js

+35-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ var supplyAllDefaults = require('../assets/supply_defaults');
1010
var assertPlotSize = require('../assets/custom_assertions').assertPlotSize;
1111
var drag = require('../assets/drag');
1212

13-
1413
describe('Test colorbar:', function() {
1514
'use strict';
1615

@@ -226,6 +225,41 @@ describe('Test colorbar:', function() {
226225
.then(done);
227226
});
228227

228+
it('can show and hide colorbars of shared color axes', function(done) {
229+
Plotly.newPlot(gd, [{
230+
y: [1, 2, 3],
231+
marker: {color: [1, 2, 3], coloraxis: 'coloraxis'}
232+
}, {
233+
y: [1, 2, 3],
234+
marker: {color: [1, 0, 3], coloraxis: 'coloraxis'}
235+
}], {
236+
showlegend: false,
237+
height: 500,
238+
width: 500,
239+
margin: {l: 50, r: 50, t: 50, b: 50}
240+
})
241+
.then(function() {
242+
assertCB('initial', true, {expandedMarginR: true});
243+
244+
return Plotly.relayout(gd, {'coloraxis.showscale': false});
245+
})
246+
.then(function() {
247+
assertCB('hidden', false, {expandedMarginR: false});
248+
249+
return Plotly.relayout(gd, {'coloraxis.showscale': true, 'coloraxis.colorbar.x': 0.7});
250+
})
251+
.then(function() {
252+
assertCB('mid-plot', true, {expandedMarginR: false});
253+
254+
return Plotly.relayout(gd, {'coloraxis.colorbar.x': 1.1});
255+
})
256+
.then(function() {
257+
assertCB('far right', true, {expandedMarginR: true});
258+
})
259+
.catch(failTest)
260+
.then(done);
261+
});
262+
229263
// histogram colorbars could not be edited before
230264
it('can show and hide histogram colorbars', function(done) {
231265
Plotly.newPlot(gd, [{

‎test/jasmine/tests/colorscale_test.js

+124
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,27 @@ describe('Test colorscale:', function() {
632632
expect(trace.autocolorscale).toBe(true);
633633
expect(trace.colorscale).toEqual(colorscale);
634634
});
635+
636+
it('should compute min/max across trace linked to same color axis', function() {
637+
gd = _supply([
638+
{type: 'heatmap', z: [[1, 3, 4], [2, 3, 1]], coloraxis: 'coloraxis'},
639+
{y: [1, 3, 1], marker: {color: [3, 4, -2], coloraxis: 'coloraxis'}},
640+
]);
641+
642+
Plots.doCalcdata(gd);
643+
644+
var fullData = gd._fullData;
645+
expect(fullData[0].zmin).toBe(undefined);
646+
expect(fullData[0].zmax).toBe(undefined);
647+
expect(fullData[1].marker.cmin).toBe(undefined);
648+
expect(fullData[1].marker.cmax).toBe(undefined);
649+
650+
var fullLayout = gd._fullLayout;
651+
expect(fullLayout.coloraxis.cmin).toBe(-2);
652+
expect(fullLayout.coloraxis._cmin).toBe(-2);
653+
expect(fullLayout.coloraxis.cmax).toBe(4);
654+
expect(fullLayout.coloraxis._cmax).toBe(4);
655+
});
635656
});
636657

637658
describe('extractScale + makeColorScaleFunc', function() {
@@ -686,6 +707,29 @@ describe('Test colorscale:', function() {
686707
expect(color3).toEqual(color4);
687708
expect(color4).toEqual('rgb(5, 10, 172)');
688709
});
710+
711+
it('should extract coloraxis options, if present', function() {
712+
var trace = {
713+
_colorAx: {
714+
colorscale: scale,
715+
cmin: 2,
716+
cmax: 3
717+
}
718+
};
719+
720+
var specs = Colorscale.extractScale(trace);
721+
var sclFunc = Colorscale.makeColorScaleFunc(specs);
722+
723+
var color1 = sclFunc(1);
724+
var color2 = sclFunc(2);
725+
var color3 = sclFunc(3);
726+
var color4 = sclFunc(4);
727+
728+
expect(color1).toEqual(color2);
729+
expect(color1).toEqual('rgb(5, 10, 172)');
730+
expect(color3).toEqual(color4);
731+
expect(color4).toEqual('rgb(178, 10, 28)');
732+
});
689733
});
690734
});
691735

@@ -977,6 +1021,86 @@ describe('Test colorscale restyle calls:', function() {
9771021
.then(done);
9781022
});
9791023

1024+
it('should be able to toggle between autocolorscale true/false and set colorscales (coloraxis case)', function(done) {
1025+
function _assert(msg, exp) {
1026+
var mcc = [];
1027+
d3.selectAll('path.point').each(function() { mcc.push(getFill(this)); });
1028+
expect(mcc).toEqual(exp.mcc);
1029+
1030+
expect(gd._fullLayout.coloraxis.colorscale).toEqual(exp.colorscale);
1031+
expect(gd._fullLayout.coloraxis.autocolorscale).toBe(exp.autocolorscale, msg);
1032+
expect((gd.layout.coloraxis || {}).colorscale).toEqual(exp.colorscaleIn);
1033+
expect((gd.layout.coloraxis || {}).autocolorscale).toBe(exp.autocolorscaleIn, msg);
1034+
}
1035+
1036+
// update via, assert then assert again (and again ;) after non-calc edits
1037+
function _run(msg, updateObj, exp) {
1038+
return Plotly.relayout(gd, updateObj)
1039+
.then(function() { _assert(msg, exp); })
1040+
.then(function() { return Plotly.relayout(gd, 'xaxis.range', [-1, 5]); })
1041+
.then(function() { _assert(msg + ' after axrange relayout', exp); })
1042+
.then(function() { return Plotly.relayout(gd, 'xaxis.autorange', true); })
1043+
.then(function() { _assert(msg + ' after autorange', exp); })
1044+
.then(function() { return Plotly.restyle(gd, 'marker.symbol', 'square'); })
1045+
.then(function() { _assert(msg + ' after marker.symbol restyle', exp); })
1046+
.then(function() { return Plotly.restyle(gd, 'marker.symbol', null); })
1047+
.then(function() { _assert(msg + ' back to original marker.symbol', exp); });
1048+
}
1049+
1050+
var rdbu = ['rgb(5, 10, 172)', 'rgb(53, 70, 208)', 'rgb(227, 153, 104)',
1051+
'rgb(53, 70, 208)', 'rgb(53, 70, 208)', 'rgb(178, 10, 28)'];
1052+
var grns = ['rgb(0, 68, 27)', 'rgb(12, 119, 52)', 'rgb(174, 222, 167)',
1053+
'rgb(12, 119, 52)', 'rgb(12, 119, 52)', 'rgb(247, 252, 245)'];
1054+
1055+
Plotly.plot(gd, [{
1056+
mode: 'markers',
1057+
y: [1, 2, 3],
1058+
marker: {color: [-1, 0, 3], coloraxis: 'coloraxis'}
1059+
}, {
1060+
mode: 'markers',
1061+
y: [2, 3, 4],
1062+
marker: {color: [0, 0, 5], coloraxis: 'coloraxis'}
1063+
}])
1064+
.then(function() {
1065+
_assert('base (autocolorscale:true by dflt)', {
1066+
mcc: rdbu,
1067+
autocolorscale: true,
1068+
autocolorscaleIn: undefined,
1069+
colorscale: Colorscale.scales.RdBu,
1070+
colorscaleIn: undefined
1071+
});
1072+
})
1073+
.then(function() {
1074+
return _run('set *Greens* colorscale', {'coloraxis.colorscale': 'Greens'}, {
1075+
mcc: grns,
1076+
autocolorscale: false,
1077+
autocolorscaleIn: false,
1078+
colorscale: Colorscale.scales.Greens,
1079+
colorscaleIn: 'Greens'
1080+
});
1081+
})
1082+
.then(function() {
1083+
return _run('back to autocolorscale:true', {'coloraxis.autocolorscale': true}, {
1084+
mcc: rdbu,
1085+
autocolorscale: true,
1086+
autocolorscaleIn: true,
1087+
colorscale: Colorscale.scales.RdBu,
1088+
colorscaleIn: 'Greens'
1089+
});
1090+
})
1091+
.then(function() {
1092+
return _run('back to autocolorscale:false w/ colorscale set', {'coloraxis.autocolorscale': false}, {
1093+
mcc: grns,
1094+
autocolorscale: false,
1095+
autocolorscaleIn: false,
1096+
colorscale: Colorscale.scales.Greens,
1097+
colorscaleIn: 'Greens'
1098+
});
1099+
})
1100+
.catch(failTest)
1101+
.then(done);
1102+
});
1103+
9801104
it('should work with templates', function(done) {
9811105
function _assert(msg, exp) {
9821106
var mcc = [];

0 commit comments

Comments
 (0)
Please sign in to comment.