@@ -20,6 +20,7 @@ var makeCrossings = require('./make_crossings');
20
20
var findAllPaths = require ( './find_all_paths' ) ;
21
21
var endPlus = require ( './end_plus' ) ;
22
22
var constants = require ( './constants' ) ;
23
+ var costConstants = constants . LABELOPTIMIZER ;
23
24
24
25
25
26
module . exports = function plot ( gd , plotinfo , cdcontours ) {
@@ -379,7 +380,7 @@ function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
379
380
var plotDiagonal = Math . sqrt ( xLen * xLen + yLen * yLen ) ;
380
381
381
382
// the path length to use to scale the number of labels to draw:
382
- var normLength = plotDiagonal /
383
+ var normLength = constants . LABELDISTANCE * plotDiagonal /
383
384
Math . max ( 1 , pathinfo . length / constants . LABELINCREASE ) ;
384
385
385
386
linegroup . each ( function ( d ) {
@@ -401,22 +402,59 @@ function makeLinesAndLabels(plotgroup, pathinfo, gd, cd0, contours, perimeter) {
401
402
402
403
d3 . select ( this ) . selectAll ( 'path' ) . each ( function ( ) {
403
404
var path = this ;
404
- var pathLen = path . getTotalLength ( ) ;
405
+ var pathBounds = Lib . getVisibleSegment ( path , bounds , textHeight / 2 ) ;
406
+ if ( ! pathBounds ) return ;
407
+
408
+ var onPlotMin = pathBounds . min ;
409
+ var onPlotMax = pathBounds . max ;
410
+ var totalPathLen = pathBounds . total ;
411
+ var pathLen = onPlotMax - onPlotMin ;
412
+
413
+ var isOpen = d3 . select ( this ) . classed ( 'openline' ) ;
405
414
406
415
if ( pathLen < textWidth * constants . LABELMIN ) return ;
407
416
408
- var labelCount = Math . ceil ( pathLen / normLength ) ;
409
- for ( var i = 0.5 ; i < labelCount ; i ++ ) {
410
- var positionOnPath = i * pathLen / labelCount ;
411
- var loc = getLocation ( path , pathLen , positionOnPath , textOpts ) ;
412
- // TODO: no optimization yet: just get display mechanics working
413
- labelClipPathData += addLabel ( loc , textOpts , labelData ) ;
414
- }
417
+ var maxLabels = Math . min ( Math . ceil ( pathLen / normLength ) ,
418
+ constants . LABELMAX ) ;
419
+ var dp , p0 , pMax , minCost , location , pMin ;
420
+
421
+ for ( var i = 0 ; i < maxLabels ; i ++ ) {
422
+ // simple optimization by a wide search followed by a binary search
423
+ if ( isOpen ) {
424
+ dp = ( pathLen - textWidth ) / ( costConstants . INITIALSEARCHPOINTS + 1 ) ;
425
+ p0 = onPlotMin + dp + textWidth / 2 ;
426
+ pMax = onPlotMax - ( dp + textWidth ) / 2 ;
427
+ }
428
+ else {
429
+ dp = pathLen / costConstants . INITIALSEARCHPOINTS ;
430
+ p0 = onPlotMin + dp / 2 ;
431
+ pMax = onPlotMax ;
432
+ }
433
+
434
+ minCost = Infinity ;
435
+ for ( var j = 0 ; j < costConstants . ITERATIONS ; j ++ ) {
436
+ for ( var p = p0 ; p < pMax ; p += dp ) {
437
+ var newLocation = Lib . getTextLocation ( path , totalPathLen , p , textWidth ) ;
438
+ var newCost = locationCost ( newLocation , textOpts , labelData , bounds ) ;
439
+ if ( newCost < minCost ) {
440
+ minCost = newCost ;
441
+ location = newLocation ;
442
+ pMin = p ;
443
+ }
444
+ }
445
+ if ( minCost > costConstants . MAXCOST * 2 ) break ;
446
+
447
+ // subsequent iterations just look half steps away from the
448
+ // best we found in the previous iteration
449
+ p0 = pMin - dp / 2 ;
450
+ if ( j ) dp /= 2 ;
451
+ pMax = p0 + dp * 1.5 ;
452
+ }
453
+ if ( minCost > costConstants . MAXCOST ) break ;
415
454
455
+ labelClipPathData += addLabel ( location , textOpts , labelData ) ;
456
+ }
416
457
} ) ;
417
- // - iterate over paths for this level, finding the best position(s)
418
- // for label(s) on that path, given all the other labels we've
419
- // already placed
420
458
} ) ;
421
459
422
460
dummyText . remove ( ) ;
@@ -461,6 +499,63 @@ function straightClosedPath(pts) {
461
499
return 'M' + pts . join ( 'L' ) + 'Z' ;
462
500
}
463
501
502
+ /*
503
+ * locationCost: a cost function for label locations
504
+ * composed of three kinds of penalty:
505
+ * - for open paths, being close to the end of the path
506
+ * - the angle away from horizontal
507
+ * - being too close to already placed neighbors
508
+ */
509
+ function locationCost ( location , textOpts , labelData , bounds ) {
510
+ var halfWidth = textOpts . width / 2 ;
511
+ var halfHeight = textOpts . height / 2 ;
512
+ var x = location . x ;
513
+ var y = location . y ;
514
+ var theta = location . theta ;
515
+ var dx = Math . cos ( theta ) * halfWidth ;
516
+ var dy = Math . sin ( theta ) * halfWidth ;
517
+
518
+ // cost for being near an edge
519
+ var normX = ( ( x > bounds . center ) ? ( bounds . right - x ) : ( x - bounds . left ) ) /
520
+ ( dx + Math . abs ( Math . sin ( theta ) * halfHeight ) ) ;
521
+ var normY = ( ( y > bounds . middle ) ? ( bounds . bottom - y ) : ( y - bounds . top ) ) /
522
+ ( Math . abs ( dy ) + Math . cos ( theta ) * halfHeight ) ;
523
+ if ( normX < 1 || normY < 1 ) return Infinity ;
524
+ var cost = costConstants . EDGECOST * ( 1 / ( normX - 1 ) + 1 / ( normY - 1 ) ) ;
525
+
526
+ // cost for not being horizontal
527
+ cost += costConstants . ANGLECOST * theta * theta ;
528
+
529
+ // cost for being close to other labels
530
+ var x1 = x - dx ;
531
+ var y1 = y - dy ;
532
+ var x2 = x + dx ;
533
+ var y2 = y + dy ;
534
+ for ( var i = 0 ; i < labelData . length ; i ++ ) {
535
+ var labeli = labelData [ i ] ;
536
+ var dxd = Math . cos ( labeli . theta ) * labeli . width / 2 ;
537
+ var dyd = Math . sin ( labeli . theta ) * labeli . width / 2 ;
538
+ var dist = Lib . segmentDistance (
539
+ x1 , y1 ,
540
+ x2 , y2 ,
541
+ labeli . x - dxd , labeli . y - dyd ,
542
+ labeli . x + dxd , labeli . y + dyd
543
+ ) * 2 / ( textOpts . height + labeli . height ) ;
544
+
545
+ var sameLevel = labeli . level === textOpts . level ;
546
+ var distOffset = sameLevel ? costConstants . SAMELEVELDISTANCE : 1 ;
547
+
548
+ if ( dist <= distOffset ) return Infinity ;
549
+
550
+ var distFactor = costConstants . NEIGHBORCOST *
551
+ ( sameLevel ? costConstants . SAMELEVELFACTOR : 1 ) ;
552
+
553
+ cost += distFactor / ( dist - distOffset ) ;
554
+ }
555
+
556
+ return cost ;
557
+ }
558
+
464
559
function addLabel ( loc , textOpts , labelData ) {
465
560
var halfWidth = textOpts . width / 2 ;
466
561
var halfHeight = textOpts . height / 2 ;
0 commit comments