@@ -8,6 +8,7 @@ var strTranslate = Lib.strTranslate;
88var _ = Lib . _ ;
99var Color = require ( '../../components/color' ) ;
1010var Drawing = require ( '../../components/drawing' ) ;
11+ var svgTextUtils = require ( '../../lib/svg_text_utils' ) ;
1112var setConvert = require ( '../cartesian/set_convert' ) ;
1213var extendFlat = require ( '../../lib/extend' ) . extendFlat ;
1314var Plots = require ( '../plots' ) ;
@@ -170,6 +171,141 @@ proto.updateLayers = function(ternaryLayout) {
170171
171172var whRatio = Math . sqrt ( 4 / 3 ) ;
172173
174+ proto . drawSpikelines = function ( hoverPoint , spikePoint , cursorXVal , cursorYVal ) {
175+ const fullLayout = this . graphDiv . _fullLayout ;
176+ const axes = [ this . aaxis , this . baxis , this . caxis ] ;
177+
178+ if ( ! axes . some ( ( axis ) => axis . showspikes ) ) return ;
179+
180+ const span = this . sum - axes [ 0 ] . min - axes [ 1 ] . min - axes [ 2 ] . min ;
181+
182+ const contrastColor = Color . combine ( fullLayout [ this . id ] . bgcolor , fullLayout . paper_bgcolor ) ;
183+
184+ let layer ;
185+
186+ for ( let i = 0 ; i < axes . length ; i ++ ) {
187+ const axis = axes [ i ] ;
188+ if ( ! axis . showspikes ) continue ;
189+
190+ const snap = axis . spikesnap ;
191+
192+ // 'hovered data' uses the current hover point
193+ // 'data' and 'cursor' use the closest point within spikedistance
194+ // 'data' draws at that point, while 'cursor' draws at the cursor position
195+ const selectedPoint = snap === 'hovered data' ? hoverPoint : spikePoint || hoverPoint ;
196+
197+ if ( ! selectedPoint ) continue ;
198+
199+ const snapToCursor = snap === 'cursor' ;
200+
201+ if ( snapToCursor && ( ! Number . isFinite ( cursorXVal ) || ! Number . isFinite ( cursorYVal ) ) ) {
202+ continue ;
203+ }
204+
205+ const calcPoint = selectedPoint . cd [ selectedPoint . index ] ;
206+
207+ // ternary plots use synthetic x/y axes (x = c - b, y = a), see src/traces/scatterternary/calc.js
208+ // cursor positions are expressed in these coordinates,
209+ // so we need to convert them to a/b/c for the ternary geometry below
210+ const a = snapToCursor ? cursorYVal : calcPoint . a ;
211+ const b = snapToCursor ? ( this . sum - cursorYVal - cursorXVal ) / 2 : calcPoint . b ;
212+ const c = snapToCursor ? ( this . sum - cursorYVal + cursorXVal ) / 2 : calcPoint . c ;
213+
214+ if ( ! this . xaxis . isPtWithinRange ( { a, b, c} ) ) continue ;
215+
216+ if ( ! layer ) {
217+ layer = fullLayout . _hoverlayer
218+ . insert ( 'g' , ':first-child' )
219+ . attr ( 'class' , 'ternary-spikes' )
220+ . attr ( 'transform' , strTranslate ( this . x0 , this . y0 ) )
221+ . style ( 'pointer-events' , 'none' ) ;
222+ }
223+
224+ // normalize to the visible ternary range so the spike
225+ // geometry also works when plot is zoomed, and constrain fraction at [0, 1] boundaries
226+ const fa = Lib . constrain ( ( a - axes [ 0 ] . min ) / span , 0 , 1 ) ;
227+ const fb = Lib . constrain ( ( b - axes [ 1 ] . min ) / span , 0 , 1 ) ;
228+ const fc = Lib . constrain ( ( c - axes [ 2 ] . min ) / span , 0 , 1 ) ;
229+
230+ const x = this . w * ( fc + fa / 2 ) ;
231+ const y = this . h * ( 1 - fa ) ;
232+
233+ // intersections of the line with the target axis and the opposite triangle edge
234+ const [ axisX , axisY , acrossX , acrossY ] = [
235+ [ this . w * fa / 2 , y , this . w * ( 1 - fa / 2 ) , y ] , // aaxis
236+ [ this . w * ( 1 - fb ) , this . h , this . w * ( 1 - fb ) / 2 , this . h * fb ] , // baxis
237+ [ this . w * ( 1 + fc ) / 2 , this . h * fc , this . w * fc , this . h ] // caxis
238+ ] [ i ] ;
239+
240+ const pointColor = selectedPoint . color || axis . color ;
241+ const dfltColor = Color . wcagContrast ( pointColor , contrastColor ) < 1.5
242+ ? Color . contrast ( contrastColor )
243+ : pointColor ;
244+ const color = axis . spikecolor || dfltColor ;
245+ const thickness = axis . spikethickness ;
246+ const mode = axis . spikemode ;
247+
248+ if ( mode . indexOf ( 'toaxis' ) !== - 1 || mode . indexOf ( 'across' ) !== - 1 ) {
249+ const across = mode . indexOf ( 'across' ) !== - 1 ;
250+ layer . append ( 'line' ) . attr ( {
251+ class : 'spikeline' ,
252+ x1 : across ? acrossX : x ,
253+ y1 : across ? acrossY : y ,
254+ x2 : axisX ,
255+ y2 : axisY ,
256+ 'stroke-width' : thickness ,
257+ 'stroke-dasharray' : Drawing . dashStyle ( axis . spikedash , thickness )
258+ } ) . call ( Color . stroke , color ) ;
259+ }
260+
261+ if ( mode . indexOf ( 'marker' ) !== - 1 ) {
262+ layer . append ( 'circle' ) . attr ( {
263+ class : 'spikeline' ,
264+ cx : axisX ,
265+ cy : axisY ,
266+ r : thickness
267+ } ) . call ( Color . fill , color ) ;
268+ }
269+
270+ // draw the axis value label (similar to commonlabel for Cartesian) at the spike intersection
271+ // note that strictly speaking, spikelines feature shouldn't include this label,
272+ // in Carteisan, the label behavior is controlled by hovermode
273+ // but in ternary plot, the current hovermode options are not fit and it is very intuitive
274+ // that we want the labels to show together with spikelines
275+ const label = layer . append ( 'g' )
276+ . attr ( 'class' , 'ternary-spikelabel' )
277+ . attr ( 'transform' , strTranslate ( axisX , axisY ) ) ;
278+
279+ const text = label . append ( 'text' )
280+ . attr ( 'text-anchor' , 'middle' )
281+ . call ( Drawing . font , axis . tickfont || fullLayout . font )
282+ . text ( Axes . tickText ( axis , [ a , b , c ] [ i ] , true ) . text )
283+ . call ( svgTextUtils . convertToTspans , this . graphDiv ) ;
284+
285+ const box = Drawing . bBox ( text . node ( ) ) ;
286+ const width = box . width + 6 ;
287+ const height = box . height + 4 ;
288+ const arrow = Math . min ( 6 , height / 2 , width / 2 ) ;
289+ const left = i === 0 ? - arrow - width : i === 1 ? - width / 2 : 0 ;
290+ const top = i === 0 ? - height / 2 : i === 1 ? arrow : - height ;
291+
292+ let outline ;
293+ if ( i === 0 ) {
294+ outline = `M0,0L${ - arrow } ,${ - arrow } V${ top } H${ left } v${ height } H${ - arrow } V${ arrow } Z` ;
295+ } else if ( i === 1 ) {
296+ outline = `M0,0L${ arrow } ,${ arrow } H${ width / 2 } v${ height } H${ left } V${ arrow } H${ - arrow } Z` ;
297+ } else {
298+ outline = `M0,0V${ top } h${ width } V0Z` ;
299+ }
300+
301+ label . insert ( 'path' , 'text' ) . attr ( 'd' , outline ) . call ( Color . fill , color ) ;
302+ text . call ( svgTextUtils . positionText ,
303+ left + width / 2 - box . left - box . width / 2 ,
304+ top + height / 2 - box . top - box . height / 2 ) ;
305+ text . call ( Color . fill , Color . contrast ( color ) ) ;
306+ }
307+ } ;
308+
173309proto . adjustLayout = function ( ternaryLayout , graphSize ) {
174310 var _this = this ;
175311 var domain = ternaryLayout . domain ;
0 commit comments