Skip to content

Commit 1c893e2

Browse files
committed
Introduces attribute "source" to image traces featuring fast rendering
1 parent 29b456e commit 1c893e2

17 files changed

+335
-50
lines changed

src/traces/image/attributes.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ for(var i = 0; i < cm.length; i++) {
2222
}
2323

2424
module.exports = extendFlat({
25+
source: {
26+
valType: 'string',
27+
role: 'info',
28+
editType: 'calc',
29+
description: [
30+
'Specifies the data URI of the image to be visualized.',
31+
'The URI consists of "data:[<media type>][;base64],<data>"'
32+
].join(' ')
33+
},
2534
z: {
2635
valType: 'data_array',
2736
role: 'info',

src/traces/image/calc.js

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,27 @@ var constants = require('./constants');
1313
var isNumeric = require('fast-isnumeric');
1414
var Axes = require('../../plots/cartesian/axes');
1515
var maxRowLength = require('../../lib').maxRowLength;
16+
var sizeOf = require('image-size');
17+
var dataUri = require('../../snapshot/helpers').IMAGE_URL_PREFIX;
18+
var Buffer = require('buffer/').Buffer; // note: the trailing slash is important!
1619

1720
module.exports = function calc(gd, trace) {
21+
var h;
22+
var w;
23+
if(trace._isFromZ) {
24+
h = trace.z.length;
25+
w = maxRowLength(trace.z);
26+
} else if(trace._isFromSource) {
27+
var size = getImageSize(trace.source);
28+
h = size.height;
29+
w = size.width;
30+
}
31+
1832
var xa = Axes.getFromId(gd, trace.xaxis || 'x');
1933
var ya = Axes.getFromId(gd, trace.yaxis || 'y');
2034

2135
var x0 = xa.d2c(trace.x0) - trace.dx / 2;
2236
var y0 = ya.d2c(trace.y0) - trace.dy / 2;
23-
var h = trace.z.length;
24-
var w = maxRowLength(trace.z);
2537

2638
// Set axis range
2739
var i;
@@ -84,3 +96,10 @@ function makeScaler(trace) {
8496
return c;
8597
};
8698
}
99+
100+
// Get image size
101+
function getImageSize(src) {
102+
var data = src.replace(dataUri, '');
103+
var buff = new Buffer(data, 'base64');
104+
return sizeOf(buff);
105+
}

src/traces/image/defaults.js

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,12 @@ module.exports = function supplyDefaults(traceIn, traceOut) {
1616
function coerce(attr, dflt) {
1717
return Lib.coerce(traceIn, traceOut, attributes, attr, dflt);
1818
}
19+
var source = coerce('source');
20+
traceOut._isFromSource = !!source;
21+
1922
var z = coerce('z');
20-
if(z === undefined || !z.length || !z[0] || !z[0].length) {
23+
traceOut._isFromZ = !(z === undefined || !z.length || !z[0] || !z[0].length);
24+
if(!traceOut._isFromZ && !traceOut._isFromSource) {
2125
traceOut.visible = false;
2226
return;
2327
}
@@ -26,10 +30,16 @@ module.exports = function supplyDefaults(traceIn, traceOut) {
2630
coerce('y0');
2731
coerce('dx');
2832
coerce('dy');
29-
var colormodel = coerce('colormodel');
3033

31-
coerce('zmin', constants.colormodel[colormodel].min);
32-
coerce('zmax', constants.colormodel[colormodel].max);
34+
if(traceOut._isFromZ) {
35+
coerce('colormodel');
36+
coerce('zmin', constants.colormodel[traceOut.colormodel].min);
37+
coerce('zmax', constants.colormodel[traceOut.colormodel].max);
38+
} else if(traceOut._isFromSource) {
39+
traceOut.colormodel = 'rgba';
40+
traceOut.zmin = constants.colormodel[traceOut.colormodel].min;
41+
traceOut.zmax = constants.colormodel[traceOut.colormodel].max;
42+
}
3343

3444
coerce('text');
3545
coerce('hovertext');

src/traces/image/hover.js

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,15 @@ module.exports = function hoverPoints(pointData, xval, yval) {
2828
var nx = Math.floor((xval - cd0.x0) / trace.dx);
2929
var ny = Math.floor(Math.abs(yval - cd0.y0) / trace.dy);
3030

31+
var pixel;
32+
if(trace._isFromZ) {
33+
pixel = cd0.z[ny][nx];
34+
} else if(trace._isFromSource) {
35+
pixel = trace._canvas.getContext('2d').getImageData(nx, ny, 1, 1).data;
36+
}
37+
3138
// return early if pixel is undefined
32-
if(!cd0.z[ny][nx]) return;
39+
if(!pixel) return;
3340

3441
var hoverinfo = cd0.hi || trace.hoverinfo;
3542
var fmtColor;
@@ -41,7 +48,7 @@ module.exports = function hoverPoints(pointData, xval, yval) {
4148

4249
var colormodel = trace.colormodel;
4350
var dims = colormodel.length;
44-
var c = trace._scaler(cd0.z[ny][nx]);
51+
var c = trace._scaler(pixel);
4552
var s = constants.colormodel[colormodel].suffix;
4653

4754
var colorstring = [];
@@ -64,7 +71,7 @@ module.exports = function hoverPoints(pointData, xval, yval) {
6471
var py = ya.c2p(cd0.y0 + (ny + 0.5) * trace.dy);
6572
var xVal = cd0.x0 + (nx + 0.5) * trace.dx;
6673
var yVal = cd0.y0 + (ny + 0.5) * trace.dy;
67-
var zLabel = '[' + cd0.z[ny][nx].slice(0, trace.colormodel.length).join(', ') + ']';
74+
var zLabel = '[' + pixel.slice(0, trace.colormodel.length).join(', ') + ']';
6875
return [Lib.extendFlat(pointData, {
6976
index: [ny, nx],
7077
x0: xa.c2p(cd0.x0 + nx * trace.dx),

src/traces/image/plot.js

Lines changed: 119 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,24 @@ var Lib = require('../../lib');
1313
var xmlnsNamespaces = require('../../constants/xmlns_namespaces');
1414
var constants = require('./constants');
1515

16+
function compatibleAxis(ax) {
17+
return ax.type === 'linear' &&
18+
// y axis must be reversed but x axis mustn't be
19+
((ax.range[1] > ax.range[0]) === (ax._id.charAt(0) === 'x'));
20+
}
21+
1622
module.exports = function plot(gd, plotinfo, cdimage, imageLayer) {
1723
var xa = plotinfo.xaxis;
1824
var ya = plotinfo.yaxis;
1925

26+
var supportsPixelatedImage = !Lib.isSafari() && !gd._context._exportedPlot;
27+
2028
Lib.makeTraceGroups(imageLayer, cdimage, 'im').each(function(cd) {
2129
var plotGroup = d3.select(this);
2230
var cd0 = cd[0];
2331
var trace = cd0.trace;
32+
var fastImage = supportsPixelatedImage && trace._isFromSource && compatibleAxis(xa) && compatibleAxis(ya);
33+
trace._fastImage = fastImage;
2434

2535
var z = cd0.z;
2636
var x0 = cd0.x0;
@@ -66,11 +76,14 @@ module.exports = function plot(gd, plotinfo, cdimage, imageLayer) {
6676
}
6777

6878
// Reduce image size when zoomed in to save memory
69-
var extra = 0.5; // half the axis size
70-
left = Math.max(-extra * xa._length, left);
71-
right = Math.min((1 + extra) * xa._length, right);
72-
top = Math.max(-extra * ya._length, top);
73-
bottom = Math.min((1 + extra) * ya._length, bottom);
79+
if(!fastImage) {
80+
var extra = 0.5; // half the axis size
81+
left = Math.max(-extra * xa._length, left);
82+
right = Math.min((1 + extra) * xa._length, right);
83+
top = Math.max(-extra * ya._length, top);
84+
bottom = Math.min((1 + extra) * ya._length, bottom);
85+
}
86+
7487
var imageWidth = Math.round(right - left);
7588
var imageHeight = Math.round(bottom - top);
7689

@@ -82,48 +95,118 @@ module.exports = function plot(gd, plotinfo, cdimage, imageLayer) {
8295
return;
8396
}
8497

85-
// Draw each pixel
86-
var canvas = document.createElement('canvas');
87-
canvas.width = imageWidth;
88-
canvas.height = imageHeight;
89-
var context = canvas.getContext('2d');
90-
91-
var ipx = function(i) {return Lib.constrain(Math.round(xa.c2p(x0 + i * dx) - left), 0, imageWidth);};
92-
var jpx = function(j) {return Lib.constrain(Math.round(ya.c2p(y0 + j * dy) - top), 0, imageHeight);};
93-
94-
var fmt = constants.colormodel[trace.colormodel].fmt;
95-
var c;
96-
for(i = 0; i < cd0.w; i++) {
97-
var ipx0 = ipx(i); var ipx1 = ipx(i + 1);
98-
if(ipx1 === ipx0 || isNaN(ipx1) || isNaN(ipx0)) continue;
99-
for(var j = 0; j < cd0.h; j++) {
100-
var jpx0 = jpx(j); var jpx1 = jpx(j + 1);
101-
if(jpx1 === jpx0 || isNaN(jpx1) || isNaN(jpx0) || !z[j][i]) continue;
102-
c = trace._scaler(z[j][i]);
103-
if(c) {
104-
context.fillStyle = trace.colormodel + '(' + fmt(c).join(',') + ')';
105-
} else {
106-
// Return a transparent pixel
107-
context.fillStyle = 'rgba(0,0,0,0)';
98+
// Create a new canvas and draw magnified pixels on it
99+
function drawMagnifiedPixelsOnCanvas(readPixel) {
100+
var colormodel = trace.colormodel;
101+
var canvas = document.createElement('canvas');
102+
canvas.width = imageWidth;
103+
canvas.height = imageHeight;
104+
var context = canvas.getContext('2d');
105+
106+
var ipx = function(i) {return Lib.constrain(Math.round(xa.c2p(x0 + i * dx) - left), 0, imageWidth);};
107+
var jpx = function(j) {return Lib.constrain(Math.round(ya.c2p(y0 + j * dy) - top), 0, imageHeight);};
108+
109+
var fmt = constants.colormodel[colormodel].fmt;
110+
var c;
111+
for(i = 0; i < cd0.w; i++) {
112+
var ipx0 = ipx(i); var ipx1 = ipx(i + 1);
113+
if(ipx1 === ipx0 || isNaN(ipx1) || isNaN(ipx0)) continue;
114+
for(var j = 0; j < cd0.h; j++) {
115+
var jpx0 = jpx(j); var jpx1 = jpx(j + 1);
116+
if(jpx1 === jpx0 || isNaN(jpx1) || isNaN(jpx0) || !readPixel(i, j)) continue;
117+
c = trace._scaler(readPixel(i, j));
118+
if(c) {
119+
context.fillStyle = colormodel + '(' + fmt(c).join(',') + ')';
120+
} else {
121+
// Return a transparent pixel
122+
context.fillStyle = 'rgba(0,0,0,0)';
123+
}
124+
context.fillRect(ipx0, jpx0, ipx1 - ipx0, jpx1 - jpx0);
108125
}
109-
context.fillRect(ipx0, jpx0, ipx1 - ipx0, jpx1 - jpx0);
110126
}
127+
128+
return canvas;
129+
}
130+
131+
function sizeImage(image) {
132+
image.attr({
133+
height: imageHeight,
134+
width: imageWidth,
135+
x: left,
136+
y: top
137+
});
111138
}
112139

140+
var data = (trace._isFromSource && !fastImage) ? [cd, {hidden: true}] : [cd];
113141
var image3 = plotGroup.selectAll('image')
114-
.data(cd);
142+
.data(data);
115143

116144
image3.enter().append('svg:image').attr({
117145
xmlns: xmlnsNamespaces.svg,
118146
preserveAspectRatio: 'none'
119147
});
120148

121-
image3.attr({
122-
height: imageHeight,
123-
width: imageWidth,
124-
x: left,
125-
y: top,
126-
'xlink:href': canvas.toDataURL('image/png')
149+
if(fastImage) sizeImage(image3);
150+
image3.exit().remove();
151+
152+
// Pixelated image rendering
153+
// http://phrogz.net/tmp/canvas_image_zoom.html
154+
// https://developer.mozilla.org/en-US/docs/Web/CSS/image-rendering
155+
image3
156+
.attr('style', 'image-rendering: optimizeSpeed; image-rendering: -moz-crisp-edges; image-rendering: -o-crisp-edges; image-rendering: -webkit-optimize-contrast; image-rendering: optimize-contrast; image-rendering: crisp-edges; image-rendering: pixelated;');
157+
158+
new Promise(function(resolve) {
159+
if(trace._isFromZ) {
160+
resolve();
161+
} else if(trace._isFromSource) {
162+
// Transfer image to a canvas to access pixel information
163+
trace._canvas = trace._canvas || document.createElement('canvas');
164+
trace._canvas.width = w;
165+
trace._canvas.height = h;
166+
var context = trace._canvas.getContext('2d');
167+
168+
var sel;
169+
if(fastImage) {
170+
// Use the displayed image
171+
sel = image3;
172+
} else {
173+
// Use the hidden image
174+
sel = d3.select(image3[0][1]);
175+
}
176+
177+
var image = sel.node();
178+
image.onload = function() {
179+
context.drawImage(image, 0, 0);
180+
// we need to wait for the image to be loaded in order to redraw it from the canvas
181+
if(!fastImage) resolve();
182+
};
183+
sel.attr('xlink:href', trace.source);
184+
if(fastImage) resolve();
185+
}
186+
})
187+
.then(function() {
188+
if(!fastImage) {
189+
var canvas;
190+
if(trace._isFromZ) {
191+
canvas = drawMagnifiedPixelsOnCanvas(function(i, j) {return z[j][i];});
192+
} else if(trace._isFromSource) {
193+
var context = trace._canvas.getContext('2d');
194+
var data = context.getImageData(0, 0, w, h).data;
195+
canvas = drawMagnifiedPixelsOnCanvas(function(i, j) {
196+
var index = 4 * (j * w + i);
197+
return [
198+
data[index + 0],
199+
data[index + 1],
200+
data[index + 2],
201+
data[index + 3]
202+
];
203+
});
204+
}
205+
var href = canvas.toDataURL('image/png');
206+
var displayImage = d3.select(image3[0][0]);
207+
sizeImage(displayImage);
208+
displayImage.attr('xlink:href', href);
209+
}
127210
});
128211
});
129212
};

src/traces/image/style.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ var d3 = require('d3');
1313
module.exports = function style(gd) {
1414
d3.select(gd).selectAll('.im image')
1515
.style('opacity', function(d) {
16-
return d.trace.opacity;
16+
if(d && d.hidden) return 0;
17+
return d[0].trace.opacity;
1718
});
1819
};
-9.6 KB
Loading
-167 Bytes
Loading

test/image/baselines/image_cat.png

-5.37 KB
Loading
-19 Bytes
Loading
Loading
-192 Bytes
Loading
-11 Bytes
Loading
174 Bytes
Loading

test/image/compare_pixels_test.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ if(allMock || argv.filter) {
9393
allMockList = allMockList.filter(function(mockName) {
9494
var cond = !(
9595
mockName === 'font-wishlist' ||
96-
mockName.indexOf('mapbox_') !== -1
96+
mockName.indexOf('mapbox_') !== -1 ||
97+
mockName.match(/image_.*source/)
9798
);
9899
if(!cond) console.log(' -', mockName);
99100
return cond;

test/image/mocks/image_labuda_droplets_source.json

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)