-
Notifications
You must be signed in to change notification settings - Fork 16
CreatingOneDay
oneday.js
provides the higher-level functions for constructing a view of a multi-typed data set where twenty-four hours of the data is visible at a time, plotted against hours of the day on the x-axis. The twenty-four hour window of visible data can be adjusted by clicking and dragging the visible data. A scrollbar can also (optionally) be rendered, and functions are available for programmatically scrolling the data forward or backward by twenty-four hours at a time; these can be attached to click handlers on extra-tideline application components (forward and backward arrow buttons in a navigation bar, for example).
chartdailyfactory.js
in the plugins/blip/
folder in this repository provides a concrete example of creating a one-day view using the functions in oneday.js
and may be a good companion to reference while reading this section.
Create a new chart
by passing an EventEmitter emitter
to tideline.oneDay
:
var chart = tideline.oneDay(emitter);
To set up the chart, you'll want to pass an ID, a width, and a height to chart
using the id()
, width()
, and height()
functions. You can pass these functions the id, width, and height of the HTML <div>
element that is intended to contain chart
for an easy way to ensure the right fit. Next, set the base URL for your server's img/
directory for tideline's use with imagesBaseUrl()
. Finally, pass the container HTML element el
to chart
and set up the chart's basic SVG structure with d3.select(el).call(chart);
.
The next step is setting up each of the data pools that you would like to have in the one-day view. tideline.oneDay
has a newPool()
method for creating pools. This function returns the pool and can then be chained with other methods from tideline.pool
to continue setting up the pool. Generally, the setup of a new pool will look something like the following:
thisPool = chart.newPool()
.id('thisPool', chart.poolGroup())
.label('This is a pool')
.index(chart.pools().indexOf(thisPool))
.weight(0.5)
.gutterWeight(1.0);
Calling id()
with an ID for the newly created pool and the D3 selection of the element to which the pool is to be appended (yielded in this case by the getter function tideline.oneDay.poolGroup()
) is required.
The label is optional; it controls the text that may appear (stickily; that is, it doesn't move when the user scrolls along the time axis) above the data pool on the left.
The legend is optional. It accepts an array of strings, each of which correspond to a key defining a legend factory in tideline.plot.util.legend
.
The index controls the order of the pool with respect to the other pools. The code snippet above just uses the index at which the pool was created as its permanent index, but the index()
function exists so that pool creation and ordering in the display can be decoupled, if so desired.
The weight of a pool controls how much vertical space will be allotted to it given the total height available and the weights of the other pools. The recommended default is 1.0 as a baseline; smaller or larger pools can be given different weights relative to that baseline - 0.5 for a half-height pool or 2.0 for a double-height pool.
The gutter weight of a pool controls how much vertical space will appear above the pool. If you want the pool to directly abut the pool (or other element, such as an axis) above it, set a gutter weight of 0.0. Otherwise, 1.0 is the normal gutter weight sized to allow adequate space for a pool label, and values larger than 1.0 can be used to yield an extra-large gutter.
After creating all the pools in the view, a call to tideline.oneDay.arrangePools()
will set the dimensions of each pool group and apply the appropriate transform
attribute to each pool's SVG group <g>
element to position it.
Finally, if any of the pools you've added will require annotations and/or tooltips, use tideline.oneDay.setAnnotation()
and/or tideline.oneDay.setTooltip()
, respectively, to set up the master annotations and/or tooltips group(s), then add as many annotation and/or tooltip sets to chart
as necessary (keeping in mind each pool can be associated with more than one annotation or tooltip set, as for example the carbohydrate intake and bolus insulin pool is in the tideline example):
chart.annotations().addGroup(d3.select('#' + chart.id()).select('#' + thisPool.id()), 'poolDataType');
chart.tooltips().addGroup(d3.select('#' + chart.id()).d3.select('#' + thisPool.id()), 'poolDataType');
Setting up the annotations and/or tooltips marks the end of the chart setup that can be accomplished without the data that's to be visualized in the chart. All of the above setup steps can be usefully grouped together into a wrapper function that only needs to be called once per visualization. (See the chart.setupPools()
function in chartdailyfactory.js
for an example.)
The next logical grouping of steps is all of the steps that are dependent on the data to be visualized and need to be called every time new data is passed to the visualization. (These have been grouped in the chart.load(tidelineData)
function in chartdailyfactory.js
.)
The first steps are to load the data, set the axes, and set up the desired navigation components:
chart.data(tidelineData).setAxes().setNav().setScrollNav();
Setting scrollbar navigation is completely optional; all of the other navigation components (click-and-drag to pan; attaching button clicks to the programmatic tideline.oneDay.panForward()
and tideline.oneDay.panBack()
functions) will work as intended without adding the scrollbar.
The next step is to set the data-dependent attributes in each of the pools. The most complex of these attributes is the pool's y-axis (or axes, as can also occur). For diabetes data, scales can be generated using the datatype-appropriate functions in tideline.plot.util.scales
. The scale(s) is then used to generate a D3 axis that is passed to the pool using the yAxis()
function, which will accept multiple axes. In the tideline example, the setup for the axes of the pool that displays carbohydrate intake and bolus insulin data is the following:
var scaleBolus = tideline.plot.util.scales.bolus(tidelineData.grouped.bolus, poolBolus);
var scaleCarbs = tideline.plot.util.scales.carbs(tidelineData.grouped.carbs, poolBolus);
// set up y-axis for bolus
poolBolus.yAxis(d3.svg.axis()
.scale(scaleBolus)
.orient('left')
.outerTickSize(0)
.ticks(2));
// set up y-axis for carbs
poolBolus.yAxis(d3.svg.axis()
.scale(scaleCarbs)
.orient('left')
.outerTickSize(0)
.ticks(2));
After the axes are added to the pool, the final step is to set up the plot functions that will be called whenever new data needs to be rendered - that is, at the chart's initial rendering, and whenever the user navigates past the point (a certain distance from the end of rendered data) that triggers the rendering of more data. pool.addPlotType()
takes three arguments:
- the datatype
- the plot function - see plot modules below for further details
- a Boolean indicating whether the plot function renders data
- a Boolean indicating whether what the plot function renders is rendered on the main x-axis and thus should move upon navigation
In the tideline example, three plot types are added to the pool that displays carbohydrate intake and bolus insulin information:
// add background fill rectangles to bolus pool
poolBolus.addPlotType('fill', fill(poolBolus, {
endpoints: chart.endpoints,
guidelines: [
{
'class': 'd3-line-divider',
height: poolBolus.height()/2
}
],
yScale: scaleDivider
}), true, true);
// add carbs data to bolus pool
poolBolus.addPlotType('carbs', tideline.plot.carbs(poolBolus, {
yScale: scaleCarbs,
emitter: emitter,
data: tidelineData.grouped.carbs
}), true, true);
// add bolus data to bolus pool
poolBolus.addPlotType('bolus', tideline.plot.bolus(poolBolus, {
yScale: scaleBolus,
emitter: emitter,
data: tidelineData.grouped.bolus
}), true, true);
The first plot type is the background fill over which the data in the pool will be plotted - in this case variably colored rectangles at three-hour intervals.
The only plot type that does not get rendered on the x-axis is the stats widget; passing false
for the final Boolean argument when adding stats as a plot type is what allows this component to remain in the same place while the rest of the rendered pools move on navigation.
The plot modules are found in js/plot/
. Each plot module provides a function for plotting a particular view of one type of data. (Sometimes there will be multiple plot modules for a single data type, if more than one way of plotting this data type is desirable. So far in this repository, there are two plot modules for smbg values, smbg.js
and smbgtime.js
.)
Creating new plot modules for data types not currently visualized in tideline is one of the easiest ways to contribute this repository. Whether a new plot module visualizes additional diabetes data (e.g., a more detailed food log), contextual data (e.g., calendar data or activity data from a Fitbit or similar) to be displayed alongside the data types already in tideline, or data that's unrelated to type 1 diabetes and represents a new use for this visualization library (e.g., GitHub activity data!), we'd welcome any and all contributions.
Most of the plot modules share a common structure. Where they have different requirements, an opts
object is passed to the module to set the additional needed parameters. For example, from the code above demonstrating how plot types are added to the carbohydrate intake and bolus insulin pool in the tideline example, there is variation in the object passed to the fill plot module:
tideline.plot.util.fill(poolBolus, {
endpoints: chart.endpoints,
guidelines: [
{
'class': 'd3-line-divider',
height: poolBolus.height()/2
}
],
yScale: scaleDivider
})
versus the bolus plot module:
tideline.plot.bolus(poolBolus, {
yScale: scaleBolus,
emitter: emitter,
data: tidelineData.grouped.bolus
})
Both take the pool (poolBolus
) where the plot module is to be rendered as their first argument, but the second opts
object argument contains the specific attribute-value pairs needed for that module.
The inner function of each plot module is saved as a plotType.plot
attribute by pool.js
and called whenever new data needs to be rendered. Each inner function takes a D3 selection as an argument; this selection is the SVG group element within which the rendered data will live. The inner function is also where we make use of D3's enter
and exit
selections to render new data (enter
) and remove stale (i.e., far out of view after the user has navigated) rendered data from the DOM (exit
).
The simplest plot module may contain nothing but an inner function, but other functions are added as needed. Plot types that have a tooltip interaction on hover will have a plotType.addTooltip()
function. A simple template for a plot module can be found in this repository in dev/templates/plottemplate.js
.
Once all the axes and plot types for each pool have been set, the data-dependent setup for a one-day chart is complete.
The final step is rendering the one-day view. In chartdailyfactory.js
we group the rendering steps together in a chart.locate([datetime])
function since rendering the one-day view also means choosing which particular day of the data to initialize the view at. Failing to pass a datetime
argument to chart.locate()
results in rendering the most recent twenty-four hours of data. In other cases, the assumption is that the datetime
passed represents the point in time that should appear in the center of the one-day view. Since tideline speaks more naturally in terms of domain endpoints - the edges of the view, rather than the center - some computation is performed to translate the datetime
into an edgepoint that is passed into tideline.oneDay.setAtDate()
.
As can be seen in chartdailyfactory.js
, there is an additional step between computing the edgepoint for tideline.oneDay.setAtDate()
and calling this function. First the start and end edgepoints are passed to the function that uses Crossfilter to manage the data currently being rendered:
chart.renderedData([start, end]);
In penultimate position is the code to render data in each pool:
_.each(chart.pools(), function(pool) {
pool.render(chart.poolGroup(), chart.renderedData());
});
And the call to tideline.oneDay.setAtDate()
conclues; this function accepts the edgepoint and a boolean reflecting whether the most recent data is being rendered.