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 ) ;
@@ -315,53 +317,108 @@ function getSources({channels}) {
315317function formatData ( format , data ) {
316318 return function ( i ) {
317319 let result = format . call ( this , data [ i ] , i ) ;
318- if ( isObject ( result ) ) result = Object . entries ( result ) . map ( ( [ name , value ] ) => ( { name , value} ) ) ;
320+ if ( isObject ( result ) ) result = Object . entries ( result ) . map ( ( [ label , value ] ) => ( { label , value} ) ) ;
319321 return result ;
320322 } ;
321323}
322324
323- function formatTitle ( i , { title} ) {
325+ // Requirements
326+ // - To add a channel to the tip (e.g., to add the “name” field)
327+ // - To control how a channel value is formatted (e.g., ".2f" for x)
328+ // - To remove a channel from the tip (e.g., to suppress x) [optional]
329+ // - To change how a channel is labeled (alternative to label scale option?) [optional]
330+ // Note: mutates channels!
331+ function maybeTipFormat ( channels , format ) {
332+ if ( format === undefined ) return ;
333+ if ( typeof format === "function" ) return ( { data} ) => formatData ( format , data ) ;
334+ format = Array . from ( format , ( f ) => {
335+ if ( typeof f === "string" ) f = channels [ f ] ? { channel : f } : { value : f } ; // shorthand string
336+ f = maybeValue ( f ) ; // shorthand function, array, etc.
337+ if ( typeof f . format === "string" ) f = { ...f , format : numberFormat ( f . format ) } ; // shorthand format; TODO dates
338+ if ( f . value !== undefined ) f = { ...f , channel : deriveChannel ( channels , f ) } ; // shorthand channel
339+ return f ;
340+ } ) ;
341+ return ( ) => {
342+ return function * ( i , index , channels , scales , values ) {
343+ for ( const { label, channel : key , format : formatValue } of format ) {
344+ for ( const l of formatChannel ( key , i , index , channels , scales , values , formatValue ) ) {
345+ if ( label !== undefined ) l . label = label ; // TODO clean this up
346+ yield l ;
347+ }
348+ }
349+ } ;
350+ } ;
351+ }
352+
353+ let nextTipId = 0 ;
354+
355+ // Note: mutates channels!
356+ function deriveChannel ( channels , f ) {
357+ const key = `--tip-${ ++ nextTipId } ` ; // TODO better anonymous channels
358+ const { value, label = labelof ( value ) ?? "" } = f ;
359+ channels [ key ] = { label, value, filter : null } ;
360+ return key ;
361+ }
362+
363+ function formatTitle ( i , index , { title} ) {
324364 return formatDefault ( title . value [ i ] ) ;
325365}
326366
327- function * formatChannels ( i , channels , scales , values ) {
367+ function * formatChannels ( i , index , channels , scales , values ) {
328368 for ( const key in channels ) {
329- if ( key === "x1" && "x2" in channels ) continue ;
330- if ( key === "y1" && "y2" in channels ) continue ;
331- const channel = channels [ key ] ;
332- const value = channel . value [ i ] ;
333- if ( ! defined ( value ) && channel . scale == null ) continue ;
334- if ( key === "x2" && "x1" in channels ) {
335- yield { name : formatPairLabel ( scales , channels . x1 , channel , "x" ) , value : formatPair ( channels . x1 , channel , i ) } ;
336- } else if ( key === "y2" && "y1" in channels ) {
337- yield { name : formatPairLabel ( scales , channels . y1 , channel , "y" ) , value : formatPair ( channels . y1 , channel , i ) } ;
338- } else {
339- const scale = channel . scale ;
340- const line = { name : formatLabel ( scales , channel , key ) , value : formatDefault ( value ) } ;
341- if ( scale === "color" || scale === "opacity" ) line [ scale ] = values [ key ] [ i ] ;
342- yield line ;
343- }
369+ if ( key === "scales" ) continue ; // not really a channel… TODO make this non-enumerable?
370+ yield * formatChannel ( key , i , index , channels , scales , values ) ;
344371 }
345372}
346373
347- function formatFacetedChannels ( index , scales ) {
348- const { fx, fy} = scales ;
374+ function * formatChannel (
375+ key ,
376+ i ,
377+ index ,
378+ channels ,
379+ scales ,
380+ values ,
349381 // We borrow the scale’s tick format for facet channels; this is safe for
350382 // ordinal scales (but not continuous scales where the display value may need
351383 // higher precision), and generally better than the default format.
352- const formatFx = fx && inferTickFormat ( fx ) ;
353- const formatFy = fy && inferTickFormat ( fy ) ;
354- return function * ( i , channels , scales , values ) {
355- yield * formatChannels ( i , channels , scales , values ) ;
356- if ( fx ) yield { name : String ( fx . label ?? "fx" ) , value : formatFx ( index . fx ) } ;
357- if ( fy ) yield { name : String ( fy . label ?? "fy" ) , value : formatFy ( index . fy ) } ;
384+ // TODO inferring the tick format each time we format is too slow!
385+ formatValue = key === "fx" ? inferTickFormat ( scales . fx ) : key === "fy" ? inferTickFormat ( scales . fy ) : formatDefault
386+ ) {
387+ if ( key === "x1" && "x2" in channels ) return ;
388+ if ( key === "y1" && "y2" in channels ) return ;
389+ const channel = key === "fx" ? { scale : "fx" } : key === "fy" ? { scale : "fy" } : channels [ key ] ;
390+ let value = key === "fx" ? index . fx : key === "fy" ? index . fy : channel . value [ i ] ;
391+ if ( ! defined ( value ) && channel . scale == null ) return ;
392+ let label , color , opacity ;
393+ if ( key === "x2" && "x1" in channels ) {
394+ label = formatPairLabel ( scales , channels . x1 , channel , "x" ) ;
395+ value = formatPair ( formatValue , channels . x1 , channel , i ) ;
396+ } else if ( key === "y2" && "y1" in channels ) {
397+ label = formatPairLabel ( scales , channels . y1 , channel , "y" ) ;
398+ value = formatPair ( formatValue , channels . y1 , channel , i ) ;
399+ } else {
400+ const scale = channel . scale ;
401+ label = formatLabel ( scales , channel , key ) ;
402+ value = formatValue ( value ) ;
403+ if ( scale === "color" ) color = values [ key ] [ i ] ;
404+ else if ( scale === "opacity" ) opacity = values [ key ] [ i ] ;
405+ }
406+ yield { label, value, color, opacity} ;
407+ }
408+
409+ function formatFacetedChannels ( scales ) {
410+ const { fx, fy} = scales ;
411+ return function * ( i , index , channels , scales , values ) {
412+ yield * formatChannels ( i , index , channels , scales , values ) ;
413+ if ( fx ) yield * formatChannel ( "fx" , i , index , channels , scales , values ) ;
414+ if ( fy ) yield * formatChannel ( "fy" , i , index , channels , scales , values ) ;
358415 } ;
359416}
360417
361- function formatPair ( c1 , c2 , i ) {
418+ function formatPair ( formatValue , c1 , c2 , i ) {
362419 return c2 . hint ?. length // e.g., stackY’s y1 and y2
363- ? `${ formatDefault ( c2 . value [ i ] - c1 . value [ i ] ) } `
364- : `${ formatDefault ( c1 . value [ i ] ) } –${ formatDefault ( c2 . value [ i ] ) } ` ;
420+ ? `${ formatValue ( c2 . value [ i ] - c1 . value [ i ] ) } `
421+ : `${ formatValue ( c1 . value [ i ] ) } –${ formatValue ( c2 . value [ i ] ) } ` ;
365422}
366423
367424function formatPairLabel ( scales , c1 , c2 , defaultLabel ) {
0 commit comments