@@ -13,11 +13,13 @@ var d3 = require('d3');
13
13
14
14
var Lib = require ( '../../lib' ) ;
15
15
var Drawing = require ( '../../components/drawing' ) ;
16
+ var svgTextUtils = require ( '../../lib/svg_text_utils' ) ;
16
17
17
18
var heatmapPlot = require ( '../heatmap/plot' ) ;
18
19
var makeCrossings = require ( './make_crossings' ) ;
19
20
var findAllPaths = require ( './find_all_paths' ) ;
20
21
var endPlus = require ( './end_plus' ) ;
22
+ var constants = require ( './constants' ) ;
21
23
22
24
23
25
module . exports = function plot ( gd , plotinfo , cdcontours ) {
@@ -80,7 +82,7 @@ function plotOne(gd, plotinfo, cd) {
80
82
var plotGroup = makeContourGroup ( plotinfo , cd , id ) ;
81
83
makeBackground ( plotGroup , perimeter , contours ) ;
82
84
makeFills ( plotGroup , pathinfo , perimeter , contours ) ;
83
- makeLines ( plotGroup , pathinfo , contours ) ;
85
+ makeLines ( plotGroup , pathinfo , gd , cd [ 0 ] , contours , perimeter ) ;
84
86
clipGaps ( plotGroup , plotinfo , fullLayout . _defs , cd [ 0 ] , perimeter ) ;
85
87
}
86
88
@@ -259,7 +261,11 @@ function joinAllPaths(pi, perimeter) {
259
261
return fullpath ;
260
262
}
261
263
262
- function makeLines ( plotgroup , pathinfo , contours ) {
264
+ var TRAILING_ZEROS = / \. ? 0 + $ / ;
265
+
266
+ function makeLines ( plotgroup , pathinfo , gd , cd0 , contours , perimeter ) {
267
+ var defs = gd . _fullLayout . _defs ;
268
+
263
269
var smoothing = pathinfo [ 0 ] . smoothing ;
264
270
265
271
var lineContainer = plotgroup . selectAll ( 'g.contourlines' ) . data ( [ 0 ] ) ;
@@ -296,6 +302,190 @@ function makeLines(plotgroup, pathinfo, contours) {
296
302
} )
297
303
. style ( 'stroke-miterlimit' , 1 )
298
304
. style ( 'vector-effect' , 'non-scaling-stroke' ) ;
305
+
306
+ var showLabels = contours . showlabels ;
307
+ var clipId = showLabels ? 'clipline' + cd0 . trace . uid : null ;
308
+
309
+ var lineClip = defs . select ( '.clips' ) . selectAll ( '#' + clipId )
310
+ . data ( showLabels ? [ 0 ] : [ ] ) ;
311
+ lineClip . exit ( ) . remove ( ) ;
312
+
313
+ lineClip . enter ( ) . append ( 'clipPath' )
314
+ . classed ( 'contourlineclip' , true )
315
+ . attr ( 'id' , clipId ) ;
316
+
317
+ Drawing . setClipUrl ( lineContainer , clipId ) ;
318
+
319
+ var labelGroup = plotgroup . selectAll ( 'g.contourlabels' )
320
+ . data ( showLabels ? [ 0 ] : [ ] ) ;
321
+
322
+ labelGroup . exit ( ) . remove ( ) ;
323
+
324
+ labelGroup . enter ( ) . append ( 'g' )
325
+ . classed ( 'contourlabels' , true ) ;
326
+
327
+ if ( showLabels ) {
328
+ var labelClipPathData = straightClosedPath ( perimeter ) ;
329
+
330
+ var labelData = [ ] ;
331
+
332
+ var contourFormat ;
333
+ if ( contours . labelformat ) {
334
+ contourFormat = d3 . format ( contours . labelformat ) ;
335
+ }
336
+ else {
337
+ // round to 2 digits past magnitude of contours.size,
338
+ // then remove trailing zeroes
339
+ var valRound = 2 - Math . floor ( Math . log ( contours . size ) / Math . LN10 + 0.01 ) ;
340
+ if ( valRound <= 0 ) {
341
+ contourFormat = function ( v ) { return v . toFixed ( ) ; } ;
342
+ }
343
+ else {
344
+ contourFormat = function ( v ) {
345
+ var valStr = v . toFixed ( valRound ) ;
346
+ return valStr . replace ( TRAILING_ZEROS , '' ) ;
347
+ } ;
348
+ }
349
+ }
350
+
351
+ var dummyText = defs . append ( 'text' )
352
+ . attr ( 'data-notex' , 1 )
353
+ . call ( Drawing . font , contours . font ) ;
354
+
355
+ var plotDiagonal = Math . sqrt ( Math . pow ( pathinfo [ 0 ] . xaxis . _length , 2 ) +
356
+ Math . pow ( pathinfo [ 0 ] . yaxis . _length , 2 ) ) ;
357
+
358
+ // the path length to use to scale the number of labels to draw:
359
+ var normLength = plotDiagonal /
360
+ Math . max ( 1 , pathinfo . length / constants . LABELINCREASE ) ;
361
+
362
+ linegroup . each ( function ( d ) {
363
+ // - make a dummy label for this level and calc its bbox
364
+ var text = contourFormat ( d . level ) ;
365
+ dummyText . text ( text )
366
+ . call ( svgTextUtils . convertToTspans , gd ) ;
367
+ var bBox = Drawing . bBox ( dummyText . node ( ) ) ;
368
+ var textWidth = bBox . width ;
369
+ var textHeight = bBox . height ;
370
+ var dy = ( bBox . top + bBox . bottom ) / 2 ;
371
+ var textOpts = {
372
+ text : text ,
373
+ width : textWidth ,
374
+ height : textHeight ,
375
+ level : d . level ,
376
+ dy : dy
377
+ } ;
378
+
379
+ d3 . select ( this ) . selectAll ( 'path' ) . each ( function ( ) {
380
+ var path = this ;
381
+ var pathLen = path . getTotalLength ( ) ;
382
+
383
+ if ( pathLen < textWidth * constants . LABELMIN ) return ;
384
+
385
+ var labelCount = Math . ceil ( pathLen / normLength ) ;
386
+ for ( var i = 0.5 ; i < labelCount ; i ++ ) {
387
+ var positionOnPath = i * pathLen / labelCount ;
388
+ var loc = getLocation ( path , pathLen , positionOnPath , textOpts ) ;
389
+ // TODO: no optimization yet: just get display mechanics working
390
+ labelClipPathData += addLabel ( loc , textOpts , labelData ) ;
391
+ }
392
+
393
+ } ) ;
394
+ // - iterate over paths for this level, finding the best position(s)
395
+ // for label(s) on that path, given all the other labels we've
396
+ // already placed
397
+ } ) ;
398
+
399
+ dummyText . remove ( ) ;
400
+
401
+ var labels = labelGroup . selectAll ( 'text' )
402
+ . data ( labelData , function ( d ) {
403
+ return d . text + ',' + d . x + ',' + d . y + ',' + d . theta ;
404
+ } ) ;
405
+
406
+ labels . exit ( ) . remove ( ) ;
407
+
408
+ labels . enter ( ) . append ( 'text' )
409
+ . attr ( {
410
+ 'data-notex' : 1 ,
411
+ 'text-anchor' : 'middle'
412
+ } )
413
+ . each ( function ( d ) {
414
+ var x = d . x + Math . sin ( d . theta ) * d . dy ;
415
+ var y = d . y - Math . cos ( d . theta ) * d . dy ;
416
+ d3 . select ( this )
417
+ . text ( d . text )
418
+ . attr ( {
419
+ x : x ,
420
+ y : y ,
421
+ transform : 'rotate(' + ( 180 * d . theta / Math . PI ) + ' ' + x + ' ' + y + ')'
422
+ } )
423
+ . call ( svgTextUtils . convertToTspans , gd )
424
+ . call ( Drawing . font , contours . font . family , contours . font . size ) ;
425
+ } ) ;
426
+
427
+ var lineClipPath = lineClip . selectAll ( 'path' ) . data ( [ 0 ] ) ;
428
+ lineClipPath . enter ( ) . append ( 'path' ) ;
429
+ lineClipPath . attr ( 'd' , labelClipPathData ) ;
430
+ }
431
+
432
+ }
433
+
434
+ function straightClosedPath ( pts ) {
435
+ return 'M' + pts . join ( 'L' ) + 'Z' ;
436
+ }
437
+
438
+ function addLabel ( loc , textOpts , labelData ) {
439
+ var halfWidth = textOpts . width / 2 ;
440
+ var halfHeight = textOpts . height / 2 ;
441
+
442
+ var x = loc . x ;
443
+ var y = loc . y ;
444
+ var theta = loc . theta ;
445
+
446
+ var sin = Math . sin ( theta ) ;
447
+ var cos = Math . cos ( theta ) ;
448
+ var dxw = halfWidth * cos ;
449
+ var dxh = halfHeight * sin ;
450
+ var dyw = halfWidth * sin ;
451
+ var dyh = - halfHeight * cos ;
452
+ var bBoxPts = [
453
+ [ x - dxw - dxh , y - dyw - dyh ] ,
454
+ [ x + dxw - dxh , y + dyw - dyh ] ,
455
+ [ x + dxw + dxh , y + dyw + dyh ] ,
456
+ [ x - dxw + dxh , y - dyw + dyh ] ,
457
+ ] ;
458
+
459
+ labelData . push ( {
460
+ text : textOpts . text ,
461
+ x : x ,
462
+ y : y ,
463
+ dy : textOpts . dy ,
464
+ theta : theta ,
465
+ level : textOpts . level ,
466
+ width : textOpts . width ,
467
+ height : textOpts . height
468
+ } ) ;
469
+
470
+ return straightClosedPath ( bBoxPts ) ;
471
+ }
472
+
473
+ function getLocation ( path , pathLen , positionOnPath , textOpts ) {
474
+ var halfWidth = textOpts . width / 2 ;
475
+
476
+ // for the angle, use points on the path separated by the text width
477
+ // even though due to curvature, the text will cover a bit more than that
478
+ var p0 = path . getPointAtLength ( Lib . mod ( positionOnPath - halfWidth , pathLen ) ) ;
479
+ var p1 = path . getPointAtLength ( Lib . mod ( positionOnPath + halfWidth , pathLen ) ) ;
480
+ // note: atan handles 1/0 nicely
481
+ var theta = Math . atan ( ( p1 . y - p0 . y ) / ( p1 . x - p0 . x ) ) ;
482
+ // center the text at 2/3 of the center position plus 1/3 the p0/p1 midpoint
483
+ // that's the average position of this segment, assuming it's roughly quadratic
484
+ var pCenter = path . getPointAtLength ( positionOnPath ) ;
485
+ var x = ( pCenter . x * 4 + p0 . x + p1 . x ) / 6 ;
486
+ var y = ( pCenter . y * 4 + p0 . y + p1 . y ) / 6 ;
487
+
488
+ return { x : x , y : y , theta : theta } ;
299
489
}
300
490
301
491
function clipGaps ( plotGroup , plotinfo , defs , cd0 , perimeter ) {
0 commit comments