MorphCharts is a low-level JavaScript library for interactive and ray traced data visualization in both 2D and 3D.
See the gallery for a selection of visualizations created with the MorphCharts raytrace renderer.
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.
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).
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.
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.
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.
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.
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, such as maps, can be added to provide context. Images can be rectangular or spherical.
Labels can be added individually or in groups for higher performance.
Support for immersive experiences with virtual reality headsets and controllers is supported via WebXR in the basic renderer.
Stereo rendering is supported in the basic renderer with left, right, split-screen and anaglyph modes.
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
).
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.
The key classes required to create a visualization are the A A renderer creates an HTML Canvas. To place this canvas in a specifc HTML Element, use the
If no The following examples render simple procedural data using different layouts and the basic renderer.
A layout is used to update the Line charts are created using multiple cylindrical line segments. 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). 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 Renderers are hot-swappable, but not all features are available in all renderers. For example, the
A default 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. A renderer defines a set of 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 can be added individually as a Similarly to axes and chart layouts, a Animation can be used to transition between chart layouts, with or without staggering. The Staggering can be applied during the layout's update pass using
Text is drawn using a dynamically generated SDF (signed distance field) font. The default font can be configured using the constructor options for the Whereas the basic and advanced renderers only support color, the raytrace renderer also supports
materials.
This image shows (from left to right) The raytrace renderer also supports Signed Distance Field unit types, which can be used to add
rounded
edges.
Information is written to the console according to the configured logging level. For more verbose
logging,
set: In addition, interactive debug information can be gathered by setting: This information can then be written to an HTML element as follows:Coordinates
Initialization
Core
, a renderer (e.g.
BasicRenderer
), and a TransitionBuffer
.
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.
Core
options container
parameter. The size can be specified either in the
renderer
width
and height
options, or on the HTML Element container itself.
<!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>
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>
Layouts
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
<!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>
Bar Chart
<!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>
Stack Chart (Count)
<!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>
Stack Chart (Sum)
<!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>
Groups
Line Chart
<!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>
Squarified Treemap
<!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>
Facets
Wrap
<!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>
Cross-Facet
<!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>
Layout Per Facet
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.
<!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>
Renderers
Cartesian2dAxes
and Cartesian3dAxes
are currently only supported in the
basic
and
advanced renderers, so axes must currently be drawn manually for the raytrace renderer.
<!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>
Interaction
MorphCharts.Input.Manager
processes input from touch, keyboard, mouse,
mousewheel,
stylus, Surface Dial, and VR controllers.Selection
Filtering
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.
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()
.
Labels
Label
or in groups using a LabelSet
.
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
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
.
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].
<!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">%</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">%</label>
</div>
</div>
</body>
</html>
Fonts
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
LambertianMaterial
,
GlossyMaterial
,
MetalMaterial
(fuzz: 0
), MetalMaterial
(fuzz: 0.5
)
and
DielectricMaterial
materials respectively.
<!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
Debugging
core.config.logLevel = MorphCharts.LogLevel.debug;
core.config.isDebugVisible = true;
const debug = document.getElementById("debug");
core.updateCallback = () => {
debug.innerText = core.debugText.text;
};
The following apps use the MorphCharts library.
MorphCharts is a project from Microsoft Research. For questions and issues please contact morphcharts@microsoft.com.