pnut
Flexible chart building blocks for React. (Somewhere between d3 and a charting library)
Basics
To render a chart you need three parts:
- A Series for your data
- Some Scales
- Components to render
import {Chart, Line, SingleSeries, ContinuousScale, ColorScale, Axis, layout} from './src/index';
function SavingsOverTime() {
const data = [
{day: 1, savings: 0},
{day: 2, savings: 10},
{day: 3, savings: 20},
{day: 4, savings: 15},
{day: 5, savings: 200}
];
const series = SingleSeries({data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'savings', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'savings', set: ['#ee4400']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Line scales={scales} strokeWidth="2" />
</Chart>;
}
Design Choices
Pnut chooses to require data that would match rows from an SQL query. If you have pivoted data you will need to flatten it.
[
{value: 10, type: 'apples'},
{value: 20, type: 'oranges'},
]
[
{apples: 10, oranges: 20}
]
API
Series
The first step in building a chart with pnut is to build a series object. The series defines how to group your data ready for rendering in an x/y plane. Under the hood it holds your data a two dimensional array of groups and points.
Grouped Series
Grouped series are used for things like multi line charts and stacked areas. One group per line and one point to match each x axis item.
const data = [
{day: 1, type: 'apples', value: 0},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200},
{day: 1, type: 'oranges', value: 200},
{day: 2, type: 'oranges', value: 50},
{day: 3, type: 'oranges', value: 30},
{day: 4, type: 'oranges', value: 24},
{day: 5, type: 'oranges', value: 150}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
Single Series
A single series is just like group but there is only one group.
const data = [
{day: 1, type: 'apples', value: 0},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200}
];
const series = new SingleSeries({data});
Scales
Scales take your series and create functions that convert your data points to something that can be rendered.
A classic example of this is converting your data points to a set of x/y coordinates. Each chart renderable will require specific set of scales in order to render. Each scale can be continuous, categorical or color.p
For example:
- A line chart needs a continuous x scale, a continuous y scale, and a color scale.
- A column chart needs a categorical x scale, a continuous y scale, and a color scale.
- A bubble chart needs a continuous scale for x,y and radius, and a color scale.
Continuous Scale
Continuous scales are for dimensions like numbers and dates, where the value is infinitely dividable.
type ContinuousScaleConfig = {
series: Series,
key: string,
range: [number, number]
zero?: boolean,
clamp?: boolean
};
const y = ContinuousScale({series, key: 'value', range: layout.yRange, zero: true});
const x = ContinuousScale({series, key: 'date', range: layout.xRange});
Categorical Scale
Categorical scales are for dimensions where the values cannot be infinitely divided. Things like name, type, or favourite color.
Dates can also be categorical but usually require some formatting to render properly.
type CategoricalScaleConfig = {
series: Series,
key: string,
padding?: number,
range: [number, number]
};
const x = ContinuousScale({series, key: 'favouriteColor', range: layout.xRange, padding: 0.1});
Color Scale
Color scales let you change the colors of your charts based on different attributes of your data.
There are four types:
- Key - Use the color from a data point
- Set - Assign a specific color palette to each distinct item in the data. This should pair with a CategoricalScale.
- Range - Assign a range of colors and interpolate between them based on a continuous metric. This should pair with a ContinuousScale.
- Interpolated - Take control of the colors by providing your own interpolator.
interpolate
is given a scaled value from 0 to 1.
const key = ColorScale({series, key: 'myColor'});
const set = ColorScale({series, key: 'type', set: ['red', 'green', 'blue']});
const range = ColorScale({series, key: 'age', range: ['#ccc', 'red']});
const interpolated = ColorScale({series, key: 'type', interpolate: type => {
return type >= 0.5 ? 'red' : '#ccc';
});
Layout
Because SVG uses a coordinate system originating from the top left the layout function is used to calculate the required widths, padding and flip the y axis.
type layout = (LayoutConfig) => LayoutReturn;
type LayoutConfig = {
width: number,
height: number,
top?: number,
bottom?: number,
left?: number,
right?: number
};
type LayoutReturn = {
width: number,
height: number,
padding: {
top: number,
bottom: number,
left: number,
right: number
},
xRange: [number, number],
yRange: [number, number],
};
import {layout} from 'pnut';
const ll = layout({width: 1280, height: 720, top: 32, bottom: 32, left: 32, right: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'savings', range: ll.yRange, zero: true});
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Line scales={scales} strokeWidth="2" />
</Chart>;
Renderables
Axis
Render axis based on your series
type Props = {
position: Position,
scales: {
series: Series,
x: ContinuousScale|CategoricalScale,
y: ContinuousScale|CategoricalScale
},
location?: number | string | Date,
strokeWidth: number,
strokeColor: string,
textColor: string,
textSize: number,
textOffset: number,
textFormat: (mixed) => string,
ticks: Function,
tickLength: number,
renderText?: Function,
renderAxisLine?: Function,
renderTickLine?: Function
};
Chart
Chart wraps your renderables in an svg tag and applies widths and padding.
type Props = {
children: Node,
height: number,
padding?: {top?: number, bottom?: number, left?: number, right?: number},
style?: Object,
width: number
};
Column
Render a column for each point in your series
type Props = {
scales: {
x: CategoricalScale,
y: ContinuousScale,
color: ColorScale,
series: Series
},
strokeWidth?: string,
stroke?: string,
renderPoint?: Function
};
Interaction
Get information about the closest data points relative to the mouse position.
type Props = {
scales: {
series: Series,
x: ContinuousScale|CategoricalScale,
y: ContinuousScale|CategoricalScale
},
height: number,
width: number,
fps?: number,
children: (InteractionData<A>) => Node
onClick?: (InteractionData<A>) => void,
onChange?: (InteractionData<A>) => void,
};
type InteractionData<A> = {
nearestPoint: A,
nearestPointStepped: A,
xPoints: Array<A>,
position: {
x: number,
y: number,
pageX: number,
pageY: number,
clientX: number,
clientY: number,
screenX: number,
screenY: number,
elementWidth: number,
elementHeight: number,
isOver: boolean,
isDown: boolean
},
};
Line
Render a set of lines for each group in your series
type Props = {
scales: {
x: ContinuousScale,
y: ContinuousScale,
color: ColorScale,
series: Series
},
area?: boolean,
curve?: Function,
strokeWidth?: string,
renderGroup?: Function
};
Scatter
Render a series of circles at each data point in your series
type Props = {
scales: {
x: ContinuousScale,
y: ContinuousScale,
radius: ContinuousScale,
color: CategoricalScale,
series: Series
},
strokeColor?: string,
strokeWidth?: string,
renderPoint?: Function
};
Examples
Line
import {SingleSeries, ContinuousScale, ColorScale, Axis, Line, layout} from 'pnut';
function SavingsOverTime() {
const {data} = props;
const series = new SingleSeries({data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'savings', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'savings', set: ['red']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Line scales={scales} strokeWidth="2" />
</Chart>;
}
Multi Line
import {Chart, Line, Series, ContinuousScale, ColorScale, Axis, layout} from './src/index';
function MultiLine() {
const data = [
{day: 1, type: 'apples', value: 0},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200},
{day: 1, type: 'oranges', value: 200},
{day: 2, type: 'oranges', value: 50},
{day: 3, type: 'oranges', value: 30},
{day: 4, type: 'oranges', value: 24},
{day: 5, type: 'oranges', value: 150}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'value', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'type', set: ['red', 'orange']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Line scales={scales} strokeWidth="2" />
</Chart>;
}
Stacked Area
import {Chart, Line, Series, ContinuousScale, ColorScale, Axis, layout, stack} from './src/index';
function StackedArea() {
const data = [
{day: 1, type: 'apples', value: 0},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200},
{day: 1, type: 'oranges', value: 200},
{day: 2, type: 'oranges', value: 50},
{day: 3, type: 'oranges', value: 30},
{day: 4, type: 'oranges', value: 24},
{day: 5, type: 'oranges', value: 150}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
.update(stack({key: 'value'}));
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'value', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'type', set: ['red', 'orange']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Line area={true} scales={scales} strokeWidth="2" />
</Chart>;
}
Column
import {Chart, Column, SingleSeries, CategoricalScale, ContinuousScale, ColorScale, Axis, layout} from './src/index';
function ColumnChart() {
const data = [
{fruit: 'apple', count: 20},
{fruit: 'pears', count: 10},
{fruit: 'strawberry', count: 30}
];
const series = new SingleSeries({data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = CategoricalScale({series, key: 'fruit', range: ll.xRange, padding: 0.1});
const y = ContinuousScale({series, key: 'count', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'fruit', set: ['red', 'green', 'blue']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Column scales={scales} />
</Chart>;
}
Stacked Column
import {Chart, Column, Series, CategoricalScale, ContinuousScale, ColorScale, Axis, layout, stack} from './src/index';
function StackedColumn() {
const data = [
{day: 1, type: 'apples', value: 10},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200},
{day: 1, type: 'oranges', value: 200},
{day: 2, type: 'oranges', value: 50},
{day: 3, type: 'oranges', value: 30},
{day: 4, type: 'oranges', value: 24},
{day: 5, type: 'oranges', value: 150}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
.update(stack({key: 'value'}));
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = CategoricalScale({series, key: 'day', range: ll.xRange, padding: 0.1});
const y = ContinuousScale({series, key: 'value', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'type', set: ['red', 'green']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Column scales={scales} />
</Chart>;
}
Grouped Column
import {Chart, Column, Series, CategoricalScale, ContinuousScale, ColorScale, Axis, layout} from './src/index';
function GroupedColumn() {
const data = [
{day: 1, type: 'apples', value: 10},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200},
{day: 1, type: 'oranges', value: 200},
{day: 2, type: 'oranges', value: 50},
{day: 3, type: 'oranges', value: 30},
{day: 4, type: 'oranges', value: 24},
{day: 5, type: 'oranges', value: 150}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = CategoricalScale({series, key: 'day', range: ll.xRange, padding: 0.1});
const y = ContinuousScale({series, key: 'value', range: ll.yRange, zero: true});
const color = ColorScale({series, key: 'type', set: ['red', 'green']});
const scales = {series, x, y, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Column scales={scales} />
</Chart>;
}
Scatter
import {Chart, Scatter, Series, ContinuousScale, ColorScale, Axis, layout} from './src/index';
function ScatterChart() {
const data = [
{day: 1, type: 'apples', value: 0},
{day: 2, type: 'apples', value: 10},
{day: 3, type: 'apples', value: 20},
{day: 4, type: 'apples', value: 15},
{day: 5, type: 'apples', value: 200},
{day: 1, type: 'oranges', value: 200},
{day: 2, type: 'oranges', value: 50},
{day: 3, type: 'oranges', value: 30},
{day: 4, type: 'oranges', value: 24},
{day: 5, type: 'oranges', value: 150}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'value', range: ll.yRange});
const radius = ContinuousScale({series, key: 'value', range: [2, 2]});
const color = ColorScale({series, key: 'type', set: ['red', 'orange']});
const scales = {series, x, y, radius, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Scatter scales={scales} strokeWidth="2" />
</Chart>;
}
Bubble
import {Chart, Scatter, Series, ContinuousScale, ColorScale, Axis, layout} from './src/index';
function BubbleChart() {
const data = [
{day: 1, size: 200, value: 0},
{day: 2, size: 800, value: 10},
{day: 3, size: 900, value: 20},
{day: 4, size: 200, value: 15},
{day: 5, size: 300, value: 200},
{day: 6, size: 400, value: 100},
{day: 7, size: 300, value: 20}
];
const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
const ll = layout({width: 400, height: 400, left: 32, bottom: 32});
const x = ContinuousScale({series, key: 'day', range: ll.xRange});
const y = ContinuousScale({series, key: 'value', range: ll.yRange});
const radius = ContinuousScale({series, key: 'size', range: [2, 10]});
const color = ColorScale({series, key: 'type', set: ['orange']});
const scales = {series, x, y, radius, color};
return <Chart {...ll}>
<Axis scales={scales} position="left" />
<Axis scales={scales} position="bottom" />
<Scatter scales={scales} strokeWidth="2" />
</Chart>;
}
Todo
- Bar
- Pie
- Histogram
- BinnedSeries