Menu

Introduction edit this section

What is Layer Cake?

Layer Cake is a graphics framework, built on top of Svelte that removes the boilerplate from making responsive web graphics. it gives you common elements, like a coordinate system and scales, for you to start creating your own dataviz layers, like axes, plots and annotations.

Give it some data and a target DOM element and Layer Cake will create a Svelte store that includes scales bound to your element's dimensions and the data's extents. Layer Cake also includes higher level methods to organize multiple SVG, HTML and Canvas layers that use these scales.

By breaking a part a graphic into layers, you can more easily reuse components from project to project. It also lets you easily move between web languages (SVG, Canvas, HTML, WebGL) by giving you a common coordinate system. You may be using Canvas for a scatterplot, SVG for axes and HTML for annotations but they all read from a common store and appear seamless to the viewer. You can choose the best technology for that part of the graphic without worrying about how it will interact with other elements.

Layer Cake uses D3 scales. See more in the xScale, yScale and rScale sections of the Store API.

Layer Cake is more about having a system to organize your own custom components than it is a high-level charting library. It doesn't have any built-in concepts or strong opinions about how your data should be structured.

See the flatData option in the Store API section and the flatten helper function for more info about data structure.

Getting started

Install Layer Cake in your devDependencies alongside Svelte.

npm install --save-dev layercake

The easiest way to get started is to clone down or use degit to grab the starter template at github.com/mhkeller/layercake-template.

The important files are like your index.html which has the DOM element we want to render into and the cake configuration in main.js:

my-app
 ├── index.html
 └── js
   └── main.js
index.html
<!-- The target div needs to have a width and a height -->
<div id="chart-target" style="width: 100%; height: 300px;"></div>
js/main.js
import LayerCake from 'layercake';

// Define some data
const points = [
  {x: 0, y: 1},
  {x: 10, y: 5},
  {x: 15, y: 10}
];

// Instantiate the cake, point it to our target div
// and which keys to look for on the data
const myCake = new LayerCake({
  target: document.getElementById('chart-target'),
  data: points,
  x: 'x',
  y: 'y'
});

console.log(myCake.get());

Each of the chart examples on the home page can be run locally by clicking into them and clikcing Download. If you are using Layer Cake within Sapper, the code here in main.js would go inside your components oncreate() method, which is how this examples site is built.

The myCake variable is a Svelte Store that just computed different properties to use in our chart. Because we gave Layer Cake values for x and y, it has measured the extent of our data's x- and y-dimensions and created xScale and yScale properties. It has also measured our DOM element as well as created x- and y-accessors so, for a given row of our data we can compute the value in our coordinate system.

const { x, y, xScale, yScale } = myCake.get();

points.forEach(d => {
  const firstPoint = [xScale(x(d)), yScale(y(d))];
});

You can also use the shorthand [xGet(d), yGet(d)]. See the Store API section for a full list of computed properties.

Because Layer Cake has bound the target DOM element's dimensions to your scales, all computed properties will update on resize automatically.

Layer components

While it's perfectly fine to use Layer Cake as a store and implement the rest of your project your own way, the library also comes with higher-level methods to create graphic layers and lay them out in a common coordinate space.

To do this, pass a list of Svelte components to any of the .svgLayers, .htmlLayers, .canvasLayers or .webglLayers methods. When you've added all the layers to your cake, run .render().

Here's an example creating an SVG scatter plot using the above data.

main.js
import LayerCake from 'layercake';
import Scatter from './components/Scatter.html';

const points = [
  {x: 0, y: 1},
  {x: 10, y: 5},
  {x: 15, y: 10}
];

const myCake = new LayerCake({
  x: 'x',
  y: 'y',
  data: points,
  target: document.getElementById('chart-target')
})
  .svgLayers([
	{ component: Scatter, opts: { fill: 'blue', r: 3 } } // The opts field is optional but exists to let you pass settings down to your components so they can be more reusable.
  ])

myCake.render();
/components/Scatter.html
{#each $data as d}
  <circle cx='{xGet(d)}' cy='{yGet(d)}' fill='{opts.fill}' r='{opts.r}' />
{/each}

We've defined the circle's fill color and radius size in main.js using the opts field. You could very well hardcode these values into your layer component. Passing in values from mains.js is shown here to give an example of how you can make your components more reusable. For example, you could use the same layer component to render small multiples, but pass in a color to highlight one of them.

Our DOM now looks something like this:

<svg width="<el width>" height="<el height>">
  <!-- One main g to wrap all layers -->
  <g>
	<!-- Scatter g -->
	<g>
	  <circle cx="..." cy="..." r="..." fill="blue"/>
	  <circle cx="..." cy="..." r="..." fill="blue"/>
	  <circle cx="..." cy="..." r="..." fill="blue"/>
	</g>
  </g>
</svg

More layer types

We just saw how to add SVG layers with the .svgLayers method. You also have htmlLayers and canvasLayers. For the SVG and HTML groups, every item in the array will create a new DOM element to render into, <g> for SVG and <div> for HTML. For Canvas, since there's no DOM equivalent, each layer renders into the same Canvas context. See the Scatter canvas example for details.

Layers are rendered in the order they appear and you can call these methods multiple times to create a new layout group.

main.js
import LayerCake from 'layercake';

import ScatterCanvas from './components/ScatterCanvas.html';
import AxisX from './components/AxisX.html';
import AxisY from './components/AxisY.html';
import Annotations from './components/Annotations.html';

const blurbs = [
  { x: 10, y: 20, text: 'Look at this value!'}
];

const points = [
  {x: 0, y: 1},
  {x: 10, y: 5},
  {x: 15, y: 10}
];

const myCake = new LayerCake({
  x: 'x',
  y: 'y',
  data: points,
  target: document.getElementById('chart-target')
})
  .canvasLayers([
	{ component: Scatter, opts: { fill: 'blue', r: 3 } }
  ])
  .svgLayers([
	{ component: AxisX, opts: { } },
	{ component: AxisY, opts: { } }
  ])
  .htmlLayers([
	{ component: Annotations, opts: { blurbs } }
  ]);
  // If you needed to, you could do `.svgLayers` again...

myCake.render();

Many common chart types have example pages. See the gallery at https://layercake.graphics or use the dropdown menu at the top of the page to navigate to one.

Data-less cakes

You can also use Layer Cake to simply arrange SVG, HTML, Canvas and WebGL elements on top of one another, sharing the same dimensions. For example, this would be handy if you have some SVG artwork that you want to put on top of an HTML video player.

Here's an example just setting the target value.

main.js
import LayerCake from 'layercake';
import Frame from './components/Frame.html';
import VideoPlayer from './components/VideoPlayer.html';

const myCake = new LayerCake({
  target: document.getElementById('chart-target')
})
  .svgLayers([
	{ component: Frame }
  ])
  .htmlLayers([
	{ component: VideoPlayer }
  ]);

myCake.render();

Store API edit this section

These are the options you can pass into new LayerCake(). You can also set your own custom values and they will be normal store properties. Many of the examples do this to set color scales or other values that will be used across components, for instance. To make sure the names don't conflict, the examples suffix any custom properties with an underscore, but it's not required.

target DOM Node

The DOM object you want to use as the the basis for measurements and for rendering into.

const myCake = new LayerCake({
  target: document.getElementById('my-chart')
});

Only target is required. Everything else is optional.

data Array

A list of data items. This is available on the store as $data.

x String|Function|Array

The key in each row of data that corresponds to the x-field. This can be a string or an accessor function. This property gets converted to an accessor function available on the store as $x.

const myCake = new LayerCake({
  x: 'myX',
  // equivalent to...
  x: (d) => d.myX
});

You can also give this value an array of strings or arrays of functions. While it may seem counter-intuitive to have more than one x- or y-accessor, this is the case in stacked layouts and Cleveland dot plots. See the Stacked bar, Stacked area, Stacked colummn or Cleveland dot plot for complete examples.

Here's an overview using the d3.stack() to make a horizontal bar chart, which will have two values for the x-accessor.

const data = [
  {month: new Date(2015, 3, 1), apples: 3840, bananas: 1920, cherries: 960, dates: 400},
  {month: new Date(2015, 2, 1), apples: 1600, bananas: 1440, cherries: 960, dates: 400},
  {month: new Date(2015, 1, 1), apples: 640,  bananas: 960,  cherries: 640, dates: 400},
  {month: new Date(2015, 0, 1), apples: 320,  bananas: 480,  cherries: 640, dates: 400}
];

const stack = d3.stack()
	.keys(['apples', 'bananas', 'cherries', 'dates']);

const series = stack(data);

The data is now an array of values. The month values you can't see because sneakily stashes them as a property on the array, accessible as d.data.

[
  [[   0, 3840], [   0, 1600], [   0,  640], [   0,  320]], // apples
  [[3840, 5760], [1600, 3040], [ 640, 1600], [ 320,  800]], // bananas
  [[5760, 6720], [3040, 4000], [1600, 2240], [ 800, 1440]], // cherries
  [[6720, 7120], [4000, 4400], [2240, 2640], [1440, 1840]]  // dates
]

The x- and y-accessors would then look like this:

const myCake = new LayerCake({
  x: [0, 1],
  y: d => d.data.month
});

Calls to x(dataRow) in this scenario will return the two-value array. Calls to xGet(dataRow) will return a two-value array, mapped through the xScale.

y String|Function|Array

Same as x but for the y scale. The accessor function is available on the store as $y.

r String|Function|Array

Same as x but for the r scale. The accessor function is available on the store as $r.

padding Object

An object that can specify top, right, bottom, or left padding in pixels. Any unspecified values are filled in as 0. Padding operates like CSS box-sizing: border-box; where values are subtracted from the target container's width and height, the same as a D3 margin convention.

const myCake = new LayerCake({
  padding: { top: 20, right: 10, bottom: 0, left: 0 },
  // equivalent to...
  padding: { top: 20, right: 10 }
});

Another way to set padding is to add it via normal CSS on your target div. The target element is assigned CSS of box-sizing: border-box; so padding settings won't affect the width or height. If you set any padding via CSS, the padding object will be ignored.

xScale d3.scaleLinear(undefined)

Pass in an instantiated D3 scale if you want to override the default d3.scaleLinear() or you want to add extra options.

See the Column chart for an example of passing in a d3.scaleBand() to override the default.

yScale d3.scaleLinear(undefined)

Same as xScale but for the y scale. The default is d3.scaleLinear().

rScale d3.scaleSqrt(undefined)

Same as xScale but for the r scale. The default is d3.scaleSqrt().

xDomain Array:[min: Number, max: Number]

Set a min or max on the x scale. If you want to inherit the value from the data's extent, set that value to null.

const myCake = new LayerCake({
  xDomain: [0, 100], // Fixes the x scale's domain
  // or..
  xDomain: [0, null], // Fixes the min but allows the max to be whatever is in the data
});

yDomain Array:[min: Number, max: Number]

Same as xDomain but for the y scale.

rDomain Array:[min: Number, max: Number]

Same as xDomain but for the r scale.

reverseX Boolean=false

Reverse the default x domain. By default this is false and the domain is [0, width].

reverseY Boolean=true

Reverse the default y domain. By default this is true and the domain is [height, 0].

xRange Function|Array:[min: Number, max: Number]

Override the default y range of [0, width] by setting it here to an array or function with argument ({ width, height}) that returns an array. This setting is ignored if you set reverseX to true.

const myCake = new LayerCake({
  xRange: [1, 100]
});

It can also be a function:

const myCake = new LayerCake({
  xRange: ({ width, height }) => [0, width / 2]
});

yRange Function|Array:[min: Number, max: Number]

Same as xRange but for the y scale. Override the default y range of [0, height] by setting it here to an array or function with argument ({ width, height}) that returns an array. This setting is ignored if you set reverseY to true.

rRange Function|Array:[min: Number, max: Number]

Same as xRange but for the r scale. Override the default y range of [1, 25] by setting it here to an array or function with argument ({ width, height}) that returns an array. The r scale defaults to d3.scaleSqrt so make sure you don't use a zero in your range.

xPadding Array:[leftPixels: Number, rightPixels: Number]

Assign a pixel value to add to the min or max of the x scale. This will increase the scales domain by the scale unit equivalent of the provided pixels. It uses D3 scale's invert function, so this only applies to continuous scales like scaleLinear. This is useful for adding extra space to a scatter plot so that your circles don't interfere with your y axis.

const myCake = new LayerCake({
  xPadding: [10, 10], // Add ten pixels of data units to both sides of the scale's domain
});

yPadding Array:[leftPixels: Number, rightPixels: Number]

Same as xPadding but for the y domain.

rPadding Array:[leftPixels: Number, rightPixels: Number]

Same as xPadding but for the r domain.

xNice Boolean=false

Applies D3's scale.nice() to the x domain. This is a separate option instead of being one you can apply to a passed in scale because D3's "nice" transformation only works on existing domains and does not use a state to be able to tell if your existing scale wants to be nice.

yNice Boolean=false

Same as xNice but for the y domain.

rNice Boolean=false

Same as xNice but for the r domain.

flatData Array

In order for Layer Cake to measure the extents of your data, it needs a flat array of items that the x, y and r accessors can find. If your data is not flat (often the case if your renderers prefer a more nested format), you can tell it to measure extents against a flat version. This will not change the shape of the data that gets passed to components — it is only for extent calculation.

The library also exports a flattening function to handle common use cases if you need to flatten your data and you don't already have a flat version. See the flatten helper function for more info.

Here's an example showing passing different data formats for extent calculation versus what is used by layer components.

const flatData = [
  {month: new Date(2015, 3, 1), value: 3840, group: 'apples'},
  {month: new Date(2015, 2, 1), value: 1600, group: 'apples'},
  {month: new Date(2015, 1, 1), value: 640, group:  'apples'},
  {month: new Date(2015, 0, 1), value: 320, group:  'apples'},

  {month: new Date(2015, 3, 1), value: 1920, group: 'bananas'},
  {month: new Date(2015, 2, 1), value: 1440, group: 'bananas'},
  {month: new Date(2015, 1, 1), value: 960, group:  'bananas'},
  {month: new Date(2015, 0, 1), value: 480, group:  'bananas'}
];

const data = [
  {
	key: 'apples',
	values: [{month: new Date(2015, 3, 1), value: 3840}, ...]
  },
  {
	key: 'bananas',
	values: [{month: new Date(2015, 3, 1), value: 1920}, ...]
  },
];

const myCake = new LayerCake({
	target,
	x: 'month',
	y: 'value',
	data,
	flatData
  });

Cake API edit this section

When you run new LayerCake(StoreValues) the Svelte store you get back has the following methods.

Each of the xyzLayers functions takes as a first argument an array of objects for each layer. You can optionally pass in an options object as the second argument. All of the layers methods share this pattern:

.xyzLayers([
  { component: SvelteComponent, opts: {} }
], {
  zIndex: <optional z-index number>
  // Canvas and WebGL will pass along other values put here as options to the native methods
})

If you set an opts object on your component layer, those values will be available in the component under opts, e.g. { component: MyComponent, opts: { color: '#f0c' } }.

All of the container elements created by Layers functions are absolutely positioned and use any padding that is set. That way, they share the same coordinate system and can sit one on top of another.

cake.svgLayers(components: Array[, opts: Object])

Creates a <svg> element containing one <g> wrapper element, which gets translated based on any padding.

For every component layer in the passed in array, a <g> element is created.

cake.htmlLayers(components: Array[, opts: Object])

Creates a <div> element.

For every component layer in the passed in array, a <div> element is created.

cake.canvasLayers(components: Array[, opts: Object])

Creates a <canvas> element.

Because canvas elements have no DOM representation for each layer, you get access to the canvas and the 2d context as data items in each layer component. These aren't store values since you could have multiple canvas elements in a cake.

This means you access the canvas and ctx using this.get() instead of this.store.get(). For example, here's how a scatterplot layer component would be implemented in canvas.

CanvasLayer.html
<script>
export default {
  oncreate () {
	const { canvas, ctx } = this.get();
	const { xGet, yGet, data } = this.store.get();

	data.forEach(d => {
	  ctx.beginPath();
	  ctx.arc(xGet(d), yGet(d), opts.r, 0, 2 * Math.PI, false);
	  ctx.fillStyle = opts.color;
	  ctx.fill();
	});
  }
};
</script>

Canvas layouts have more options for opts. Anything you set here (except for zIndex) will get passed as the second argument to canvas.getContext('2d', opts). See the Canvas docs for what options are possible.

cake.webglLayers(components: Array[, opts: Object])

Same as the canvas element except instead of ctx you have gl as a component-level data item, which is your webgl context. If webgl is not supported, gl will be null.

Same as in .canvasLayers, any options that you set on opts (except for zIndex) will get passed as the second argument to canvas.getContext('webgl', opts). See the Canvas docs for what options are possible.

cake.render(options: Object)

Instantiates your cake and layout groups. Returns an object { app, store } where app is the instantiated Svelte app and store is the cake's Svelte store. Any options you pass in are added as Svelte options, including hydrate.

Computed Store Properties edit this section

Some convenience functions and other internal properties are exposed to the user on the store in case they're handy.

activeKeys Array

A list of all the keys that have an accessor set.

['x', 'y', 'r']

box Object

A bounding box object of the target element with top, right, bottom, left, width and height numbers in pixels. Useful for creating tooltips.

domains Object

An object containing a key for every active key whose value is a two-value array representing the min and max values for that field in the data. This value could differ from the domain of your scale if you are manually setting a limit on your scale by setting any of the xDomain, yDomain or rDomain settings. This is used internally to set domain things but it's also useful as a reference if you want to toggle between an arbitrary domain and the measured extents of the data, such as in the small multiples example.

originalSettings Object

A shallow copy of the object passed in to new LayerCake(<settings>). This can be useful to refer to in some situations such as in the Cleveland Dot Plot example, which uses the x-accessor shorthand of providing a list of keys. This list of strings gets mapped to a list of accessor functions inside the layer component but we can reference that original list of strings to know which field we're in based on index, which gets passed to the color scale! Broadly, having access to this field can help you not repeat yourself in specifying things twice or in scenarios where Layer Cake is doing a transformation on that original value, like in accessors or domain inputs, and you want to know about the original value.

xGet(d: Object)

Often you want to get the x value from a row in your data and scale it like so: $xScale($x(d)). This function is shorthand for doing just that. Super handy!

Here's an example from a simple SVG line path generator:

computed: {
  path: ({ $data, $xGet, $yGet }) => {
	return 'M' + $data
	  .map(function (d, i) {
		return $xGet(d) + ',' + $yGet(d);
	  })
	  .join('L');
  }
}

yGet(d: Object)

Same as xGet but for the y scale.

rGet(d: Object)

Same as xGet but for the r scale.

Helper functions edit this section

Layer Cake exposes some commonly-used helper functions. If you don't use them, they will be tree-shaken so there's no added bloat!

newDiv(className: String[, styles: Object, parent: DOM Node])

Easily create new divs. If you pass a DOM node as the third argument it will attach it to that object.

Handy when creating small multiples. You can put your cake inside a loop and append a new target div for every chart.

import { default as Layercake, newDiv } from 'layercake';

const container = document.getElementById('container');

const styles = {
  position: 'relative'
};

const myDiv = newDiv('my-div', styles, container);

// Or attach yourself
const myDiv = container.appendChild(newDiv('my-div', styles));

With small multiples:

import {default as Layercake, newDiv} from 'layercake';
import AxisX from './components/AxisX.html'
import AxisY from './components/AxisY.html'
import Scatter from './components/Scatter.html'

const container = document.getElementById('container');

const styles = {
  position: 'relative',
  display: 'inline-block'
};

const datasets = [
  [data-1...],
  [data-2...],
  [data-3...]
];

datasets.forEach(data => {
  const target = newDiv('my-div', styles, container);

  const myCake = new LayerCake({
	target,
	data,
	x: 'myX',
	y: 'myY'
  })
	.svgLayers([
	  { component: AxisX },
	  { component: AxisY },
	  { component: Scatter }
	]);

  myCake.render();
});

flatten(data: Array)

Flatten an array one-level down. Handy for preparing data from stacked layouts whose extents can easily be calculated.

This data:

const data = [
  [{x: 0, y: 1}, {x: 1, y: 5}, {x: 2, y: 10}],
  [{x: 0, y: 10}, {x: 1, y: 15}, {x: 2, y: 20}]
];

Becomes this:

import { flatten } from 'layercake';

const flatData = flatten(data);
/*
  [{x: 0, y: 1}, {x: 1, y: 5}, {x: 2, y: 10},
   {x: 0, y: 10}, {x: 1, y: 15}, {x: 2, y: 20}]
*/

You can safely use this function on arrays of arrays of arrays, such as the output from d3.stack()

[
  [[   0, 3840], [   0, 1600], [   0,  640], [   0,  320]],
  [[3840, 5760], [1600, 3040], [ 640, 1600], [ 320,  800]],
  [[5760, 6720], [3040, 4000], [1600, 2240], [ 800, 1440]],
  [[6720, 7120], [4000, 4400], [2240, 2640], [1440, 1840]]
]

Becomes...

[ [ 0, 3840 ],
  [ 0, 1600 ],
  [ 0, 640 ],
  [ 0, 320 ],
  [ 3840, 5760 ],
  [ 1600, 3040 ],
  [ 640, 1600 ],
  [ 320, 800 ],
  [ 5760, 6720 ],
  [ 3040, 4000 ],
  [ 1600, 2240 ],
  [ 800, 1440 ],
  [ 6720, 7120 ],
  [ 4000, 4400 ],
  [ 2240, 2640 ],
  [ 1440, 1840 ]
]

scaleCanvas(canvas: DOM Node, ctx: Canvas Context, width: Number, height: Number)

Scale your canvas size to retina screens. This function will modify the canvas, if necessary, and return an object with the new width and height as properties.

Such as in the Scatter canvas example:

Scatter.html
<script>
import { scaleCanvas } from 'layercake';

export default {
  onstate () {
	const { canvas, ctx, opts } = this.get();
	const { width, height, xGet, yGet, data, custom } = this.store.get();

	scaleCanvas(canvas, ctx, width, height);
	ctx.clearRect(0, 0, width, height);

	data.forEach(d => {
	  ctx.beginPath();
	  ctx.arc(xGet(d), yGet(d), custom.r, 0, 2 * Math.PI, false);
	  ctx.fillStyle = opts.color;
	  ctx.fill();
	});
  }
};
</script>

calcExtents(flatData: Array, fields: Array)

Calculate the extents of any of the keys specified in fields, which is an array of objects with field and accessor keys, representing the field name (x, y, r) and an accessor function.

For example, calculating the extents for the x and y fields, which are in the data as myX and myY would look like this:

const extents = calcExtents(flatData, [
  {field: 'x', accessor: d => d.myX },
  {field: 'y', accessor: d => d.myY }
]);

console.log(extents);
/*
{
  x: [0, 10],
  y: [-20, 20]
}
*/

Returns an object whose keys are the field names specified as the first item in the key group array followed by an array of [min, max].

You can also return an array if you have an object with keys where each one is more logically associated with the min or the max like this:

const timeData = [{start: 0, end: 1}, {start: -10000, end: 0}];

const extents = calcExtents(timeData, [
  { field: 'y', accessor: d => [d.start, d.end] }
]);

console.log(extents);
/*
{
  y: [-10000, 1]
}
*/

uniques(data: Array[, accessor: String|Function, transform: Boolean])

A function get get the unique values from a list. If accessor is a string or a function, the uniqueness will be compared using that and, be default, the values in the returned list will be the ones returned by the accessor. Pass false to the transform argument if you want to return the original elements, which will be the first one that appears for every unique value.

This is different from Underscore's uniq because that function doesn't return the transformed value.

import { uniques } from 'layercake';

const data = [
  { year: '1990', x: 0, y: 1},
  { year: '1990', x: 5, y: 4},
  { year: '1991', x: 2, y: 5},
  { year: '1991', x: 6, y: 1},
  { year: '1992', x: 1, y: 6},
  { year: '1992', x: 7, y: 3},
  { year: '1993', x: 7, y: 8},
  { year: '1993', x: 3, y: 2}
];

const uniqueYears = unique(data, 'year');
// ['1990', '1991', '1992', '1993']

// this is equivalent to
const uniqueYears = unique(data, d => d.year);

// setting transform to `false` gives you the full row of the first unique element
const uniqueYears = unique(data, 'year', false);
/*
[
  {year: '1990', x: 0, y: 1},
  {year: '1991', x: 2, y: 5},
  {year: '1992', x: 1, y: 6},
  {year: '1993', x: 7, y: 8}
*/