1
- import { extent , format , timeFormat , utcFormat } from "d3" ;
1
+ import { extent , format , median , pairs , timeFormat , utcFormat } from "d3" ;
2
2
import { formatDefault } from "../format.js" ;
3
3
import { marks } from "../mark.js" ;
4
4
import { radians } from "../math.js" ;
5
5
import { arrayify , constant , identity , keyword , number , range , valueof } from "../options.js" ;
6
- import { isIterable , isNoneish , isTemporal , orderof } from "../options.js" ;
6
+ import { isIterable , isNoneish , isTemporal , isTimeInterval , orderof } from "../options.js" ;
7
7
import { maybeColorChannel , maybeNumberChannel , maybeRangeInterval } from "../options.js" ;
8
- import { isTemporalScale } from "../scales.js" ;
8
+ import { isOrdinalScale , isTemporalScale } from "../scales.js" ;
9
9
import { offset } from "../style.js" ;
10
- import { formatTimeTicks , isTimeYear , isUtcYear } from "../time.js" ;
10
+ import { formatTimeInterval , formatTimeTicks , inferTimeFormat , isTimeYear , isUtcYear } from "../time.js" ;
11
11
import { initializer } from "../transforms/basic.js" ;
12
12
import { ruleX , ruleY } from "./rule.js" ;
13
13
import { text , textX , textY } from "./text.js" ;
@@ -277,7 +277,7 @@ function axisTickKy(
277
277
...options
278
278
}
279
279
) {
280
- return axisMark ( vectorY , k , `${ k } -axis tick` , data , {
280
+ return axisMark ( vectorY , k , anchor , `${ k } -axis tick` , data , {
281
281
strokeWidth,
282
282
strokeLinecap,
283
283
strokeLinejoin,
@@ -311,7 +311,7 @@ function axisTickKx(
311
311
...options
312
312
}
313
313
) {
314
- return axisMark ( vectorX , k , `${ k } -axis tick` , data , {
314
+ return axisMark ( vectorX , k , anchor , `${ k } -axis tick` , data , {
315
315
strokeWidth,
316
316
strokeLinejoin,
317
317
strokeLinecap,
@@ -336,8 +336,7 @@ function axisTextKy(
336
336
tickSize,
337
337
tickRotate = 0 ,
338
338
tickPadding = Math . max ( 3 , 9 - tickSize ) + ( Math . abs ( tickRotate ) > 60 ? 4 * Math . cos ( tickRotate * radians ) : 0 ) ,
339
- tickFormat,
340
- text = typeof tickFormat === "function" ? tickFormat : undefined ,
339
+ text,
341
340
textAnchor = Math . abs ( tickRotate ) > 60 ? "middle" : anchor === "left" ? "end" : "start" ,
342
341
lineAnchor = tickRotate > 60 ? "top" : tickRotate < - 60 ? "bottom" : "middle" ,
343
342
fontVariant,
@@ -352,12 +351,13 @@ function axisTextKy(
352
351
return axisMark (
353
352
textY ,
354
353
k ,
354
+ anchor ,
355
355
`${ k } -axis tick label` ,
356
356
data ,
357
357
{
358
358
facetAnchor,
359
359
frameAnchor,
360
- text : text === undefined ? null : text ,
360
+ text,
361
361
textAnchor,
362
362
lineAnchor,
363
363
fontVariant,
@@ -366,7 +366,7 @@ function axisTextKy(
366
366
...options ,
367
367
dx : anchor === "left" ? + dx - tickSize - tickPadding + + insetLeft : + dx + + tickSize + + tickPadding - insetRight
368
368
} ,
369
- function ( scale , data , ticks , channels ) {
369
+ function ( scale , data , ticks , tickFormat , channels ) {
370
370
if ( fontVariant === undefined ) this . fontVariant = inferFontVariant ( scale ) ;
371
371
if ( text === undefined ) channels . text = inferTextChannel ( scale , data , ticks , tickFormat , anchor ) ;
372
372
}
@@ -383,8 +383,7 @@ function axisTextKx(
383
383
tickSize,
384
384
tickRotate = 0 ,
385
385
tickPadding = Math . max ( 3 , 9 - tickSize ) + ( Math . abs ( tickRotate ) >= 10 ? 4 * Math . cos ( tickRotate * radians ) : 0 ) ,
386
- tickFormat,
387
- text = typeof tickFormat === "function" ? tickFormat : undefined ,
386
+ text,
388
387
textAnchor = Math . abs ( tickRotate ) >= 10 ? ( ( tickRotate < 0 ) ^ ( anchor === "bottom" ) ? "start" : "end" ) : "middle" ,
389
388
lineAnchor = Math . abs ( tickRotate ) >= 10 ? "middle" : anchor === "bottom" ? "top" : "bottom" ,
390
389
fontVariant,
@@ -399,6 +398,7 @@ function axisTextKx(
399
398
return axisMark (
400
399
textX ,
401
400
k ,
401
+ anchor ,
402
402
`${ k } -axis tick label` ,
403
403
data ,
404
404
{
@@ -413,7 +413,7 @@ function axisTextKx(
413
413
...options ,
414
414
dy : anchor === "bottom" ? + dy + + tickSize + + tickPadding - insetBottom : + dy - tickSize - tickPadding + + insetTop
415
415
} ,
416
- function ( scale , data , ticks , channels ) {
416
+ function ( scale , data , ticks , tickFormat , channels ) {
417
417
if ( fontVariant === undefined ) this . fontVariant = inferFontVariant ( scale ) ;
418
418
if ( text === undefined ) channels . text = inferTextChannel ( scale , data , ticks , tickFormat , anchor ) ;
419
419
}
@@ -452,7 +452,7 @@ function gridKy(
452
452
...options
453
453
}
454
454
) {
455
- return axisMark ( ruleY , k , `${ k } -grid` , data , { y, x1, x2, ...gridDefaults ( options ) } ) ;
455
+ return axisMark ( ruleY , k , anchor , `${ k } -grid` , data , { y, x1, x2, ...gridDefaults ( options ) } ) ;
456
456
}
457
457
458
458
function gridKx (
@@ -467,7 +467,7 @@ function gridKx(
467
467
...options
468
468
}
469
469
) {
470
- return axisMark ( ruleX , k , `${ k } -grid` , data , { x, y1, y2, ...gridDefaults ( options ) } ) ;
470
+ return axisMark ( ruleX , k , anchor , `${ k } -grid` , data , { x, y1, y2, ...gridDefaults ( options ) } ) ;
471
471
}
472
472
473
473
function gridDefaults ( {
@@ -517,15 +517,17 @@ function labelOptions(
517
517
} ;
518
518
}
519
519
520
- function axisMark ( mark , k , ariaLabel , data , options , initialize ) {
520
+ function axisMark ( mark , k , anchor , ariaLabel , data , options , initialize ) {
521
521
let channels ;
522
522
523
523
function axisInitializer ( data , facets , _channels , scales , dimensions , context ) {
524
524
const initializeFacets = data == null && ( k === "fx" || k === "fy" ) ;
525
525
const { [ k ] : scale } = scales ;
526
526
if ( ! scale ) throw new Error ( `missing scale: ${ k } ` ) ;
527
- let { ticks, tickSpacing, interval} = options ;
528
- if ( isTemporalScale ( scale ) && typeof ticks === "string" ) ( interval = ticks ) , ( ticks = undefined ) ;
527
+ let { ticks, tickFormat, interval} = options ;
528
+ // TODO what if ticks is a time interval implementation?
529
+ // TODO allow ticks to be a function?
530
+ if ( hasTimeTicks ( scale ) && typeof ticks === "string" ) ( interval = ticks ) , ( ticks = undefined ) ;
529
531
if ( data == null ) {
530
532
if ( isIterable ( ticks ) ) {
531
533
data = arrayify ( ticks ) ;
@@ -542,21 +544,38 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
542
544
const [ min , max ] = extent ( scale . domain ( ) ) ;
543
545
data = interval . range ( min , interval . offset ( interval . floor ( max ) ) ) ; // inclusive max
544
546
} else {
545
- const [ min , max ] = extent ( scale . range ( ) ) ;
546
- ticks = ( max - min ) / ( tickSpacing === undefined ? ( k === "x" ? 80 : 35 ) : tickSpacing ) ;
547
+ ticks = inferTickCount ( k , scale , options ) ;
547
548
data = scale . ticks ( ticks ) ;
548
549
}
549
550
}
550
551
} else {
551
552
data = scale . domain ( ) ;
553
+ if ( isTimeInterval ( scale . interval ) ) {
554
+ const type = "utc" ; // TODO infer type of ordinal time
555
+ const [ start , stop ] = extent ( data ) ;
556
+ if ( interval !== undefined ) data = maybeRangeInterval ( interval , type ) . range ( start , + stop + 1 ) ; // inclusive stop
557
+ if ( ticks === undefined ) ticks = inferTickCount ( k , scale , options ) ;
558
+ const n = Math . max ( 1 , getSkip ( data , ticks ) ) ;
559
+ const s = getMedianStep ( data ) ;
560
+ const f = inferTimeFormat ( s * n ) ;
561
+ const [ i , I ] = f ;
562
+ // const [j, J] = inferTimeFormat(s);
563
+ data = maybeRangeInterval ( I , type ) . range ( start , + stop + 1 ) ; // inclusive stop
564
+ // TODO check if isSubsumingInterval(interval, data)
565
+ if ( tickFormat === undefined ) {
566
+ const format = utcFormat ; // TODO based on type
567
+ const template = ( f1 , f2 ) => `${ f1 } \n${ f2 } ` ; // TODO based on anchor
568
+ tickFormat = formatTimeInterval ( i , format , template ) ;
569
+ }
570
+ }
552
571
}
553
572
if ( k === "y" || k === "x" ) {
554
573
facets = [ range ( data ) ] ;
555
574
} else {
556
575
channels [ k ] = { scale : k , value : identity } ;
557
576
}
558
577
}
559
- initialize ?. call ( this , scale , data , ticks , channels ) ;
578
+ initialize ?. call ( this , scale , data , ticks , tickFormat , channels ) ;
560
579
const initializedChannels = Object . fromEntries (
561
580
Object . entries ( channels ) . map ( ( [ name , channel ] ) => {
562
581
return [ name , { ...channel , value : valueof ( data , channel . value ) } ] ;
@@ -580,8 +599,34 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
580
599
return m ;
581
600
}
582
601
602
+ // Compute the positive number n such that taking every nth value from the
603
+ // scale’s domain produces as close as possible to the desired number of ticks.
604
+ // For example, if the domain has 100 values and 5 ticks are desired, n = 20.
605
+ function getSkip ( domain , ticks ) {
606
+ return domain . length / ticks ;
607
+ }
608
+
609
+ // Compute the median step s between adjacent values from the scale’s domain.
610
+ function getMedianStep ( domain ) {
611
+ return median ( pairs ( domain , ( a , b ) => Math . abs ( b - a ) || NaN ) ) ;
612
+ }
613
+
614
+ function inferTickCount ( k , scale , options ) {
615
+ const { tickSpacing = k === "x" ? 80 : 35 } = options ;
616
+ const [ min , max ] = extent ( scale . range ( ) ) ;
617
+ return ( max - min ) / tickSpacing ;
618
+ }
619
+
620
+ // Returns true if the given interval subsumes (i.e., covers, is
621
+ // capable of generating) all of the specified values.
622
+ // function isSubsumingInterval(interval, values) {
623
+ // return values.every((v) => interval.floor(v) >= v);
624
+ // }
625
+
583
626
function inferTextChannel ( scale , data , ticks , tickFormat , anchor ) {
584
- return { value : inferTickFormat ( scale , data , ticks , tickFormat , anchor ) } ;
627
+ return {
628
+ value : typeof tickFormat === "function" ? tickFormat : inferTickFormat ( scale , data , ticks , tickFormat , anchor )
629
+ } ;
585
630
}
586
631
587
632
// D3’s ordinal scales simply use toString by default, but if the ordinal scale
@@ -647,7 +692,7 @@ function inferScaleOrder(scale) {
647
692
// Takes the scale label, and if this is not an ordinal scale and the label was
648
693
// inferred from an associated channel, adds an orientation-appropriate arrow.
649
694
function formatAxisLabel ( k , scale , { anchor, label = scale . label , labelAnchor, labelArrow} = { } ) {
650
- if ( label == null || ( label . inferred && isTemporalish ( scale ) && / ^ ( d a t e | t i m e | y e a r ) $ / i. test ( label ) ) ) return ;
695
+ if ( label == null || ( label . inferred && hasTimeTicks ( scale ) && / ^ ( d a t e | t i m e | y e a r ) $ / i. test ( label ) ) ) return ;
651
696
label = String ( label ) ; // coerce to a string after checking if inferred
652
697
if ( labelArrow === "auto" ) labelArrow = ( ! scale . bandwidth || scale . interval ) && ! / [ ↑ ↓ → ← ] / . test ( label ) ;
653
698
if ( ! labelArrow ) return label ;
@@ -684,6 +729,6 @@ function maybeLabelArrow(labelArrow = "auto") {
684
729
: keyword ( labelArrow , "labelArrow" , [ "auto" , "up" , "right" , "down" , "left" ] ) ;
685
730
}
686
731
687
- function isTemporalish ( scale ) {
688
- return isTemporalScale ( scale ) || scale . interval != null ;
732
+ function hasTimeTicks ( scale ) {
733
+ return isTemporalScale ( scale ) || ( isOrdinalScale ( scale ) && isTimeInterval ( scale . interval ) ) ;
689
734
}
0 commit comments