-
Notifications
You must be signed in to change notification settings - Fork 16
TidelineReact
This document is intended as the starting point for a discussion of how to best integrate tideline into an application/UI framework such as React.
Through late June 2014, the strategy for incorporating tideline into Tidepool's first application blip has been to define an application "page" PatientData
with a tideline Chart
as an embedded component. Many aspects of tideline's appearance are tracked in PatientData
's state, namely:
- the chart type (daily, weekly, or settings)
- the chart's current location in time
- whether the chart is located at the rightmost edge of the data (i.e., the most recent data)
- whether all smbg values are being shown or hidden in the two-week view
- whether the chart is currently undergoing an animated transition from one location in time to another
The Chart
component contains the code for generating and rendering all three of the chart types. Originally, the idea was to strive for API identity among all the chart types. In theory, all three charts should have identical or nearly identical capabilities in terms of navigation along the timeline - programmatic panning forward and backward, jumping to the most recent data, etc. However, as the visualization has developed, the APIs for each chart type have diverged, and each chart now contains functions that are unique to that chart type. For example, responding to message creation is exclusive to the one-day view, and hiding and showing values is exclusive to the two-week view. Furthermore, navigation along the timeline has not yet been implemented for the settings view.
An additional motivating factor behind creating just a single Chart
component instead of a component for each chart type was the fact that some data pre-processing was tied up with the setup of each chart, and so it was desirable to set up each chart only once and switch between (already created) charts on demand thereafter. This data pre-preprocessing has since been moved out into a tideline "plugin" (the preprocess
plugin), and thus there is no longer a need or desire to avoid repeated calls to set up a chart every time the user navigates between chart types.
In this repository's minimally functional example (contained in example/
), I have sketched out another strategy for rendering tideline's chart within a React framework.
The top-level Example
component is intended to be parallel to blip's PatientData
"page". Tracked in this component's state are:
-
chartData
= the patient data to be rendered (the result of applying blip's preprocessing and tideline'spreprocess
module to the data fetched from the Tidepool server) -
chartPrefs
= an object containing default or user-defined preferences for rendering tideline charts, including (but not limited to) the units in which blood glucose values should be expressed (mg/dL or mmol/L) and (eventually) whether the timestamps in the data are to be displayed in a timezone (and if so, which) or if the rawdeviceTime
should be used for locating the data on the timeline -
imagesBaseUrl
= a URL pointing to where tideline can find the images it needs -
chartType
= the type of chart to be rendered,daily
by default
In the tideline example, all four of these attributes are tracked in Example
's state, but in blip the chartData
, chartPrefs
, and imagesBaseUrl
could be passed the PatientData
"page" as (immutable) props. Only the chartType
necessarily needs to be constantly updated in the state (upon switching between chart types). Possibly, chartPrefs
might also need to be mutable, depending on where in the UI these preferences are determined. For example, we could decide to employ a timezone picker on the PatientData
"page" similar to the picker in Apple's Calendar application:
On the other hand, I find it easier to imagine a user setting their preference for viewing blood glucose data in mg/dL or mmol/L as part of their user/profile settings, so that preference may not be mutable within the PatientData
"page".
An additional variable is set in Example
's state: the initialDatetimeLocation
for daily and weekly charts (and, eventually, the settings page, when we support navigation through the settings history) upon switching between these charts. (Since the default is to load the most recent twenty-four hours of data, this property does not need to be set in getInitialState
but only when the user first navigates away from the default most recent daily chart.)
It is not possible to keep track of the chart's (whether daily, weekly, or settings) current datetime location in Example
's state. The reason for this is that some of the functions in some of Example
's components (namely: handleHideBasalSettings
in Daily
) destroy and then render a new tideline chart, which results in a situation where Example
's state is triggered to be updated while it is still rendering its sub-components, a fatal hiccough in the world of React.
Concluding Questions
-
How hard will it be to adapt the current
PatientData
"page" into something likeExample
(plus the things required in blip for messages)? The biggest difference is thatPatientData
renders the header and footer separately from theChart
butExample
renders one of three chart components, each of which contains aHeader
andFooter
as sub-components. -
Should all the components below the level of
Example
/PatientData
(i.e.,Daily
,Weekly
,Settings
,Header
,Footer
) be part of the tideline codebase, living inplugins/blip/
alongside the chart factories? -
What should be in
PatientData
's state, with respect to tideline? Should thechartPrefs
be broken out into separate props or state properties early on, since there may be a desire to have some of these be mutable? -
Is it a problem, thinking ahead to a future where we might like a user to be able to bookmark a particular chart view, that we can't track the current datetime location of a chart in
PatientData
? We can certainly work around this limitation with an internal bookmarking tool, but I'm not sure about browser-native bookmarking.
The higher-order daily chart component Daily
takes as props (from Example
's state) chartPrefs
, imagesBaseUrl
, patientData
, and (optionally) initialDatetimeLocation
as well as three functions: switchToDaily
, switchToSettings
, and switchToWeekly
. Its getInitialState
function returns an object with one property - hiddenPools
- and sets basalSettings: true
in that object.
Daily
renders three sub-components: a Header
, a Footer
, and a DailyChart
. Having a higher-order chart component render the header and footer is one of the main advantages of this new proposal - the current implementation of blip has grown a tangle of if/else statements to control the varying requirements of the header and footer, which are dependent on which chart type is currently being displayed.
The higher-level Daily
component also provides handler functions for interaction events (e.g., handleInTransition
, which is triggered when the chart is undergoing an animated transition from one datetime location to another). Each of the handler functions calls the appropriate function(s) in a sub-component accessible from Daily
's refs.
The embedded DailyChart
component is the component that actually renders the tideline chart. The props it takes are mostly familiar from the higher-level Daily
component - imagesBaseUrl
, initialDatetimeLocation
, and patientData
- but the chartPrefs
object has been broken down and only the properties actually needed for daily chart are passed as individual props - bgUnits
- plus the new property hiddenPools
given a default key-value pair of basalSettings: true
in the state of Daily
.
The necessary handler functions from Daily
are also passed as props. Most of these are bound directly to the appropriate event triggers, but onDatetimeLocationChange
gets called from DailyChart
's own handler (bound to the 'navigated'
event). In fact, the main reason for breaking DailyChart
out as a separate component from Daily
is to be able to properly handle the continuous updating of the datetime location title in the header without constantly tracking the current datetime location of the chart in Daily
's state (which results in the above-mentioned fatal React error when trying to show or hide basal settings). Instead, only DailyChart
tracks the current datetime location in its state (although this may be pointless), and it passes each new pair of datetimeLocationEndpoints
up to Daily
's handleDatetimeLocationChange
via this function, passed as onDatetimeLocationChange
in DailyChart
's props. Then, handleDatetimeLocationChange
calls the appropriate function in Header
to update the title string reflecting the datetime location in the header.
Concluding Questions
-
Tracking the current datetime location in
DailyChart
is the one place we can do this without running into a fatal React error, but since it's not being used for anything inDailyChart
(and since the state gets wiped out every time the component is unmounted), it's kind of...pointless, unless it could potentially be useful for updating the URL to provide bookmarking of a specific data view functionality. -
Because the datetime location and the chart options specific to one-day view (i.e.,
hiddenPools
) are tracked inDailyChart
andDaily
's state, respectively, both of these are wiped out when these components are unmounted, so if you navigate away from one-day view in blip (e.g., to settings) and then navigate back, you won't see the same view as when you left - the tabular basal settings pool will always be hidden, and you'll always be on one-day view at the most recent twenty-four hours of data. And sincechartType
is tracked inExample
/PatientData
's state and this is wiped out when navigating away from the patient data page (e.g., to your profile), you'll always come back to the default one-day view when you navigate back. Perhaps an application-widechartState
object is the only way to solve this?
The weekly chart works the same as the daily chart in most respects, with a higher-order Weekly
component that renders the Header
, Footer
, and WeeklyChart
sub-components. The only thing worth calling out specifically as an additional challenge in two-week view is the interaction between the showing and hiding values functionality and navigating to the most recent data via clicking the 'Most Recent' link in the header. As a result of the current limitations of the tideline API, it's not possible to keep values shown upon navigating to the most recent data, so handleClickMostRecent
has to set showingValues: false
in the state in order to have the footer link updated from 'Hide Values' to 'Show Values'.
Concluding Questions: Same as for Daily Chart.
The settings view is a bit simpler, at present, than the daily and weekly views since there's no navigation along the timeline. For parallelism, it uses the same higher-order Settings
and embedded SettingsChart
components.
The Header
component takes the chartType
and all necessary click handler functions for the links it contains as props. It leverages the chartType
to adjust the classes for the 'One Day', 'Two Weeks', and 'Settings' links, as well as the navigation arrows - making any or these active, inactive, or hidden as appropriate to the chart type.
Header
's state tracks whether the user is currently viewing the most recent data and whether the chart is currently undergoing an animated transition between datetime locations.
What Header
's state does not (and cannot) track is the current datetime location of the graph. Instead of updating the title string of the header reflecting the current datetime location via Header
's state, as would be most React-like, the updateTitle
function inserts an updated title string (passed in as an argument) directly into the DOM using D3. This is, quite frankly, a hack. :(
The Footer
component also takes the chartType
and necessary click handlers as props, as well as an (optional) prop reflecting whether values are currently being shown and hidden, which is relevant only for the two-week view. Unlike the Header
component, it contains no hacks! \o/