Skip to content

Latest commit

 

History

History
1611 lines (1147 loc) · 74.8 KB

DOCS.md

File metadata and controls

1611 lines (1147 loc) · 74.8 KB

The Piling.js Docs

Piling.js Preview

Getting Started

Install

npm install piling.js pixi.js

Optional: If you want to lay out piles by more than two attributes you have to install UMAP:

npm install umap-js

Quick Start

The bare minimum you need to define to get started with piling.js are the following two thing:

  1. A list of objects with a src property
  2. A renderer that understands how to render src into an image
// import the piling.js library
import createPilingJs from 'piling.js';
// import the predefined image renderer
import { createImageRenderer } from 'piling.js';

// define your dataset
const data = [{ src: 'http://example.com/my-fancy-photo.png' }, ...];

// instantiate and configure the piling.js library
// 'demo' is the dom element which piling.js will be rendered in
const piling = createPilingJs(
  document.getElementById('demo'),    // dom element in which piling.js will be rendered
  {
    renderer: createImageRenderer(),  // use the image renderer for rendering
    items: data,                      // add the images
  }
);

Et voilà 🎉

teaser-natural-images

Examples

As a first step, you have to query the DOM for an element in which you want to render the piles.

const demoEl = document.getElementById('demo');

Image Piles

To render images in Piling.js, create an array of objects (items) whose src property is an URL to an image and instantiate an image renderer. Finally you have to create a Piling.js instance.

import createPilingJs, { createImageRenderer } from 'piling.js';

const items = [{ src: 'http://example.com/my-fancy-photo.png' }, ...];
const itemRenderer = createImageRenderer();

const piling = createPilingJs(demoEl, { items, itemRenderer });

SVG Piles

To render SVG images as pile, your items src property needs to reference a SVG node or be a stringified version of a SVG node.

import createPilingJs, { createSvgRenderer } from 'piling.js';

const items = [{ src: '<svg viewBox="0,0,64,64"><rect ...></svg>' }, ...];
const itemRenderer = createSvgRenderer({
  width: 64, height: 64, background: 'white'
});

const piling = createPilingJs(demoEl, { items, itemRenderer });

D3 Piles

For convenience Piling.js also provides a createD3Renderer so you can easily use D3 for drawing the SVGs. To use D3 simply create a function that takes as input an item's source and returns a SVG node that contain the rendered visualization.

For a live demo see https://observablehq.com/@flekschas/piling-js-with-d3.

import createPilingJs, { createD3Renderer } from 'piling.js';
import { create, range, scaleBand, scaleLinear } from 'd3';

const items = Array.from({ length: 10 }, () => ({
  src: Array.from({ length: 5 }, () => Math.random()),
}));

// D3 Scales
const x = scaleBand().domain(range(5)).range([0, 64]).padding(0.1);
const y = scaleLinear().domain([0, 1]).range([64, 0]);

// D3 Renderer
const itemRenderer = createD3Renderer((itemSrc) => {
  const svg = create('svg').attr('viewBox', [0, 0, 64, 64]);

  svg
    .selectAll('rect')
    .data(itemSrc)
    .join('rect')
    .attr('x', (d, i) => x(i))
    .attr('y', (d) => y(d))
    .attr('height', (d) => y(0) - y(d))
    .attr('width', x.bandwidth());

  return svg.node();
});

const piling = createPilingJs(demoEl, { items, itemRenderer });

Vega-Lite Piles

To render piles with Vega-Lite use createVegaLiteRenderer. The factory function expects as input vega, vegaLite, and a base specification (baseSpec).

For a live demo see https://observablehq.com/@flekschas/piling-js-with-vega-lite.

import * as vega from 'vega/build/vega';
import * as vegaLite from 'vega-lite/build/vega-lite';
import createPilingJs, { createVegaLiteRenderer } from 'piling.js';

const items = Array.from({ length: 10 }, () => ({
  src: Array.from({ length: 5 }, () => Math.random()),
}));

// Vega Lite Renderer
const itemRenderer = createVegaLiteRenderer({
  vega,
  vegaLite,
  baseSpec: {
    $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
    width: itemWidth,
    height: itemHeight,
    mark: 'bar',
    encoding: {
      x: { field: 'index', type: 'ordinal' },
      y: { field: 'value', type: 'quantitative' },
    },
  },
});

const piling = createPilingJs(demoEl, { items, itemRenderer });

Observable Plot Piles

To render piles with Observable Plot use createObservablePlotRenderer. The factory function expects as input the Plot instance, a function that translates an item source into Plot marks, and Plot.plot() options.

For a live demo see https://observablehq.com/@flekschas/piling-js-with-observable-plot.

import * as Plot from '@observablehq/plot';
import createPilingJs, { createObservablePlotRenderer } from 'piling.js';

const items = Array.from({ length: 10 }, () => ({
  src: Array.from({ length: 5 }, (_, i) => ({ i, value: Math.random() })),
}));

// Observable Plot Renderer
const itemRenderer = createObservablePlotRenderer(
  Plot,
  (itemSrc) => [Plot.barY(itemSrc, { x: 'index', y: 'value' })],
  { width: itemWidth, height: itemHeight }
);

const piling = createPilingJs(demoEl, { items, itemRenderer });

Matrix Piles

First, import and instantiate a matrix renderer. If you want to have the aggregation and 1D previews of matrices when pile them up, you can also instantiate an cover renderer and a preview renderer here. (See matrix renderer for more information.)

import { createMatrixRenderer } from 'piling.js';

const itemRenderer = createMatrixRenderer({ colorMap, shape: [3, 3] });
const coverRenderer = createMatrixRenderer({
  colorMap: aggregateColorMap,
  shape: [3, 3],
});
const previewRenderer = createMatrixRenderer({ colorMap, shape: [3, 1] });

Then, you need aggregators for the aggregation and previews. So import and instantiate aggregators, and you can use mean value as the method of aggregation.

import {
  createMatrixCoverAggregator,
  createMatrixPreviewAggregator,
} from 'piling.js';

const coverAggregator = createMatrixCoverAggregator('mean');
const previewAggregator = createMatrixPreviewAggregator('mean');

Then add the renderers and aggregators to our piling.js library. Finally add the matrix data to the library.

import createPilingJs from 'piling.js';

const piling = createPilingJs(demoEl, {
  items,
  itemRenderer,
  coverRenderer,
  coverAggregator,
  previewRenderer,
  previewAggregator,
});

Data

An array of objects with one required property src and other optional user-defined properties:

  • src: the item data. This can literally be anything. The only requirement is that the renderer understands how to render it.

    Note, mixed data types are currently not supported. I.e., each item is rendered with the same renderer.

Examples:

// External image data
{
  src: 'https://github.com/test.png'
}

// A base64-encoded image
{
  src: 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAACWUlEQVR4AXTRA5DmMACG4Zxt27Ztm2vbtm3btm3r19m2zdHpu0nW6sxb92mbkEO6x3ssjuc7PvlO4Pn0u8F76XFvte1E8TyWutQYWHo2mqZ58kxv+122QGshVxwqKdgjsEFk4b5lslOzz8RtgXWdMmxr1eBQr8ly4xp0yptr4RN2MWxAJ8DOR/XQUcV1ZQMPEhyPWE8R+jCFOmFtKF/vs/dFE0W/YpuRnX5hh/Scy8OE+mKT52zol4qzh81qZBhIo/vKeccgmb4HSjFnoR0gq9MJcLKNVNpxfAaGyg3GcvtJ7EYa/aqTvpsh5HwQ1sHGKCgpwLMrb/61jglZslnZaM4e15cyEokIGTMQa1cPZch8rbFQd5NFQnY0BIJrePPuDz7+Aaoe30RUeRQMI+W1GDBnpZj4tLUKnK1nvXBr1UJkD+iLEZMJzjuogC53fn5GoaAEoSEWsDu3EfrHl6R0m0aKzNtnjItb1wDTBqGGEOgdWA13wyMwXtmHfRmFL/cn+L1lQ303gEa/Im7bJnCmjsXl2ZPZAzEDCDvmLlmExk2r27rrZLOyGzCVbN9vr64Fzt3LbV16cJ3FvXeXxn4nPKYAO44k+HUDaLKSWgX5ji5Id0lCnF0iK8EmBGmOvqDnC8L8UamvCWVxm/oegVHzhNeOHHkIs5eaYPsBN4gIW8NIwxLBLm7s4VR/F3CKC8DjNGX2CDBktlBiUIgvXj6/gTt3b0Bw+RZqODcRncb5buqWf0XZJCJSVidYqFdg2wHV+bLqzsXGDkkhQv+X3J3nG9/sARLDlxspwgCV8d4y+cSemwAAAABJRU5ErkJggg=='
}

// Matrix data
{
  src: {
    data: [3.1, 2.0, 1.1, 2.1, 3.2, 2.3, 1.0, 2.0, 3.1],
    shape: [3, 3],
    dtype: 'float32'
  }
}

// SVG string
{
  src: '<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg"><line x1="0" y1="80" x2="100" y2="20" stroke="blue" /></svg>'
}

Library

Constructors

createLibrary(domElement, initialProperties)

Arguments:

  • domElement: reference to the DOM element that will host piling.js' canvas
  • initialProperties (optional): an options object for setting initil view properties. The supported properties are the same as for set().

Returns: a new piling instance.

createLibraryAsync(domElement, initialProperties)

Arguments: Same as createLibrary().

Returns: a promise resolving to the new piling instance once the library was initialized.

createLibraryFromState(domElement, initialProperties, options)

Arguments:

  • domElement: reference to the DOM element that will host piling.js' canvas
  • state: a complete state object obtained from exportState().
  • options (optional): options from importState().

Returns: a promise resolving to the new piling instance once the state was imported.

Methods

piling.get(property)

Returns: one of the properties documented in set()

piling.set(property, value)

Arguments:

  • property: Either a string defining the property to be set or an object defining key-value pairs to set multiple properties at once.
  • value: If property is a string, value is the corresponding value. Otherwise, value is ignored.

piling.arrangeBy(type, objective, options)

Position piles with user-specified arrangement method.

Arguments:

type, objective, and options can be one of the following combinations:

Type Objective Options
null undefined (manual positioning) object
'index' a string, object, or function object
'ij' a string, object, or function object
'xy' a string, object, or function object
'uv' a string, object, or function object
'data' a string, object, function, or array of the previous types object

The following options are available for all types:

  • options.onGrouping [type: boolean default: false]: If true applies the arrangement on every piling event.

Returns: a Promise resolving once the piles have been arranged.

Notes and examples:

  • The signature of the callback function for types index, ij, xy and uv should be as follows:

    function (pileState, pileId) {
      // Based on the `type` return the index, or the ij, xy, or uv coordinate
      return pilePosition;
    }

    Alternatively, one can specify a data property holding the pile position as follows:

    piling.arrangeBy('index', 'myIndex');
    piling.arrangeBy('index', { property: 'myIndex', aggregator: 'median' });

    The property must correspond to a property of the item. The aggregator can be min, max, mean, median, sum, or a custom function with the following signature:

    function (data) {
      // Do something with `data`
      // Based on the `type` the value must be a scalar or a tuple
      return aSingleValue;
    }
  • When type === 'data', objective can either be a string, object, function, or an array of the previous types to produce a 1D ordering, 2D scatter plot, or multi-dimensional cluster plot.

    • The objective object can contain the following properties:

      • property [type: string or function]: A function that retrieves that returns a numerical value for an pile's item.

        The signature of the callback function looks as follows and must return a numerical value:

        function (itemState, itemId, itemIndex) {
          // Do something
          return aNumericalValue;
        }

        For convenience, we can automatically access item properties by their name. E.g., the following objective are identical:

        piling.arrangeBy('data', 'a');
        piling.arrangeBy('data', (itemState) => itemState.a);
      • propertyIsVector [type: boolean default: false]: If true we assume that property() is returning a numerical vector instead of a scalar.

      • aggregator [type: string or function default: mean]: A function for aggregating the property values of the piles' items.

        For convenience, we provide the following pre-defined aggregators: min, max, mean and sum.

      • scale [type: function default: d3.scaleLinear]: A D3 scale function

      • inverse [type boolean default: false]: If true, the scale will be inverted

    • For convenience the following examples are all equivalent:

      // Define the property via a simple string
      piling.arrangeBy('data', 'a');
      // Define the property callback function
      piling.arrangeBy('data', (itemState) => itemState.a); // callback function
      // Define the property callback function as part of the `objective` object
      piling.arrangeBy('data', { property: (itemState) => itemState.a });
      // Explicitly define
      piling.arrangeBy('data', ['a']);
    • 1D orderings, 2D scatter plots, or multi-dimensional cluster plots are defined by the number passed to arrangeBy('data', objectives):

        // 1D / linear ordering
        piling.arrangeBy('data', ['a']);
        // 2D scatter plot
        piling.arrangeBy('data', ['a', 'b']);
        // Multi dimensional cluster plot
        piling.arrangeBy('data', ['a', 'b', 'c', ...]);
  • When type === 'data', it is possible to further customize the behavior with the following options:

    • forceDimReduction [type: boolean default: false]: If true, dimensionality reduction is always applied.

      // This can be useful when the property itself is multidimensional. E.g.:
      const items = [
        {
          src: [0, 1, 1, 13.37, 9, ...]
        },
        ...
      ]
      piling.set('items', items);
      piling.arrangeBy('data', 'src', { forceDimReduction: true });
    • runDimReductionOnPiles [type: boolean default: false]: If true, dimensionality reduction is run on the current grouping status and updated everytime a pile changes.

      By default this is deactivated because dimensionality reduction transformations are often not deterministic and feeding even the same data to the algorithm can lead to vastly different layout. Therefore, by default we run the dimensionality reduction on the individual items and given that learned model position the piles. This allows us to keep the layout stable even as the piles change. If you want more fine-grain control over transformation updates we suggest running a dimensionalityReducer separately and using it's transform function in combination with piling.arrangeBy('uv') and piling.halt()/piling.resume().

      // Turning `runDimReductionOnPiles` on will cause a recalculation of the transformation everytime you change piles!
      piling.arrangeBy('data', ['a', 'b', 'c'], { runDimReductionOnPiles: true });

piling.groupBy(type, objective, options)

Programmatically group items and piles based on the layout, spatial proximity, or data together.

Arguments:

type, objective, and options can be one of the following combinations:

Type Objective Options
row left, center (default), or right
column top, center (default), or bottom
grid null (default) or { columns, cellAspectRatio}
overlap Overlap threshold in square pixels. Default is 0.
distance Distance threshold in pixels. Default is 0.
category A string, object, function, or array of the previous types. See examples below.
cluster A string, object, function, or array of the previous types. See examples below. object

The following options are available for all types:

  • options.onZoom [type: boolean default: false]: If true evaluates whether piles should be grouped on every (debounced) zoom event.

Returns: a Promise resolving once the piles have been grouped.

Notes and examples:

piling.groupBy('row', 'left'); // Pile by row and align pile to the left most item/pile.

piling.groupBy('column', 'top'); // Pile by column and align pile to the top most item/pile.

piling.groupBy('grid'); // Pile by grid using the current layout
piling.groupBy('grid', { columns: 10, cellAspectRatio: 1.5 }); // Pile by grid using a grid of 10 columns with a cell aspect ratio of 1.5 (= width/height)

piling.groupBy('overlap'); // Pile all overlapping items/piles
piling.groupBy('overlap', 64); // Pile all items/piles that overlap by 64 or more square pixels

piling.groupBy('distance'); // Pile all items/piles that touch each other
piling.groupBy('distance', 64); // Pile all items/piles that are 64 or less pixels apart from each other

piling.groupBy('category', 'country'); // Pile all items/piles that have the same country value
piling.groupBy('category', (item) => item.country); // Same as before
piling.groupBy('category', {
  property: 'country',
  aggregator: (countries) => countries[0],
}); // Same as before but with a custom aggregator that simply picks the first country to define the category

piling.groupBy('cluster', 'x'); // Pile all that cluster together based on the `x` property
piling.groupBy('cluster', (item) => item.x); // Same as above
piling.groupBy('cluster', { property: 'x', aggregator: 'max' }); // Same as above but with a custom aggregator that picks the max `x` value
piling.groupBy('cluster', 'x', { clusterer: dbscan }); // Same as above but with a custom clusterer
piling.groupBy('cluster', 'x', { clustererOptions: { k: 2 } }); // Same as above but with customized clusterer options

piling.destroy()

Destroys the piling instance by disposing all event listeners, the pubSub instance, canvas, and the root PIXI container.

piling.halt(options)

This will display a popup across the entire piling.js element to temporarily block all interactions. This is useful if you are doing some asynchronous job outside piling and want to prevent user interactions.

Arguments:

  • options (optional): Object with the two properties: text and spinner (default true)

piling.render()

Render the root PIXI container.

piling.resume()

This will the halting popup.

piling.splitAll()

Scatter all the piles at the same time.

piling.subscribe(eventName, eventHandler)

Subscribe to an event. eventName needs to be one of these events. eventHandler is a callback function which looks like this:

const eventHandler = (eventData) => {
  // handle event here
};

piling.unsubscribe(eventName, eventHandler)

Unsubscribe from an event. See events for all the events.

piling.exportState(options)

Arguments:

  • options (optional): Object with the following properties:
    • serialize (default false): If true Piling.js will serialize the state into a string. This operation is similar to but more specialized than JSON.stringify

Returns: current state object.

piling.importState(state, options)

Arguments:

  • state: Previously exported state object.
  • options (optional): Object with the following properties:
    • deserialize (default false): If true Piling.js will deserialize state assuming it was serialized with piling.exportState({ serialize: true })
    • overwriteState (default false): If true replaces the current store with state. Otherwise the properties in state will only override properties in the current state.

Returns: a promise that resolves once the state was imported

Utility Functions

deserializeState(serializedState)

Arguments:

Returns: a state object

serializeState(state)

Arguments:

Returns: Serialized state as a string. This operation is similar to but more specialized than JSON.stringify.

Properties

Name Type Default Constraints Unsettable
darkMode boolean false false
coverRenderer function see renderers true
backgroundColor string or int 0x000000 false
focusedPiles array [] the id of current focused pile true
coverAggregator function see aggregators true
depiledPile array [] the id of the pile to be depiled true
depileMethod string originalPos originalPos or closestPos true
easing function cubicInOut see notes true
gridColor string or int 0x787878 can be HEX, RGB, or RGBA string or hexadecimal value false
gridOpacity float 1.0 must be in [0,1] false
items array [] see data false
itemSize int number of pixels true
itemSizeRange array [0.7, 0.9] array of two numbers between (0, 1) true
columns int 10 ignored when itemSize is defined false
rowHeight int true
cellAspectRatio float ignored when rowHeight is defined false
cellPadding int true
cellSize int number of pixels true
lassoFillColor string or int 0xffffff can be HEX, RGB, or RGBA string or hexadecimal value false
lassoFillOpacity float 0.15 must be in [0,1] false
lassoShowStartIndicator boolean true false
lassoStartIndicatorOpacity float 0.1 must be in [0,1] false
lassoStrokeColor string or int 0xffffff can be HEX, RGB, or RGBA string or hexadecimal value false
lassoStrokeOpacity float 0.8 must be in [0,1] false
lassoStrokeSize int 1 must be greater or equal than 1 false
layout object read-only false
orderer function row-major see notes true
magnifiedPiles array [] the id of current magnified pile true
navigationMode string auto Can be one of auto, panZoom, or scroll false
pileBackgroundColor string, int or function can be HEX, RGB, or RGBA string or hexadecimal value false
pileBackgroundOpacity float or function 0 must be in [0,1] false
pileBackgroundColorHover string, int or function can be HEX, RGB, or RGBA string or hexadecimal value false
pileBackgroundOpacityHover float or function 0.85 must be in [0,1] false
pileBackgroundColorFocus string, int or function can be HEX, RGB, or RGBA string or hexadecimal value false
pileBackgroundOpacityFocus float or function must be in [0,1] false
pileBackgroundColorActive string, int or function can be HEX, RGB, or RGBA string or hexadecimal value false
pileBackgroundOpacityActive float or function must be in [0,1] false
pileBorderColor string, int or function 0x808080 can be HEX, RGB, or RGBA string or hexadecimal value false
pileBorderOpacity float or function 1.0 must be in [0,1] false
pileBorderColorHover string, int or function 0x808080 can be HEX, RGB, or RGBA string or hexadecimal value false
pileBorderOpacityHover float or function 1.0 must be in [0,1] false
pileBorderColorFocus string, int or function 0xeee462 can be HEX, RGB, or RGBA string or hexadecimal value false
pileBorderOpacityFocus float or function 1.0 must be in [0,1] false
pileBorderColorActive string, int or function 0xffa5da can be HEX, RGB, or RGBA string or hexadecimal value false
pileBorderOpacityActive float or function 1.0 must be in [0,1] false
pileBorderSize float or function 0 see notes true
pileCellAlignment string topLeft topLeft, topRight, bottomLeft, bottomRight or center true
pileContextMenuItems array [] see examples below true
pileCoverInvert boolean or function false see examples below false
pileCoverScale float or function 1.0 see examples below false
pileItemBrightness string, int or function 0 must be in [-1,1] where -1 refers to black and 1 refers to white false
pileItemInvert boolean or function false can only be true or false where true refers inverted colors and false are normal colors false
pileItemOffset array or function [5, 5] see notes true
pileItemOpacity float or function 1.0 see notes true
pileItemRotation float or function 0 see notes true
pileItemTint string, int or function 0xffffff can be HEX, RGB, or RGBA string or hexadecimal value true
pileLabel string, array, function or object see notes true
pileLabelAlign string or function bottom bottom or top true
pileLabelColor array or function see notes true
pileLabelFontSize int or function 8 true
pileLabelHeight float or function 2 true
pileLabelStackAlign string or function horizontal horizontal or vertical true
pileLabelSizeTransform string or function histogram see notes true
pileLabelText boolean or function false see notes true
pileLabelTextMapping array or function see notes true
pileLabelTextColor string or int 0x000000 see notes true
pileLabelTextOpacity float 1 see notes true
pileLabelTextStyle object {} see PIXI.TextStyle true
pileOpacity float or function 1.0 see notes true
pileOrderItems function see notes true
pileScale float or function 1.0 see notes true
pileSizeBadge boolean or function false if true show the pile size as a badge true
pileSizeBadgeAlign array or function ['top', 'right'] if true show the pile size as a badge true
popupBackgroundOpacity float 0.85 must be in [0,1] false
previewAggregator function see aggregators true
previewBackgroundColor string, int or function 'inherit' can be HEX, RGB, or RGBA string or hexadecimal value false
previewBackgroundOpacity float or function 'inherit' must be in [0,1] false
previewBorderColor string, int or function 0xffffff can be HEX, RGB, or RGBA string or hexadecimal value false
previewBorderOpacity float or function 0.85 must be in [0,1] false
previewItemOffset function see notes true
previewOffset number or function 2 see notes false
previewPadding number or function 2 see notes false
previewRenderer function see renderers true
previewScaling array or function [1,1] the spacing between 1D previews false
previewSpacing number or function 2 the spacing between 1D previews true
renderer function see renderers false
showGrid boolean false false
tempDepileDirection string horizontal horizontal or vertical true
tempDepileOneDNum number 6 the maximum number of items to be temporarily depiled in 1D layout true
temporaryDepiledPile array [] the id of the pile to be temporarily depiled true
zoomScale number or function 1 Allows adjusting the zoom-induced pile scale true

Examples and Notes:

  • To set a single property do:

    piling.set('propertyName', value);

    To set multiple values at once do:

    piling.set({
      propertyNameA: valueA,
      propertyNameB: valueB,
    });
  • A property is considered unsettable if its value can be removed.

  • When darkMode is true we assume that piling.js is used with a black background and the color of certain UI elements are adjusted automatically

  • orderer is the function for positioning piles, the default function is row-major orderer which looks like this:

    // The default row-major order
    
    // A function that takes as input the number of columns and outputs
    // another function that takes in as input the position of a 1D ordering and
    // outputs the an array of `x` an `y` coordinates.
    
    const rowMajor = (cols) => (index) => [
      index % cols,
      Math.floor(index / cols),
    ];
  • The following properties to define the grid: cellSize, cellPadding, columns, rowHeight and cellAspectRatio.

    One has to at least provide columns or cellSize to define a grid. If cellSize is defined columns are ignored. Similarly, when rowHeight is defined cellAspectRatio is ignored.

    When cellSize is defined, cellSize and cellPadding add up together to define the cell width. When cellSize is undefined, cellSize is defined by the derived cell width (given columns) minues cellPadding!

  • itemSize defines the size of the items. If it's not defined, it should be derived from the cell size.

  • easing is the easing function for animation, the default function is cubicInOut which looks like this:

    const cubicInOut = (t) => {
      t *= 2;
      const p = (t <= 1 ? t * t * t : (t -= 2) * t * t + 2) / 2;
      return p;
    };
  • All color properties (like backgroundColor, lassoFillColor, etc.) support HEX, RGB, and RGBA string and decimal values. E.g.,

    piling.set('lassoFillColor', '#ff0000');
    piling.set('lassoFillColor', 'rgb(255, 0, 0)');
    piling.set('lassoFillColor', 'rgba(255, 0, 0, 1)');
    piling.set('lassoFillColor', 0xff0000);

    Additionally, all lasso and pile related color properties (like lassoFillColor, pileBorderColor, etc.) support automatic setting of the opacity. E.g.,

    // The following...
    piling.set('lassoFillColor', 'rgba(255, 0, 0, 0.66)');
    // ...is a shorthand for...
    piling.set('lassoFillColor', 'rgb(255, 0, 0)');
    piling.set('lassoFillOpacity', 0.66);
  • If pileBackgroundColor is not defined, it will be set to the same color as the background depending on darkMode.

    pileBackgroundColorActive, pileBackgroundColorFocus, and pileBackgroundColorHover will inherit from pileBackgroundColor if not defined.

    pileBackgroundOpacityFocus and pileBackgroundOpacityActive will inherit from pileBackgroundOpacityHover if not defined.

  • pileContextMenuItems is an array of objects, which must have a label and callback property. Optionally, an object can also specify an id, which will be assigned to the corresponding button in the context menu, and keepOpen: true to not close the context menu after clicking on the corresponding button. The callback function is triggered whenever the user clicks on the corresponding button and it receives the pile definition, which contains the pile id, the ids of the items on the pile, and the pile's x and y coordinate.

    // Add a custom context menu
    const myClickHandler = (pile) => {
      console.log('Hi!', pile);
      // The log statement could look as follows for example:
      // Hi!, { id: 5, items: [2, 0, 8], x: 215, y: 8 }
    };
    piling.set('pileContextMenuItems', [
      {
        id: 'my-click',
        label: 'Click me!',
        callback: myClickHandler,
      },
    ]);
  • pileBorderSize, pileCoverInvert, pileCoverScale, pileOpacity and pileScale can be set to a static float value, or the user can specify a callback function to dynamically style piles. E.g.,

    // Set to a static number
    piling.set('pileScale', 2.0);
    
    // Set to a callback function
    piling.set('pileOpacity', (pile) => 1 / pile.items.length);

    The callback function is evaluated for each pile and receives the current pile. The function’s return value is then used to set each pile’s corresponding property. I.e., the function signature is as follows:

    function (pile) {
      // Do something
      return value;
    }
  • pileOrderItems is used to sort the items on a pile before positioning the items. It should be set to a callback function which will receive the current pile, and should return an array of sorted itemIDs. E.g.,

    const pileOrderItems = (pileState) => pileState.items.sort((a, b) => a - b);
    
    piling.set('pileOrderItems', pileOrderItems);

    The signature of the callback function is as follows:

      function (pileState) {
        // Sort itemIDs
        return arrayOfSortedIds;
      }
  • pileItemOffset can be set to an array or a callback function. The array should be a tuple specifying the x and y offset in pixel. E.g.,

    // Align items in y-axis
    piling.set('pileItemOffset', [0, 5]);

    See the next note for the signature of the callback function ⬇️

  • pileItemBrightness, pileItemInvert, pileItemOpacity, pileItemRotation and pileItemTint can either be set to a static value or a callback function to dynamically style items. E.g.,

    // Set to a static number
    piling.set('pileItemOpacity', 0.5);
    
    // Set to a callback function
    piling.set(
      'pileItemOpacity',
      (item, i, pile) => (pile.items.length - i) / pile.items.length
    );

    The callback function is evaluated, in order, for each item on every pile and receives the current item, the item's current index, and pile that the item belongs to. The function’s return value is then used to set the opacity of each pile’s item. I.e., the function signature is as follows:

      function (item, index, pile) {
        // Do something
        return value;
      }

    The function should return a value within [0, 1].

  • The default value of previewBackgroundColor and previewBackgroundOpacity is 'inherit', which means that their value inherits from pileBackgroundColor and pileBackgroundOpacity. If you want preview's background color to be different from pile's, you can set a specific color.

  • pileLabel can be set to a string, object, function, or array of the previous types. E.g.,

    piling.set('pileLabel', 'country');
    piling.set('pileLabel', (itemState) => itemState.country);
    piling.set('pileLabel', ['country', 'year']);
    piling.set('pileLabel', {
      property: (item) => item.country,
      aggregator: (countries) => countries[0],
    });
  • pileLabelColor can be set to a HEX, RGB string or hexadecimal value, an array of the previous types, or a callback function. E.g.,

    piling.set('pileLabelColor', '#e05aa9');
    piling.set('pileLabelColor', ['#e05aa9', '#e0722b', '#e0a638']);
    piling.set('pileLabelColor', (label, allLabels) => myOwnFancyColorMap[label]);

    The callback function receives the current label (string), and an array of all the labels, and it should return a HEX, RGB string or hexadecimal value. The signature is as follows:

      function (label, allLabels) {
        // Pick the color for the `label`
        return color;
      }
  • pileLabelSizeTransform is used to get a relative distribution of categories across a pile. It can be set to 'histogram' or a callback function. E.g.,

    // The following 2 examples are equivalent
    piling.set('pileLabelSizeTransform', 'histogram');
    piling.set('pileLabelSizeTransform', (counts, labels) => {
      // This function normalizes the counts to be in [0,1]
      const maxCount = Math.max(...counts);
      return counts.map((x) => x / maxCount);
    });

    The callback function should receive an array of the sum of each label and return an array of scale factors that ranges in [0, 1]. The signature is as follows:

      function (histogram) => {
        // Do stuff
        return arrayofScaleFactors;
      };
  • pileLabelTextMapping can be set to an array of strings, or a callback function. E.g.,

    piling.set('pileLabelTextMapping', ['red', 'blue', 'yellow', 'green']);
    piling.set(
      'pileLabelTextMapping',
      (label, allLabels) => `${abbreviation[label]}`
    );

    The callback function receives the current label (string), and an array of all the labels, and it should return a text string. The signature is as follows:

      function (label, allLabels) {
        // Create text
        return text;
      }
  • pileSizeBadge and pileSizeBadgeAlign allow to show the pile size as badge. Both can be defined dynamically using a pile-specific callback function. pileSizeBadgeAlign accepts an tuple of [yAlign, xAlign] where yAlign can be one of top, center, bottom, and xAlign can be one of left, center, right.

  • previewPadding defines how much larger the preview items' background is sized. For example, a padding of 2 means that the background of a preview item is 1 pixel larger in eath direction (top, right, bottom, left).

  • previewItemOffset is used to position the previews individually based on a per-preview item specific callback function. If it's not set, the preview will be positioned to the top of the cover by default. It should be set to a callback function which receives the current preview item, the item's current index, and the pile that the item belongs to, and it should return a tuple of xy position of the preview. I.e., the function signature is as follows:

    piling.set('previewItemOffset', (itemState, itemIndex, pileState) => {
      // Calculate the position
      return [x, y];
    });
  • previewAlignment, previewOffset and previewSpacing are used to globally position preview items. previewAlignment specifies the alignment direction, which can one of top (default), left, right, or bottom.

  • previewOffset defines the offset in pixel to the pile cover and previewSpacing defines the combined spacing around a pile. E.g., previewSpacing === 2 results in a 1px margin around the preview items. Both properties can be dynamically defines using a per-pile callback function as follows:

    piling.set('previewOffset', (pileState) => {
      // Define the offset
      return offset;
    });
  • previewScaling defines how much preview items are scaled according to the cover. Normally, the previews' scale factor is identical to the cover's scale factor. Using this property the impact of this scale factor can be adjusted. The final x and y scale will then be determined as follows xScale = 1 + (scaleFactor - 1) * scaling[0]. E.g., to not adjust the y scale to the cover but keep the x scale one can set previewScaling = [1,0]. The scaling can be determined dynamically using a per-pile callback function as follows:

    piling.set('previewScaling', (pileState) => {
      // Define the x and y scaling
      return [xScaling, yScaling];
    });

    Additionally, previewScaleToCover allows to automatically pick the scale factor such that the width or height of the preview are identical to the width and height of the cover. To do so, previewScaleToCover accepts a tuple of Boolean values (for the width and height scaling), where true will make the preview's width/height scale to the cover's with/height.

  • zoomScale allows to dynamically adjust the scale factor related to zooming. By default zooming does not affect the scale!

    piling.set('zoomScale', (cameraScale) =>
      cameraScale >= 1 ? 1 + (cameraScale - 1) / 2 : 1 - (1 - cameraScale) / 2
    );

Events

Name Event Data Description
render Published when the data has been rendered
update {action} Published when the redux store is updated
itemUpdate Published after items updates and their consequences have been applied
pileEnter {target, sourceEvent} Published when the mouse cursor enters a pile
pileLeave {target, sourceEvent} Published when the mouse cursor leaves a pile
pileDragStart {target, sourceEvent} Published when a pile drag is started
pileDragMove {target, sourceEvent} Published when a pile is dragged
pileDragEnd {target, sourceEvent} Published when a pile is dropped
pilesFocus {targets} Published when piles are focused
pilesBlur {targets} Published when piles are blurred
pilesActive {targets} Published when piles are temporarily depiled
pilesInactive {targets} Published when temporarily depiled piles are closed
pilesPositionEnd {targets} Published when piles positioning ended

Notes:

  • action is the name of the action that triggered the update
  • pile is the state of the corresponding pile
  • sourceEvent is the original browser event that triggered this event

State

In the following we describe the most important aspects of the library state. This description focuses on the user-facing aspects, primarily the pile and item state as these are used by dynamic properties and are returned by events.

state.items

A list of objects storing the item data

Type: Array of Objects

Example:

See the examples at #Data

state.piles

A list of objects with the following properties:

  • items: a list of item IDs
  • x: the current x position
  • y: the current y position

Type: Array of Objects

Example:

[
  {
    items: [5, 12],
    x: 10,
    y: 10
  },
  ...
]

Renderers

A renderer should be a function that takes as input an array of the value of src property in your data that determining the source, and outputs promises which resolve to Pixi Texture objects.

Renderer types

Piling.js support three types of renderer:

  • itemRenderer: responsible for rendering items.
  • coverRenderer: responsible for rendering the cover of a pile.
  • previewRenderer: responsible for rendering item previews.

Predefined renderers

Piling.js ships with the following set of renderers, which can be imported from piling.js.

Image renderer

Constructor:

import { createImageRenderer } from 'piling.js';
const imageRenderer = createImageRenderer();

Src/Data: image renderer can currently render images from an URL or base64 encoding. I.e., the src property in your data needs be URL pointing to an image or the base64 encoding of an image.

SVG renderer

Constructor:

import { createSvgRenderer } from 'piling.js';
const svgRenderer = createSvgRenderer(options);

Src/Data: The SVG renderer can render SVG strings and SVG DOM elements.

Options is an object of key-value pairs with support for the following properties:

Name Type Default Constraints
width int Width of the rendered texture in pixel
height int Height of the rendered texture in pixel
color string A valid CSS color property
background string A valid CSS background property

Matrix renderer

Constructor:

import { createMatrixRenderer } from 'piling.js';
const matrixRenderer = createMatrixRenderer(properties);

Src/Data: The matrix renderer requires that an item provides a src object of the following form:

{
  data: [1, 2, 3, 4],
  shape: [2, 2],
  dtype: 'float32'
}

Properties is an object of key-value pairs. The list of all understood properties is given below.

Name Type Default Constraints
colorMap array Array of rgba
shape array Matrix shape
minValue number 0
maxValue number 1

Note: shape describes the size of matrix, e.g., for a 4x5 matrix, shape should be [4, 5]

Examples:

import { interpolateRdPu } from 'd3-scale-chromatic';
import createMatrixRenderer from 'piling.js';

const rgbStrToRgba = (rgbStr, alpha = 1) => {
  return [
    ...rgbStr
      .match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/)
      .slice(1, 4)
      .map((x) => parseInt(x, 10) / 256),
    alpha,
  ];
};
const numColors = 256;
const colorMap = new Array(numColors)
  .fill(0)
  .map((x, i) => rgbStrToRgba(interpolateRdPu(i / numColors)));

const matrixRenderer = createMatrixRenderer({ colorMap, shape: [16, 16] });

Note:

You can pass in different color map or shape to create aggregateRender for matrix aggregation and previewRender for matrix preview, so that the pile will have an aggregation of all the matrices on the pile cover, and matrix previews on top of the aggregation.

For example:

const aggregateColorMap = new Array(numColors)
  .fill(0)
  .map((x, i) => rgbStr2rgba(interpolateOrRd((numColors - i) / numColors)));

const coverRenderer = createMatrixRenderer({
  colorMap: aggregateColorMap,
  shape: [16, 16],
});

const previewRenderer = createMatrixRenderer({ colorMap, shape: [16, 1] });

Note, you also need to define the appropriate cover and preview aggregators for this to work.

Representative renderer

Constructor:

import { createRepresentativeRenderer } from 'piling.js';
const representativeRenderer = createRepresentativeRenderer(
  itemRenderer,
  options
);

Src/Data: Since the item-based rendering is done by itemRenderer, the data needs to be in a format understood by itemRenderer.

itemRenderer: The basic item renderer.

Options is an object of key-value pairs with support for the following properties:

Name Type Default Constraints
size int 96 Size of the longer side, which is determined by the number of representatives
innerPadding int 2 Padding in pixels between representative images
outerPadding int 0 Padding in pixels the the outer border
maxNumberOfRepresentatives int 9 Maximum number of representatives to be shown. You can typically ignore this setting as this number is derived by the representative aggregator.
backgroundColor int 0x000000 A background color for the gallery in form of a hexa-decimal number

Note:

Define your own renderer

If you want to define your own renderer to render your own data, you can do something as follows:

// The actual renderer
const renderCustomTexture = (src, options) => {
  // A complicated function that turns the src into a PIXI texture object
  return PIXI.Texture.from(...);
}

// Factory function
const createCustomRenderer = options => sources => {
  Promise.all(
    sources.map(src => {
      return new Promise((resolve, reject) => {
        const texture = renderCustomTexture(src, options);

        if (!texture) reject(new Error('Could not render texture'));

        resolve(texture);
      });
    })
  )
};

Add renderers to piling.js library

Call set method to add renderers to the library.

// for all the items
piling.set('itemRenderer', itemRenderer);

// for the aggregation of a pile
piling.set('coverRenderer', coverRenderer);

// for the item preview
piling.set('previewRenderer', previewRenderer);

Aggregators

Aggregators are used to aggregate items.

An aggregator is a function that takes as input an array of items and outputs a promise which resolve to an array of aggregated source values that can be passed to the renderers.

Aggregator types

Piling.js support three types of renderer:

  • coverAggregagtor: responsible for aggregagting items for the cover.
  • previewAggregagtor: responsible for aggregagting full items to previews items.

Predefined aggregators

Piling.js ships with the following set of aggregators, which can be imported from piling.js.

Matrix cover aggregator

The aggregator for all the matrices on a pile, it will be shown on the cover of the pile.

Constructor:

import { createMatrixCoverAggregator } from 'piling.js';
const matrixCoverAggregator = createMatrixCoverAggregator(aggregator);
  • Aggregator is the method of aggregation, could be 'mean', 'variance', 'std'. The default value is 'mean'.

Matrix preview aggregator

The 1D preview aggregator for each matrix on a pile, it will be shown on top of the pile cover.

Constructor:

import { createMatrixPreviewAggregator } from 'piling.js';
const matrixPreviewAggregator = createMatrixPreviewAggregator(aggregator);
  • Aggregator is the method of aggregation, could be 'mean', 'variance', 'std'. The default value is 'mean'.

Representative aggregator

The representative aggregator selects k number of representative items from all the items on a pile. It should be used as a cover aggregator in combination with the representative cover renderer.

Constructor:

import { createRepresentativeAggregator } from 'piling.js';
const representativeAggregator = createRepresentativeAggregator(k, options);

The representative aggregator uses kmeans++ to determine k clusters in the data and then selects the items closest to the cluster centroids. See piling.js.org/?example=vitessce for an example.

  • k: the number of representatives to select.

  • options: is an object of key-value pairs with support for the following properties:

    Name Type Default Constraints
    distanceFunction function or string l2 The string can be one of l1, mahattan, l2, euclidean and falls back to l2
    initialization array or string kmpp A list of initial centroids or kmeans++ initialization
    maxIterations int 1000 * log10(n) A valid CSS color property
    valueGetter function x => x A function defining how to access the data representation to be used for clustering. The accessed value must either be a number or an array.

Define your own aggregator

If you want to define your own aggregator, you can do something as follows:

const customAggregator = (items) =>
  new Promise((resolve, reject) => {
    // Aggregate items somehow
    const aggregatedSrc = myMagicAggregation(items);

    if (!aggregatedSrc) reject(new Error('Aggregation failed'));

    // The resolve source must be understood by the aggregate renderer!
    resolve(aggregatedSrc);
  });

Add aggregators to piling.js library

Call set method to add aggregators to the library.

piling.set('coverAggregator', coverAggregator);
piling.set('previewAggregator', previewAggregator);

Subsampling previews

To limit the number of shown previews, the preview aggregator function can resolve a list of aggregated sources where some of the entries are null. These null entries will be filtered out by piling.js.

For example, the following aggregator limits the previews to those with an even index.

const customAggregator = (items) =>
  Promise.resolve(items.map((item, i) => i % 2 === 0 ? item : null);

It's important that the number of items and number of aggregated previews match, otherwise piling.js wouldn't be able to match the previews to their associated item.


Dimensionality Reducers

A dimensionality reducer is a transformation function that that reduced multi-dimensional input data down to two normalized dimension.

A dimensionality reducer should be a function that takes as input a 2D nested numerical array, and output promises which resolve to an array of aggregated source value that can be passed to the renderers.

Predefined dimensionality reducers

We currently provide predefined dimensionality reducers for UMAP.

UMAP dimensionality reducer

The 1D preview aggregator for each matrix on a pile, it will be shown on top of the pile cover.

Constructor:

import { createUmap } from 'piling.js';
const umap = createUmap(config, options);
  • config is an object that lets you customize UMAP's parameters.
  • options is an object for customizing the output transformation with the follwing properties:
Name Type Default Constraints
padding float 0.1 Must be greater than zero

Define your own dimensionality reducer

If you want to define your own dimensionality reducer, you can do something as follows:

// Factory function
const createCustomAggregator = () => {
  // Your code here

  return {
    fit(data) {
      // The following function must be asynchronous and return a promise that
      // resolves once the fitting is done.
      return asyncFitFunction(data);
    },
    transform(data) {
      return getTransformedData(data);
    },
  };
};

Add dimensionality reducers to piling.js library

Call set method to add aggregators to the library.

piling.set('dimensionalityReducer', umap);

Clusterers


Interactions

Grouping

Drag and Drop Grouping

  1. Click and hold on a pile
  2. Move the mouse onto another single pile
  3. Release the mouse
See demo

Drag and drop grouping

Multi-select Grouping

  1. Hold down SHIFT

  2. Click on the piles you want to select

  3. Click on a previously selected pile to group all selected piles onto this target pile

    See demo

    Multiselect grouping

  4. Alternatively, double click on a new pile to group all selected piles onto this target pile

    See demo

    Multiselect grouping with double-click

Lasso Grouping

  1. Click on the background. A translucent circle will appear
  2. Click and hold into the circle to activate the lasso.
  3. Drag the cursor around the pile you want to group.
  4. Release the mouse to trigger the grouping.
See demo

Multiselect grouping with lasso

Browsing

Browsing in-place

  1. Click on the pile you want to browse
  2. Hover the mouse on the item's visible part to see the item
See demo

Browsing in-place

Browsing via previews

  1. Click on the pile you want to browse
  2. Hover the mouse on one item's preview to see the item
See demo

Browsing via previews

Browsing via temporarily de-pile

  1. Double click on the pile to temporarily de-pile the pile

  2. Double click again on the pile or on the background to close temporarily de-pile

    See demo

    Browsing via temporarily de-pile

  3. Alternatively, right click on the pile to open the context menu

  4. Click on Temp. Depile button to temporarily de-pile the pile

  5. Right click on the pile again and click on Close Temp. Depile button to close temporarily de-pile

    See demo

    Browsing via context menu - temp depile

Browsing separately

  1. Right click on the pile to open the context menu
  2. Click on Browse Separately button to browse the pile hierarchically
  3. Click on the breadcrumb trail to go back to the previous level
See demo

Browse separately

De-piling

  1. Hold down ALT

  2. Click on a pile to de-pile it

    See demo

    Depile-alt

  3. Alternatively, right click on the pile to open the context menu

  4. Click on Depile button to de-pile

    See demo

    Depile-context menu

Others

Magnify a pile

  1. Hold down ALT

  2. Hover the mouse on a pile and scroll to manually magnify it

  3. Click on the background to automatically unmagnify it

    See demo

    Magnify by wheel

  4. Alternatively, right click on the pile to open the context menu

  5. Click on Magnify button to automatically magnify the pile

  6. Right click on a magnified pile

  7. Click on Unmagnify button to automatically unmagnify the pile

    See demo

    Magnify by context menu

Show grid

  1. Right click on the background to open the context menu
  2. Click on Show Grid button to show the grid
  3. Right click on the background again and click on Hide Grid button to hide the grid
See demo

Show grid

Context menu

  1. Right click will open the custormized context menu
  2. While pressing ALT, right click will show the default context menu in the browser
See demo

Context menu