@flourish/info-popup
Advanced tools
Comparing version 1.0.1 to 2.0.0
{ | ||
"name": "@flourish/info-popup", | ||
"version": "1.0.1", | ||
"version": "2.0.0", | ||
"description": "Popup with data insertion", | ||
@@ -5,0 +5,0 @@ "main": "info-popup.js", |
113
README.md
# Flourish Info Popup | ||
Enable popups with styling and data binding from Flourish | ||
Add data-driven popups (tooltips) and panels to any Flourish template, with style settings. | ||
## Install | ||
## Installation | ||
`npm install @flourish/info-popup` | ||
## Initialization | ||
## Usage | ||
The `settings.yml` file should be imported into your template’s `template.yml` file, like this: | ||
There are typically five steps to using the module: | ||
1. Add a state property for the module, with an object as a value. E.g.: | ||
``` | ||
var state = { | ||
my_popup: {} // Add any properties to this object to override the default settings | ||
} | ||
``` | ||
2. Initialize the popup, passing in the state property and optionally containers than determine the position of the popups and panels. E.g.: | ||
``` | ||
import InfoPopup from "@flourish/info-popup"; | ||
var popup = InfoPopup(state.my_popup, panel_container, popup_container)`; | ||
``` | ||
If you want to support "locked" popups properly, for example to position popups programmatically in story slides based on saved state, you should also add a `getLockedPosition` function during initialisation. See below. | ||
3. Import the settings into your `template.yml` file. E.g.: | ||
```yaml | ||
- Popups | ||
- property: mypopup | ||
- property: my_popup | ||
import: "@flourish/info-popup" | ||
``` | ||
`var popup = Popup(state.mypopup, panel_container, popup_container)` creates an Info Popup module with the defaults from `state.mypopup`. `panel_container` and `popup_container` are optional and are used to constrain the popup and panel. | ||
4. Attach event-based methods to data points to open/close the popups when the user clicks or mouseovers, as described below. | ||
Add column names to the popup by calling `popup.addColumnNames(data.data.column_names)`. | ||
5. Make sure in your update function you call `.update()` and `.updateColumnNames()`, as described below. | ||
### Showing and hiding the Info Popup | ||
### Showing and hiding the popups in response to user events | ||
You can specify specific user events when activating the Info Popup. So you only have to specify when the Info Popup was triggered by a click, clickout, mouse or mouseout - and the module will take care of sticky popups and when to enable/disable the panel. | ||
The module provides event-based functions to simplify working with the many combinations of events and settings. | ||
`popup.click(coords_or_node, data, id, callback)` | ||
`popup.clickout()` | ||
`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 (see below). `locked_id` is an identifier for the clicked data point. Assuming `locked_id` is not undefined, the popup will be "locked" and will have CSS pointer events enabled, so for example the end viewer can click a link in a popup. If `locked_id` is undefined, the popup won't be locked. | ||
`popup.mouseover(coords_or_node, data, callback)` | ||
Opens the popup or panel depending if it's not locked open. (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)` | ||
For touch devices. 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()` | ||
Closes or resets the popup or panel unless it is locked. | ||
- `coords_or_node` – can be an array with x, y coordinates or a Node element. | ||
- `data` – is an object with data to display in the popup. | ||
- `id` – is an id to remember which id should be made sticky | ||
`popup.clickout()` | ||
Closes the popup if it's locked; typically used in a click handler for the background (e.g. an SVG or canvas element). | ||
If the visibility of the popup is triggered by something other than a user event, you can user `popup.update(coords_or_node, d, callback)`. This is useful for complex templates or when showing/hiding sticky popup values. | ||
### Updating popups/panels in your update function | ||
### Positional methods | ||
Make sure you call `popup.updateColumnNames()` and `popup.update([callback])` in your update function so that: | ||
`popup.popupDirections(directions)` – gets or sets the popup directions. | ||
* the user will be able to see the results of changes to their popup or panel settings immediately. | ||
`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. | ||
* popups will hide/show as needed when changing slide in a Flourish story. | ||
For the latter to work properly, you need define a `getLockedPosition` function so that, when update is called, if the popup is in a "locked" state, the popup module knows where to position the popup. This function is called, passing in the `locked_id` as an argument and needs to return an array in the form `[node_or_coordinate, data]`. | ||
### Working with data and column names | ||
When opening or updating a popup, you will typically pass in `data` representing the clicked/hovered/searched data point. | ||
Typically this object will have internal property names, such as data binding keys. To convert these into readable names you also need to call `addColumnNames` to provide a display value for each key. | ||
(The name of this function reflects the fact that very often the display names are column headers from a user's data table.) | ||
For example, if the user click/hovers on a data point on a scatter plot, you could pass all the data associated with that point to the popup as an object, which might look like this: | ||
`{ id: "John", x_value: 24, y_value: 198, index: 10 }` | ||
None of these would appear in the popup, however, unless you've also called addColumnNames, such as: | ||
`addColumnNames({ id: "Name", x_value: "Age", y_value: "Weight" })` | ||
The result would be popup content such as: | ||
``` | ||
Name: John | ||
Age: 24 | ||
Weight: 198 | ||
``` | ||
(In this example `index` doesn't show up as it is not included when calling addColumnNames.) | ||
### Setting a title and subtitle | ||
It's possible to choose a data point that should be treated as a title or subtitle. The title and subtitle have different visual weight from the rest of the content. | ||
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. | ||
@@ -51,10 +105,23 @@ `popup.titleKey(data_binding_name)` | ||
### Specifying a default template | ||
It's also possible to specify a different template for the popup content. For instance if you want to add an <img> tag or content needs to show in a specific order. | ||
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: | ||
It's possible to pass in a default template when initializing the Info Popup. Adding data binding names between curly brackets, like `{{name_of_data_binding}}`; | ||
`popup.popupDefaultTemplate(template_string)` | ||
`popup.panelDefaultTemplate(template_string)` | ||
The `template_string` can include data binding keys between curly brackets, such as `{{name_of_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. | ||
### Other methods | ||
`popup.locked()` – gets the current `locked_id` from the popup state. Returns `null` if the popup is not in a "locked" state. | ||
`popup.hidePopup()` will hide the popup without resetting the `locked_id`. | ||
`popup.hidePanel()` will hide the panel without resetting the `locked_id`. |
@@ -0,1 +1,10 @@ | ||
# 2.0.0 | ||
* Add getLockedPosition method | ||
* add hidePopup and hidePanel method | ||
* Improve layout position behaviour | ||
* Don't change custom default | ||
* Fix overlapping tables | ||
* Add locked() method | ||
* Improve panel close button placement | ||
# 1.0.1 | ||
@@ -2,0 +11,0 @@ * Fix bug with custom panel content |
@@ -20,2 +20,3 @@ import Popup from "@flourish/popup"; | ||
this._popup_directions = ["top", "bottom", "left", "right"]; | ||
this._getLockedPosition = function() { return null; }; | ||
@@ -34,4 +35,24 @@ for (var key in DEFAULTS) { | ||
InfoPopup.prototype.update = function(coords_or_node, d, callback) { | ||
if (this._state.locked_id !== null) this.click(coords_or_node, d, this._state.locked_id, callback); | ||
// ============ // | ||
// INIT METHODS // | ||
// ============ // | ||
InfoPopup.prototype.getLockedPosition = function(getLockedPositionFunction) { | ||
if (typeof getLockedPositionFunction != "function") { | ||
console.error("Invalid function passed to @flourish/info-popup getLockedPosition"); | ||
} | ||
else this._getLockedPosition = getLockedPositionFunction; | ||
return this; | ||
}; | ||
// ====== // | ||
// UDPATE // | ||
// ====== // | ||
InfoPopup.prototype.update = function(callback) { | ||
if (this._state.locked_id !== null) { | ||
var selected = this._getLockedPosition(this._state.locked_id); | ||
if (selected) this.click(selected[0], selected[1], this._state.locked_id, callback); | ||
else console.warn("@flourish/info-popup: failed to return value from getLockedPosition function"); | ||
} | ||
else this.clickout(); | ||
@@ -133,2 +154,10 @@ }; | ||
// ======= // | ||
// GETTERS // | ||
// ======= // | ||
InfoPopup.prototype.locked = function() { | ||
return this._state.locked_id; | ||
}; | ||
// ====================== // | ||
@@ -141,19 +170,8 @@ // DATA & CONTENT METHODS // | ||
// Default custom template to first column so easier for user to understand | ||
if (!this._state.popup_custom_template && this._first_load) { | ||
for (var name in this._column_names) { | ||
if (!Array.isArray(this._column_names[name])) { | ||
var column_name = this._column_names[name]; | ||
this._state.popup_custom_template = column_name + ": {{" + column_name + "}}"; | ||
break; | ||
} | ||
} | ||
this._first_load = false; | ||
} | ||
return this; | ||
}; | ||
// ============ // | ||
// DRAW METHODS // | ||
// ============ // | ||
// =================== // | ||
// DRAW & HIDE METHODS // | ||
// =================== // | ||
@@ -169,3 +187,3 @@ InfoPopup.prototype.onPanelClose = function(f) { | ||
this.popup.draw(); | ||
if (callback) callback(this.popup._getElement()); | ||
if (callback) callback(this.popup._getElement(), d); | ||
return this; | ||
@@ -190,6 +208,16 @@ }; | ||
else this.panel.html(getPanelHTML(instance, d, "panel")).draw(); | ||
if (callback) callback(this.panel._getElement()); | ||
if (callback) callback(this.panel._getElement(), d); | ||
return this; | ||
}; | ||
InfoPopup.prototype.hidePanel = function() { | ||
this.panel.hide(); | ||
return this; | ||
}; | ||
InfoPopup.prototype.hidePopup = function () { | ||
this.popup.hide(); | ||
return this; | ||
}; | ||
// ================== // | ||
@@ -210,15 +238,16 @@ // POSITIONAL METHODS // | ||
var constrainer_bbox = this._popup_container.getBoundingClientRect(); | ||
var best_side, biggest_gap = 0, arrow_direction; | ||
var best_side, biggest_gap = 0, arrow_directions; | ||
var gaps = { | ||
"right": ((constrainer_bbox.width + constrainer_bbox.left) - (node_bbox.width + node_bbox.left)) / constrainer_bbox.width, | ||
"bottom": ((constrainer_bbox.height + constrainer_bbox.top) - (node_bbox.height + node_bbox.top)) / constrainer_bbox.height, | ||
"left": (node_bbox.left - constrainer_bbox.left) / constrainer_bbox.width, | ||
"right": ((constrainer_bbox.width + constrainer_bbox.left) - (node_bbox.width + node_bbox.left)) / constrainer_bbox.width, | ||
"top": (node_bbox.top - constrainer_bbox.top) / constrainer_bbox.height, | ||
"bottom": ((constrainer_bbox.height + constrainer_bbox.top) - (node_bbox.height + node_bbox.top)) / constrainer_bbox.height | ||
}; | ||
var arrow_directions = { | ||
"right": "left", | ||
"left": "right", | ||
"top": "bottom", | ||
"bottom": "top" | ||
var all_arrow_directions = { | ||
"right": ["left", "leftFlexible", "topLeft", "bottomLeft", "bottomFlexible", "bottom", "topFlexible", "top", "bottomRight", "topRight", "rightFlexible", "right"], | ||
"left": ["right", "rightFlexible", "topRight", "bottomRight", "top", "topFlexible", "bottom", "bottomFlexible", "bottomLeft", "topLeft", "leftFlexible", "left"], | ||
"top": ["bottom", "bottomFlexible", "bottomRight", "bottomLeft", "rightFlexible", "leftFlexible", "right", "left", "topRight", "topLeft", "topFlexible", "top"], | ||
"bottom": ["top", "topFlexible", "topLeft", "topRight", "left", "right", "leftFlexible", "rightFlexible", "bottomLeft", "bottomRight", "bottomFlexible", "bottom"] | ||
}; | ||
@@ -231,3 +260,3 @@ | ||
best_side = side; | ||
arrow_direction = arrow_directions[side]; | ||
arrow_directions = all_arrow_directions[side]; | ||
} | ||
@@ -245,3 +274,3 @@ } | ||
"side": best_side, | ||
"arrow_direction": arrow_direction + "Flexible", | ||
"arrow_directions": arrow_directions, | ||
"x": x, | ||
@@ -255,3 +284,3 @@ "y": y | ||
InfoPopup.prototype._point = function(coords_or_node) { | ||
var x, y, arrow_direction = this._popup_directions; | ||
var x, y, arrow_directions = this._popup_directions; | ||
if (Array.isArray(coords_or_node)) { | ||
@@ -261,3 +290,3 @@ x = coords_or_node[0]; | ||
} | ||
else { | ||
else if (coords_or_node && coords_or_node.getBoundingClientRect) { | ||
var node_rect = coords_or_node.getBoundingClientRect(); | ||
@@ -267,6 +296,10 @@ var best_side = this._getBiggestGap(node_rect); | ||
y = best_side.y; | ||
arrow_direction = [best_side.arrow_direction]; | ||
arrow_directions = best_side.arrow_directions; | ||
} | ||
else { | ||
console.warn("@flourish/info-popup: Invalid positional value passed in point function, should be a coordinates array, or a node. Current value is", coords_or_node); | ||
return this; | ||
} | ||
this.panel.point(coords_or_node); | ||
this.popup.directions(arrow_direction).point(x, y); | ||
this.popup.directions(arrow_directions).point(x, y); | ||
return this; | ||
@@ -273,0 +306,0 @@ }; |
@@ -72,9 +72,8 @@ import { select, event as d3_event } from "d3-selection"; | ||
.on("interrupt", function() { | ||
select(this).style("display", "none"); | ||
select(this).style("display", panel._is_open ? "block" : "none"); | ||
}) | ||
.on("end", function() { | ||
select(this).style("display", "none"); | ||
}); | ||
select(panel._inner) | ||
.transition().duration(duration/2) | ||
}) | ||
.select(".flourish-panel-close") | ||
.style("opacity", 0); | ||
@@ -134,3 +133,2 @@ | ||
panel._content.innerHTML = panel._html; | ||
if (panel._closeable) addCloseButton(panel); | ||
openPanel(panel); | ||
@@ -170,4 +168,5 @@ return this; | ||
select(panel._getElement()).style("display", "block") | ||
.transition().duration(duration).delay(duration * 0.25) | ||
.style("background", panel._position == "overlay" ? panel._underlay_color : "rgba(255, 255, 255, 0)"); | ||
.transition().duration(duration).delay(duration * 0.25) | ||
.style("background", overlay ? panel._underlay_color : "rgba(255, 255, 255, 0)") | ||
.style("pointer-events", overlay ? null : "none"); | ||
@@ -212,4 +211,5 @@ select(panel._inner) | ||
select(panel._inner).select(".flourish-panel-close") | ||
.transition().duration(duration/4).delay(duration) | ||
.style("opacity", 1); | ||
.style("opacity", 0) | ||
.transition().duration(duration / 4).delay(duration) | ||
.style("opacity", panel._closeable ? 1 : 0); | ||
@@ -240,2 +240,7 @@ panel._is_open = true; | ||
el.id = id; | ||
el.addEventListener("click", function(e) { | ||
e.stopPropagation(); | ||
panel.hide(); | ||
if (panel._onclose) panel._onclose(); | ||
}); | ||
@@ -277,2 +282,4 @@ var s = el.style; | ||
addCloseButton(panel); | ||
panel._container.appendChild(el); | ||
@@ -279,0 +286,0 @@ } |
@@ -63,3 +63,4 @@ import { color } from "d3-color"; | ||
.style("font-weight", "bold") | ||
.style("margin-bottom", "0.5em"); | ||
.style("margin-bottom", "0.5em") | ||
.style("padding-right", "1rem"); | ||
@@ -94,3 +95,4 @@ styles.select(".flourish-popup main") | ||
.style("width", "100%") | ||
.style("border-collapse", "collapse"); | ||
.style("border-collapse", "collapse") | ||
.style("table-layout", "fixed"); | ||
@@ -108,3 +110,5 @@ styles.select(".flourish-popup table tr, .flourish-panel table tr") | ||
.style("text-align", "left") | ||
.style("font-weight", "normal"); | ||
.style("font-weight", "normal") | ||
.style("overflow", "hidden") | ||
.style("text-overflow", "ellipsis"); | ||
@@ -111,0 +115,0 @@ styles.select(".flourish-popup .data-heading") |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
180534
4621
126
0