1
- import { extent , format , timeFormat , utcFormat } from "d3" ;
1
+ import { InternSet , extent , format , 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 , isInterval , orderof } from "../options.js" ;
7
7
import { maybeColorChannel , maybeNumberChannel , maybeRangeInterval } from "../options.js" ;
8
- import { isTemporalScale } from "../scales.js" ;
9
8
import { offset } from "../style.js" ;
10
- import { formatTimeTicks , isTimeYear , isUtcYear } from "../time.js" ;
9
+ import { generalizeTimeInterval , inferTimeFormat , intervalDuration } from "../time.js" ;
11
10
import { initializer } from "../transforms/basic.js" ;
11
+ import { warn } from "../warnings.js" ;
12
12
import { ruleX , ruleY } from "./rule.js" ;
13
13
import { text , textX , textY } from "./text.js" ;
14
14
import { vectorX , vectorY } from "./vector.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,46 +517,83 @@ 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
+ const domain = scale . domain ( ) ;
528
+ let { interval, ticks, tickFormat, tickSpacing = k === "x" ? 80 : 35 } = options ;
529
+ // For a scale with a temporal domain, also allow the ticks to be specified
530
+ // as a string which is promoted to a time interval. In the case of ordinal
531
+ // scales, the interval is interpreted as UTC.
532
+ if ( typeof ticks === "string" && hasTemporalDomain ( scale ) ) ( interval = ticks ) , ( ticks = undefined ) ;
533
+ // The interval axis option is an alternative method of specifying ticks;
534
+ // for example, for a numeric scale, ticks = 5 means “about 5 ticks” whereas
535
+ // interval = 5 means “ticks every 5 units”. (This is not to be confused
536
+ // with the interval scale option, which affects the scale’s behavior!)
537
+ // Lastly use the tickSpacing option to infer the desired tick count.
538
+ if ( ticks === undefined ) ticks = maybeRangeInterval ( interval , scale . type ) ?? inferTickCount ( scale , tickSpacing ) ;
529
539
if ( data == null ) {
530
540
if ( isIterable ( ticks ) ) {
541
+ // Use explicit ticks, if specified.
531
542
data = arrayify ( ticks ) ;
532
- } else if ( scale . ticks ) {
533
- if ( ticks !== undefined ) {
534
- data = scale . ticks ( ticks ) ;
543
+ } else if ( isInterval ( ticks ) ) {
544
+ // Use the tick interval, if specified.
545
+ data = inclusiveRange ( ticks , ...extent ( domain ) ) ;
546
+ } else if ( scale . interval ) {
547
+ // If the scale interval is a standard time interval such as "day", we
548
+ // may be able to generalize the scale interval it to a larger aligned
549
+ // time interval to create the desired number of ticks.
550
+ let interval = scale . interval ;
551
+ if ( scale . ticks ) {
552
+ const [ min , max ] = extent ( domain ) ;
553
+ const n = ( max - min ) / interval [ intervalDuration ] ; // current tick count
554
+ // We don’t explicitly check that given interval is a time interval;
555
+ // in that case the generalized interval will be undefined, just like
556
+ // a nonstandard interval. TODO Generalize integer intervals, too.
557
+ interval = generalizeTimeInterval ( interval , n / ticks ) ?? interval ;
558
+ data = inclusiveRange ( interval , min , max ) ;
535
559
} else {
536
- interval = maybeRangeInterval ( interval === undefined ? scale . interval : interval , scale . type ) ;
537
- if ( interval !== undefined ) {
538
- // For time scales, we could pass the interval directly to
539
- // scale.ticks because it’s supported by d3.utcTicks; but
540
- // quantitative scales and d3.ticks do not support numeric
541
- // intervals for scale.ticks, so we compute them here.
542
- const [ min , max ] = extent ( scale . domain ( ) ) ;
543
- data = interval . range ( min , interval . offset ( interval . floor ( max ) ) ) ; // inclusive max
544
- } else {
545
- const [ min , max ] = extent ( scale . range ( ) ) ;
546
- ticks = ( max - min ) / ( tickSpacing === undefined ? ( k === "x" ? 80 : 35 ) : tickSpacing ) ;
547
- data = scale . ticks ( ticks ) ;
548
- }
560
+ data = domain ;
561
+ const n = data . length ; // current tick count
562
+ interval = generalizeTimeInterval ( interval , n / ticks ) ?? interval ;
563
+ if ( interval !== scale . interval ) data = inclusiveRange ( interval , ... extent ( data ) ) ;
564
+ }
565
+ if ( interval === scale . interval ) {
566
+ // If we weren’t able to generalize the scale’s interval, compute the
567
+ // positive number n such that taking every nth value from the scale’s
568
+ // domain produces as close as possible to the desired number of
569
+ // ticks. For example, if the domain has 100 values and 5 ticks are
570
+ // desired, n = 20.
571
+ const n = Math . round ( data . length / ticks ) ;
572
+ if ( n > 1 ) data = data . filter ( ( d , i ) => i % n === 0 ) ;
549
573
}
574
+ } else if ( scale . ticks ) {
575
+ data = scale . ticks ( ticks ) ;
550
576
} else {
551
- data = scale . domain ( ) ;
577
+ // For ordinal scales, the domain will already be generated using the
578
+ // scale’s interval, if any.
579
+ data = domain ;
580
+ }
581
+ if ( ! scale . ticks && data . length && data !== domain ) {
582
+ // For ordinal scales, intersect the ticks with the scale domain since
583
+ // the scale is only defined on its domain. If all of the ticks are
584
+ // removed, then warn that the ticks and scale domain may be misaligned
585
+ // (e.g., "year" ticks and "4 weeks" interval).
586
+ const domainSet = new InternSet ( domain ) ;
587
+ data = data . filter ( ( d ) => domainSet . has ( d ) ) ;
588
+ if ( ! data . length ) warn ( `Warning: the ${ k } -axis ticks appear to not align with the scale domain, resulting in no ticks. Try different ticks?` ) ; // prettier-ignore
552
589
}
553
590
if ( k === "y" || k === "x" ) {
554
591
facets = [ range ( data ) ] ;
555
592
} else {
556
593
channels [ k ] = { scale : k , value : identity } ;
557
594
}
558
595
}
559
- initialize ?. call ( this , scale , data , ticks , channels ) ;
596
+ initialize ?. call ( this , scale , data , ticks , tickFormat , channels ) ;
560
597
const initializedChannels = Object . fromEntries (
561
598
Object . entries ( channels ) . map ( ( [ name , channel ] ) => {
562
599
return [ name , { ...channel , value : valueof ( data , channel . value ) } ] ;
@@ -580,29 +617,39 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
580
617
return m ;
581
618
}
582
619
620
+ function inferTickCount ( scale , tickSpacing ) {
621
+ const [ min , max ] = extent ( scale . range ( ) ) ;
622
+ return ( max - min ) / tickSpacing ;
623
+ }
624
+
583
625
function inferTextChannel ( scale , data , ticks , tickFormat , anchor ) {
584
626
return { value : inferTickFormat ( scale , data , ticks , tickFormat , anchor ) } ;
585
627
}
586
628
587
629
// D3’s ordinal scales simply use toString by default, but if the ordinal scale
588
630
// domain (or ticks) are numbers or dates (say because we’re applying a time
589
- // interval to the ordinal scale), we want Plot’s default formatter.
631
+ // interval to the ordinal scale), we want Plot’s default formatter. And for
632
+ // time ticks, we want to use the multi-line time format (e.g., Jan 26) if
633
+ // possible, or the default ISO format (2014-01-26). TODO We need a better way
634
+ // to infer whether the ordinal scale is UTC or local time.
590
635
export function inferTickFormat ( scale , data , ticks , tickFormat , anchor ) {
591
- return tickFormat === undefined && isTemporalScale ( scale )
592
- ? formatTimeTicks ( scale , data , ticks , anchor )
636
+ return typeof tickFormat === "function"
637
+ ? tickFormat
638
+ : tickFormat === undefined && data && isTemporal ( data )
639
+ ? inferTimeFormat ( data , anchor ) ?? formatDefault
593
640
: scale . tickFormat
594
- ? scale . tickFormat ( isIterable ( ticks ) ? null : ticks , tickFormat )
641
+ ? scale . tickFormat ( typeof ticks === "number" ? ticks : null , tickFormat )
595
642
: tickFormat === undefined
596
- ? isUtcYear ( scale . interval )
597
- ? utcFormat ( "%Y" )
598
- : isTimeYear ( scale . interval )
599
- ? timeFormat ( "%Y" )
600
- : formatDefault
643
+ ? formatDefault
601
644
: typeof tickFormat === "string"
602
645
? ( isTemporal ( scale . domain ( ) ) ? utcFormat : format ) ( tickFormat )
603
646
: constant ( tickFormat ) ;
604
647
}
605
648
649
+ function inclusiveRange ( interval , min , max ) {
650
+ return interval . range ( min , interval . offset ( interval . floor ( max ) ) ) ;
651
+ }
652
+
606
653
const shapeTickBottom = {
607
654
draw ( context , l ) {
608
655
context . moveTo ( 0 , 0 ) ;
@@ -647,7 +694,7 @@ function inferScaleOrder(scale) {
647
694
// Takes the scale label, and if this is not an ordinal scale and the label was
648
695
// inferred from an associated channel, adds an orientation-appropriate arrow.
649
696
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 ;
697
+ if ( label == null || ( label . inferred && hasTemporalDomain ( scale ) && / ^ ( d a t e | t i m e | y e a r ) $ / i. test ( label ) ) ) return ;
651
698
label = String ( label ) ; // coerce to a string after checking if inferred
652
699
if ( labelArrow === "auto" ) labelArrow = ( ! scale . bandwidth || scale . interval ) && ! / [ ↑ ↓ → ← ] / . test ( label ) ;
653
700
if ( ! labelArrow ) return label ;
@@ -684,6 +731,6 @@ function maybeLabelArrow(labelArrow = "auto") {
684
731
: keyword ( labelArrow , "labelArrow" , [ "auto" , "up" , "right" , "down" , "left" ] ) ;
685
732
}
686
733
687
- function isTemporalish ( scale ) {
688
- return isTemporalScale ( scale ) || scale . interval != null ;
734
+ function hasTemporalDomain ( scale ) {
735
+ return isTemporal ( scale . domain ( ) ) ;
689
736
}
0 commit comments