@flourish/info-popup

Popup with data insertion

Downloads in past

Stats

StarsIssuesVersionUpdatedCreatedSize
@flourish/info-popup
6.0.011 days ago2 years agoMinified + gzip package size for @flourish/info-popup in KB

Readme

Flourish Info Popup
Add data-driven popups (tooltips) and panels to any Flourish template, with style settings.
There are typically seven steps to using the module:

1 - Installation

npm install @flourish/info-popup

2 - Add a state property for the module, with an object as a value

const state = {
    popup: {} // Add any properties to this object to override the default settings
}

3 - Import the settings into your template.yml file

- Popups & panels
- property: popup
  import: "@flourish/info-popup"

4 - Initialize the popup

Initialize the popup, passing in the state property and optionally containers that determine the position of the popups and panels
import initialisePopup from "@flourish/info-popup";

const popup = initialisePopup(state.popup, panel_container, popup_container);
If you want the panel to fill the whole page (i.e. cover the header & footer), just ignore the panel_container and popup_container arguments.

5 - Attach event-based methods to the data

The important thing to understand with this module is that the template passes on information about user triggered events (e.g. click, mouseover etc) and then the module decides what should happen as a result of this event. This is because there are many different ways that popups and panels behave in response to user events depending on the settings selected. For example here are some of the different scenarios for the mouseover event:
  • When in Popup mode: popup shows over data point
  • When in Popup mode, with sticky content: nothing happens
  • When in Panel mode: nothing happens
  • When in Panel mode, Always show turned on: content loads in panel
  • When in Panel mode, Always show turned on, with sticky content: nothing happens
  • When in Both mode: Popup with preview content shows over data point

So for consistency this logic is abstracted from the template. This is why there is no method for actions such as opening a popup because instead we call popup.mouseover() and let the module look at all the different settings and decide what should happen as a result of that event.
All event based methods are listed below in the methods section but a common implementation will look something like this:
import { popup } from "./popup";

function updateGraphic() {
    d3.selectAll(...) // datapoints
        .on("mouseover", function() {
            const el = this;
            popup.mouseover(el, el.__data__);
        })
        .on("mouseout", function() {
            popup.mouseout();
        })
        .on("click", function() {
            const el = this;
            const data = el.__data__
            const locked_id = data.id
            popup.click(el, data, locked_id); // See step 8 for more information on locked_id
        });
}

6 - Making sure the popup displays the correct, formatted, data with .setColumnNames() and .setFormatters()

For each popup we want to make sure we're displaying the correct labels and formatted values from the data. There are two parts to making this work:

Part One - passing in the data

This happens as part of step 5 when passing data in to popup.click, popup.mouseover or popup.touch. You pass in the data that is bound to the node that you are interacting with. If you had the following data bindings: binding_one,binding_two, binding_three and binding_four the data on each data point (and therefore the data you would be passing to the popup module) would look something like this:
{
    binding_one: "Joe Biden",
    binding_two: "President",
    binding_three: "https://public.flourish.studio/uploads/ff3f9984-470c-4ed9-beb8-2a86bb7019ea.jpg",
    binding_four: "Democrat"
}

Part Two - setting the column names

Currently the popup has been passed a set of values that correlate to data bindings, we now have to set a column name for each data binding we wish to appear in the popup:
popup
    .setColumnNames({
        binding_one: "Name",
        binding_two: "Job title",
        binding_four: "Party"
    })
    .update()

Data will only show if we have set a column name for that data binding. We didnt set a column name for binding_three so the resulting information displayed in the popup would be:
Name: Joe Biden
Job title: President
Party: Democrat

If there are no columns that you wish to exclude you can pass in the column_names object from the Flourish dataset:
popup
    .setColumnNames(data.data.column_names)
    .update()

If there are columns you'd like to be available for custom content, but don't want to show in the default popup or panel content (eg. long series data), you can exclude them using .hideColumnNames(). This takes an array of binding names. In the above example, calling .hideColumnNames(["binding_four"]) will not show the Party information in the default popup or panel.

Part Three - setting the formatters

Optionally, you can also set formatters for each data binding. These must match the data bindings passed in .setColumnNames() but you do not need to provide a formatter for all bindings.
Formatters take two forms:
  • A function taking a value and returning a string. Any formatters generated by initNumberFormatter in @flourish/formatters should by passed like this so that formatting changes in the settings panel are reflected in the popup.
  • An object containing an output_format_id string property used in the @flourish/interpreter module. This is passed to @flourish/formatters to generate a formatter function.

popup
    .setColumnNames({
        binding_one: "Name",
        binding_two: "Cost",
        binding_three: "Sold", 
        binding_four: "Released"
    })
    .setFormatters({
        binding_two: d => `£${d}`,
        binding_three: { output_format_id: "number$comma_point" } 
        binding_four: { output_format_id: "datetime$%d/%m/%Y" },
    })
    .update()

If the data below was passed to the popup:
{
    binding_one: "Pain au chocolat",
    binding_two: 2,
    binding_three: 1235000,
    binding_four: new Date("2022-05-12T03:24:00")
}

The resulting information would be displayed in the popup:
Name: Pain au chocolat
Cost: £2
Sold: 1,235,000
Released: 05/12/2022

If you do not want to pass any custom formatter functions you can set formatters for all bindings by passing the metadata object for a dataset. This object includes entries with an output_format_id string property for each binding (these can be set by the user in the data panel).
popup
    .setColumnNames(data.data.column_names)
    .setFormatters(data.data.metadata)
    .update()

7 - Updating the popup with .update()

To make sure that the popup is using the latest settings, adds the correct content based on what's passed in via .setColumnNames(), we need to call popup.update() inside the template's update() loop.

8 - Set up locking functionality (optional)

Locked functionality is the ability to 'lock' a popup in an open state. A locked popup will remain open until clickout is triggered and while locked it will have CSS pointer events enabled, so that the end viewer can for example click a link inside the popup.
To set up locking functionality you need to define a getLockedPosition function. If there is a locked popup this function will retreive the locked node or coordinate information of the locked item, and the data that's associated with it. It returns an array: [node_or_coordinate, data]
This function needs only be called once so can be called straight after the popup is initialised, but can also be called as part of an update function.
popup.getLockedPosition(getLockedPositionCallback)

function getLockedPositionCallback(locked_id) {
    // get the node that relates to this id (this is just one common way you might do this)
    const el_node = select(`#${locked_id}`).node();

    // if there is no locked popup, or there is currently no element on the screen
    // corresponding to the stored locked popup return null
    if (!el_node) return null;

    // get the data from that node
    const datum = el_node && el_node.__data__;

    // return node and data
    return [el_node, datum];
}

Methods

Event-based methods

  • popup.click(coords_or_node, data, locked_id, callback)
Used when the viewer clicks a data point. coords_or_node can be a coordinate array (x, y) or an DOM element. data is the data to display (usually the data attached to the element you are clicking). locked_id is an identifier for the clicked data point. If locked_id is undefined it wont be locked, if it is defined the value will be stored as the lockedid and passed to the getLockedPositionCallback function.
  • popup.mouseover(coords_or_node, data, callback)
Used when the viewer mouseovers a data point. (Note: this doesn't take the locked_id argument since you can never lock a popup by mousing over a data point.)
  • popup.touch(coords_or_node, data, locked_id, callback)
Used when the viewer is on a touch device and touches a data point. It has the same behavior as mouseover except that in "Both" mode it makes it possible to touch once to open a popup and then touch the popup to open the panel. (Hence it needs the locked
id argument since this second touch locks the panel open.)
  • popup.mouseout()
Used when the viewer mouse leaves a data point. It closes or resets the popup or panel unless it is locked.
  • popup.clickout()
Used when the viewer clicks on something that isnt a data point. It closes the popup if it's locked; typically used in a click handler for the background (e.g. an SVG or canvas element). Note: this method can make code look quite counterintuitive e.g:
```javascript
      document.body.onclick = function() { popups.clickout(); };
```
This is because to the browser you are **clicking** but to the popups you are **clicking out** (because you are clicking on something that isnt a data point).

Setting a title and subtitle

It's possible to specify that certain keys in the data represent the title and subtitle for the popup or panel. These appear in the header section of the popup with different styling from the rest of the popup.
  • popup.titleKey(data_binding_name)
  • popup.subtitleKey(data_binding_name)

Specifying a default template

You can specify a different template for the popup or panel content, for instance if you want to add an <img> tag or the content needs to be shown in a specific order. This is done when initializing:
  • popup.popupDefaultTemplate(template_string)
  • popup.panelDefaultTemplate(template_string)

The template_string can include data binding keys between curly brackets, such as <h1>{{name}}</h1> where name is your data binding.

Positional methods

  • popup.popupDirections(directions) – gets or sets the popup directions.
  • popup.margins() – gets the space on the visualisation edges being used for the panel. This is useful for when you don't want the panel to overlay over the content.

Hiding methods

  • popup.hidePopup() will hide the popup without resetting the locked_id.
  • popup.hidePanel() will hide the panel without resetting the locked_id.

Getter methods

  • popup.locked() – gets the current locked_id from the popup state. Returns null if there is currently no popup "locked" open.
  • popup.mode() – gets the current mode from the popup state.

Misc methods

  • popup.onPanelClose(fn) will fire when the panel gets closed

Adjusting which data bindings to show in the custom content setting

On default, all data bindings are used as little buttons below the custom content setting. You can change this by adding a setting override:
overrides:
      - property: [ popup_custom_main, popup_custom_header ]
        editor:
          style: true
          url: true
          flourish_embed: true
          bindings: [ dataset_name.databinding_name_1, dataset_name.databinding_name_2 ]

Templates to reference

Templates that use info-popups in a fairly straightforward way inculde:
A template that uses info-popups with canvas include:

Importable tests

This module has importable tests :tada:. To run them in a template:
  1. Create a new file in cypress/integrations called info_popup.js
  2. Import the tests at the top of the file:
```
import { infoPopupTests } from '@flourish/info-popup/template_test'

```
  1. Write a triggerPopupClick function that triggers the clicking of a popup e.g:
```
function triggerPopupClick({state, data}) {
   // you have access to state and data but you may not need to use it
	    cy.get(".bubble:first-of-type").click();

	}
```
  1. Write a triggerPopupMouseover function
```
function triggerPopupMouseover({state, data}) {
   // you have access to state and data but you may not need to use it
	    cy.get(".bubble:first-of-type").trigger("mouseover");
}
```
  1. Call infoPopupTests() and pass it an object:
```

infoPopupTests({
    triggerPopupClick,
    triggerPopupMouseover,
    primary_datasheet_name // optional (see below)
})
```
  1. Run cypress tests as usual and these tests should run as part of the existing template cypress tests!

For templates with multiple datasheets

If a template has multiple datasheets you will need to pass in the name of the datasheet that the info popup binding relates to. If it is not passed in the tests will use the first datasheet in the column_names object.