@@ -34,7 +34,10 @@ export class Tip extends Mark {
34
34
lineHeight = 1 ,
35
35
lineWidth = 20 ,
36
36
frameAnchor,
37
- textAnchor = "start"
37
+ textAnchor = "start" ,
38
+ textPadding = 8 ,
39
+ pointerSize = 12 ,
40
+ pathFilter = "drop-shadow(0 3px 4px rgba(0,0,0,0.2))"
38
41
} = options ;
39
42
super (
40
43
data ,
@@ -53,8 +56,9 @@ export class Tip extends Mark {
53
56
this . previousAnchor = this . anchor ?? "top-left" ;
54
57
this . frameAnchor = maybeFrameAnchor ( frameAnchor ) ;
55
58
this . textAnchor = impliedString ( textAnchor , "middle" ) ;
56
- this . textPadding = 8 ; // TODO option
57
- this . pointerSize = 12 ; // TODO option
59
+ this . textPadding = + textPadding ;
60
+ this . pointerSize = + pointerSize ;
61
+ this . pathFilter = string ( pathFilter ) ;
58
62
this . lineHeight = + lineHeight ;
59
63
this . lineWidth = + lineWidth ;
60
64
this . monospace = ! ! monospace ;
@@ -63,21 +67,25 @@ export class Tip extends Mark {
63
67
this . fontStyle = string ( fontStyle ) ;
64
68
this . fontVariant = string ( fontVariant ) ;
65
69
this . fontWeight = string ( fontWeight ) ;
66
- this . imageFilter = "drop-shadow(0 3px 4px rgba(0,0,0,0.2))" ; // TODO option
67
70
}
68
71
render ( index , scales , channels , dimensions , context ) {
69
72
const mark = this ;
70
73
const { x, y, fx, fy} = scales ;
71
74
const { ownerSVGElement : svg , document} = context ;
72
- const { anchor, monospace, lineHeight, lineWidth, textPadding : r , pointerSize : m } = this ;
75
+ const { anchor, monospace, lineHeight, lineWidth} = this ;
76
+ const { textPadding : r , pointerSize : m , pathFilter} = this ;
73
77
const { marginTop, marginLeft} = dimensions ;
74
78
75
79
// The anchor position is the middle of x1 & y1 and x2 & y2, if available,
76
80
// or x & y; the former is considered more specific because it’s how we
77
81
// disable the implicit stack and interval transforms. If any dimension is
78
- // unspecified, we fallback to the frame anchor.
82
+ // unspecified, we fallback to the frame anchor. We also need to know the
83
+ // facet offsets to detect when the tip would draw outside the plot, and
84
+ // thus we need to change the orientation.
79
85
const { x : X , y : Y , x1 : X1 , y1 : Y1 , x2 : X2 , y2 : Y2 , channels : sources } = channels ;
80
86
const [ cx , cy ] = applyFrameAnchor ( this , dimensions ) ;
87
+ const ox = fx ? fx ( index . fx ) - marginLeft : 0 ;
88
+ const oy = fy ? fy ( index . fy ) - marginTop : 0 ;
81
89
const px = X2 ? ( i ) => ( X1 [ i ] + X2 [ i ] ) / 2 : X ? ( i ) => X [ i ] : ( ) => cx ;
82
90
const py = Y2 ? ( i ) => ( Y1 [ i ] + Y2 [ i ] ) / 2 : Y ? ( i ) => Y [ i ] : ( ) => cy ;
83
91
@@ -105,13 +113,15 @@ export class Tip extends Mark {
105
113
. attr ( "transform" , ( i ) => `translate(${ px ( i ) } ,${ py ( i ) } )` )
106
114
. call ( applyDirectStyles , this )
107
115
. call ( applyChannelStyles , this , channels )
108
- . call ( ( g ) => g . append ( "path" ) )
116
+ . call ( ( g ) => g . append ( "path" ) . attr ( "filter" , pathFilter ) )
109
117
. call ( ( g ) =>
110
118
g . append ( "text" ) . each ( function ( i ) {
111
119
const that = select ( this ) ;
120
+ // prevent style inheritance (from path)
112
121
this . setAttribute ( "fill" , "currentColor" ) ;
113
122
this . setAttribute ( "fill-opacity" , 1 ) ;
114
123
this . setAttribute ( "stroke" , "none" ) ;
124
+ // iteratively render each channel value
115
125
for ( const key in sources ) {
116
126
const channel = getSource ( sources , key ) ;
117
127
if ( ! channel ) continue ; // e.g., dodgeY’s y
@@ -123,8 +133,8 @@ export class Tip extends Mark {
123
133
renderLine (
124
134
that ,
125
135
scales [ channel . scale ] ?. label ?? channel . label ?? key ,
126
- channel2
127
- ? channel2 . hint ?. length
136
+ channel2 // e.g., binX’s x1 and x2
137
+ ? channel2 . hint ?. length // e.g., stackY’s y1 and y2
128
138
? `${ formatDefault ( value2 - value1 ) } `
129
139
: `${ formatDefault ( value1 ) } –${ formatDefault ( value2 ) } `
130
140
: formatDefault ( value1 )
@@ -137,7 +147,12 @@ export class Tip extends Mark {
137
147
)
138
148
) ;
139
149
140
- function renderLine ( that , name , value ) {
150
+ // Renders a single line (a name-value pair) to the tip, truncating the text
151
+ // as needed, and adding a title if the text is truncated. Note that this is
152
+ // just the initial layout of the text; in postrender we will compute the
153
+ // exact text metrics and translate the text as needed once we know the
154
+ // tip’s orientation (anchor).
155
+ function renderLine ( selection , name , value ) {
141
156
let title ;
142
157
let w = lineWidth * 100 ;
143
158
const [ j ] = cut ( name , w , widthof , ee ) ;
@@ -155,23 +170,23 @@ export class Tip extends Mark {
155
170
title = value . trim ( ) ;
156
171
}
157
172
}
158
- const line = that . append ( "tspan" ) . attr ( "x" , 0 ) . attr ( "dy" , `${ lineHeight } em` ) ;
173
+ const line = selection . append ( "tspan" ) . attr ( "x" , 0 ) . attr ( "dy" , `${ lineHeight } em` ) ;
159
174
line . append ( "tspan" ) . attr ( "font-weight" , "bold" ) . text ( name ) ;
160
175
if ( value ) line . append ( ( ) => document . createTextNode ( value ) ) ;
161
176
if ( title ) line . append ( "title" ) . text ( title ) ;
162
177
}
163
178
179
+ // Only after the plot is attached to the page can we compute the exact text
180
+ // metrics needed to determine the tip size and orientation (anchor).
164
181
function postrender ( ) {
165
182
const { width, height} = svg . getBBox ( ) ;
166
- const ox = fx ? fx ( index . fx ) - marginLeft : 0 ;
167
- const oy = fy ? fy ( index . fy ) - marginTop : 0 ;
168
183
g . selectChildren ( ) . each ( function ( i ) {
169
- const x = px ( i ) + ox ;
170
- const y = py ( i ) + oy ;
171
184
const { x : tx , width : w , height : h } = this . getBBox ( ) ;
172
- let a = anchor ;
185
+ let a = anchor ; // use the specified anchor, if any
173
186
if ( a === undefined ) {
174
- a = mark . previousAnchor ;
187
+ a = mark . previousAnchor ; // favor the previous anchor, if it fits
188
+ const x = px ( i ) + ox ;
189
+ const y = py ( i ) + oy ;
175
190
const fitLeft = x + w + r * 2 < width ;
176
191
const fitRight = x - w - r * 2 > 0 ;
177
192
const fitTop = y + h + m + r * 2 + 7 < height ;
@@ -180,19 +195,22 @@ export class Tip extends Mark {
180
195
const ay = ( / ^ t o p - / . test ( a ) ? fitTop || ! fitBottom : fitTop && ! fitBottom ) ? "top" : "bottom" ;
181
196
a = mark . previousAnchor = `${ ay } -${ ax } ` ;
182
197
}
183
- const path = this . firstChild ;
184
- const text = this . lastChild ;
198
+ const path = this . firstChild ; // note: assumes exactly two children!
199
+ const text = this . lastChild ; // note: assumes exactly two children!
185
200
path . setAttribute ( "d" , getPath ( a , m , r , w , h ) ) ;
186
201
if ( tx ) for ( const t of text . childNodes ) t . setAttribute ( "x" , - tx ) ;
187
202
text . setAttribute ( "y" , `${ + getLineOffset ( a , text . childNodes . length , lineHeight ) . toFixed ( 6 ) } em` ) ;
188
203
text . setAttribute ( "transform" , getTextTransform ( a , m , r , w , h ) ) ;
189
204
} ) ;
190
205
}
191
206
192
- // Wait until the Plot is inserted into the page so that we can use getBBox
193
- // to compute the exact text dimensions. Perhaps this could be done
194
- // synchronously; getting the dimensions of the SVG is easy, and although
195
- // accurate text metrics are hard, we could use approximate heuristics.
207
+ // Wait until the plot is inserted into the page so that we can use getBBox
208
+ // to compute the exact text dimensions. If the SVG is already connected, as
209
+ // when the pointer interaction triggers the re-render, use a faster
210
+ // microtask instead of an animation frame; if this SSR (e.g., JSDOM), skip
211
+ // this step. Perhaps this could be done synchronously; getting the
212
+ // dimensions of the SVG is easy, and although accurate text metrics are
213
+ // hard, we could use approximate heuristics.
196
214
if ( svg . isConnected ) Promise . resolve ( ) . then ( postrender ) ;
197
215
else if ( typeof requestAnimationFrame !== "undefined" ) requestAnimationFrame ( postrender ) ;
198
216
0 commit comments