-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Introducing 3D annotations #1638
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 20 commits
aa58118
8b8bdea
1a8f1ed
f4b14e6
4dc16e4
8e6ab7d
c5f6eb2
b7ab656
fbdcee8
981b1a1
94b940d
8ac2551
ababc49
7cfba36
8f9e370
1bbabad
9520943
f61cd4d
8cb10c1
a31549d
bd9c867
21c8cc6
adfc305
c084da0
ef985cf
0b4214f
8b8a088
1d5606a
46ab63d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,7 +36,8 @@ var drawArrowHead = require('./draw_arrow_head'); | |
|
||
module.exports = { | ||
draw: draw, | ||
drawOne: drawOne | ||
drawOne: drawOne, | ||
drawRaw: drawRaw | ||
}; | ||
|
||
/* | ||
|
@@ -62,47 +63,65 @@ function draw(gd) { | |
* index (int): the annotation to draw | ||
*/ | ||
function drawOne(gd, index) { | ||
var layout = gd.layout, | ||
fullLayout = gd._fullLayout, | ||
gs = gd._fullLayout._size; | ||
var fullLayout = gd._fullLayout; | ||
var options = fullLayout.annotations[index] || {}; | ||
var xa = Axes.getFromId(gd, options.xref); | ||
var ya = Axes.getFromId(gd, options.yref); | ||
|
||
// remove the existing annotation if there is one | ||
fullLayout._infolayer.selectAll('.annotation[data-index="' + index + '"]').remove(); | ||
drawRaw(gd, options, index, xa, ya); | ||
} | ||
|
||
// remember a few things about what was already there, | ||
var optionsIn = (layout.annotations || [])[index], | ||
options = fullLayout.annotations[index]; | ||
/* | ||
* drawRaw: draw a single annotation, potentially with modifications | ||
* | ||
* options (object): this annotation's options | ||
* index (int): the annotation to draw | ||
* xa (object | undefined): full x-axis object to compute subplot pos-to-px | ||
* ya (object | undefined): ... y-axis | ||
*/ | ||
function drawRaw(gd, options, index, xa, ya) { | ||
var fullLayout = gd._fullLayout; | ||
var gs = gd._fullLayout._size; | ||
|
||
var className = options._scene ? | ||
'annotation-' + options._scene : | ||
'annotation'; | ||
|
||
var annbase = options._scene ? | ||
options._scene + '.annotations[' + index + ']' : | ||
'annotations[' + index + ']'; | ||
|
||
// remove the existing annotation if there is one | ||
fullLayout._infolayer | ||
.selectAll('.' + className + '[data-index="' + index + '"]') | ||
.remove(); | ||
|
||
var annClipID = 'clip' + fullLayout._uid + '_ann' + index; | ||
|
||
// this annotation is gone - quit now after deleting it | ||
// TODO: use d3 idioms instead of deleting and redrawing every time | ||
if(!optionsIn || options.visible === false) { | ||
if(!options._input || options.visible === false) { | ||
d3.selectAll('#' + annClipID).remove(); | ||
return; | ||
} | ||
|
||
var xa = Axes.getFromId(gd, options.xref), | ||
ya = Axes.getFromId(gd, options.yref), | ||
|
||
// calculated pixel positions | ||
// x & y each will get text, head, and tail as appropriate | ||
annPosPx = {x: {}, y: {}}, | ||
// calculated pixel positions | ||
// x & y each will get text, head, and tail as appropriate | ||
var annPosPx = {x: {}, y: {}}, | ||
textangle = +options.textangle || 0; | ||
|
||
// create the components | ||
// made a single group to contain all, so opacity can work right | ||
// with border/arrow together this could handle a whole bunch of | ||
// cleanup at this point, but works for now | ||
var annGroup = fullLayout._infolayer.append('g') | ||
.classed('annotation', true) | ||
.classed(className, true) | ||
.attr('data-index', String(index)) | ||
.style('opacity', options.opacity); | ||
|
||
// another group for text+background so that they can rotate together | ||
var annTextGroup = annGroup.append('g') | ||
.classed('annotation-text-g', true) | ||
.attr('data-index', String(index)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 another relic 🔪 |
||
.classed('annotation-text-g', true); | ||
|
||
var annTextGroupInner = annTextGroup.append('g') | ||
.style('pointer-events', options.captureevents ? 'all' : null) | ||
|
@@ -111,7 +130,7 @@ function drawOne(gd, index) { | |
gd._dragging = false; | ||
gd.emit('plotly_clickannotation', { | ||
index: index, | ||
annotation: optionsIn, | ||
annotation: options._input, | ||
fullAnnotation: options, | ||
event: d3.event | ||
}); | ||
|
@@ -170,7 +189,7 @@ function drawOne(gd, index) { | |
var font = options.font; | ||
|
||
var annText = annTextGroupInner.append('text') | ||
.classed('annotation', true) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you've hit on a flaw in how github displays comments... I'm reading:
but I'm looking at a31549d which came after ababc49 and rereverts the change originally made in 1a8f1ed 💫 Anyway, totally approve of this change. My best guess is these got the same There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In brief, as of a31549d, the annotations |
||
.classed('annotation-text', true) | ||
.attr('data-unformatted', options.text) | ||
.text(options.text); | ||
|
||
|
@@ -189,11 +208,11 @@ function drawOne(gd, index) { | |
|
||
function drawGraphicalElements() { | ||
// if the text has *only* a link, make the whole box into a link | ||
var anchor = annText.selectAll('a'); | ||
if(anchor.size() === 1 && anchor.text() === annText.text()) { | ||
var anchor3 = annText.selectAll('a'); | ||
if(anchor3.size() === 1 && anchor3.text() === annText.text()) { | ||
var wholeLink = annTextGroupInner.insert('a', ':first-child').attr({ | ||
'xlink:xlink:href': anchor.attr('xlink:href'), | ||
'xlink:xlink:show': anchor.attr('xlink:show') | ||
'xlink:xlink:href': anchor3.attr('xlink:href'), | ||
'xlink:xlink:show': anchor3.attr('xlink:show') | ||
}) | ||
.style({cursor: 'pointer'}); | ||
|
||
|
@@ -238,10 +257,13 @@ function drawOne(gd, index) { | |
} | ||
|
||
var annotationIsOffscreen = false; | ||
['x', 'y'].forEach(function(axLetter) { | ||
var axRef = options[axLetter + 'ref'] || axLetter, | ||
var letters = ['x', 'y']; | ||
|
||
for(var i = 0; i < letters.length; i++) { | ||
var axLetter = letters[i], | ||
axRef = options[axLetter + 'ref'] || axLetter, | ||
tailRef = options['a' + axLetter + 'ref'], | ||
ax = Axes.getFromId(gd, axRef), | ||
ax = {x: xa, y: ya}[axLetter], | ||
dimAngle = (textangle + (axLetter === 'x' ? 0 : -90)) * Math.PI / 180, | ||
// note that these two can be either positive or negative | ||
annSizeFromWidth = outerWidth * Math.cos(dimAngle), | ||
|
@@ -363,7 +385,7 @@ function drawOne(gd, index) { | |
// size/shift are used during dragging | ||
options['_' + axLetter + 'size'] = annSize; | ||
options['_' + axLetter + 'shift'] = textShift; | ||
}); | ||
} | ||
|
||
if(annotationIsOffscreen) { | ||
annTextGroupInner.remove(); | ||
|
@@ -417,17 +439,15 @@ function drawOne(gd, index) { | |
annTextGroup.attr({transform: 'rotate(' + textangle + ',' + | ||
annPosPx.x.text + ',' + annPosPx.y.text + ')'}); | ||
|
||
var annbase = 'annotations[' + index + ']'; | ||
|
||
/* | ||
* add the arrow | ||
* uses options[arrowwidth,arrowcolor,arrowhead] for styling | ||
* dx and dy are normally zero, but when you are dragging the textbox | ||
* while the head stays put, dx and dy are the pixel offsets | ||
*/ | ||
var drawArrow = function(dx, dy) { | ||
d3.select(gd) | ||
.selectAll('.annotation-arrow-g[data-index="' + index + '"]') | ||
annGroup | ||
.selectAll('.annotation-arrow-g') | ||
.remove(); | ||
|
||
var headX = annPosPx.x.head, | ||
|
@@ -484,8 +504,7 @@ function drawOne(gd, index) { | |
|
||
var arrowGroup = annGroup.append('g') | ||
.style({opacity: Color.opacity(arrowColor)}) | ||
.classed('annotation-arrow-g', true) | ||
.attr('data-index', String(index)); | ||
.classed('annotation-arrow-g', true); | ||
|
||
var arrow = arrowGroup.append('path') | ||
.attr('d', 'M' + tailX + ',' + tailY + 'L' + headX + ',' + headY) | ||
|
@@ -496,7 +515,7 @@ function drawOne(gd, index) { | |
|
||
// the arrow dragger is a small square right at the head, then a line to the tail, | ||
// all expanded by a stroke width of 6px plus the arrow line width | ||
if(gd._context.editable && arrow.node().parentNode) { | ||
if(gd._context.editable && arrow.node().parentNode && !options._scene) { | ||
var arrowDragHeadX = headX; | ||
var arrowDragHeadY = headY; | ||
if(options.standoff) { | ||
|
@@ -505,10 +524,9 @@ function drawOne(gd, index) { | |
arrowDragHeadY += options.standoff * (tailY - headY) / arrowLength; | ||
} | ||
var arrowDrag = arrowGroup.append('path') | ||
.classed('annotation', true) | ||
.classed('annotation-arrow', true) | ||
.classed('anndrag', true) | ||
.attr({ | ||
'data-index': String(index), | ||
d: 'M3,3H-3V-3H3ZM0,0L' + (tailX - arrowDragHeadX) + ',' + (tailY - arrowDragHeadY), | ||
transform: 'translate(' + arrowDragHeadX + ',' + arrowDragHeadY + ')' | ||
}) | ||
|
@@ -607,7 +625,7 @@ function drawOne(gd, index) { | |
|
||
drawArrow(dx, dy); | ||
} | ||
else { | ||
else if(!options._scene) { | ||
if(xa) update[annbase + '.x'] = options.x + dx / xa._m; | ||
else { | ||
var widthFraction = options._xsize / gs.w, | ||
|
@@ -635,6 +653,7 @@ function drawOne(gd, index) { | |
); | ||
} | ||
} | ||
else return; | ||
|
||
annTextGroup.attr({ | ||
transform: 'translate(' + dx + ',' + dy + ')' + baseTextTransform | ||
|
@@ -661,14 +680,17 @@ function drawOne(gd, index) { | |
options.text = _text; | ||
this.attr({'data-unformatted': options.text}); | ||
this.call(textLayout); | ||
|
||
var update = {}; | ||
update['annotations[' + index + '].text'] = options.text; | ||
update[annbase + '.text'] = options.text; | ||
|
||
if(xa && xa.autorange) { | ||
update[xa._name + '.autorange'] = true; | ||
} | ||
if(ya && ya.autorange) { | ||
update[ya._name + '.autorange'] = true; | ||
} | ||
|
||
Plotly.relayout(gd, update); | ||
}); | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
elsewhere
_scene
is an object, but this one is an id - can we call it_sceneId
instead?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call. Done in c084da0