1- import { extent , format , timeFormat , utcFormat } from "d3" ;
1+ import { InternSet , extent , format , utcFormat } from "d3" ;
22import { formatDefault } from "../format.js" ;
33import { marks } from "../mark.js" ;
44import { radians } from "../math.js" ;
55import { 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" ;
77import { maybeColorChannel , maybeNumberChannel , maybeRangeInterval } from "../options.js" ;
8- import { isTemporalScale } from "../scales.js" ;
98import { offset } from "../style.js" ;
10- import { formatTimeTicks , isTimeYear , isUtcYear } from "../time.js" ;
9+ import { generalizeTimeInterval , inferTimeFormat , intervalDuration } from "../time.js" ;
1110import { initializer } from "../transforms/basic.js" ;
11+ import { warn } from "../warnings.js" ;
1212import { ruleX , ruleY } from "./rule.js" ;
1313import { text , textX , textY } from "./text.js" ;
1414import { vectorX , vectorY } from "./vector.js" ;
@@ -277,7 +277,7 @@ function axisTickKy(
277277 ...options
278278 }
279279) {
280- return axisMark ( vectorY , k , `${ k } -axis tick` , data , {
280+ return axisMark ( vectorY , k , anchor , `${ k } -axis tick` , data , {
281281 strokeWidth,
282282 strokeLinecap,
283283 strokeLinejoin,
@@ -311,7 +311,7 @@ function axisTickKx(
311311 ...options
312312 }
313313) {
314- return axisMark ( vectorX , k , `${ k } -axis tick` , data , {
314+ return axisMark ( vectorX , k , anchor , `${ k } -axis tick` , data , {
315315 strokeWidth,
316316 strokeLinejoin,
317317 strokeLinecap,
@@ -336,8 +336,7 @@ function axisTextKy(
336336 tickSize,
337337 tickRotate = 0 ,
338338 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,
341340 textAnchor = Math . abs ( tickRotate ) > 60 ? "middle" : anchor === "left" ? "end" : "start" ,
342341 lineAnchor = tickRotate > 60 ? "top" : tickRotate < - 60 ? "bottom" : "middle" ,
343342 fontVariant,
@@ -352,12 +351,13 @@ function axisTextKy(
352351 return axisMark (
353352 textY ,
354353 k ,
354+ anchor ,
355355 `${ k } -axis tick label` ,
356356 data ,
357357 {
358358 facetAnchor,
359359 frameAnchor,
360- text : text === undefined ? null : text ,
360+ text,
361361 textAnchor,
362362 lineAnchor,
363363 fontVariant,
@@ -366,7 +366,7 @@ function axisTextKy(
366366 ...options ,
367367 dx : anchor === "left" ? + dx - tickSize - tickPadding + + insetLeft : + dx + + tickSize + + tickPadding - insetRight
368368 } ,
369- function ( scale , data , ticks , channels ) {
369+ function ( scale , data , ticks , tickFormat , channels ) {
370370 if ( fontVariant === undefined ) this . fontVariant = inferFontVariant ( scale ) ;
371371 if ( text === undefined ) channels . text = inferTextChannel ( scale , data , ticks , tickFormat , anchor ) ;
372372 }
@@ -383,8 +383,7 @@ function axisTextKx(
383383 tickSize,
384384 tickRotate = 0 ,
385385 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,
388387 textAnchor = Math . abs ( tickRotate ) >= 10 ? ( ( tickRotate < 0 ) ^ ( anchor === "bottom" ) ? "start" : "end" ) : "middle" ,
389388 lineAnchor = Math . abs ( tickRotate ) >= 10 ? "middle" : anchor === "bottom" ? "top" : "bottom" ,
390389 fontVariant,
@@ -399,6 +398,7 @@ function axisTextKx(
399398 return axisMark (
400399 textX ,
401400 k ,
401+ anchor ,
402402 `${ k } -axis tick label` ,
403403 data ,
404404 {
@@ -413,7 +413,7 @@ function axisTextKx(
413413 ...options ,
414414 dy : anchor === "bottom" ? + dy + + tickSize + + tickPadding - insetBottom : + dy - tickSize - tickPadding + + insetTop
415415 } ,
416- function ( scale , data , ticks , channels ) {
416+ function ( scale , data , ticks , tickFormat , channels ) {
417417 if ( fontVariant === undefined ) this . fontVariant = inferFontVariant ( scale ) ;
418418 if ( text === undefined ) channels . text = inferTextChannel ( scale , data , ticks , tickFormat , anchor ) ;
419419 }
@@ -452,7 +452,7 @@ function gridKy(
452452 ...options
453453 }
454454) {
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 ) } ) ;
456456}
457457
458458function gridKx (
@@ -467,7 +467,7 @@ function gridKx(
467467 ...options
468468 }
469469) {
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 ) } ) ;
471471}
472472
473473function gridDefaults ( {
@@ -517,46 +517,83 @@ function labelOptions(
517517 } ;
518518}
519519
520- function axisMark ( mark , k , ariaLabel , data , options , initialize ) {
520+ function axisMark ( mark , k , anchor , ariaLabel , data , options , initialize ) {
521521 let channels ;
522522
523523 function axisInitializer ( data , facets , _channels , scales , dimensions , context ) {
524524 const initializeFacets = data == null && ( k === "fx" || k === "fy" ) ;
525525 const { [ k ] : scale } = scales ;
526526 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 ) ;
529539 if ( data == null ) {
530540 if ( isIterable ( ticks ) ) {
541+ // Use explicit ticks, if specified.
531542 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 ) ;
535559 } 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 ) ;
549573 }
574+ } else if ( scale . ticks ) {
575+ data = scale . ticks ( ticks ) ;
550576 } 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
552589 }
553590 if ( k === "y" || k === "x" ) {
554591 facets = [ range ( data ) ] ;
555592 } else {
556593 channels [ k ] = { scale : k , value : identity } ;
557594 }
558595 }
559- initialize ?. call ( this , scale , data , ticks , channels ) ;
596+ initialize ?. call ( this , scale , data , ticks , tickFormat , channels ) ;
560597 const initializedChannels = Object . fromEntries (
561598 Object . entries ( channels ) . map ( ( [ name , channel ] ) => {
562599 return [ name , { ...channel , value : valueof ( data , channel . value ) } ] ;
@@ -580,29 +617,39 @@ function axisMark(mark, k, ariaLabel, data, options, initialize) {
580617 return m ;
581618}
582619
620+ function inferTickCount ( scale , tickSpacing ) {
621+ const [ min , max ] = extent ( scale . range ( ) ) ;
622+ return ( max - min ) / tickSpacing ;
623+ }
624+
583625function inferTextChannel ( scale , data , ticks , tickFormat , anchor ) {
584626 return { value : inferTickFormat ( scale , data , ticks , tickFormat , anchor ) } ;
585627}
586628
587629// D3’s ordinal scales simply use toString by default, but if the ordinal scale
588630// 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.
590635export 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
593640 : scale . tickFormat
594- ? scale . tickFormat ( isIterable ( ticks ) ? null : ticks , tickFormat )
641+ ? scale . tickFormat ( typeof ticks === "number" ? ticks : null , tickFormat )
595642 : tickFormat === undefined
596- ? isUtcYear ( scale . interval )
597- ? utcFormat ( "%Y" )
598- : isTimeYear ( scale . interval )
599- ? timeFormat ( "%Y" )
600- : formatDefault
643+ ? formatDefault
601644 : typeof tickFormat === "string"
602645 ? ( isTemporal ( scale . domain ( ) ) ? utcFormat : format ) ( tickFormat )
603646 : constant ( tickFormat ) ;
604647}
605648
649+ function inclusiveRange ( interval , min , max ) {
650+ return interval . range ( min , interval . offset ( interval . floor ( max ) ) ) ;
651+ }
652+
606653const shapeTickBottom = {
607654 draw ( context , l ) {
608655 context . moveTo ( 0 , 0 ) ;
@@ -647,7 +694,7 @@ function inferScaleOrder(scale) {
647694// Takes the scale label, and if this is not an ordinal scale and the label was
648695// inferred from an associated channel, adds an orientation-appropriate arrow.
649696function 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 ;
651698 label = String ( label ) ; // coerce to a string after checking if inferred
652699 if ( labelArrow === "auto" ) labelArrow = ( ! scale . bandwidth || scale . interval ) && ! / [ ↑ ↓ → ← ] / . test ( label ) ;
653700 if ( ! labelArrow ) return label ;
@@ -684,6 +731,6 @@ function maybeLabelArrow(labelArrow = "auto") {
684731 : keyword ( labelArrow , "labelArrow" , [ "auto" , "up" , "right" , "down" , "left" ] ) ;
685732}
686733
687- function isTemporalish ( scale ) {
688- return isTemporalScale ( scale ) || scale . interval != null ;
734+ function hasTemporalDomain ( scale ) {
735+ return isTemporal ( scale . domain ( ) ) ;
689736}
0 commit comments