1- import { select } from "d3" ;
1+ import { select , format as numberFormat } from "d3" ;
22import { getSource } from "../channel.js" ;
33import { create } from "../context.js" ;
44import { defined } from "../defined.js" ;
55import { formatDefault } from "../format.js" ;
66import { anchorX , anchorY } from "../interactions/pointer.js" ;
77import { Mark } from "../mark.js" ;
8- import { maybeAnchor , maybeFrameAnchor , maybeFunction , maybeTuple , number , string } from "../options.js" ;
8+ import { maybeAnchor , maybeFrameAnchor , maybeTuple , number , string } from "../options.js" ;
99import { applyDirectStyles , applyFrameAnchor , applyIndirectStyles , applyTransform , impliedString } from "../style.js" ;
10- import { identity , isIterable , isTextual , isObject } from "../options.js" ;
10+ import { identity , isIterable , isTextual , isObject , labelof , maybeValue } from "../options.js" ;
1111import { inferTickFormat } from "./axis.js" ;
1212import { applyIndirectTextStyles , defaultWidth , ellipsis , monospaceWidth } from "./text.js" ;
1313import { cut , clipper , splitter , maybeTextOverflow } from "./text.js" ;
@@ -83,7 +83,7 @@ export class Tip extends Mark {
8383 for ( const key in defaults ) if ( key in this . channels ) this [ key ] = defaults [ key ] ; // apply default even if channel
8484 this . splitLines = splitter ( this ) ;
8585 this . clipLine = clipper ( this ) ;
86- this . format = maybeFunction ( format ) ;
86+ this . format = maybeTipFormat ( this . channels , format ) ;
8787 }
8888 render ( index , scales , values , dimensions , context ) {
8989 const mark = this ;
@@ -119,12 +119,12 @@ export class Tip extends Mark {
119119 // Determine the appropriate formatter.
120120 const format =
121121 this . format !== undefined
122- ? formatData ( this . format , values . data ) // use the custom format, if any
122+ ? this . format ( values ) // use the custom format, if any
123123 : "title" in sources // if there is a title channel
124124 ? formatTitle // display the title as-is
125125 : index . fi == null // if this mark is not faceted
126126 ? formatChannels // display name-value pairs for channels
127- : formatFacetedChannels ( index , scales ) ; // same, plus facets
127+ : formatFacetedChannels ( scales ) ; // same, plus facets
128128
129129 // We don’t call applyChannelStyles because we only use the channels to
130130 // derive the content of the tip, not its aesthetics.
@@ -149,17 +149,17 @@ export class Tip extends Mark {
149149 this . setAttribute ( "fill-opacity" , 1 ) ;
150150 this . setAttribute ( "stroke" , "none" ) ;
151151 // iteratively render each channel value
152- const names = new Set ( ) ;
153- const lines = format . call ( mark , i , sources , scales , values ) ;
152+ const labels = new Set ( ) ;
153+ const lines = format . call ( mark , i , index , sources , scales , values ) ;
154154 if ( typeof lines === "string" ) {
155155 for ( const line of mark . splitLines ( lines ) ) {
156156 renderLine ( that , { value : mark . clipLine ( line ) } ) ;
157157 }
158158 } else {
159159 for ( const line of lines ) {
160- const { name = "" } = line ;
161- if ( name && names . has ( name ) ) continue ;
162- else names . add ( name ) ;
160+ const { label = "" } = line ;
161+ if ( label && labels . has ( label ) ) continue ;
162+ else labels . add ( label ) ;
163163 renderLine ( that , line ) ;
164164 }
165165 }
@@ -172,27 +172,29 @@ export class Tip extends Mark {
172172 // just the initial layout of the text; in postrender we will compute the
173173 // exact text metrics and translate the text as needed once we know the
174174 // tip’s orientation (anchor).
175- function renderLine ( selection , { name = "" , value = "" , color, opacity} ) {
175+ function renderLine ( selection , { label, value, color, opacity} ) {
176+ label ??= "" ; // TODO fix earlier?
177+ value ??= "" ; // TODO fix earlier?
176178 const swatch = color != null || opacity != null ;
177179 let title ;
178180 let w = lineWidth * 100 ;
179- const [ j ] = cut ( name , w , widthof , ee ) ;
181+ const [ j ] = cut ( label , w , widthof , ee ) ;
180182 if ( j >= 0 ) {
181- // name is truncated
182- name = name . slice ( 0 , j ) . trimEnd ( ) + ellipsis ;
183+ // label is truncated
184+ label = label . slice ( 0 , j ) . trimEnd ( ) + ellipsis ;
183185 title = value . trim ( ) ;
184186 value = "" ;
185187 } else {
186- if ( name || ( ! value && ! swatch ) ) value = " " + value ;
187- const [ k ] = cut ( value , w - widthof ( name ) , widthof , ee ) ;
188+ if ( label || ( ! value && ! swatch ) ) value = " " + value ;
189+ const [ k ] = cut ( value , w - widthof ( label ) , widthof , ee ) ;
188190 if ( k >= 0 ) {
189191 // value is truncated
190192 value = value . slice ( 0 , k ) . trimEnd ( ) + ellipsis ;
191193 title = value . trim ( ) ;
192194 }
193195 }
194196 const line = selection . append ( "tspan" ) . attr ( "x" , 0 ) . attr ( "dy" , `${ lineHeight } em` ) . text ( "\u200b" ) ; // zwsp for double-click
195- if ( name ) line . append ( "tspan" ) . attr ( "font-weight" , "bold" ) . text ( name ) ;
197+ if ( label ) line . append ( "tspan" ) . attr ( "font-weight" , "bold" ) . text ( label ) ;
196198 if ( value ) line . append ( ( ) => document . createTextNode ( value ) ) ;
197199 if ( swatch ) line . append ( "tspan" ) . text ( " ■" ) . attr ( "fill" , color ) . attr ( "fill-opacity" , opacity ) . style ( "user-select" , "none" ) ; // prettier-ignore
198200 if ( title ) line . append ( "title" ) . text ( title ) ;
@@ -319,53 +321,108 @@ function getSources({channels}) {
319321function formatData ( format , data ) {
320322 return function ( i ) {
321323 let result = format . call ( this , data [ i ] , i ) ;
322- if ( isObject ( result ) ) result = Object . entries ( result ) . map ( ( [ name , value ] ) => ( { name , value} ) ) ;
324+ if ( isObject ( result ) ) result = Object . entries ( result ) . map ( ( [ label , value ] ) => ( { label , value} ) ) ;
323325 return result ;
324326 } ;
325327}
326328
327- function formatTitle ( i , { title} ) {
329+ // Requirements
330+ // - To add a channel to the tip (e.g., to add the “name” field)
331+ // - To control how a channel value is formatted (e.g., ".2f" for x)
332+ // - To remove a channel from the tip (e.g., to suppress x) [optional]
333+ // - To change how a channel is labeled (alternative to label scale option?) [optional]
334+ // Note: mutates channels!
335+ function maybeTipFormat ( channels , format ) {
336+ if ( format === undefined ) return ;
337+ if ( typeof format === "function" ) return ( { data} ) => formatData ( format , data ) ;
338+ format = Array . from ( format , ( f ) => {
339+ if ( typeof f === "string" ) f = channels [ f ] ? { channel : f } : { value : f } ; // shorthand string
340+ f = maybeValue ( f ) ; // shorthand function, array, etc.
341+ if ( typeof f . format === "string" ) f = { ...f , format : numberFormat ( f . format ) } ; // shorthand format; TODO dates
342+ if ( f . value !== undefined ) f = { ...f , channel : deriveChannel ( channels , f ) } ; // shorthand channel
343+ return f ;
344+ } ) ;
345+ return ( ) => {
346+ return function * ( i , index , channels , scales , values ) {
347+ for ( const { label, channel : key , format : formatValue } of format ) {
348+ for ( const l of formatChannel ( key , i , index , channels , scales , values , formatValue ) ) {
349+ if ( label !== undefined ) l . label = label ; // TODO clean this up
350+ yield l ;
351+ }
352+ }
353+ } ;
354+ } ;
355+ }
356+
357+ let nextTipId = 0 ;
358+
359+ // Note: mutates channels!
360+ function deriveChannel ( channels , f ) {
361+ const key = `--tip-${ ++ nextTipId } ` ; // TODO better anonymous channels
362+ const { value, label = labelof ( value ) ?? "" } = f ;
363+ channels [ key ] = { label, value, filter : null } ;
364+ return key ;
365+ }
366+
367+ function formatTitle ( i , index , { title} ) {
328368 return formatDefault ( title . value [ i ] ) ;
329369}
330370
331- function * formatChannels ( i , channels , scales , values ) {
371+ function * formatChannels ( i , index , channels , scales , values ) {
332372 for ( const key in channels ) {
333- if ( key === "x1" && "x2" in channels ) continue ;
334- if ( key === "y1" && "y2" in channels ) continue ;
335- const channel = channels [ key ] ;
336- const value = channel . value [ i ] ;
337- if ( ! defined ( value ) && channel . scale == null ) continue ;
338- if ( key === "x2" && "x1" in channels ) {
339- yield { name : formatPairLabel ( scales , channels . x1 , channel , "x" ) , value : formatPair ( channels . x1 , channel , i ) } ;
340- } else if ( key === "y2" && "y1" in channels ) {
341- yield { name : formatPairLabel ( scales , channels . y1 , channel , "y" ) , value : formatPair ( channels . y1 , channel , i ) } ;
342- } else {
343- const scale = channel . scale ;
344- const line = { name : formatLabel ( scales , channel , key ) , value : formatDefault ( value ) } ;
345- if ( scale === "color" || scale === "opacity" ) line [ scale ] = values [ key ] [ i ] ;
346- yield line ;
347- }
373+ if ( key === "scales" ) continue ; // not really a channel… TODO make this non-enumerable?
374+ yield * formatChannel ( key , i , index , channels , scales , values ) ;
348375 }
349376}
350377
351- function formatFacetedChannels ( index , scales ) {
352- const { fx, fy} = scales ;
378+ function * formatChannel (
379+ key ,
380+ i ,
381+ index ,
382+ channels ,
383+ scales ,
384+ values ,
353385 // We borrow the scale’s tick format for facet channels; this is safe for
354386 // ordinal scales (but not continuous scales where the display value may need
355387 // higher precision), and generally better than the default format.
356- const formatFx = fx && inferTickFormat ( fx ) ;
357- const formatFy = fy && inferTickFormat ( fy ) ;
358- return function * ( i , channels , scales , values ) {
359- yield * formatChannels ( i , channels , scales , values ) ;
360- if ( fx ) yield { name : String ( fx . label ?? "fx" ) , value : formatFx ( index . fx ) } ;
361- if ( fy ) yield { name : String ( fy . label ?? "fy" ) , value : formatFy ( index . fy ) } ;
388+ // TODO inferring the tick format each time we format is too slow!
389+ formatValue = key === "fx" ? inferTickFormat ( scales . fx ) : key === "fy" ? inferTickFormat ( scales . fy ) : formatDefault
390+ ) {
391+ if ( key === "x1" && "x2" in channels ) return ;
392+ if ( key === "y1" && "y2" in channels ) return ;
393+ const channel = key === "fx" ? { scale : "fx" } : key === "fy" ? { scale : "fy" } : channels [ key ] ;
394+ let value = key === "fx" ? index . fx : key === "fy" ? index . fy : channel . value [ i ] ;
395+ if ( ! defined ( value ) && channel . scale == null ) return ;
396+ let label , color , opacity ;
397+ if ( key === "x2" && "x1" in channels ) {
398+ label = formatPairLabel ( scales , channels . x1 , channel , "x" ) ;
399+ value = formatPair ( formatValue , channels . x1 , channel , i ) ;
400+ } else if ( key === "y2" && "y1" in channels ) {
401+ label = formatPairLabel ( scales , channels . y1 , channel , "y" ) ;
402+ value = formatPair ( formatValue , channels . y1 , channel , i ) ;
403+ } else {
404+ const scale = channel . scale ;
405+ label = formatLabel ( scales , channel , key ) ;
406+ value = formatValue ( value ) ;
407+ if ( scale === "color" ) color = values [ key ] [ i ] ;
408+ else if ( scale === "opacity" ) opacity = values [ key ] [ i ] ;
409+ }
410+ yield { label, value, color, opacity} ;
411+ }
412+
413+ function formatFacetedChannels ( scales ) {
414+ const { fx, fy} = scales ;
415+ return function * ( i , index , channels , scales , values ) {
416+ yield * formatChannels ( i , index , channels , scales , values ) ;
417+ if ( fx ) yield * formatChannel ( "fx" , i , index , channels , scales , values ) ;
418+ if ( fy ) yield * formatChannel ( "fy" , i , index , channels , scales , values ) ;
362419 } ;
363420}
364421
365- function formatPair ( c1 , c2 , i ) {
422+ function formatPair ( formatValue , c1 , c2 , i ) {
366423 return c2 . hint ?. length // e.g., stackY’s y1 and y2
367- ? `${ formatDefault ( c2 . value [ i ] - c1 . value [ i ] ) } `
368- : `${ formatDefault ( c1 . value [ i ] ) } –${ formatDefault ( c2 . value [ i ] ) } ` ;
424+ ? `${ formatValue ( c2 . value [ i ] - c1 . value [ i ] ) } `
425+ : `${ formatValue ( c1 . value [ i ] ) } –${ formatValue ( c2 . value [ i ] ) } ` ;
369426}
370427
371428function formatPairLabel ( scales , c1 , c2 , defaultLabel ) {
0 commit comments