Skip to content

SVGTooltips

Jana E. Beck edited this page Aug 12, 2014 · 3 revisions

Encoding Native SVG Tooltips

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.

The Eas(y/ier) Case: Fixed Size Tooltips

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):

Target-range default CBG tooltip

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:

High-range default CBG tooltip

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:

Low-range right edge CBG tooltip

For high blood glucose values, this reversal happens at the left edge of the display:

High-range left edge CBG tooltip

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).

Tooltip Orientations

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.

The Challenging Case: Flexible Tooltips

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:

Plain basal tooltip

Temp basal tooltip

Even more dramatically, the tooltips for bolus(/wizard) events sometimes hardly need display much information at all:

Quick bolus tooltip

But sometimes there is a lot of information to display:

Very large bolus tooltip

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:

Basal tooltip hugging right edge

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.

Basal tooltip pointsFn variables

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.

Generic tooltip pointsFn variables

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.