Skip to content

Latest commit

 

History

93 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

predtimechart

A JavaScript (ES6 ECMAScript) module for forecast visualization.

Steps to use the component

To use the component in your app, you'll need to add the following to your HTML:

  1. add <script> and stylesheet <link> tags for dependencies to the <head>
  2. add a <div> to the <body> for the component to fill
  3. add a <script> in the <body> that initializes the component via the (default) App object that's exported

See the "HTML file example" section below for an example, and see the "JavaScript API" section for how to initialize the component.

HTML file example

In your HTML file, load the required CSS and JavaScript files:

  1. In the <head>, load Bootstrap 5 and other required library files:
<!-- jquery -->
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"></script>

<!-- Bootstrap 5 -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-ndDqU0Gzau9qJ1lfW4pNLlhNTkCfHzAVBReH9diLvGRem5+R9g2FzA8ZGN954O5Q"
        crossorigin="anonymous"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.7/dist/css/bootstrap.min.css" rel="stylesheet"
      integrity="sha384-LN+7fdVzj6u52u30Kp6M/trliBMCMKTyK833zpbD+pXdCLuTusPj697FH4R/5mcr" crossorigin="anonymous">

<!-- Bootstrap Icons -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css"
      integrity="sha384-CK2SzKma4jA5H/MXDUU7i1TqZlCFaD4T01vtyDFvPlD97JQyS+IsSh1nI2EFbpyk"
      crossorigin="anonymous">

<!-- https://www.daterangepicker.com/ -->
<script type="text/javascript" src="https://cdn.jsdelivr.net/momentjs/latest/moment.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/daterangepicker/daterangepicker.css"/>

<!-- plotly -->
<script src="https://cdn.plot.ly/plotly-2.12.1.min.js"></script>
  1. In the <body>, add the row <div> that will hold the component:
<div id="forecastViz_row" class="row"></div>
  1. In the <body>, load and use the predtimechart module:
<script type="module">
    // import the module's entry point (the `App` object)
    import App from 'https://cdn.jsdelivr.net/gh/reichlab/predtimechart@2.0.9/dist/predtimechart.js';

    // set up _fetchData and options
    function _fetchData(isForecast, targetKey, taskIDs, referenceDate) { ... }
    
    const options = {...};

    // initialize the component: componentDiv, _fetchData, isIndicateRedraw, options
    App.initialize('forecastViz_row', _fetchData, false, options);
</script>

JavaScript API

The component is accessed via the App object, and is initialized via the App.initialize() function. After that, everything else is taken care of by the app.

About task ID variables

Predtimechart has adopted a simplified version of the modeling tasks concept defined in Modeling Hub task ID variables, and specifically the idea of task ID variables. These variables are used in these ways (details follow):

  1. Initializing the application's UI: The task_ids object passed to App.initialize() via the containing options object (see the "Options object" section below) configures the dropdown menus in the options section of the UI. Specifically, each task ID gets its own dropdown, positioned between the fixed Outcome and Interval dropdowns. For example, if the options object's task_ids specifies two task IDs ("scenario_id" and "location") then there will be two dropdowns between Outcome and Interval: "scenario_id" and "location". Those dropdowns' values will contain the values passed in the task_ids object.
  2. Initializing the dropdown default values: Predtimechart uses the options object's initial_task_ids to select the initial values that the user sees.
  3. Args to the _fetchData function: When the user takes an action that requires obtaining data, predtimechart will pass the currently-selected values in the above dropdowns to the _fetchData function.

App.initialize() args

App.initialize(componentDiv, _fetchData, isIndicateRedraw, options) takes these args:

  • componentDiv: id of the empty <div> to place the component into.
  • _fetchData: function to retrieve truth and forecast data. It is called whenever the plot needs updating. See the "fetchData data format" section for details. It takes these args:
    • isForecast: boolean indicating type of data to retrieve. true means retrieve forecast data and false means get truth data.
    • targetKey: string naming the target of interest. Must be one of the values in the options object's target_variables value.
    • taskIDs: object specifying which modeling task to retrieve data for (see task ID variables above). Must be consistent with the options object's task_ids value (see the "Options object" section for details).
    • referenceDate: "" reference date "". Must be one of the values in the options object's available_as_ofs value.
  • isIndicateRedraw: boolean that controls whether the plot area should be grayed out while waiting for data requests. Useful for applications that have a noticeable delay when fetching data.
  • options: object that contains initialization data. See the "Options object" section for details.

Options object

The component is initialized by a JavaScript object with the following keys and values. See the "Example options object" for a detailed example. See src/schema.json for the options object's JSON Schema.

  • available_as_ofs: object that maps target_variables value to an array of dates in 'YYYY-MM-DD' format that have truth and/or forecasts available
  • current_date: available_as_ofs value key to use for the initial plot
  • disclaimer: string providing any important information users should know
  • initial_as_of: string specifying the initial date from 'available_as_ofs' (in 'YYYY-MM-DD' format) to use for the initially-selected as_of date
  • initial_checked_models: models value(s) to use for the initial plot
  • initial_interval: intervals value to use for the initial plot
  • initial_season: integer specifying the season to plot initially, identified by the calendar year the season starts in - e.g., 2024 for the '2024-2025' season with the default August start. Only applies when initial_season_mode is true, and must be a season that the initial target variable's available_as_ofs dates fall in. Selecting it moves the initial "as of" date to that season's first available one. See "Season mode (beta)" below
  • initial_season_mode: boolean specifying whether season mode starts out on. Defaults to false
  • initial_season_start_month: integer from 1 (January) through 12 (December) specifying the month a season starts in. Defaults to 8 (August)
  • initial_target_var: target_variables value key to use for the initial plot
  • initial_task_ids: an object to use for the initial plot. Its format is identical to _fetchData()'s taskIDs arg above.
  • initial_xaxis_range: array of two dates in 'YYYY-MM-DD' format that specify the initial xaxis range to use. To not initialize the range, pass null for its value
  • initial_yaxis_range: array of two values (format depends on outcome variable) that specify the initial yaxis range to use. To not initialize the range, pass null for its value
  • intervals: array of one or more prediction interval widths to offer in the Interval dropdown, each a string of the form '<integer>%' from '0%' to '99%' (no zero padding, and '100%' is not accepted). '50%', '80%', and '95%' are plotted as shaded bands, built from the q0.25/q0.75, q0.1/q0.9, and q0.025/q0.975 forecast data keys respectively. '0%' plots the median line alone, as does any other width, since we have no quantile keys to map it to. A width is also skipped for any model whose forecast data is missing its two backing quantiles — so a hub that doesn't submit q0.1/q0.9 should simply omit '80%' here.
  • models: array of model names (strings) that provide data
  • model_urls: object where the keys are the model names in models (strings) and the values are URLs to pages showing model information (strings)
  • target_variables: array of objects defining the target variables in the data. Each object contains three keys:
    • 'value': used as the main value that's passed around for the target
    • 'text': human-readable text
    • 'plot_text': plot text (purpose: TBD)
  • task_ids: object defining the tasks in the data as described in _fetchData's taskIDs arg above. Like available_as_ofs, the keys must match target_variables value. Each of those is an object that contains a string key for each task ID variable, the value of which is an array of objects defining possible values. The keys should consist of only ASCII letters, digits, _, and -, and should start with a letter. (Note that, for display, the IDs are split on _ and - and the title-cased.) The objects have two keys:
    • value: used as the main value that's passed around for the task ID
    • text: human-readable text

Example options object

Here's a real-world example from the COVID-19 Forecast Hub project. Only the first two items in the lists are shown.

{
  "available_as_ofs": {
    "day_ahead_cumulative_deaths": [
      "2020-03-15",
      "2020-03-22",
      "..."
    ],
    "day_ahead_incident_deaths": [
      "2020-03-15",
      "2020-03-22",
      "..."
    ],
    "...": "..."
  },
  "current_date": "2022-10-22",
  "disclaimer": "Most forecasts have failed to reliably predict rapid changes in the trends of reported cases and hospitalizations...",
  "initial_as_of": "2022-10-12",
  "initial_checked_models": [
    "COVIDhub-baseline",
    "COVIDhub-ensemble"
  ],
  "initial_interval": "95%",
  "initial_target_var": "week_ahead_incident_deaths",
  "initial_task_ids": {"unit": "48"},
  "initial_xaxis_range": null,
  "initial_yaxis_range": null,
  "intervals": ["0%", "50%", "80%", "95%"],
  "models": [
    "COVIDhub-baseline",
    "COVIDhub-ensemble",
    "..."
  ],
  "model_urls": {
    "COVIDhub-baseline": "https://github.com/CDCgov/covid19-forecast-hub/blob/main/model-metadata/CovidHub-baseline.yaml",
    "COVIDhub-ensemble": "https://github.com/CDCgov/covid19-forecast-hub/blob/main/model-metadata/CovidHub-ensemble.yaml"
  },
  "target_variables": [
    {
      "value": "day_ahead_cumulative_deaths",
      "text": "day ahead cumulative deaths",
      "plot_text": "day ahead cumulative deaths"
    },
    {
      "value": "day_ahead_incident_deaths",
      "text": "day ahead incident deaths",
      "plot_text": "day ahead incident deaths"
    },
    "..."
  ],
  "task_ids": {
    "week_ahead_incident_deaths": {
      "unit": [
        {"value": "48", "text": "Texas"},
        {"value": "US", "text": "US"},
        "..."
      ]
    }
  }
}

fetchData data format

As described above, the fetchData(isForecast, targetKey, taskIDs, referenceDate) function passed to App.initialize() is responsible for returning truth and forecast data as directed by the isForecast arg. It uses the other three args to retrieve and return the requested data. The data is in the following formats.

fetchData truth data format

Truth data is represented as an object with x/y pairs represented as columns, where x=date and y=truth_value. The dates must correspond to those in the options object's available_as_ofs. For example:

{
  "date": ["2020-03-15", "2020-03-22", "..."],
  "y": [0, 15, "..."]
}

fetchData forecasts data format

Forecast data is an object with one entry for each model in the options object's models, each of which is in turn an object with entries for target end date of the forecast and the quantiles required to display point predictions and prediction intervals. q0.5 gives the point prediction, and each interval width in the options object's intervals is drawn from its two backing quantiles as documented there. The q0.1 and q0.9 entries backing the '80%' interval are optional; a model that omits them simply gets no '80%' band. For example:

{
  "UChicagoCHATTOPADHYAY-UnIT": {
    "target_end_date": ["2021-09-11", "2021-09-18"],
    "q0.025": [1150165.71, 1176055.78],
    "q0.1": [1150793.36, 1177824.19],
    "q0.25": [1151044.42, 1178626.67],
    "q0.5": [1151438.21, 1179605.9],
    "q0.75": [1152121.55, 1180758.16],
    "q0.9": [1152583.94, 1181790.27],
    "q0.975": [1152907.55, 1182505.14]
  },
  "USC-SI_kJalpha": {
    "target_end_date": ["2021-09-11", "2021-09-18"],
    "q0.025": [941239.7761, 775112.557],
    "q0.1": [988431.0294, 856894.311],
    "q0.25": [1010616.1863, 896160.705],
    "q0.5": [1149400.162, 1137280.4614],
    "q0.75": [1313447.0159, 1461013.716],
    "q0.9": [1398215.4382, 1637168.583],
    "q0.975": [1456851.692, 1771312.0932]
  },
  "...": "..."
}

Development

Following is how to do development-related activities. You must first install the required Node.js packages via npm install --save-dev.

Running unit tests

We use QUnit for our unit tests. To run the tests, execute the package.json test script: npm run test. You should see output at the bottom like this:

...
# pass 22
# skip 0
# todo 0
# fail 0

Trying the app locally

We've included src/index.html as a simple example of the app in action. The file hard-codes truth and forecast data, and has ata for only one referenceDate, but can be useful during development. To use it, follow the below packaging step and then serve dist/index.html from your development environment via these commands:

Note: You can't open the file directly in your browser due to CORS restrictions (e.g., "CORS request not http" in Firefox).

 cd <this_repo>/dist
 python3 -m http.server --cgi 8080
 # visit: http://127.0.0.1:8080

Updating /src/schema.json

We use Ajv to validate App.initialize()'s options object. More specifically we use a standalone validation function generated from the schema, which is saved to schema-validator.cjs. This file must be regenerated whenever the schema file /src/schema.json changes. To do so, execute the package.json ajv_compile script via npm run ajv_compile.

Packaging the component

We use webpack to package up all dependencies into a single dist/predtimechart.bundle.js file for end users. To do so, execute the package.json build script via npm run build, which will update all files in dist/.

Overall usage and features

TBC

Options panel layout

The options panel's dropdowns put their label and <SELECT> on one line (Outcome: [ ... ]) rather than stacking them, and the whole panel is set at 14px so that the dropdowns, their labels, the section headers, and the model list are all one size. This matters most in season mode, which adds two more dropdowns to the panel.

The panel is divided into sections - "Season mode (beta)", "Select Target Data", and "Select Models" - each with a bold header and a hairline rule above it. The first rule also sets off the Outcome/task ID/Interval dropdowns at the top, which have no header of their own. The rule's color is a translucent gray rather than a fixed one so that it reads correctly against whatever theme the host page uses.

Each group of dropdowns is a two-column CSS grid, so the label column is exactly as wide as its longest label and every dropdown starts at the same x, leaving the rest of the panel's width to the dropdowns themselves. That matters because the panel is narrow and an outcome variable's name can be long.

The layout lives in predtimechart.css rather than using Bootstrap's grid, deliberately: some consumers embed the component in pages whose Bootstrap build ships the form styles but not the grid - a Quarto site, for instance - and there a row/col-sm-* layout silently does nothing, putting each label back above its dropdown. Below 576px wide the label and dropdown stack, as before.

Note that the options and plot columns themselves are still Bootstrap's col-md-3 and col-md-9. Consumers whose pages lack the Bootstrap grid map those to their own layout - the Quarto dashboards do it by reading the number out of the class name - so col-md-N needs to stay the first class on those two <DIV>s.

Season mode (beta)

By default the plot shows all the target data going back as far as the data reaches. Checking the Season mode (beta) checkbox in the options panel instead focuses the plot on a single season, with the other seasons drawn in light gray behind it, ala the old FluSight Network site. Two dropdowns appear below the checkbox when it's checked:

  • Season: the season to plot. The choices are the seasons that the current target variable's available_as_ofs dates fall in, so every season offered is one you can navigate within. Selecting a season moves the "as of" date to that season's first available one, so you start at the top of the season and can walk forward through it. (The initial page load is different: it stays on the caller's initial_as_of, which is typically the most recent forecast of the most recent season.)
  • Season start: the month a season starts in - a season runs from the first of that month through the day before the first of that month in the following year. The default is August. Picking January makes a season a single calendar year, which is also how it's named (2025 rather than 2025-2026).

Season mode also changes the "Select Target Data" checkboxes:

Checkbox Meaning
Selected season, as of data a vintage of target data as of the selected/navigated-to date (black)
Selected season, current data target data from the most recent vintage for the selected season (dark gray)
Other seasons, current data the most recent vintage from the other seasons, overlaid (light gray)

The last one only applies in season mode and is therefore hidden when it's off. The other seasons' dates are shifted by whole years so that they line up with the selected season, and their tooltips are just the season's name. Both earlier and later seasons are shown, so selecting an older season still shows the ones that followed it.

While in season mode, the left/right arrow keys and the < / > buttons stop at the selected season's first and last "as of" dates - the buttons are disabled there - rather than crossing into the neighboring season. The plot's x-axis is also pinned to the selected season's extent, though you can still zoom and pan within it, and the range slider below the plot is drawn at about half its usual height, since a one-season-wide plot doesn't need it to navigate a long time series. Turning season mode off restores all of the above to the way the component behaves without it.

Configuring season mode

Season mode is off by default, with August as the season start, and can be configured two ways:

  • through the options object, via initial_season_mode, initial_season, and initial_season_start_month
  • through the URL, via the season_mode, season, and season_start search params documented under "URL parameters" below

Either way, initial_season/season only applies when season mode is on - there's no season to be in when it's off.

URL parameters

The app's shareable state is kept in the page's URL search params, which are rewritten as you interact with the component. Copying the URL and opening it elsewhere reproduces what you were looking at. The params:

Param Maps to the option Notes
as_of initial_as_of 'YYYY-MM-DD'
interval initial_interval
target_var initial_target_var
model initial_checked_models repeatable - one per checked model
xaxis_range initial_xaxis_range repeated twice: start and end
yaxis_range initial_yaxis_range ""
any task_ids key initial_task_ids e.g., location=NYC
season_mode initial_season_mode true or false
season initial_season the season's start year, e.g., 2024
season_start initial_season_start_month 1 through 12

URL params override the options object's values. The merged result is validated against src/schema.json, and if anything is invalid the whole set is ignored, a dialog says so, and the URL is rewritten to the state actually in use.

The three season params are only written to the URL while season mode is on, since off is the default - which does mean a non-default "Season start" isn't captured by a URL copied while season mode is off.

If as_of and season disagree - only possible in a hand-edited URL - as_of wins and the season is derived from it. A season given without an as_of wins instead, moving to that season's first available "as of" date.

In season mode, an xaxis_range is only honored if it falls within the season - a zoom within it, typically, from a copied URL. One that reaches outside the season, such as a multi-season initial_xaxis_range meant for when season mode is off, is ignored in favor of the season's extent. yaxis_range is honored either way.

Jump to as_of date

To jump to a specific "as of" date (i.e., reference date), use the calendar icon in the plot's modebar, which is located in the top right of the plot. This strip of gray icons is shown by hovering over the chart. (See the below screenshot for an example.) Clicking the icon will pop up a date picker where you can navigate to a date and then click "Apply" to set it as the current "as of" date. Note that the year select box above calendar is constrained to the current target's data date range. Because not every calendar data has a corresponding "as of" date, picking a date that corresponds to the currently-set "as of" date will not update the display.

Screenshot

screenshot

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages