-
Notifications
You must be signed in to change notification settings - Fork 16
SVGTooltips
js/plot/util/tooltips/
contains the modules necessary for adding tooltips to plotted data in tideline. One of the modules contained within - shapes.js
- translates tooltip shapes from SVG image files obtained from a designer into a data structure that tooltip.js
can use to generate native SVG group elements to encode the background for each tooltip type; this is in contrast to the previous strategy employed in tideline, which was to ping the server whenever a tooltip was requested (i.e., on hover) and layer the image retrieved from the server under the text for the tooltip.
This wiki page documents the process of translating a tooltip background image obtained from a designer (exported as SVG) into a shape in shapes.js
.
In tideline, the tooltips that appear upon hovering over a blood glucose data point obtained from a CGM (datatype 'cbg') are the simplest. The tooltip is simply a teardrop shape with a border the same color as the category of blood glucose value (low, target, or high), and the blood glucose value is printed in the center. The teardrops are designed to be large enough to fit the two- or three-digit blood glucose values that are possible for blood glucose (in mg/dL units):
For low and target blood glucose values, the tooltip extends up and to the right (as pictured above), while for high blood glucose values, the tooltip extends down and to the left:
All tooltips mirror their orientation over the y-axis when they are within three hours of the edge of the one-day view timeline, so that they do not escape the visible area of the display. For low and target blood glucose values, this reversal happens at the right edge of the display:
For high blood glucose values, this reversal happens at the left edge of the display:
In other words, the only flexibility built into the cbg tooltips is all four orientation possibilities; the shape itself doesn't stretch or otherwise reshape to fit its contents.
Everything but the text containing the cbg value is defined in the cbg
attribute in shapes.js
. In order to tour this shape, we'll need to look at the code of the original SVG image for reference:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="24.169px" height="24.172px" viewBox="0 0 24.169 24.172" enable-background="new 0 0 24.169 24.172" xml:space="preserve">
<g opacity="0.7">
<g>
<path fill="#FFFFFF" d="M4.529,4.199C2.75,5.978,1.725,8.215,1.43,10.529l-0.396,12.61l12.611-0.395
c2.314-0.297,4.553-1.32,6.332-3.098c4.264-4.266,4.264-11.182-0.002-15.449C15.711-0.066,8.793-0.066,4.529,4.199z"/>
<path fill="#76D3A6" d="M12.252,1c2.794,0,5.589,1.066,7.722,3.198c4.266,4.267,4.266,11.183,0.002,15.449
c-1.779,1.777-4.018,2.801-6.332,3.098L1.033,23.139l0.396-12.61c0.295-2.314,1.32-4.552,3.099-6.33C6.662,2.066,9.457,1,12.252,1
M12.252,0C9.067,0,6.073,1.24,3.822,3.492c-1.876,1.875-3.046,4.265-3.384,6.911L0.432,10.45L0.43,10.498l-0.396,12.61L0,24.172
l1.064-0.034l12.611-0.395l0.049-0.001l0.047-0.006c2.646-0.34,5.037-1.51,6.912-3.383c4.648-4.649,4.646-12.214-0.002-16.863
C18.43,1.24,15.438,0,12.252,0L12.252,0z"/>
</g>
</g>
</svg>
The first attribute in shapes.cbg
is an els
attribute: this is an array of all the SVG elements that are necessary to build the shape:
els: [
{
el: 'path',
attrs: {
d: 'M4.529,4.199C2.75,5.978,1.725,8.215,1.43,10.529l-0.396,12.61l12.611-0.395c2.314-0.297,4.553-1.32,6.332-3.098c4.264-4.266,4.264-11.182-0.002-15.449C15.711-0.066,8.793-0.066,4.529,4.199z',
fill: '#FFFFFF'
}
},
{
el: 'path',
attrs: {
d: 'M12.252,1c2.794,0,5.589,1.066,7.722,3.198c4.266,4.267,4.266,11.183,0.002,15.449c-1.779,1.777-4.018,2.801-6.332,3.098L1.033,23.139l0.396-12.61c0.295-2.314,1.32-4.552,3.099-6.33C6.662,2.066,9.457,1,12.252,1M12.252,0C9.067,0,6.073,1.24,3.822,3.492c-1.876,1.875-3.046,4.265-3.384,6.911L0.432,10.45L0.43,10.498l-0.396,12.61L0,24.172l1.064-0.034l12.611-0.395l0.049-0.001l0.047-0.006c2.646-0.34,5.037-1.51,6.912-3.383c4.648-4.649,4.646-12.214-0.002-16.863C18.43,1.24,15.438,0,12.252,0L12.252,0z',
'class': 'tooltip-outline'
}
}
]
In this case, there are two path
elements composing the shape. Because SVG has no z-index and relies instead of the ordering of nodes in the XML DOM to determine the layering of elements, we need to preserve the order of elements from the original file when copying them into the els
array. Thus, the first el
in els
is the background path with fill #FFFFFF
, and we copy the relevant attributes (here the path definition d
and fill
) into the el
directly. The second element is another path
with a d
attribute that we copy directly, but here we leave out the fill
because the fill for the second path is the teardrop border, and we want this to change depending on the category of blood glucose value, so instead of adding the fill
attribute in the el
we'll add a CSS class instead and use the class to select the element(s) later and apply the correct fill
attribute according to the blood glucose value:
g.svg-tooltip-cbg {
opacity: 0.7;
&.d3-bg-low path.tooltip-outline {
fill: @bg-low;
}
&.d3-bg-target path.tooltip-outline {
fill: @bg-target;
}
&.d3-bg-high path.tooltip-outline {
fill: @bg-high;
}
}
We provide a unique ID and a CSS class for this shape of tooltip in the id
and mainClass
attributes. By convention, these are <datatype>Tooltip
(camelCased) for the ID, and svg-tooltip-<datatype>
for the class.
Next we provide the viewBox
directly from the original SVG image - viewBox: '0 0 24.169 24.172'
. This is very, very important! Always directly copy and paste the viewBox
from the original SVG image! Do not round the values contained within, do not pass Go, do not collect $200.
There are two more attributes to the cbg
shape in shapes.js
- orientations
and addText
. But before discussing these, it's necessary to talk about what tooltip.js
does with the attributes already discussed, the els
, viewBox
, etc.
A tooltip group is added to the main tooltips group in the chart factory, for example:
chart.tooltips().addGroup(poolBG, {
type: 'cbg',
classes: ['d3-bg-low', 'd3-bg-target', 'd3-bg-high']
});
The addGroup
method cycles through the classes passed in the options and adds an element to the <defs>
element that is a child of the main #tidelineTooltips
group. It isn't possible to set attributes other than x
, y
, width
, and height
from a <use>
of an SVG definition, so we use several classes to distinguish definitions according to other properties (in this case, the color of the border path according to the category of the BG value). For each class, the defineShape
function in tooltip.js
constructs the SVG definition according to the information from the shape extracted from shapes.js
according to the shape type passed as an option when adding the tooltip group; it sets the appropriate class, ID, and viewBox
attributes, then cycles through the els
array defined in the shape and adds the elements and attributes listed therein.
tooltip.js
also keeps a defs
hash to keep track of which shapes the module has already built SVG definitions for, to avoid duplicating these.
When a hover handler calls the addFixedTooltip
method in tooltip.js
, it is expected to pass an opts
hash containing at least a datum
(the datapoint that trigger the tooltip on hover), xPosition
and yPosition
functions (usually these are exactly the same as those used to position the datapoint), and of course a shape
for the tooltip. A CSS class and an orientation object for the tooltip. The orientation object is expected to define at most three properties: a default orientation, a special orientation for when the datapoint is located within three hours of the left edge of the display (on one-day or two-week view) and/or an orientation for when the datapoint is located within three hours of the right edge of the display (again on either view).
The cbg
shape also has an orientations
property, which defines all possible orientations of the tooltip shape. In this case, these are normal
, leftAndDown
, leftAndUp
, and rightAndDown
. These four orientations correspond to the default orientation for target and low blood glucose values (normal
), the orientation for target and low blood glucose values within three hours of the right edge of the display (leftAndUp
), the default orientation for high blood glucose values (leftAndDown
) and the orientation for high blood glucose values at the left edge of the display (rightAndDown
).
Each of these orientations is a function that takes one argument - the D3 selection of a tooltip group for a particular datum. It applies a class to the group to identify the orientation (this is useful mainly for testing), and applies the necessary transformations to result in the proper orientation such that the placement coordinates of the tooltip do not have to be changed.
For every plotted datatype that uses a fixed-size tooltip, the plot function (in this case js/plot/cbg.js
) binds the mouseover
event on each datum to an addTooltip
function. This function constructs a hash of options and passes it to the addFixedTooltip
function in js/plot/util/tooltips/tooltip.js
.
addFixedTooltip
uses the xPosition
function included in the opts
along with a helper function locationInWindow
defined in tooltip.js
to determine whether the current datum subject to hover interaction is located at the right or left edge of the data display. It then uses the shape
defined in the opts
to append a tooltip group <g>
element to the datatype-appropriate tooltips group, translate this group into the correct position using the xPosition
and yPosition
functions defined in the opts
, and add a use <use>
element that links to the proper fixed tooltip shape, using the cssClass
defined in the opts
to select from variations in the shape if necessary. The boolean values for atLeftEdge
and atRightEdge
in combination with the orientation
object passed with the opts
are used to select the proper function from the orientations
property of the shape and call it: this translates the shape in whatever ways are necessary to achieve the proper orientation.
Finally, the shape's addText
function is called with the tooltip group and the same opts
object that was passed to addFixedTooltip
as arguments. addText
appends an SVG text <text>
element to the group containing the text of the tooltip. Positioning this element is slightly tricky, however, as the positioning must be sensitive---but not identical---to the translations the tooltip group has undergone for its orientation. All the addText
function needs to know is whether the shape was mirrored over either (or both) of the x- and y-axes, so in defining the x
and y
attributes for the text
, addText
looks for the tell-tale signs of mirroring (scale(-1,1)
and scale(1,-1)
for mirroring over the x- and y-axis, respectively) and subtracts or adds the correct bounding box dimension if found.
In tideline, all the tooltips except those that appear on hover over CGM data (datatype cbg
) are flexible, which is to say that they will grow indefinitely to fit the size of their contents. For example, the tooltips for basal segments get larger when the segment is a temp basal, because there is more information to display:
Even more dramatically, the tooltips for bolus(/wizard) events sometimes hardly need display much information at all:
But sometimes there is a lot of information to display:
Like the fixed-size tooltips for CGM data, flexible tooltips may come in many orientations depending on category of blood glucose value (for tooltips appearing on data from a blood glucose meter, datatype smbg
) and/or depending on the datum's proximity to the right and/or left edge of the data display. It's worth noting that the tooltips for basal rate segments do not change orientation (the shape is orientation-less) at the edges of the data display, but their x-coordinate is shifted so that the tooltip "hugs" the left or right edge of the display instead of running off of it:
There are three shapes of flexible tooltips defined---basal
, smbg
, and generic
---in shapes.js
. The smbg
and generic
shapes are nearly identical; the same shape almost works for both, but we decided to make the "speech bubble"-style triangle at the corner a bit longer for the generic
shape (used for bolus(/wizard) tooltips) so that the tooltip wouldn't cover the yellow circle containing the carbohydrate value. (It was easier in the short term to change the shape of the tooltip than to adjust the anchoring of the tooltip to be either on the left or right edge of the bolus bar (depending on orientation) instead of always in the center of the bolus bar; we could revisit this later.)
While SVG image files from a designer were referenced in order to encode the els
for the flexible tooltips, the process was not a matter of copying and pasting certain attributes from the SVG reference directly as it was for the fixed-size cbg
tooltip shape. Rather, in the case of the flexible tooltips, we used the points
attribute of the main polygon
element in each image to map out the basic shape and then work out the corresponding function to generate the shape around a flexible box for the main text area of the tooltip. In the simplest case of the basal tooltips, this was just a matter of determining the dimensions of the triangular extensions at the right and left of the main text box (which are defined in the code as pointHalfHeight
* 2 = 20px for the height of the triangle, and pointWidth
= 15px for the width of the triangle from its point to where it connects with the main text box) and writing a formula to generate such extensions at the mid-point (height-wise) to either side of a plain rectangle.
A function, of course, is not a proper SVG attribute, so we name the function pointsFn
, and later code (namely, makeShape
in tooltip.js
) will first (1) call the function with the appropriate opts
to generate a true points
attribute for an SVG polygon
and (2) delete the pointsFn
attribute from the attrs
so that it doesn't get added to the polygon
element and cause an SVG error.
In the case of the smbg
and generic
shapes, the main challenge in defining a pointsFn
to generate the points
for an SVG polygon
with a usable text area of height h
and width w
was defining the (unchanging) shape of the "speech-bubble"-like triangular corner in relation to h
and w
. For future reference, the variables in the formula refer to the dimensions in the following diagram, although adjustments were made to orient the tooltip properly given that the SVG coordinate system, unlike the Cartesian system, places (0,0)
at the upper left corner, with increasing (positive) y
values referring to locations further down in the element.
A hover event handler on the appropriate datatype calls an addTooltip
function in the datatype's plot function. addTooltip
prepares the options for drawing the appropriate tooltip, but instead of calling addFixedTooltip
from tooltip.js
, it calls addForeignObjTooltip
, which returns a results object res
.
The first part of addForeignObjTooltip
is similar to addFixedTooltip
. The function uses the xPosition
function from the opts
to determine the location of the datum currently subject to hover interaction within the data display, then creates a tooltip group with the appropriate translation to anchor the location of the tooltip to the datum. For the basal tooltips, this means translating the tooltip horizontally if the basal segment is at the left or right edge of the data display, so that it doesn't run off the edge of the data display. Into the newly-created tooltip group addForeignObjTooltip
inserts an SVG foreign object element <foreignObject>
, then a top-level HTML <div>
inside of the foreign object. (Very briefly: an SVG foreign object element allows for embedded HTML inside an SVG image.) Initially, the foreign object is set a width
of 200 pixels, and its visibility
is set to hidden
. The width
is set in order to give the embedded HTML object something to shape itself within. And the foreign object is hidden initially to avoid any flicker when the object is later resized to the exact needed dimensions. (It is only shown after this resizing occurs.) The top-level HTML div
also receives a class. Two main classes for flexible tooltips are defined in tideline-tooltip.less
: tooltip-div
is used for the basal
and smbg
shapes, which feature centered text in their layouts, and bolus-wizard
is used for the generic
shape, which features a main title and a 2- or (eventually) 3-column table for layout. addForeignObjTooltip
returns an object containing the foreign object as a D3 selection and an edge
property which can take the values 'left'
, 'right'
, or null
depending on whether the datum currently subject to hover interaction is located on the left edge, right edge, or not near either edge of the data display, respectively.
In contrast to the fixed-size tooltips, the generation of flexible tooltips is not as simple as calling one function (i.e., addFixedTooltip
) in the tooltip module. The generation of a flexible tooltip is more of a conversation back and forth between the tooltip module and the original plot function for the datatype. After the plot function receives the results object back from its call to addForeignObjTooltip
, it passes the resulting D3 selection of the newly-created foreign object along with the datum subject to hover interaction along to its own tooltipHtml
function to append the HTML content for the tooltip directly into the top-level div
element within the foreign object.
Once the HTML for the tooltip is created, it can be queried (using the DOM function getBoundingClientRect()
) to determine the final dimensions for the foreign object. The dimensions (namely, width
and height
) are produced via a call to foreignObjDimensions
in the tooltip module. Once these dimensions have been obtained, they are used to construct the options to pass to the final method the plot function calls in the tooltip module, anchorForeignObj
, which takes the D3 selection of the SVG foreign object element and a set of options, including the final intended width
, height
, the tooltip shape
, and the edge
property returned as part of the results from the initial call to addForeignObjTooltip
.
anchorForeignObj
handles all the final positioning of the tooltip and the drawing of the SVG polygon for the tooltip background. The anchoring of the tooltip varies considerably depending on whether the tooltip shape is orientation-less (i.e., basal segment tooltips) or varies with orientation, as the smbg
and generic
tooltip shapes do. The orientation-less tooltips get passed off to a anchorForeignObjNoOrienation
function, which adjusts the translation of the foreign object, if necessary (i.e., if and only if the datum is location near the right edge of the data display). The anchorForeignObj
function itself handles the translation adjustment for the flexible tooltips that do have varying orientation. After the translation adjustments for the foreign object are handled, what's left is to set the final dimensions of the foreign object (and make it visible) and draw the SVG polygon providing the tooltip background. These two tasks are accomplished via the tooltip module's setForeignObjectDimensions
and makeShape
functions.