2020-07-03 13:08:14 +03:00
|
|
|
---
|
|
|
|
layout: developer-doc
|
|
|
|
title: Visualization Workflow
|
|
|
|
category: product
|
|
|
|
tags: [product]
|
|
|
|
---
|
|
|
|
|
|
|
|
# Visualization Workflow
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
## Purpose of visualizations
|
2021-03-10 13:09:45 +03:00
|
|
|
|
2020-05-01 01:30:51 +03:00
|
|
|
Visualizations have two main purposes:
|
|
|
|
|
|
|
|
- **Display results of nodes**
|
|
|
|
Each node can be assigned with one or more visualization. After a node
|
|
|
|
computes its new value, the visualization shows it in an understandable way to
|
|
|
|
the user. Please note that a single node can be assigned with multiple
|
|
|
|
visualizations at the same time. For example, a node might want to display a
|
2020-07-11 04:00:26 +03:00
|
|
|
map of locations, and their list at the same time next to each other.
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
- **Provide interactive way to generate new data**
|
|
|
|
In a widget mode (described in detail later), visualizations provide users
|
|
|
|
with an interactive GUI to define data. For example, a map visualization can
|
2020-07-11 04:00:26 +03:00
|
|
|
both display locations, as well as allowing the user to pick locations by
|
|
|
|
clicking with a mouse. Similarly, the histogram can both display a list of
|
|
|
|
numbers, and can be manually draw with the mouse producing such a list.
|
|
|
|
Several numbers can be visualized as a table of sliders, which can also be
|
|
|
|
used to interactively generate a table of numbers. Image visualizations can
|
|
|
|
behave like an image editor, etc.
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
## Visualization Display Forms
|
2021-03-10 13:09:45 +03:00
|
|
|
|
2020-05-01 01:30:51 +03:00
|
|
|
Visualizations can be displayed in the following ways:
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- **Attached to nodes** In this mode, visualizations display the most recent
|
|
|
|
result of the node. They behave like an integrated part of the node. Whenever
|
|
|
|
you move the node, the visualization moves as well. This mode can be toggled
|
|
|
|
by tapping the spacebar.
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
- **Fullscreen**
|
|
|
|
Visualization attached to node can grow (animate) to ocupy full IDE visual
|
|
|
|
space. This mode can be triggered on the recently selected node (in case many
|
|
|
|
nodes are selected, the last selected node will be used) by either pressing
|
|
|
|
keeping the spacebar pressed for longer than approx 0.5s, or by tapping it
|
|
|
|
twice. In the former case, the visualization shrinks to each original form
|
|
|
|
whenever we release space, in the later, whenever we press space again.
|
|
|
|
|
|
|
|
- **Detached**
|
|
|
|
Visualizations attached to nodes can be detached, scaled, and placed freely
|
|
|
|
across the visual canvas (we might introduce a special place where you can put
|
2021-03-10 13:09:45 +03:00
|
|
|
such visualizations). This is useful when defining dashboards or reports. We
|
|
|
|
also plan to provide a notebook-like experience where you can write text mixed
|
|
|
|
with visualizations (including widgets for an interactive experience).
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
- **Widgets**
|
|
|
|
In this mode visualizations behave like nodes but do not display expressions.
|
|
|
|
They have one input and one output port. If the input port is connected, the
|
|
|
|
visualization displays its value and passes its to the output port. In case it
|
|
|
|
is not connected, the visualization becomes an interactive widget allowing the
|
|
|
|
user to specify data. For example, a map visualization will allow the user to
|
|
|
|
manually pick locations. After each change, the new locations will be sent to
|
|
|
|
the output port. Under the hood, widgets are represented as nodes and their
|
2020-07-11 04:00:26 +03:00
|
|
|
code lines are assigned with a dedicated "visualization" metadata.
|
2020-05-01 01:30:51 +03:00
|
|
|
Visualizations generate expressions always in the form of `name = data`, where
|
|
|
|
data is a hardcoded data produced from the visualization. For example, when
|
|
|
|
user clicks the map to define locations, the data could be a string literal
|
|
|
|
containing locations encoded in JSON.
|
|
|
|
|
|
|
|
### Choosing a Visualization Type.
|
2021-03-10 13:09:45 +03:00
|
|
|
|
2020-05-01 01:30:51 +03:00
|
|
|
When a new data is provided to a visualization, the visualization registry
|
|
|
|
searches for all visualizations that match it (see visualization registry to
|
|
|
|
learn more). For example, when a data of type `[Int]` (list of ints) is
|
|
|
|
produced, all visualizations which matches `[Int]`, like `[Int]`, `[a]`, or `a`
|
|
|
|
will be found. Each type can be associated with a default visualization. For
|
|
|
|
example, `[Int]` might define that its default visualization is a plot. If no
|
|
|
|
default visualization is defined, a JSON visualization is used. Each
|
|
|
|
visualization has a drop-down menu allowinh the user switching to another
|
|
|
|
visualization type.
|
|
|
|
|
|
|
|
### Active Visualizations
|
2021-03-10 13:09:45 +03:00
|
|
|
|
2020-05-01 01:30:51 +03:00
|
|
|
When visualizations are displayed on the stage, they are not active by default,
|
|
|
|
which means, they do not capture keyboard shortcuts. Visualization becomes
|
|
|
|
active when user clicks it. Visualizations are deactivated by clicking in the
|
|
|
|
background of the node editor. When a visualization is active, all other
|
|
|
|
elements should be slightly dimmed, or the visualization should get a selection
|
|
|
|
border (to be defined). Active visualizations capture all keyboard shortcuts,
|
|
|
|
but the space bar presses. Fullscreen visualizations are considered active by
|
|
|
|
default.
|
|
|
|
|
|
|
|
## HTML and Native Visualizations
|
2021-03-10 13:09:45 +03:00
|
|
|
|
2020-05-01 01:30:51 +03:00
|
|
|
There are two main types of visualizations - Html and Native. The later uses the
|
|
|
|
BaseGL shape API to draw on the screen. We prefer the later as it integrates
|
|
|
|
tightly with our framework and allows for much better performance. However,
|
|
|
|
there is already many visualizations in HTML/JS and we need to provide support
|
|
|
|
for them as well. HTML visualizations are required to be displayed in dedicated
|
|
|
|
div elements. This has several consequences. Firstly, the browser needs to
|
|
|
|
layout them, taking into account the current camera view, etc. It is costly.
|
|
|
|
Refreshing CSS3D styles of 100 visualizations can absolutely kill the
|
|
|
|
interactive performance. On the other hand, refreshing the position of 10k
|
|
|
|
Native visualizations is almost free. Secondly, they need to be handled by our
|
|
|
|
engine in such way that we can interact with them. For that purpose, the current
|
|
|
|
Scene implementation defines three layers - top HTML layer, middle WebGL layer,
|
|
|
|
and bottom HTML layer. The HTML visualizations are created and displayed on the
|
|
|
|
bottom layer by default. Whenever an HTML visualization gets active, it should
|
|
|
|
be moved to the top layer.
|
|
|
|
|
|
|
|
## Visualization Registry
|
2021-03-10 13:09:45 +03:00
|
|
|
|
2020-05-01 01:30:51 +03:00
|
|
|
Visualizations are user-defined. Enso ships with a set of predefined
|
|
|
|
visualizations, but they are in no way different than user-defined, they are
|
|
|
|
just defined for you. Visualizations can be defined either as HTML or native
|
2021-03-10 13:09:45 +03:00
|
|
|
visualization and can be defined in JS or WASM (or any language that compiles to
|
|
|
|
one of these). Visualizations are stored on disk on the server-side and are
|
2020-05-01 01:30:51 +03:00
|
|
|
provided to the GUI by the server. Users can upload their custom visualizations
|
|
|
|
as well. Each visualization is registered in the visualization map. The map maps
|
|
|
|
an Enso type to a set of visualizations defined for that type. The type might be
|
|
|
|
very generic, like `[a]` (which in Enso terms means list of any elements).
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
## Defining a Visualization
|
|
|
|
|
|
|
|
Currently only JavaScript visualizations can be defined. Support for native
|
|
|
|
visualizations is planned.
|
|
|
|
|
|
|
|
### Defining a JavaScript Visualization
|
2020-07-11 04:00:26 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
JavaScript visualizations are defined by placing `*.js` files in the
|
|
|
|
`visualization` subfolder in the project's root directory. As IDE currently
|
|
|
|
allows only editing `Main.enso` file, users have to create `.js` file manually,
|
|
|
|
editing it outside IDE.
|
2020-07-11 04:00:26 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
## Custom JavaScript Visualization Example
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
Every visualization must reside in the `visualization` folder of the user's
|
|
|
|
project. For instance:
|
|
|
|
|
|
|
|
```
|
2021-03-10 13:09:45 +03:00
|
|
|
└─ Project_Name
|
2020-07-11 04:00:26 +03:00
|
|
|
├─ src
|
|
|
|
│ └─ Main.enso
|
|
|
|
└─ visualization
|
|
|
|
└─ bubble.js
|
|
|
|
```
|
|
|
|
|
|
|
|
Visualizations can be defined as a JavaScript function which returns a class of
|
2021-03-10 13:09:45 +03:00
|
|
|
a shape specified below. Consider the following sample definition:
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
```javascript
|
2021-03-10 13:09:45 +03:00
|
|
|
console.log("Hi, this definition is being registered now!");
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
return class BubbleVisualization extends Visualization {
|
2021-03-10 13:09:45 +03:00
|
|
|
static inputType = "Any";
|
2020-07-11 04:00:26 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
onDataReceived(data) {
|
|
|
|
const xmlns = "http://www.w3.org/2000/svg";
|
|
|
|
while (this.dom.firstChild) {
|
|
|
|
this.dom.removeChild(this.dom.lastChild);
|
2020-07-11 04:00:26 +03:00
|
|
|
}
|
2021-03-10 13:09:45 +03:00
|
|
|
const width = this.dom.getAttributeNS(null, "width");
|
|
|
|
const height = this.dom.getAttributeNS(null, "height");
|
|
|
|
const svgElem = document.createElementNS(xmlns, "svg");
|
|
|
|
svgElem.setAttributeNS(null, "id", "vis-svg");
|
|
|
|
svgElem.setAttributeNS(null, "viewBox", "0 0 " + width + " " + height);
|
|
|
|
svgElem.setAttributeNS(null, "width", "100%");
|
|
|
|
svgElem.setAttributeNS(null, "height", "100%");
|
|
|
|
this.dom.appendChild(svgElem);
|
|
|
|
data.forEach((data) => {
|
|
|
|
const bubble = document.createElementNS(xmlns, "circle");
|
|
|
|
bubble.setAttributeNS(null, "stroke", "black");
|
|
|
|
bubble.setAttributeNS(null, "fill", "red");
|
|
|
|
bubble.setAttributeNS(null, "r", data[2]);
|
|
|
|
bubble.setAttributeNS(null, "cx", data[0]);
|
|
|
|
bubble.setAttributeNS(null, "cy", data[1]);
|
|
|
|
svgElem.appendChild(bubble);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
setSize(size) {
|
|
|
|
this.dom.setAttributeNS(null, "width", size[0]);
|
|
|
|
this.dom.setAttributeNS(null, "height", size[1]);
|
|
|
|
}
|
|
|
|
};
|
2020-07-11 04:00:26 +03:00
|
|
|
```
|
|
|
|
|
|
|
|
In particular:
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Required] Source code
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
Visualization definition has to be a valid body of JavaScript function which
|
|
|
|
returns a class definition. Instances of that class will be considered
|
|
|
|
separate visualizations. You are allowed to use global variables / global
|
|
|
|
state across visualizations of the same type, but you are highly advised not
|
|
|
|
to do so.
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Required] `Visualization` superclass
|
|
|
|
|
2020-07-11 04:00:26 +03:00
|
|
|
The class returned by the definition function should extend the predefined
|
|
|
|
`Visualization` class. Classes which do not extend it, will not be registered
|
|
|
|
as visualizations. The superclass defines a default constructor and a set of
|
2021-02-03 20:06:10 +03:00
|
|
|
utilities:
|
2020-07-11 04:00:26 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- #### Method `setPreprocessorCode(code)`
|
|
|
|
Set an Enso code which will be evaluated on the server-side before sending
|
|
|
|
data to visualization. If not called, a default unspecified code is used
|
|
|
|
that will provide some JSON representation of the value. See
|
|
|
|
[Lazy visualizations](#lazy-visualizations) section for details.
|
|
|
|
- #### Method `setPreprocessorModule(module)`
|
|
|
|
Define in which module's context the preprocessor code should be evaluated.
|
|
|
|
If not called, the `Main` module of the project that defines visualization
|
|
|
|
will be used. See [Lazy visualizations](#lazy-visualizations) section for
|
|
|
|
details.
|
|
|
|
- #### Method `setPreprocessor(code,mode)`
|
|
|
|
Set both code and its module context at once. If both need to be updated,
|
|
|
|
using this method can save an update processing and needless evaluation.
|
|
|
|
Note that using both `setPreprocessorCode` and `setPreprocessorModule` from
|
|
|
|
the visualization's custom constructor will not cause any unnecessary
|
|
|
|
updates, as the preprocessor is applied only after visualization is fully
|
|
|
|
constructed. See [Lazy visualizations](#lazy-visualizations) section for
|
|
|
|
details.
|
|
|
|
- #### Field `dom`
|
|
|
|
It is initialized in the constructor to the DOM symbol used to host the
|
|
|
|
visualization content. Users are free to modify the DOM element, including
|
|
|
|
adding other elements as its children.
|
2021-03-24 21:08:42 +03:00
|
|
|
- #### Field `theme`
|
|
|
|
The IDE's current color theme. Exposes the following methods.
|
|
|
|
- ##### Method `getColorForType`
|
2021-03-30 10:36:42 +03:00
|
|
|
Takes a qualified type name and returns the color that is used in the GUI
|
|
|
|
to represent that type.
|
2021-03-24 21:08:42 +03:00
|
|
|
- ##### Method `getForegroundColorForType`
|
2021-03-30 10:36:42 +03:00
|
|
|
Takes a qualified type name and returns the color that should be used for
|
|
|
|
foreground elements (e.g. text) that are shown on top of the background
|
|
|
|
color returned by `getColorForType`.
|
2021-03-24 21:08:42 +03:00
|
|
|
- ##### Method `get`
|
2021-03-30 10:36:42 +03:00
|
|
|
Takes a style sheet path as string and returns the corresponding value
|
|
|
|
from the theme. For example, `get("graph_editor.node.error.panic")`
|
|
|
|
returns the orange color that is used to mark nodes in an error state.
|
2021-03-10 13:09:45 +03:00
|
|
|
|
|
|
|
- ### [Optional] Field `label`
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
The static field `label` is an user-facing name used to identify the
|
|
|
|
visualization. You are not allowed to define several visualizations of the
|
|
|
|
same name in the same Enso library. In case the field is missing, the name
|
|
|
|
will be inferred from the class name by splitting the camel-case name into
|
|
|
|
chunks and converting them to lowercase string.
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Optional] Field `inputType`
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
The static field `inputType` is used to determine which Enso data types this
|
|
|
|
visualization can be used for. Its value should be a valid Enso type, like
|
2021-03-10 13:09:45 +03:00
|
|
|
`"String | Int"`. In case the field is an empty string or it is missing, it
|
|
|
|
will default to `"Any"`, which is a type containing all other types. It is a
|
|
|
|
rare case when you want to define a visualization which is able to work with
|
|
|
|
just any data type, so you are highly advised to provide the type definition.
|
2020-07-11 04:00:26 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Optional] Field `inputFormat`
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
The static field `inputFormat` is used to determine what format the data
|
|
|
|
should be provided to the `onDataReceived` function. Currently, the only valid
|
|
|
|
option is "json", but it will be possible to set it to "binary" in the future.
|
|
|
|
In the later case, it is up to the visualization author to manage the binary
|
|
|
|
stream received from the server.
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Optional] Constructor
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
The visualization will be instantiated by providing the constructor with a
|
|
|
|
configuration object. The shape of the configuration object is not part of the
|
|
|
|
public API and can change between releases of this library. You have to pass
|
|
|
|
it unchanged to the superclass constructor.
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Optional] Function `onDataReceived`
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
The `onDataReceived(data)` method is called on every new data chunk received
|
|
|
|
from the server. Note that the visualization will receive the "full data" if
|
|
|
|
you are not using the `setPreprocessor` method.
|
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
- ### [Optional] Function `setSize`
|
2020-07-11 04:00:26 +03:00
|
|
|
|
|
|
|
The `setSize(size)` method is called on every size change of the
|
|
|
|
visualization. You should not draw outside of the provided area, however, if
|
|
|
|
you do so, it will be clipped to the provided area automatically. The `size`
|
|
|
|
parameter contains two fields `width` and `height` expressed in pixels.
|
2020-05-01 01:30:51 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
## Sending Data to Visualizations
|
|
|
|
|
|
|
|
### Lazy Visualizations
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
Very important information is how visualization architecture works to make them
|
|
|
|
interactive and fast. Whenever new data is computed by the compiler and
|
|
|
|
visualization is attached to it, it is sent to GUI to be displayed. However,
|
2021-02-03 20:06:10 +03:00
|
|
|
sending huge chunks of data will kill the performance. When defining a
|
2020-11-30 14:38:35 +03:00
|
|
|
visualization user is capable of defining a chunk of Enso code (as a string).
|
2020-05-01 01:30:51 +03:00
|
|
|
This code is part of the visualization definition and is stored server-side.
|
2021-02-03 20:06:10 +03:00
|
|
|
Visualizations are allowed to change the code at runtime (in JavaScript
|
|
|
|
visualization you may use the `setPreprocessor` method). This code defines an
|
2020-05-01 01:30:51 +03:00
|
|
|
Enso function, which will be run by the compiler on data the visualization is
|
2021-03-10 13:09:45 +03:00
|
|
|
attached to. Only the results of this code will be sent to the GUI. In the case
|
|
|
|
of the JSON input format, the result of the call should be a valid JSON string.
|
|
|
|
The code will be evaluated in the context of the `Main` module in the project
|
|
|
|
where visualization is defined - you may use any symbol defined or imported in
|
|
|
|
that module.
|
|
|
|
|
|
|
|
For example, imagine you want to display a heatmap of 10 million points on a
|
|
|
|
map, and these points change rapidly. Sending such an amount of information via
|
|
|
|
WebSocket could be too much, and you (as the visualization author) might decide
|
|
|
|
that the visualization image should be generated on the server, and your
|
2021-02-03 20:06:10 +03:00
|
|
|
visualization is meant only to display the resulting image. In such a scenario,
|
2021-03-10 13:09:45 +03:00
|
|
|
you can define in your visualization an Enso function which will compute the the
|
|
|
|
image on the server!
|
2021-02-03 20:06:10 +03:00
|
|
|
|
2021-03-10 13:09:45 +03:00
|
|
|
### Binary and Text (JSON) Formats
|
2020-05-01 01:30:51 +03:00
|
|
|
|
|
|
|
Each visualization can choose whether it supports either binary or JSON input.
|
|
|
|
The input format defaults to JSON. The data from the server is always sent to
|
|
|
|
GUI in a binary channel, however, when JSON format is selected, it is first
|
2021-03-10 13:09:45 +03:00
|
|
|
converted to JSON representation on the server side. We can assume that all Enso
|
|
|
|
data types have defined conversion to JSON by default. If the visualization
|
2020-05-01 01:30:51 +03:00
|
|
|
input is defined as JSON input, the binary stream will be converted to JSON by
|
|
|
|
the GUI engine before passing to visualization. It is up to the visualization
|
2021-03-10 13:09:45 +03:00
|
|
|
author to handle the textual or binary form.
|
2021-03-30 10:36:42 +03:00
|
|
|
|
|
|
|
## Builtin Visualizations
|
|
|
|
|
|
|
|
IDE comes with a set of predefined visualizations, including charts.
|
|
|
|
|
|
|
|
### Dataframes Support
|
|
|
|
|
|
|
|
Some of the predefined visualizations have some special support for `Table` from
|
|
|
|
Enso Dataframes library.
|
|
|
|
|
|
|
|
#### Histogram
|
|
|
|
|
|
|
|
When using `Histogram` visualization on a `Table` value it will first look for a
|
|
|
|
column named `value`. If present, it will be used as a data source. Otherwise,
|
|
|
|
`Histogram` will use the first numerical column.
|
|
|
|
|
|
|
|
#### Scatter Plot
|
|
|
|
|
|
|
|
The `Scatter Plot` visualization has several properties for each point. If a
|
|
|
|
column of a matching name is present in the `Table` it will be used.
|
|
|
|
|
|
|
|
- `x` — position on horizontal axis. If not present, the index column will be
|
|
|
|
used. If there is no index set, the row indices will be used. If this column
|
|
|
|
has a missing value, the point will be omitted.
|
|
|
|
- `y` — position on vertical axis. If not present, first numerical column of
|
|
|
|
unrecognized name will be used. If not present, first numerical column will be
|
|
|
|
used. If this column has a missing value, the point will be omitted.
|
|
|
|
- `color` — color of the point. The default color is `black` and will be used if
|
|
|
|
column is not present or for its missing values. `color` should be a `Text`
|
|
|
|
column with elements being in a
|
|
|
|
[CSS colors format](https://www.w3schools.com/cssref/css_colors_legal.asp):
|
|
|
|
- Hexadecimal formats, like `#RGB`, `#RRGGBB` and `#RRGGBBAA`.
|
|
|
|
- RGB function-like syntax, e.g. `rgb(255,0,128)` or `rgba(255,0,128,0.5)`.
|
|
|
|
- HSL function-like syntax, e.g. `hsl(120, 100%, 50%)` or
|
|
|
|
`hsla(120, 100%, 50%, 0.3)`.
|
|
|
|
- name of one of
|
|
|
|
[predefined colors](https://www.w3schools.com/colors/colors_names.asp), e.g.
|
|
|
|
`red` or `SteelBlue`.
|
|
|
|
- `label` — text to be displayed next to the point.
|
|
|
|
- `shape` — shape of the point. Supported shapes are:
|
|
|
|
|
|
|
|
- `cross`;
|
|
|
|
- `diamond`;
|
|
|
|
- `square`;
|
|
|
|
- `star`;
|
|
|
|
- `triangle`.
|
|
|
|
|
|
|
|
The default shape is a circle.
|
|
|
|
|
|
|
|
- `size` — size of the point as a (possible floating point) number. Default size
|
|
|
|
of the point is `1.0`.
|