Skip to content

Commit c5f6eb2

Browse files
committed
introduce 3D annotations
- add attrs and defaults to scene containers - N.B. x/y/z are always in data coords ax/ay are always in px coords - mock xaxis and yaxis on scene updates so that their l2p method updates the projection data coord in the x-y plane - and annotations handler in render loop
1 parent 8e6ab7d commit c5f6eb2

File tree

3 files changed

+256
-2
lines changed

3 files changed

+256
-2
lines changed

src/plots/gl3d/layout/defaults.js

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111

1212
var Lib = require('../../../lib');
1313
var Color = require('../../../components/color');
14+
var Axes = require('../../cartesian/axes');
1415

1516
var handleSubplotDefaults = require('../../subplot_defaults');
16-
var layoutAttributes = require('./layout_attributes');
17+
var handleArrayContainerDefaults = require('../../array_container_defaults');
1718
var supplyGl3dAxisLayoutDefaults = require('./axis_defaults');
19+
var layoutAttributes = require('./layout_attributes');
1820

1921

2022
module.exports = function supplyLayoutDefaults(layoutIn, layoutOut, fullData) {
@@ -97,6 +99,92 @@ function handleGl3dDefaults(sceneLayoutIn, sceneLayoutOut, coerce, opts) {
9799
calendar: opts.calendar
98100
});
99101

102+
handleArrayContainerDefaults(sceneLayoutIn, sceneLayoutOut, {
103+
name: 'annotations',
104+
handleItemDefaults: handleAnnotationDefaults,
105+
font: opts.font,
106+
scene: opts.id
107+
});
108+
100109
coerce('dragmode', opts.getDfltFromLayout('dragmode'));
101110
coerce('hovermode', opts.getDfltFromLayout('hovermode'));
102111
}
112+
113+
function handleAnnotationDefaults(annIn, annOut, sceneLayout, opts, itemOpts) {
114+
115+
function coerce(attr, dflt) {
116+
return Lib.coerce(annIn, annOut, layoutAttributes.annotations, attr, dflt);
117+
}
118+
119+
function coercePosition(axLetter) {
120+
var axName = axLetter + 'axis';
121+
var gdMock = { _fullLayout: {} };
122+
123+
// mock in such way that getFromId grabs correct 3D axis
124+
gdMock._fullLayout[axName] = sceneLayout[axName];
125+
126+
// hard-set here for completeness
127+
annOut[axLetter + 'ref'] = axLetter;
128+
129+
return Axes.coercePosition(annOut, gdMock, coerce, axLetter, axLetter, 0.5);
130+
}
131+
132+
var visible = coerce('visible', !itemOpts.itemIsNotPlainObject);
133+
if(!visible) return annOut;
134+
135+
coerce('opacity');
136+
coerce('align');
137+
coerce('bgcolor');
138+
139+
var borderColor = coerce('bordercolor'),
140+
borderOpacity = Color.opacity(borderColor);
141+
142+
coerce('borderpad');
143+
144+
var borderWidth = coerce('borderwidth');
145+
var showArrow = coerce('showarrow');
146+
147+
coerce('text', showArrow ? ' ' : 'new text');
148+
coerce('textangle');
149+
Lib.coerceFont(coerce, 'font', opts.font);
150+
151+
coerce('width');
152+
coerce('align');
153+
154+
var h = coerce('height');
155+
if(h) coerce('valign');
156+
157+
coercePosition('x');
158+
coercePosition('y');
159+
coercePosition('z');
160+
161+
// if you have one coordinate you should all three
162+
Lib.noneOrAll(annIn, annOut, ['x', 'y', 'z']);
163+
164+
coerce('xanchor');
165+
coerce('yanchor');
166+
coerce('xshift');
167+
coerce('yshift');
168+
169+
if(showArrow) {
170+
annOut.axref = 'pixel';
171+
annOut.ayref = 'pixel';
172+
173+
// TODO maybe default values should be bigger than the 2D case?
174+
coerce('ax', -10);
175+
coerce('ay', -30);
176+
177+
// if you have one part of arrow length you should have both
178+
Lib.noneOrAll(annIn, annOut, ['ax', 'ay']);
179+
180+
coerce('arrowcolor', borderOpacity ? annOut.bordercolor : Color.defaultLine);
181+
coerce('arrowhead');
182+
coerce('arrowsize');
183+
coerce('arrowwidth', ((borderOpacity && borderWidth) || 1) * 2);
184+
coerce('standoff');
185+
}
186+
187+
annOut._scene = opts.scene;
188+
189+
return annOut;
190+
}

src/plots/gl3d/layout/layout_attributes.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
'use strict';
1111

1212
var gl3dAxisAttrs = require('./axis_attributes');
13+
var annAtts = require('../../../components/annotations/attributes');
1314
var extendFlat = require('../../../lib/extend').extendFlat;
1415

1516
function makeVector(x, y, z) {
@@ -33,6 +34,8 @@ function makeVector(x, y, z) {
3334
}
3435

3536
module.exports = {
37+
_arrayAttrRegexps: [/^scene([2-9]|[1-9][0-9]+)?\.annotations/],
38+
3639
bgcolor: {
3740
valType: 'color',
3841
role: 'style',
@@ -139,6 +142,83 @@ module.exports = {
139142
yaxis: gl3dAxisAttrs,
140143
zaxis: gl3dAxisAttrs,
141144

145+
annotations: {
146+
_isLinkedToArray: 'annotation',
147+
148+
visible: annAtts.visible,
149+
x: {
150+
valType: 'any',
151+
role: 'info',
152+
description: [
153+
'Sets the annotation\'s x position.'
154+
].join(' ')
155+
},
156+
y: {
157+
valType: 'any',
158+
role: 'info',
159+
description: [
160+
'Sets the annotation\'s y position.'
161+
].join(' ')
162+
},
163+
z: {
164+
valType: 'any',
165+
role: 'info',
166+
description: [
167+
'Sets the annotation\'s z position.'
168+
].join(' ')
169+
},
170+
ax: {
171+
valType: 'any',
172+
role: 'info',
173+
description: [
174+
'Sets the x component of the arrow tail about the arrow head.'
175+
].join(' ')
176+
},
177+
ay: {
178+
valType: 'any',
179+
role: 'info',
180+
description: [
181+
'Sets the y component of the arrow tail about the arrow head.'
182+
].join(' ')
183+
},
184+
185+
xanchor: annAtts.xanchor,
186+
xshift: annAtts.xshift,
187+
yanchor: annAtts.yanchor,
188+
yshift: annAtts.yshift,
189+
190+
text: annAtts.text,
191+
textangle: annAtts.textangle,
192+
font: annAtts.font,
193+
width: annAtts.width,
194+
height: annAtts.height,
195+
opacity: annAtts.opacity,
196+
align: annAtts.align,
197+
valign: annAtts.valign,
198+
bgcolor: annAtts.bgcolor,
199+
bordercolor: annAtts.bordercolor,
200+
borderpad: annAtts.borderpad,
201+
borderwidth: annAtts.borderwidth,
202+
showarrow: annAtts.showarrow,
203+
arrowcolor: annAtts.arrowcolor,
204+
arrowhead: annAtts.arrowhead,
205+
arrowsize: annAtts.arrowsize,
206+
arrowwidth: annAtts.arrowwidth,
207+
standoff: annAtts.standoff,
208+
209+
// maybes later
210+
// clicktoshow: annAtts.clicktoshow,
211+
// xclick: annAtts.xclick,
212+
// yclick: annAtts.yclick,
213+
214+
// not needed!
215+
// axref: 'pixel'
216+
// ayref: 'pixel'
217+
// xref: 'x'
218+
// yref: 'y
219+
// zref: 'z'
220+
},
221+
142222
dragmode: {
143223
valType: 'enumerated',
144224
role: 'info',

src/plots/gl3d/scene.js

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
var createPlot = require('gl-plot3d');
1313
var getContext = require('webgl-context');
1414

15+
var Registry = require('../../registry');
1516
var Lib = require('../../lib');
1617

1718
var Axes = require('../../plots/cartesian/axes');
@@ -122,6 +123,8 @@ function render(scene) {
122123
Fx.loneUnhover(svgContainer);
123124
scene.graphDiv.emit('plotly_unhover', oldEventData);
124125
}
126+
127+
scene.handleAnnotations();
125128
}
126129

127130
function initializeGLPlot(scene, fullLayout, canvas, gl) {
@@ -711,11 +714,94 @@ proto.toImage = function(format) {
711714
};
712715

713716
proto.setConvert = function() {
714-
for(var i = 0; i < 3; ++i) {
717+
var i;
718+
719+
for(i = 0; i < 3; i++) {
715720
var ax = this.fullSceneLayout[axisProperties[i]];
716721
Axes.setConvert(ax, this.fullLayout);
717722
ax.setScale = Lib.noop;
718723
}
724+
725+
var anns = this.fullSceneLayout.annotations;
726+
for(i = 0; i < anns.length; i++) {
727+
mockAnnAxes(anns[i], this);
728+
}
729+
730+
this.fullLayout._infolayer
731+
.selectAll('.annotation-' + this.id)
732+
.remove();
719733
};
720734

735+
proto.handleAnnotations = function() {
736+
var drawAnnotation = Registry.getComponentMethod('annotations', 'drawRaw');
737+
var fullSceneLayout = this.fullSceneLayout;
738+
var dataScale = this.dataScale;
739+
var anns = fullSceneLayout.annotations;
740+
var axLetters = ['x', 'y', 'z'];
741+
742+
for(var i = 0; i < anns.length; i++) {
743+
var ann = anns[i];
744+
var annotationIsOffscreen = false;
745+
746+
for(var j = 0; j < 3; j++) {
747+
var axLetter = axLetters[j];
748+
var posFraction = fullSceneLayout[axLetter + 'axis'].r2fraction(ann[axLetter]);
749+
750+
if(posFraction < 0 || posFraction > 1) {
751+
annotationIsOffscreen = true;
752+
break;
753+
}
754+
}
755+
756+
if(annotationIsOffscreen) {
757+
this.fullLayout._infolayer
758+
.select('.annotation-' + this.id + '[data-index="' + i + '"]')
759+
.remove();
760+
} else {
761+
ann.pdata = project(this.glplot.cameraParams, [
762+
fullSceneLayout.xaxis.d2l(ann.x) * dataScale[0],
763+
fullSceneLayout.yaxis.d2l(ann.y) * dataScale[1],
764+
fullSceneLayout.zaxis.d2l(ann.z) * dataScale[2]
765+
]);
766+
767+
drawAnnotation(this.graphDiv, ann, i, ann._xa, ann._ya);
768+
}
769+
}
770+
};
771+
772+
function mockAnnAxes(ann, scene) {
773+
var fullSceneLayout = scene.fullSceneLayout;
774+
var domain = fullSceneLayout.domain;
775+
var size = scene.fullLayout._size;
776+
777+
var base = {
778+
// this gets fill in on render
779+
pdata: null,
780+
781+
// to get setConvert to not execute cleanly
782+
type: 'linear',
783+
784+
// set infinite range so that annotation draw routine
785+
// does not try to remove 'outside-range' annotations,
786+
// this case is handled in the render loop
787+
range: [-Infinity, Infinity]
788+
};
789+
790+
ann._xa = {};
791+
Lib.extendFlat(ann._xa, base);
792+
Axes.setConvert(ann._xa);
793+
ann._xa._offset = size.l + domain.x[0] * size.w;
794+
ann._xa.l2p = function() {
795+
return 0.5 * (1 + ann.pdata[0] / ann.pdata[3]) * size.w * (domain.x[1] - domain.x[0]);
796+
};
797+
798+
ann._ya = {};
799+
Lib.extendFlat(ann._ya, base);
800+
Axes.setConvert(ann._ya);
801+
ann._ya._offset = size.t + (1 - domain.y[1]) * size.h;
802+
ann._ya.l2p = function() {
803+
return 0.5 * (1 - ann.pdata[1] / ann.pdata[3]) * size.h * (domain.y[1] - domain.y[0]);
804+
};
805+
}
806+
721807
module.exports = Scene;

0 commit comments

Comments
 (0)