Home | Gallery

Interactive & Ray Traced Data Visualization

The Demography of the World Population from 1950 to 2100The Demography of the World Population from 1950 to 2100The Demography of the World Population from 1950 to 2100LEGO Color TimelineLEGO Color TimelineLEGO Color TimelineUS Polio Cases 1928-1968US Polio Cases 1928-1968US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019US Tornados 1950-2019Les Misérables Co-occurrence NetworkLes Misérables Co-occurrence NetworkLes Misérables Co-occurrence NetworkLes Misérables Co-occurrence MatrixLes Misérables Co-occurrence MatrixUS COVID-19 Cumulative DeathsUS COVID-19 Cumulative DeathsUS COVID-19 Cumulative CasesWorldwide COVID-19 Cases per 100,000 PeopleWorldwide COVID-19 Cases per 100,000 PeopleTitanic Passenger & Crew Survival by Gender and ClassTitanic Passenger & Crew Survival by Gender and ClassTitanic Passenger & Crew Survival by Gender and ClassTitanic Passenger & Crew Survival by Gender and ClassTitanic Passenger & Crew Survival by Gender and ClassTitanic Passenger & Crew Survival by Gender and ClassTitanic Passenger & Crew Survival by Gender and ClassUS 2016 Presidential Election by County and Party, Facetted by StateUS 2016 Presidential Election by County and Party, Facetted by StateLife ExpectancyLife ExpectancyLife ExpectancyLife ExpectancyChicago Air QualityChicago Air QualityChicago Air QualityChicago Air QualityChicago Air QualityChicago Air QualityChicago HumidityChicago HumidityChicago HumidityGoogle Merchandise PurchasesGoogle Merchandise PurchasesLos Angeles FlightsKobe Bryant ShotsKobe Bryant ShotsKobe Bryant ShotsKobe Bryant ShotsKobe Bryant ShotsKobe Bryant ShotsUS Music SalesUS Music SalesUS Music SalesFireballsFireballsFireballsHouse Sales in King County, WAHouse Sales in King County, WAHouse Sales in King County, WAAdventureWorks SalesAdventureWorks SalesWorld PopulationWorld PopulationWorld PopulationWorld PopulationRich-AT DNA 20merRich-AT DNA 20merCaffeineCaffeineBuckminsterfullereneBuckminsterfullerene
Ray traced data visualizations.

Contents

  1. Introduction
  2. Gallery
  3. Features
    1. Renderers
    2. Layout
    3. Transitions
    4. Interaction
    5. Axes
    6. Data
    7. Background Images
    8. Labels
    9. Virtual Reality
  4. Getting Started
    1. Coordinates
    2. Initialization
    3. Layouts
      1. Scatter Plot
      2. Bar Chart
      3. Stack Chart (Count)
      4. Stack Chart (Sum)
      5. Line Chart
      6. Squarified Treemap
    4. Facets
      1. Wrap
      2. Cross-Facet
      3. Layout Per Facet
    5. Renderers
    6. Interaction
      1. Selection
      2. Filtering
    7. Palettes
    8. Axes
    9. Transitions
    10. Fonts
    11. Data
    12. Ray Tracing
      1. Materials
      2. Unit Types
    13. Debugging
  5. Apps
  6. About

Introduction

MorphCharts is a low-level JavaScript library for interactive and ray traced data visualization in both 2D and 3D.

Features

Renderers

Multiple renderers are provided:

The basic and advanced renderers are designed for interactive data visualization, whereas the raytrace renderer is designed for creating high-quality images and videos.

Ray-traced data visualization showing a hex-binned histogram of tornados in the United States.
Ray-traced data visualization showing a hex-binned histogram of tornados in the United States.

Further examples using the raytrace renderer are available in the gallery.

Support for WebGPU in Chrome and Edge browsers is currently experimental. In order to use this renderer, download Chrome or Edge Canary and enable WebGPU in the experimental features (via chrome://flags and edge://flags, respectively).

Layout

Chart layouts are provided for:

Layouts can be combined to create new chart types, for example a node-link graph can be created by combining a scatter plot (for the nodes) and a line chart (for the edges).

Helper functions are provided to support faceting layouts.

Transitions

Visualizations can specify a pair of layouts such that they can be smoothly animated. This helps a user maintain context while switching layouts, and facilitates building animated data stories.

Data points can transition simultaneously, or in a custom order to create "staggered" transitions.

Unit histograms of US Tornados by strength, showing animated transitions between binning by month,
            hour and year.
Unit histograms of US Tornados by strength, showing animated transitions between binning by month, hour and year.

Interaction

A default interaction manager is provided for zooming, panning, and rotating the view. Mouse, touch, stylus, keyboard and Surface Dial are supported.

Selections can be made from the axes or with lasso gestures.

Alt-azimuth and arc-ball cameras are provided.

Axes

Helper functions are provided for working with 2D and 3D axes. Multiple axes can be used to support faceted views.

Selections can be made using grid divisions along the axes, using grid cells, or using labels.

3D axes dynamically adapt the text orientation and edge visibility to the viewing angle.

Unit histograms of Titanic passengers by gender and ticket class and colored by strength, rotating
            to show dynamic axes.
Unit histograms of Titanic passengers by gender and ticket class and colored by strength, rotating to show dynamic axes.

Data

Helper functions are provided for working with delimited (CSV) text files, however clients are free to load data from any data source.

A data-table abstraction is provided for:

Background Images

Background images, such as maps, can be added to provide context. Images can be rectangular or spherical.

World population with flat background image.
World population with flat background image.
World population with spherical background image.
World population with spherical background image.

Labels

Labels can be added individually or in groups for higher performance.

Virtual Reality

Support for immersive experiences with virtual reality headsets and controllers is supported via WebXR in the basic renderer.

Stereo

Stereo rendering is supported in the basic renderer with left, right, split-screen and anaglyph modes.

Buckminsterfullerine (C60) molecule in anaglyph stereo.
Buckminsterfullerine (C60) molecule in anaglyph stereo.

Getting Started

MoprhCharts is a low-level JavaScript library. The recommended way to consume the library is using TypeScript and importing the library from npm. The TypeScript declaration files will provide IntelliSense in development tools such as Visual Studio Code.

The following examples are presented as plain JavaScript with an ES6 import of the library from a CDN, so they can be run directly without a bundler. Note that for the purposes of simplicity, the samples occasionally use different types from the TypeScript declaration files (for example, Array instead of glMatrix.vec3).

Coordinates

MorphCharts uses right-hand coordinates, so if the x-axis is to the right and the y-axis is up, the z-axis is towards the viewer. 2D layouts and axes use the x-axis and y-axis. 3D layouts and axes add the z-axis.

Initialization

The key classes required to create a visualization are the Core, a renderer (e.g. BasicRenderer), and a TransitionBuffer.

A TransitionBuffer contains information for a specific number of "units". Each unit could, for example correspond to a record in a tabular dataset. The TransitionBuffer has a currentBuffer and previousBuffer, each of which can be updated with independent positions, sizes, orientations, colors etc for each unit, allowing an animated transition between views. This helps users maintain context and enables animated data stories.

A renderer creates an HTML Canvas. To place this canvas in a specifc HTML Element, use the Core options container parameter. The size can be specified either in the renderer width and height options, or on the HTML Element container itself.

Minimal initialization for 2D sheet.
Minimal initialization for 2D sheet.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Initialization</title>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core({ container: document.getElementById("container") });

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main({
                // width: 1280,
                // height: 720,
            });

            // Data
            const ids = new Uint32Array(1024);
            for (let i = 0; i < ids.length; i++) { ids[i] = i; }

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];

            // Layout
            const sheet = new MorphCharts.Layouts.Sheet(core);
            sheet.layout(transitionBuffer.currentBuffer, ids, { side: Math.ceil(Math.sqrt(ids.length)) });
            sheet.update(transitionBuffer.currentBuffer, ids, { thickness: 0.01, padding: 0.1 });
        };
    </script>
</head>

<body>
    <div id="container" style="width:1280px;height:720px;max-width:100%"></div>
</body>

</html>
Minimal initialization for 2D sheet, using a container of specified size.

If no container is passed to the Core, the renderer will add the canvas to the HTML Document and use the available space. In the following example, a style is added to ensure the the height is 100%.

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Initialization</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const ids = new Uint32Array(1024);
            for (let i = 0; i < ids.length; i++) { ids[i] = i; }

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];

            // Layout
            const sheet = new MorphCharts.Layouts.Sheet(core);
            sheet.layout(transitionBuffer.currentBuffer, ids, { side: Math.ceil(Math.sqrt(ids.length)) });
            sheet.update(transitionBuffer.currentBuffer, ids, { padding: 0.1 });
        };
    </script>
</head>

<body></body>

</html>
Minimal initialization for 2D sheet, filling the page.

Layouts

The following examples render simple procedural data using different layouts and the basic renderer.

A layout is used to update the TransitionBuffer. Each layout has a layout() and update() method which specify the buffer to use, an array of ids (which can in be a specific order, or a subset of the TransitionBuffer's ids for faceted layouts), and a set of options. layout() is used purely for calculating layout-related properties. update() is used to update the GPU buffer, and includes any non-layout related properties such as color, selection etc. This allows these properties to be updated without re-calculating the layout.

layout() measures a set of bounds for each axis in the same units as the data (model units). update() will normalize these bounds to the range [0,1], such that the longest dimension will be unit length. By default, update() will use the bounds measured in layout(), however they can also be specified explicitly in the update() options. This allows, for example alignment with specific ranges of axes, or with other layouts. A modelPosition vector, scalar modelScale, and modelRotation quaternion is then applied by the Core.

Scatter Plot

Simple 3D scatter plot with spheres sized and colored by a scaled random value.
Simple 3D scatter plot with spheres sized and colored by a scaled random value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Scatter Plot</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 100;
            const ids = new Uint32Array(count);
            const positionsX = new Float64Array(count);
            const positionsY = new Float64Array(count);
            const positionsZ = new Float64Array(count);
            const sizes = new Float64Array(count);
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                positionsX[i] = Math.random();
                positionsY[i] = Math.random();
                positionsZ[i] = Math.random();
                sizes[i] = Math.pow(Math.random(), 2);
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, 32, false);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.sphere;
            transitionBuffer.currentPalette.colors = palette;

            // Layout
            const scatter = new MorphCharts.Layouts.Scatter(core);
            scatter.layout(transitionBuffer.currentBuffer, ids, {
                positionsX: positionsX,
                positionsY: positionsY,
                positionsZ: positionsZ,
            });
            scatter.update(transitionBuffer.currentBuffer, ids, {
                sizes: sizes,
                sizeScaling: 0.1,
                colors: sizes,
            });

            // Axes
            const axes = MorphCharts.Axes.Cartesian3dAxesHelper.create(core, {
                titleX: "x",
                titleY: "y",
                titleZ: "z",
                labelsX: (value) => { return value.toFixed(1); },
                labelsY: (value) => { return value.toFixed(1); },
                labelsZ: (value) => { return value.toFixed(1); }
            });
            core.renderer.currentAxes = [core.renderer.createCartesian3dAxesVisual(axes)];

            // Alt-azimuth camera
            const camera = core.camera;
            camera.setPosition([0, 0, 0.2], false);
        };
    </script>
</head>

<body></body>

</html>
Simple 3D scatter plot with spheres sized and colored by a scaled random value.

Bar Chart

Simple 3D bar chart with cylinders sized and colored by a scaled random value.
Simple 3D bar chart with cylinders sized and colored by a scaled random value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bar Chart</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 50;
            const ids = new Uint32Array(count);
            const positionsX = new Float64Array(count);
            const positionsZ = new Float64Array(count);
            const sizes = new Float64Array(count);
            const width = 10;
            const depth = 5;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                positionsX[i] = i % width;
                positionsZ[i] = Math.floor(i / width);
                sizes[i] = Math.pow(Math.random(), 2);
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, 32, false);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.cylinder;
            transitionBuffer.currentPalette.colors = palette;

            // Layout
            const heightScaling = 2;
            const bar = new MorphCharts.Layouts.Bar(core);
            bar.layout(transitionBuffer.currentBuffer, ids, {
                positionsX: positionsX,
                positionsZ: positionsZ,
                heights: sizes,
                heightScaling: heightScaling,
                paddingX: 0.25,
                paddingZ: 0.25,
            });
            bar.update(transitionBuffer.currentBuffer, ids, {
                colors: sizes,
                minBoundsY: 0,
                maxBoundsY: heightScaling,
            });

            // Axes
            const axes = MorphCharts.Axes.Cartesian3dAxesHelper.create(core, {
                minBoundsX: bar.minModelBoundsX,
                maxBoundsX: bar.maxModelBoundsX,
                minBoundsY: 0,
                maxBoundsY: heightScaling,
                minBoundsZ: bar.minModelBoundsZ,
                maxBoundsZ: bar.maxModelBoundsZ,
                minValueX: 0,
                maxValueX: width - 1,
                minValueY: 0,
                maxValueY: heightScaling,
                minValueZ: 0,
                maxValueZ: depth - 1,
                titleX: "x",
                titleY: "y",
                titleZ: "z",
                isDiscreteX: true,
                isDiscreteY: false,
                isDiscreteZ: true,
                labelsX: (value) => { return value.toString(); },
                labelsY: (value) => { return value.toFixed(1); },
                labelsZ: (value) => { return value.toString(); }
            });
            core.renderer.currentAxes = [core.renderer.createCartesian3dAxesVisual(axes)];

            // Alt-azimuth camera
            const camera = core.camera;
            camera.setPosition([0, 0, -0.1], false);
            camera.setAltAzimuth(MorphCharts.Helpers.AngleHelper.degreesToRadians(30), 0, false);
        };
    </script>
</head>

<body></body>

</html>
Simple 3D bar chart with cylinders sized and colored by a scaled random value.

Stack Chart (Count)

Simple 3D histogram stack chart with blocks colored by a scaled random value.
Simple 3D histogram stack chart with blocks colored by a scaled random value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Stack Chart (Count)</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 1000;
            const ids = new Uint32Array(count);
            const binIdsX = new Float64Array(count);
            const binIdsZ = new Float64Array(count);
            const values = new Float64Array(count);
            const binsX = 5;
            const binsZ = 1;
            const sizeX = 5;
            const sizeZ = 5;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                binIdsX[i] = Math.floor(binsX * Math.pow(Math.random(), 2));
                binIdsZ[i] = Math.floor(binsZ * Math.pow(Math.random(), 2));
                values[i] = Math.pow(Math.random(), 2);
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, 32, false);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.block;
            transitionBuffer.currentPalette.colors = palette;

            // Order by value
            const orderedIds = new Uint32Array(ids);
            // orderedIds.sort(function (a, b) { return values[a] - values[b]; });

            // Layout
            const stack = new MorphCharts.Layouts.Stack(core);
            stack.layout(transitionBuffer.currentBuffer, orderedIds, {
                binsX: binsX,
                binsZ: binsZ,
                binIdsX: binIdsX,
                binIdsZ: binIdsZ,
                sizeX: sizeX,
                sizeZ: sizeZ,
                spacingX: 1,
                spacingZ: 1,
            });
            stack.update(transitionBuffer.currentBuffer, ids, {
                colors: values,
                padding: 0.025,
            });

            // Axes
            const axes = MorphCharts.Axes.Cartesian3dAxesHelper.create(core, {
                minBoundsX: stack.minModelBoundsX,
                minBoundsY: stack.minModelBoundsY,
                minBoundsZ: stack.minModelBoundsZ,
                maxBoundsX: stack.maxModelBoundsX,
                maxBoundsY: stack.maxModelBoundsY,
                maxBoundsZ: stack.maxModelBoundsZ,
                minValueX: 0,
                maxValueX: binsX - 1,
                minValueY: 0,
                maxValueY: stack.maxLevel * sizeX * sizeZ,
                minValueZ: 0,
                maxValueZ: binsZ - 1,
                titleX: "x",
                titleY: "y",
                titleZ: "z",
                isDiscreteX: true,
                isDiscreteZ: true,
                labelsX: (value) => { return value.toString(); },
                labelsY: (value) => { return Math.round(value).toString(); },
                labelsZ: (value) => { return value.toString(); }
            });
            // Hide zero lines
            axes.zero[0] = -1;
            axes.zero[2] = -1;
            core.renderer.currentAxes = [core.renderer.createCartesian3dAxesVisual(axes)];

            // Alt-azimuth camera
            const camera = core.camera;
            camera.setPosition([0, 0, -0.05], false);
            camera.setAltAzimuth(MorphCharts.Helpers.AngleHelper.degreesToRadians(30), 0, false);
        };
    </script>
</head>

<body></body>

</html>
Simple 3D histogram stack chart with blocks colored by a scaled random value.

Stack Chart (Sum)

Simple 3D stack chart with blocks sized and colored by a scaled random value.
Simple 3D stack chart with blocks sized and colored by a scaled random value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Stack Chart (Sum)</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 1000;
            const ids = new Uint32Array(count);
            const binIdsX = new Float64Array(count);
            const binIdsZ = new Float64Array(count);
            const values = new Float64Array(count);
            const binsX = 5;
            const binsZ = 1;
            const sizeX = 5;
            const sizeZ = 5;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                binIdsX[i] = Math.floor(binsX * Math.pow(Math.random(), 2));
                binIdsZ[i] = Math.floor(binsZ * Math.pow(Math.random(), 2));
                values[i] = Math.pow(Math.random(), 2);
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, 32, false);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.block;
            transitionBuffer.currentPalette.colors = palette;

            // Layout
            const stack = new MorphCharts.Layouts.StackTreeMap(core);
            stack.layout(transitionBuffer.currentBuffer, ids, {
                binsX: binsX,
                binsZ: binsZ,
                sizes: values,
                binIdsX: binIdsX,
                binIdsZ: binIdsZ,
                sizeX: sizeX,
                sizeZ: sizeZ,
                spacingX: 1,
                spacingZ: 1,
            });
            stack.update(transitionBuffer.currentBuffer, ids, {
                colors: values,
                padding: 0.025,
            });

            // Axes
            const axes = MorphCharts.Axes.Cartesian3dAxesHelper.create(core, {
                minBoundsX: stack.minModelBoundsX,
                minBoundsY: stack.minModelBoundsY,
                minBoundsZ: stack.minModelBoundsZ,
                maxBoundsX: stack.maxModelBoundsX,
                maxBoundsY: stack.maxModelBoundsY,
                maxBoundsZ: stack.maxModelBoundsZ,
                minValueX: 0,
                maxValueX: binsX - 1,
                minValueY: 0,
                maxValueY: stack.maxTotal,
                minValueZ: 0,
                maxValueZ: binsZ - 1,
                titleX: "x",
                titleY: "y",
                titleZ: "z",
                isDiscreteX: true,
                isDiscreteZ: true,
                labelsX: (value) => { return value.toString(); },
                labelsY: (value) => { return Math.round(value).toString(); },
                labelsZ: (value) => { return value.toString(); }
            });
            // Hide zero lines
            axes.zero[0] = -1;
            axes.zero[2] = -1;
            core.renderer.currentAxes = [core.renderer.createCartesian3dAxesVisual(axes)];

            // Alt-azimuth camera
            const camera = core.camera;
            camera.setPosition([0, 0, -0.05], false);
            camera.setAltAzimuth(MorphCharts.Helpers.AngleHelper.degreesToRadians(30), 0, false);
        };
    </script>
</head>

<body></body>

</html>
Simple 3D stack chart with blocks sized and colored by a scaled random value.
Groups

Line Chart

Line charts are created using multiple cylindrical line segments.

Simple 3D line chart using cylinders colored by line.
Simple 3D line chart using cylinders colored by line.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Line Chart</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 1000;
            const ids = new Uint32Array(count);
            const positionsX = new Float64Array(count);
            const values = new Float64Array(count);
            const seriesValues = new Float64Array(count);
            const series = 10;
            const minValue = -10;
            const maxValue = 10;
            const stepSize = 1;
            const threshold = 0.5;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                seriesValues[i] = i % series;
                const seriesIndex = Math.floor(i / series);
                positionsX[i] = seriesIndex;
                if (seriesIndex == 0) {
                    values[i] = minValue + Math.random() * (maxValue - minValue);
                }
                else {
                    // Random walk
                    let value = Math.random() * stepSize;
                    value = values[i - series] + (Math.random() > threshold ? value : -value);
                    values[i] = Math.min(Math.max(value, minValue), maxValue);
                }
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, 32, false);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.cylinder;
            transitionBuffer.currentPalette.colors = palette;

            // Connect
            const fromIds = ids;
            const toIds = new Uint32Array(count);
            const lineHelper = new MorphCharts.Helpers.LineHelper(core);
            lineHelper.connect(ids, seriesValues, toIds);

            // Layout
            const line = new MorphCharts.Layouts.Line(core);
            const positionScalingY = 1;
            const positionScalingZ = 5;
            line.layout(transitionBuffer.currentBuffer, ids, fromIds, toIds, {
                positionsX: positionsX,
                positionsY: values,
                positionsZ: seriesValues,
                positionScalingY: positionScalingY,
                positionScalingZ: positionScalingZ,
                sizeScaling: 0.5,
            });
            line.update(transitionBuffer.currentBuffer, ids, fromIds, toIds, {
                minBoundsY: minValue * positionScalingY,
                maxBoundsY: maxValue * positionScalingY,
                lineColors: seriesValues,
                lineMinColor: 0,
                lineMaxColor: series - 1,
                hover: seriesValues,
            });

            // Axes
            const date = new Date();
            const dateTimeFormat = new Intl.DateTimeFormat("en-us", { month: "short", day: "numeric" });
            const axes = MorphCharts.Axes.Cartesian3dAxesHelper.create(core, {
                minBoundsX: line.minModelBoundsX,
                maxBoundsX: line.maxModelBoundsX,
                minBoundsY: line.minModelBoundsY,
                maxBoundsY: line.maxModelBoundsY,
                minBoundsZ: line.minModelBoundsZ - 0.5 * positionScalingZ,
                maxBoundsZ: line.maxModelBoundsZ + 0.5 * positionScalingZ,
                minValueX: new Date("January 1, 2000 00:00:00").getTime(),
                maxValueX: new Date("December 31, 2000 23:59:59").getTime(),
                minValueY: minValue,
                maxValueY: maxValue,
                minValueZ: 0,
                maxValueZ: series - 1,
                isDiscreteZ: true,
                divisionsZ: series,
                labelOrientationX: MorphCharts.AxesTextOrientation.perpendicular,
                labelsX: (value) => { return dateTimeFormat.format(value); },
                labelsY: (value) => { return value.toFixed(1); },
                labelsZ: (value) => { return value.toString(); }
            });
            axes.zero[2] = -1; // Hide zero-line
            core.renderer.currentAxes = [core.renderer.createCartesian3dAxesVisual(axes)];

            // Alt-azimuth camera
            const camera = core.camera;
            camera.setPosition([0, -0.01, -0.1], false);
            camera.setAltAzimuth(MorphCharts.Helpers.AngleHelper.degreesToRadians(30), 0, false);
        };
    </script>
</head>

<body></body>

</html>
Simple 3D line chart using cylinders colored by line.

Squarified Treemap

Simple 3D squarified treemap with blocks colored and sized by a scaled random value.
Simple 3D squarified treemap with blocks colored and sized by a scaled random value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Squarified Treemap</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 100;
            const ids = new Uint32Array(count);
            const values = new Float64Array(count);
            const minValue = 0.1;
            const maxValue = 1;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                values[i] = minValue + Math.pow(Math.random(), 2) * (maxValue - minValue);
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, 32, false);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.block;
            transitionBuffer.currentPalette.colors = palette;

            // Order
            const orderedIds = new Uint32Array(ids);
            orderedIds.sort(function (a, b) { return values[a] - values[b]; });

            // Layout
            const treemap = new MorphCharts.Layouts.SquarifiedTreeMap(core);
            treemap.layout(transitionBuffer.currentBuffer, orderedIds, {
                minBoundsX: 0,
                maxBoundsX: 1,
                minBoundsY: 0,
                maxBoundsY: 1,
                minBoundsZ: 0,
                maxBoundsZ: 0.1,
                sizes: values,
            });
            treemap.update(transitionBuffer.currentBuffer, orderedIds, {
                padding: 0.005,
                heights: values,
                colors: values,
                minColor: minValue,
                maxColor: maxValue,
            });
        };
    </script>
</head>

<body></body>

</html>
Simple 3D squarified treemap with blocks colored and sized by a scaled random value.

Facets

Charts can be faceted by providing a set of facet ids and layout offsets.

Helper functions are provided for binning and layout (wrapped or 1D, 2D or 3D grid layout).

Wrap

Scatter plot faceted and wrapped by x-value.
Scatter plot faceted and wrapped by x-value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Wrapped, Facetted Scatter Plot</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 100;
            const ids = new Uint32Array(count);
            const valuesX = new Float64Array(count);
            const valuesY = new Float64Array(count);
            const minValueX = 0;
            const maxValueX = 10;
            const minValueY = 0;
            const maxValueY = 10;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                valuesX[i] = Math.random() * (maxValueX - minValueX) + minValueX;;
                valuesY[i] = Math.random() * (maxValueY - minValueY) + minValueY;;
            }

            // Bin
            const bins = 10;
            const binIds = new Uint32Array(count);
            const binCounts = new Uint32Array(bins);
            const binFroms = new Float64Array(bins);
            const binTos = new Float64Array(bins);
            MorphCharts.Helpers.BinHelper.bin({
                ids: ids,
                values: valuesX,
                bins: bins,
                minValue: minValueX,
                maxValue: maxValueX,
                isDiscrete: false,
                binIds: binIds,
                counts: binCounts,
                froms: binFroms,
                tos: binTos,
            });

            // Wrap facets
            const columns = 5;
            const facetsX = new Uint32Array(count);
            const facetsY = new Uint32Array(count);
            const facetHelper = new MorphCharts.Helpers.FacetHelper(core);
            const rows = facetHelper.wrap1d(ids, binIds, columns, facetsX, facetsY);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentPalette.colors = [0xa6, 0xbd, 0xdb, 0xff];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.sphere;

            // Layout
            const scatter = new MorphCharts.Layouts.Scatter(core);
            scatter.layout(transitionBuffer.currentBuffer, ids, {
                positionsX: valuesX,
                positionsY: valuesY,
            });
            scatter.update(transitionBuffer.currentBuffer, ids, {
                minBoundsX: minValueX,
                maxBoundsX: maxValueX,
                minBoundsY: minValueY,
                maxBoundsY: maxValueY,

                // Facets
                facetCoordsX: facetsX,
                facetCoordsY: facetsY,
                facetsX: columns,
                facetsY: rows,
                facetSpacingX: 0.5,
                facetSpacingY: 0.75,
            });

            // Axes
            core.renderer.currentAxes = [];
            for (let i = 0; i < bins; i++) {
                const facetX = i % columns;
                const facetY = Math.floor(i / columns);
                const axes = MorphCharts.Axes.Cartesian2dAxesHelper.create(core, {
                    minBoundsX: minValueX,
                    maxBoundsX: maxValueX,
                    minBoundsY: minValueY,
                    maxBoundsY: maxValueY,
                    minValueX: minValueX,
                    maxValueX: maxValueX,
                    minValueY: minValueY,
                    maxValueY: maxValueY,
                    divisionsX: 5,
                    divisionsY: 5,
                    labelMinorSizeX: 0.01,
                    labelMinorSizeY: 0.01,
                    labelMajorSizeX: 0.01,
                    labelMajorSizeY: 0.01,
                    headingX: `x ${Math.round(binFroms[i])} - ${Math.round(binTos[i])}`,
                    headingSizeX: 0.02,
                    labelsX: (value) => { return value.toString(); },
                    labelsY: (value) => { return value.toString(); },
                    scaling: scatter.facetScaling,
                });
                axes.offsetX = scatter.offsetX(facetX);
                axes.offsetY = scatter.offsetY(facetY);
                axes.gridPickDivisionHeight = 0.01;
                axes.isEdgeVisible[MorphCharts.Edge2D.right] = false;
                axes.isEdgeVisible[MorphCharts.Edge2D.top] = false;
                axes.isHeadingVisible[MorphCharts.Edge2D.top] = true;
                axes.isHeadingVisible[MorphCharts.Edge2D.bottom] = false;
                core.renderer.currentAxes.push(core.renderer.createCartesian2dAxesVisual(axes));
            }

            // Alt-azimuth camera
            core.camera.setPosition([0, 0, -0.2], false);
        };
    </script>
</head>

<body></body>

</html>
Scatter plot faceted and wrapped by x-value.

Cross-Facet

Scatter plot cross-faceted by x-value and y-value.
Scatter plot cross-faceted by x-value and y-value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Cross-Facetted Scatter Plot</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 100;
            const ids = new Uint32Array(count);
            const valuesX = new Float64Array(count);
            const valuesY = new Float64Array(count);
            const minValueX = 0;
            const maxValueX = 10;
            const minValueY = 0;
            const maxValueY = 10;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                valuesX[i] = Math.random() * (maxValueX - minValueX) + minValueX;;
                valuesY[i] = Math.random() * (maxValueY - minValueY) + minValueY;;
            }

            // Bin
            const binsX = 5;
            const binsY = 2;
            const bins = binsX * binsY;
            const binIdsX = new Uint32Array(count);
            const binCountsX = new Uint32Array(binsX);
            const binFromsX = new Float64Array(binsX);
            const binTosX = new Float64Array(binsX);
            MorphCharts.Helpers.BinHelper.bin({
                ids: ids,
                values: valuesX,
                bins: binsX,
                minValue: minValueX,
                maxValue: maxValueX,
                isDiscrete: false,
                binIds: binIdsX,
                counts: binCountsX,
                froms: binFromsX,
                tos: binTosX,
            });
            const binIdsY = new Uint32Array(count);
            const binCountsY = new Uint32Array(binsY);
            const binFromsY = new Float64Array(binsY);
            const binTosY = new Float64Array(binsY);
            MorphCharts.Helpers.BinHelper.bin({
                ids: ids,
                values: valuesY,
                bins: binsY,
                minValue: minValueY,
                maxValue: maxValueY,
                isDiscrete: false,
                binIds: binIdsY,
                counts: binCountsY,
                froms: binFromsY,
                tos: binTosY,
            });

            // Cross-facet
            const orderedIds = new Uint32Array(count);
            const facetIds = new Uint32Array(count);
            const offsets = new Uint32Array(bins);
            const counts = new Uint32Array(bins);
            const facetHelper = new MorphCharts.Helpers.FacetHelper(core);
            facetHelper.split2d(ids, binsX, binsY, binIdsX, binIdsY, orderedIds, facetIds, offsets, counts);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentPalette.colors = [0xa6, 0xbd, 0xdb, 0xff];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.sphere;

            // Layout
            const scatter = new MorphCharts.Layouts.Scatter(core);
            scatter.layout(transitionBuffer.currentBuffer, ids, {
                positionsX: valuesX,
                positionsY: valuesY,
            });
            scatter.update(transitionBuffer.currentBuffer, ids, {
                minBoundsX: minValueX,
                maxBoundsX: maxValueX,
                minBoundsY: minValueY,
                maxBoundsY: maxValueY,

                // Facets
                facetCoordsX: binIdsX,
                facetCoordsY: binIdsY,
                facetsX: binsX,
                facetsY: binsY,
                facetSpacingX: 0.5,
                facetSpacingY: 0.75,
            });

            // Axes
            core.renderer.currentAxes = [];
            for (let i = 0; i < bins; i++) {
                const facetX = i % binsX;
                const facetY = Math.floor(i / binsX);
                const axes = MorphCharts.Axes.Cartesian2dAxesHelper.create(core, {
                    minBoundsX: minValueX,
                    maxBoundsX: maxValueX,
                    minBoundsY: minValueY,
                    maxBoundsY: maxValueY,
                    minValueX: minValueX,
                    maxValueX: maxValueX,
                    minValueY: minValueY,
                    maxValueY: maxValueY,
                    divisionsX: 5,
                    divisionsY: 5,
                    labelMinorSizeX: 0.01,
                    labelMinorSizeY: 0.01,
                    labelMajorSizeX: 0.01,
                    labelMajorSizeY: 0.01,
                    headingX: `x ${Math.round(binFromsX[facetX])} - ${Math.round(binTosX[facetX])}`,
                    headingY: `y ${Math.round(binFromsY[facetY])} - ${Math.round(binTosY[facetY])}`,
                    headingSizeX: 0.02,
                    headingSizeY: 0.02,
                    labelsX: (value) => { return value.toString(); },
                    labelsY: (value) => { return value.toString(); },
                    scaling: scatter.facetScaling,
                });
                axes.offsetX = scatter.offsetX(facetX);
                axes.offsetY = scatter.offsetY(facetY);
                axes.gridPickDivisionHeight = 0.01;
                axes.isEdgeVisible[MorphCharts.Edge2D.right] = false;
                axes.isEdgeVisible[MorphCharts.Edge2D.top] = false;
                axes.isHeadingVisible[MorphCharts.Edge2D.top] = true;
                axes.isHeadingVisible[MorphCharts.Edge2D.bottom] = false;
                axes.isHeadingVisible[MorphCharts.Edge2D.left] = true;
                axes.isHeadingVisible[MorphCharts.Edge2D.right] = false;
                core.renderer.currentAxes.push(core.renderer.createCartesian2dAxesVisual(axes));
            }

            // Alt-azimuth camera
            core.camera.setPosition([0, 0, -0.2], false);
        };
    </script>
</head>

<body></body>

</html>
Scatter plot cross-faceted by x-value and y-value.

Layout Per Facet

If faceting is intended to alter the layout (e.g. for stacks, treemaps etc), layout can be called per facet.

Layouts will track the maximum dimensions across multiple layout() calls, after calling resetCumulativeLayoutBounds(). The cumulative bounds are then available in [min,max]CumulativeLayoutBounds[X,Y,Z]. Bounds can be specified manually, as in this example.

Sheet chart cross-faceted by x-value and y-value.
Sheet chart cross-faceted by x-value and y-value.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Cross-Facetted Sheet Chart</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Core
            const core = new MorphCharts.Core();

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 100;
            const ids = new Uint32Array(count);
            const valuesX = new Float64Array(count);
            const valuesY = new Float64Array(count);
            const minValueX = 0;
            const maxValueX = 10;
            const minValueY = 0;
            const maxValueY = 10;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                valuesX[i] = Math.random() * (maxValueX - minValueX) + minValueX;;
                valuesY[i] = Math.random() * (maxValueY - minValueY) + minValueY;;
            }

            // Bin
            const binsX = 5;
            const binsY = 2;
            const bins = binsX * binsY;
            const binIdsX = new Uint32Array(count);
            const binCountsX = new Uint32Array(binsX);
            const binFromsX = new Float64Array(binsX);
            const binTosX = new Float64Array(binsX);
            MorphCharts.Helpers.BinHelper.bin({
                ids: ids,
                values: valuesX,
                bins: binsX,
                minValue: minValueX,
                maxValue: maxValueX,
                isDiscrete: false,
                binIds: binIdsX,
                counts: binCountsX,
                froms: binFromsX,
                tos: binTosX,
            });
            const binIdsY = new Uint32Array(count);
            const binCountsY = new Uint32Array(binsY);
            const binFromsY = new Float64Array(binsY);
            const binTosY = new Float64Array(binsY);
            MorphCharts.Helpers.BinHelper.bin({
                ids: ids,
                values: valuesY,
                bins: binsY,
                minValue: minValueY,
                maxValue: maxValueY,
                isDiscrete: false,
                binIds: binIdsY,
                counts: binCountsY,
                froms: binFromsY,
                tos: binTosY,
            });

            // Cross-facet
            const orderedIds = new Uint32Array(count);
            const facetIds = new Uint32Array(count);
            const offsets = new Uint32Array(bins);
            const counts = new Uint32Array(bins);
            const facetHelper = new MorphCharts.Helpers.FacetHelper(core);
            facetHelper.split2d(ids, binsX, binsY, binIdsX, binIdsY, orderedIds, facetIds, offsets, counts);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentPalette.colors = [0xa6, 0xbd, 0xdb, 0xff];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.block;

            // Reset cumulative bounds for multiple layout passes
            const sheet = new MorphCharts.Layouts.Sheet(core);
            sheet.resetCumulativeLayoutBounds();

            // Layout per facet
            const side = Math.ceil(Math.sqrt(count));
            for (let facetId = 0; facetId < bins; facetId++) {
                sheet.layout(transitionBuffer.currentBuffer, orderedIds, {
                    offset: offsets[facetId],
                    count: counts[facetId],
                    side: side,
                });
            }
            sheet.update(transitionBuffer.currentBuffer, ids, {
                thickness: 0.01,
                padding: 0.1,

                // Extend bounds (layout is centroid-only)
                minBoundsX: -0.5,
                maxBoundsX: side - 0.5,
                minBoundsY: -0.5,
                maxBoundsY: side - 0.5,

                // Facets
                facetCoordsX: binIdsX,
                facetCoordsY: binIdsY,
                facetsX: binsX,
                facetsY: binsY,
                facetSpacingX: 0.5,
                facetSpacingY: 0.75,
            });

            // Axes
            core.renderer.currentAxes = [];
            core.config.axesGridMajorColor = [0.5, 0.5, 0.5];
            for (let i = 0; i < bins; i++) {
                const facetX = i % binsX;
                const facetY = Math.floor(i / binsX);
                const axes = MorphCharts.Axes.Cartesian2dAxesHelper.create(core, {
                    minBoundsX: minValueX,
                    maxBoundsX: maxValueX,
                    minBoundsY: minValueY,
                    maxBoundsY: maxValueY,
                    minValueX: minValueX,
                    maxValueX: maxValueX,
                    minValueY: minValueY,
                    maxValueY: maxValueY,
                    arePickDivisionsVisibleX: false,
                    arePickDivisionsVisibleY: false,
                    divisionsX: 1,
                    divisionsY: 1,
                    labelMinorSizeX: 0.01,
                    labelMinorSizeY: 0.01,
                    labelMajorSizeX: 0.01,
                    labelMajorSizeY: 0.01,
                    headingX: `x ${Math.round(binFromsX[facetX])} - ${Math.round(binTosX[facetX])}`,
                    headingY: `y ${Math.round(binFromsY[facetY])} - ${Math.round(binTosY[facetY])}`,
                    headingSizeX: 0.02,
                    headingSizeY: 0.02,
                    scaling: sheet.facetScaling,
                });
                axes.offsetX = sheet.offsetX(facetX);
                axes.offsetY = sheet.offsetY(facetY);
                axes.minorGridlines[0] = 1;
                axes.minorGridlines[1] = 1;
                axes.zero[0] = -1;
                axes.zero[1] = -1;
                axes.isEdgeVisible[MorphCharts.Edge2D.right] = false;
                axes.isEdgeVisible[MorphCharts.Edge2D.top] = false;
                axes.isHeadingVisible[MorphCharts.Edge2D.top] = true;
                axes.isHeadingVisible[MorphCharts.Edge2D.bottom] = false;
                axes.isHeadingVisible[MorphCharts.Edge2D.left] = true;
                axes.isHeadingVisible[MorphCharts.Edge2D.right] = false;
                core.renderer.currentAxes.push(core.renderer.createCartesian2dAxesVisual(axes));
            }

            // Alt-azimuth camera
            core.camera.setPosition([0, 0, -0.2], false);
        };
    </script>
</head>

<body></body>

</html>
Sheet chart cross-faceted by x-value and y-value.

Renderers

Renderers are hot-swappable, but not all features are available in all renderers. For example, the Cartesian2dAxes and Cartesian3dAxes are currently only supported in the basic and advanced renderers, so axes must currently be drawn manually for the raytrace renderer.

Basic renderer.
Basic renderer.
Advanced renderer with shadows.
Advanced renderer with shadows.
Raytrace renderer with metal material.
Raytrace renderer with metal material.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Renderers</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }

        .radio {
            margin: 8px;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Container
            const container = document.getElementById("container");

            // Core
            const core = new MorphCharts.Core({ container: container });

            // Renderers
            const radioGroup = document.getElementsByName("radioGroup");
            for (let i = 0; i < radioGroup.length; i++) {
                const radio = radioGroup[i];
                const value = radio.value;
                switch (value) {
                    case "advanced":
                        radio.disabled = !new MorphCharts.Renderers.Advanced.Main().isSupported;
                        break;
                    case "raytracewebgpu":
                        radio.disabled = !new MorphCharts.Renderers.RayTraceWebGPU.Main().isSupported;
                        break;
                }
                radio.onchange = () => {
                    switch (value) {
                        case "basic":
                            core.renderer = new MorphCharts.Renderers.Basic.Main();
                            core.renderer.labelSets[0].isVisible = core.renderer.labelSets[1].isVisible = false;
                            break;
                        case "advanced":
                            core.renderer = new MorphCharts.Renderers.Advanced.Main();
                            core.renderer.config.isFxaaEnabled = true;
                            core.renderer.config.shadowWidth = core.renderer.config.shadowHeight = 4096;
                            core.renderer.labelSets[0].isVisible = core.renderer.labelSets[1].isVisible = false;
                            break;
                        case "raytracewebgpu":
                            core.renderer = new MorphCharts.Renderers.RayTraceWebGPU.Main();
                            core.renderer.lights = core.renderer.standardLighting({ azimuthOffset: 0 });
                            core.renderer.materials = materials;
                            core.renderer.labelSets[0].isVisible = core.renderer.labelSets[1].isVisible = true;
                            break;
                    }
                };
            }
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Data
            const count = 1000;
            const ids = new Uint32Array(count);
            const binIdsX = new Float64Array(count);
            const binIdsZ = new Float64Array(count);
            const values = new Float64Array(count);
            const binsX = 3;
            const binsZ = 3;
            const sizeX = 5;
            const sizeZ = 5;
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                binIdsX[i] = Math.floor(binsX * (Math.random() + Math.random()) / 2);
                binIdsZ[i] = Math.floor(binsZ * (Math.random() + Math.random()) / 2);
                values[i] = Math.random();
            }

            // Palette
            const paletteCount = 32;
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, paletteCount, false);

            // Materials
            const materialIds = new Uint32Array(count);
            for (let i = 0; i < count; i++) {
                materialIds[i] = Math.min(Math.floor(values[i] * paletteCount), paletteCount - 1);
            }
            const materials = [];
            for (let i = 0; i < paletteCount; i++) {
                const r = palette[i * 4] / 0xff;
                const g = palette[i * 4 + 1] / 0xff;
                const b = palette[i * 4 + 2] / 0xff;
                materials.push(new MorphCharts.Renderers.RayTraceWebGPU.MetalMaterial({ texture: new MorphCharts.Renderers.RayTraceWebGPU.SolidColorTexture({ color: [r, g, b] }), fuzz: 0.5 }));
            }

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.blockSdf;
            transitionBuffer.currentPalette.colors = palette;

            // Layout
            const stack = new MorphCharts.Layouts.Stack(core);
            stack.layout(transitionBuffer.currentBuffer, ids, {
                binsX: binsX,
                binsZ: binsZ,
                binIdsX: binIdsX,
                binIdsZ: binIdsZ,
                sizeX: sizeX,
                sizeZ: sizeZ,
                spacingX: 1,
                spacingZ: 1,
            });
            stack.update(transitionBuffer.currentBuffer, ids, {
                colors: values,
                padding: 0.05,

                // RayTraceWebGPU
                materials: materialIds,
                rounding: 0.05,
            });

            // Axes
            const axes = MorphCharts.Axes.Cartesian3dAxesHelper.create(core, {
                minBoundsX: stack.minModelBoundsX,
                minBoundsY: stack.minModelBoundsY,
                minBoundsZ: stack.minModelBoundsZ,
                maxBoundsX: stack.maxModelBoundsX,
                maxBoundsY: stack.maxModelBoundsY,
                maxBoundsZ: stack.maxModelBoundsZ,
                minValueX: 0,
                maxValueX: binsX - 1,
                minValueY: 0,
                maxValueY: stack.maxLevel * sizeX * sizeZ,
                minValueZ: 0,
                maxValueZ: binsZ - 1,
                isDiscreteX: true,
                isDiscreteZ: true,
                labelsX: (value) => { return value.toString(); },
                labelsY: (value) => { return Math.round(value).toString(); },
                labelsZ: (value) => { return value.toString(); }
            });
            // Hide zero lines
            axes.zero[0] = -1;
            axes.zero[2] = -1;
            core.renderer.currentAxes = [core.renderer.createCartesian3dAxesVisual(axes)];

            // Labels x
            let scale = 1;
            let glyphCount = 0;
            let axis = MorphCharts.Helpers.AxisHelper.discrete({
                min: 0,
                max: binsX - 1,
                divisions: binsX,
            });
            let positionsX = new Float64Array(axis.labels.length);
            let positionsY = new Float64Array(axis.labels.length);
            let positionsZ = new Float64Array(axis.labels.length);
            for (let i = 0; i < axis.labels.length; i++) {
                glyphCount += axis.labels[i].length;
                positionsX[i] = stack.minModelBoundsX + (stack.maxModelBoundsX - stack.minModelBoundsX) * axis.labelPositions[i];
                positionsY[i] = stack.minModelBoundsY;
                positionsZ[i] = stack.maxModelBoundsZ + scale / 2;
            }
            let labelSet = new MorphCharts.Components.LabelSet(core, {
                text: axis.labels,
                maxGlyphs: glyphCount,
                scale: scale,
            });
            labelSet.minBoundsX = stack.minModelBoundsX;
            labelSet.minBoundsY = stack.minModelBoundsY;
            labelSet.minBoundsZ = stack.minModelBoundsZ;
            labelSet.maxBoundsX = stack.maxModelBoundsX;
            labelSet.maxBoundsY = stack.maxModelBoundsY;
            labelSet.maxBoundsZ = stack.maxModelBoundsZ;
            labelSet.positionsX = positionsX;
            labelSet.positionsY = positionsY;
            labelSet.positionsZ = positionsZ;
            labelSet.rotation = [-0.7071068, 0, 0, 0.7071068];
            core.renderer.labelSets.push(core.renderer.createLabelSetVisual(labelSet));

            // Labels z
            glyphCount = 0;
            axis = MorphCharts.Helpers.AxisHelper.discrete({
                min: 0,
                max: binsZ - 1,
                divisions: binsZ,
            });
            positionsX = new Float64Array(axis.labels.length);
            positionsY = new Float64Array(axis.labels.length);
            positionsZ = new Float64Array(axis.labels.length);
            for (let i = 0; i < axis.labels.length; i++) {
                glyphCount += axis.labels[i].length;
                positionsX[i] = stack.maxModelBoundsX + scale / 2;
                positionsY[i] = stack.minModelBoundsY;
                positionsZ[i] = stack.minModelBoundsZ + (stack.maxModelBoundsZ - stack.minModelBoundsZ) * axis.labelPositions[i];
            }
            labelSet = new MorphCharts.Components.LabelSet(core, {
                text: axis.labels,
                maxGlyphs: glyphCount,
                scale: scale,
            });
            labelSet.minBoundsX = stack.minModelBoundsX;
            labelSet.minBoundsY = stack.minModelBoundsY;
            labelSet.minBoundsZ = stack.minModelBoundsZ;
            labelSet.maxBoundsX = stack.maxModelBoundsX;
            labelSet.maxBoundsY = stack.maxModelBoundsY;
            labelSet.maxBoundsZ = stack.maxModelBoundsZ;
            labelSet.positionsX = positionsX;
            labelSet.positionsY = positionsY;
            labelSet.positionsZ = positionsZ;
            labelSet.rotation = [-0.5, 0.5, 0.5, 0.5];
            core.renderer.labelSets.push(core.renderer.createLabelSetVisual(labelSet));
            core.renderer.labelSets[0].isVisible = core.renderer.labelSets[1].isVisible = false;

            // Alt-azimuth camera
            const camera = new MorphCharts.Cameras.AltAzimuthCamera(core);
            camera.setPosition([0, -0.02, 0.2], false);
            camera.setAltAzimuth(MorphCharts.Helpers.AngleHelper.degreesToRadians(15), MorphCharts.Helpers.AngleHelper.degreesToRadians(-45), false);
            core.camera = camera;
        };
    </script>
</head>

<body>
    <div id="container" class="container" style="width:100%;height:100%;"></div>
    <div style="margin:8px;position:absolute;left:0;bottom:0;background-color:white;">
        <div><label><input type="radio" class="radio" name="radioGroup" value="basic" checked />Basic (WebGL1)</label>
        </div>
        <div><label><input type="radio" class="radio" name="radioGroup" value="advanced" disabled />Advanced
                (WebGL2)</label></div>
        <div><label><input type="radio" class="radio" name="radioGroup" value="raytracewebgpu" disabled />Raytrace
                (WebGPU)</label>
        </div>
    </div>
</body>

</html>
Switching renderers.

Interaction

A default MorphCharts.Input.Manager processes input from touch, keyboard, mouse, mousewheel, stylus, Surface Dial, and VR controllers.

Selection

Filtering

Palettes

The basic and advanced renderers use palettes for applying color to units. A palette is simply an array of RGBA values in the range [0,255]. Helper functions are provided for creating palettes, for example by interpolating between RBG or HSV values, or by resampling a set of standard palettes.

Axes

Cartesian2dAxes and Cartesian3dAxes are provided for 2D and 3D charts respectively. The labels are not billboarded, but support arbitrary viewing directions by flipping their orientation.

A renderer defines a set of previousAxes and a set of currentAxes, such that a different set of axes can be defined for the renderer's TransitionBuffer.previousBuffer and TransitionBuffer.currentBuffer. The relevant axes can then be shown by setting the renderer's axesVisibility to AxesVisibility.previous, AxesVisibility.current, or AxesVisibility.none. They can be flipped by calling renderer.swapAxes().

Axes are not automatically aligned to chart layouts, but instead require the relevant coordinate space to the specified using a set of bounds. Typically the calculated layout bounds can be used to align the axes with the layout, or a manual set of bounds can be used to align the layout with the axes.

Labels

Labels can be added individually as a Label or in groups using a LabelSet.

Similarly to axes and chart layouts, a LabelSet can be aligned by specifying a set of bounds in an arbitrary coordinate space. The allows, for example the same data values used for the layouts to be used to position labels in a LabelSet.

Transitions

Animation can be used to transition between chart layouts, with or without staggering.

The TransitionBuffer supports animated transitions by providing a currentBuffer and previousBuffer object. The renderer's transitionTime property, a value in the range [0,1] interpolates between these two buffers. The buffers can be flipped by calling the swap() method on the TransitionBuffer.

Staggering can be applied during the layout's update pass using IVertexOptions.staggerOrders, which accepts an array of values between [0,1]. If values outside the range [0,1] are available in an existing array, the min and max can be passed using minStaggerOrder and maxStaggerOrder respectively to allow the existing array to be normalized. The order can also be reversed using the Boolean property staggerOrderReverse. If no values are passed, the data order is used for the stagger order. A single value can be used by setting IVertexOptions.staggerOrder to a number in the range [0,1].

Animated transition between layouts.
Animated transition between layouts.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Transition</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }

        .range {
            margin: 8px;
        }
    </style>
    <script type="module">
        import * as MorphCharts from "https://cdn.skypack.dev/morphcharts";
        window.onload = () => {
            // Container
            const container = document.getElementById("container");

            // Core
            const core = new MorphCharts.Core({ container: container });

            // Renderer
            core.renderer = new MorphCharts.Renderers.Basic.Main();

            // Transition
            const updateTransition = () => {
                const transitionTime = core.renderer.transitionTime;
                transitionTimeRange.value = transitionTime.toString();
                transitionTimeLabel.innerText = Math.round(100 * transitionTime).toString();
                const staggerPcnt = core.config.transitionStaggering / (core.config.transitionStaggering + core.config.transitionDuration);
                transitionStaggeringRange.value = staggerPcnt.toString();
                transitionStaggeringLabel.innerText = Math.round(100 * staggerPcnt).toString();
                core.renderer.axesVisibility = transitionTime == 0 ? MorphCharts.AxesVisibility.previous : MorphCharts.AxesVisibility.current;
            };
            const transitionTimeRange = document.getElementById("transitionTimeRange");
            const transitionTimeLabel = document.getElementById("transitionTimeRangeLabel");
            transitionTimeRange.oninput = () => {
                core.renderer.transitionTime = parseFloat(transitionTimeRange.value);
                updateTransition();
            };
            const transitionStaggeringRange = document.getElementById("transitionStaggeringRange");
            const transitionStaggeringLabel = document.getElementById("transitionStaggeringRangeLabel");
            transitionStaggeringRange.oninput = () => {
                const staggerPcnt = parseFloat(transitionStaggeringRange.value);
                core.config.transitionStaggering = staggerPcnt * core.config.transitionDuration / (1 - staggerPcnt);
                updateTransition();
            };
            core.renderer.transitionTime = 0;
            updateTransition();

            // Data
            const count = 4096;
            const categories = 10;
            const ids = new Uint32Array(count);
            const values = new Float64Array(count);
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                values[i] = Math.floor(Math.random() * categories);
            }

            // Palette
            const palette = MorphCharts.Helpers.PaletteHelper.resample(core.paletteResources.palettes[MorphCharts.PaletteName.blues].colors, categories, true);

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.block;
            transitionBuffer.previousBuffer.unitType = MorphCharts.UnitType.block;
            transitionBuffer.currentPalette.colors = palette;
            transitionBuffer.previousPalette.colors = palette;

            // Previous layout
            const sizeX = Math.ceil(Math.sqrt(count) / categories);
            const stack = new MorphCharts.Layouts.Stack(core);
            stack.layout(transitionBuffer.previousBuffer, ids, {
                binsX: categories,
                binIdsX: values, // #bins = #categories
                spacingX: 1,
                sizeX: sizeX,
            });
            stack.update(transitionBuffer.previousBuffer, ids, {
                maxColor: categories - 1,
                colors: values,
                padding: 0.1,
                thickness: 0.01,
            });

            // Current layout
            const sheet = new MorphCharts.Layouts.Sheet(core);
            sheet.layout(transitionBuffer.currentBuffer, ids, {});
            sheet.update(transitionBuffer.currentBuffer, ids, {
                maxColor: categories - 1,
                colors: values,
                padding: 0.1,
                thickness: 0.01,
            });

            // Axes
            const axes = MorphCharts.Axes.Cartesian2dAxesHelper.create(core, {
                minBoundsX: stack.minModelBoundsX,
                minBoundsY: stack.minModelBoundsY,
                maxBoundsX: stack.maxModelBoundsX,
                maxBoundsY: stack.maxModelBoundsY,
                arePickDivisionsVisibleX: false,
                arePickDivisionsVisibleY: false,
                minValueX: 0,
                maxValueX: categories - 1,
                minValueY: 0,
                maxValueY: stack.maxLevel * sizeX,
                isDiscreteX: true,
                labelsX: (value) => { return value.toString(); },
                labelsY: (value) => { return Math.round(value).toString(); },
            });
            axes.zero[0] = axes.zero[2] = -1; // Hide zero lines
            axes.minorGridlines[1] = 1;
            axes.isEdgeVisible[MorphCharts.Edge2D.right] = false;
            axes.isEdgeVisible[MorphCharts.Edge2D.top] = false;
            core.renderer.previousAxes = [core.renderer.createCartesian2dAxesVisual(axes)];
        };
    </script>
</head>

<body>
    <div id="container" class="container" style="width:100%;height:100%;"></div>
    <div style="margin:8px;position:absolute;left:0;bottom:0;background-color:white;">
        <div>
            <label class="rangeLabel"><input type="range" min="0" max="1" step="any"
                    id="transitionTimeRange" />Time</label>
            <label class="rangeValue" id="transitionTimeRangeLabel"></label><label class="rangeValue">&percnt;</label>
        </div>
        <div>
            <label class="rangeLabel"><input type="range" class="range" min="0" max="0.99" step="any"
                    id="transitionStaggeringRange" />Stagger</label>
            <label class="rangeValue" class="range" id="transitionStaggeringRangeLabel"></label><label
                class="rangeValue">&percnt;</label>
        </div>
    </div>
</body>

</html>
Animated transition between layouts.

Fonts

Text is drawn using a dynamically generated SDF (signed distance field) font.

The default font can be configured using the constructor options for the Core. For example, to increase the quality of the default font, use the following option:

// High-quality SDF font
fontRasterizerOptions: {
    fontAtlas: new MorphCharts.FontAtlas(2048, 2048),
    fontSize: 192,
    border: 24,
    fontFamily: "\"segoe ui semibold\", sans-serif",
    fontWeight: "normal",
    fontStyle: "normal",
    baseline: "alphabetic",
    maxDistance: 64,
    edgeValue: 0xc0,
},

Data

Ray Tracing

Materials

Whereas the basic and advanced renderers only support color, the raytrace renderer also supports materials. This image shows (from left to right) LambertianMaterial, GlossyMaterial, MetalMaterial (fuzz: 0), MetalMaterial (fuzz: 0.5) and DielectricMaterial materials respectively.

Raytrace renderer material sample using spheres.
Raytrace renderer material sample using spheres.

Try it

<!DOCTYPE html>
<html lang="en-us">

<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Raytrace Materials</title>
    <style type="text/css">
        html,
        body {
            height: 100%;
            margin: 0;
        }
    </style>
    <script type="module">
        import * as MorphCharts from 'https://cdn.skypack.dev/morphcharts';
        window.onload = () => {
            // Renderer
            const renderer = new MorphCharts.Renderers.RayTraceWebGPU.Main();

            // WebGPU support
            if (!renderer.isSupported) {
                container.innerText = "WebGPU not supported";
                return;
            }

            // Core
            const core = new MorphCharts.Core();
            core.renderer = renderer;

            // Data
            const count = 5;
            const ids = new Uint32Array(count);
            const positionsX = new Float64Array(count);
            const materialIds = new Uint32Array(count);
            for (let i = 0; i < count; i++) {
                ids[i] = i;
                positionsX[i] = i;
                materialIds[i] = i;
            }

            // Transition buffer
            const transitionBuffer = core.renderer.createTransitionBuffer(ids);
            core.renderer.transitionBuffers = [transitionBuffer];
            transitionBuffer.currentBuffer.unitType = MorphCharts.UnitType.sphere;

            // Layout
            const scatter = new MorphCharts.Layouts.Scatter(core);
            scatter.layout(transitionBuffer.currentBuffer, ids, {
                positionsX: positionsX,
            });
            scatter.update(transitionBuffer.currentBuffer, ids, {
                sizeScaling: 0.9,
                materials: materialIds,
                minBoundsX: -0.5,
                maxBoundsX: count - 0.5,
                minBoundsY: -0.45,
                maxBoundsY: 0.45,
                minBoundsZ: -1.5,
                maxBoundsZ: 1.5,
            });

            // Materials
            const color1 = new MorphCharts.Renderers.RayTraceWebGPU.SolidColorTexture({ color: [0.4, 0.5, 0.6] });
            const color2 = new MorphCharts.Renderers.RayTraceWebGPU.SolidColorTexture({ color: [0.8, 0.7, 0.6] });
            renderer.materials = [
                new MorphCharts.Renderers.RayTraceWebGPU.LambertianMaterial({ texture: color1 }),
                new MorphCharts.Renderers.RayTraceWebGPU.GlossyMaterial({ texture: color1, fuzz: 0 }),
                new MorphCharts.Renderers.RayTraceWebGPU.MetalMaterial({ texture: color2, fuzz: 0 }),
                new MorphCharts.Renderers.RayTraceWebGPU.MetalMaterial({ texture: color2, fuzz: 0.5 }),
                new MorphCharts.Renderers.RayTraceWebGPU.DielectricMaterial(),
            ];

            // Aperture
            renderer.config.aperture = 0.005;

            // Ground
            renderer.ground.material = new MorphCharts.Renderers.RayTraceWebGPU.GlossyMaterial({
                texture: new MorphCharts.Renderers.RayTraceWebGPU.CheckerTexture({
                    color0: [0.48, 0.48, 0.48],
                    color1: [0.52, 0.52, 0.52],
                    size: [0.01, 0.01],
                    offset: [0.005, 0.005],
                }),
                fuzz: 0.05,
            });

            // Lighting
            renderer.lights = renderer.standardLighting();

            // Alt-azimuth camera
            const camera = core.camera;
            camera.setPosition([0, 0, -0.15], false);
            camera.setAltAzimuth(MorphCharts.Helpers.AngleHelper.degreesToRadians(30), 0, false);
        };
    </script>
</head>

<body>
    <div id="container"></div>
</body>

</html>

Unit Types

The raytrace renderer also supports Signed Distance Field unit types, which can be used to add rounded edges.

Raytrace renderer material sample using cylinders with rounded edges.
Raytrace renderer material sample using cylinders with rounded edges.

Debugging

Information is written to the console according to the configured logging level. For more verbose logging, set:

core.config.logLevel = MorphCharts.LogLevel.debug;

In addition, interactive debug information can be gathered by setting:

core.config.isDebugVisible = true;

This information can then be written to an HTML element as follows:

const debug = document.getElementById("debug");
core.updateCallback = () => {
    debug.innerText = core.debugText.text;
};

Apps

The following apps use the MorphCharts library.

About

MorphCharts is a project from Microsoft Research. For questions and issues please contact morphcharts@microsoft.com.

Contact Us | Privacy & Cookies | Trademarks | Terms of Use | © 2022
Microsoft