Coder Social home page Coder Social logo

pnut's Introduction

pnut

Flexible chart building blocks for React. (Somewhere between d3 and a charting library)

Basics

To render a chart you need three parts:

  1. A Series for your data
  2. Some Scales
  3. 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}
    ];

    // Define our series with day as the primary dimension
    const series = SingleSeries({data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x, y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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.

// good
[
    {value: 10, type: 'apples'},
    {value: 20, type: 'oranges'},
]
// bad
[
    {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, // A series object
    key: string, // Which key on your data points
    range: [number, number] // The min and max this scale should map to. (Often layout.yRange)
    zero?: boolean, // Force the scale to start at zero
    clamp?: boolean // Clamp values outside the series to min and max
};

// Examples
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, // A series object
    key: string, // Which key on your data points
    padding?: number, // How much space to place between categories (Only needs for column charts)
    range: [number, number] // The min and max this scale should map to. (Often layout.xRange)
};

// Examples
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.
// Get the color from `point.myColor`
const key = ColorScale({series, key: 'myColor'});


// Assign either red, green or blue based on `point.type`
const set = ColorScale({series, key: 'type', set: ['red', 'green', 'blue']});

// Blend age values from grey to red as they get older
const range = ColorScale({series, key: 'age', range: ['#ccc', 'red']});


// Apply custom interpolation to make the top half of values 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, // Original width - left and right
    height: number, // Original height - top and bottom
    padding: {
        top: number,
        bottom: number,
        left: number,
        right: number
    },
    xRange: [number, number], // tuple from 0 to processed width
    yRange: [number, number], // flipped tuple from processed height to zero
};

// example
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 = {
    // required
    position: Position,
    scales: {
        series: Series,
        x: ContinuousScale|CategoricalScale,
        y: ContinuousScale|CategoricalScale
    },

    // optional
    location?: number | string | Date,

    // style
    strokeWidth: number,
    strokeColor: string,
    textColor: string,
    textSize: number,
    textOffset: number,
    textFormat: (mixed) => string,
    ticks: Function,
    tickLength: number,

    // component
    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 = {
    // Required scales. Must have a categorical x and a continuous y
    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
    },

    // The height and width from your layout
    height: number,
    width: number,

    // How many times per second to update changes and call props.children or props.onChange
    fps?: number,

    // A render function that is given the current InteractionData.
    children: (InteractionData<A>) => Node

    // A callback fired when the user clicks on the chart somewhere.
    onClick?: (InteractionData<A>) => void,

    // A callback that is fired when the user moves their mouse.
    onChange?: (InteractionData<A>) => void,
};

type InteractionData<A> = {
    // The nearest point in the series to the mouse
    nearestPoint: A,
    
    // For instances like area or column charts the nearest point is not always 
    // the point your mouse is over. Nearest point stepped has a larger threshold 
    // before changing to the next point.
    nearestPointStepped: A,

    // An array of points that are close on the x axis to the mouse.
    // Useful for stacked charts to show all values in a tooltip.
    xPoints: Array<A>,

    // Details on the current mouse position
    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
    },

    // Render line as an area chart
    area?: boolean,

    // A function that returns the chosen d3 curve generator
    curve?: Function,

    // Set the width of the line that is drawn
    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
    },

    // Set the outline width and color of each circle
    strokeColor?: string,
    strokeWidth?: string,

    renderPoint?: Function
};

Examples

Line

import {SingleSeries, ContinuousScale, ColorScale, Axis, Line, layout} from 'pnut';

function SavingsOverTime() {
    const {data} = props;
        
    // Define our series with day as the primary dimension
    const series = new SingleSeries({data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x, y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with type as the grouping and day as the primary dimension
    const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x, y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with type as the grouping and day as the primary dimension
    const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
        .update(stack({key: 'value'})); // stack savings metric

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x, y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with fruit as the primary dimension
    const series = new SingleSeries({data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x,y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with day as the primary dimension
    const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});
        .update(stack({key: 'value'})); // stack savings metric

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x,y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with day as the primary dimension
    const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x,y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with day as the primary dimension
    const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x, y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, radius, color};

    // render a chart with two axis and a line
    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}
    ];

    // Define our series with type as the grouping and day as the primary dimension
    const series = new GroupedSeries({groupKey: 'type', pointKey: 'day', data});

    // calculate chart width, height and padding
    const ll = layout({width: 400, height: 400, left: 32, bottom: 32});

    // Set up scales to define our x, y and color
    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']});


    // create a scales object for each of our renderable components
    const scales = {series, x, y, radius, color};

    // render a chart with two axis and a line
    return <Chart {...ll}>
        <Axis scales={scales} position="left" />
        <Axis scales={scales} position="bottom" />
        <Scatter scales={scales} strokeWidth="2" />
    </Chart>;
}

Todo

  • Bar
  • Pie
  • Histogram
  • BinnedSeries

pnut's People

Contributors

allanhortle avatar blakehowe avatar dancoates avatar dependabot[bot] avatar dxinteractive avatar mriccid avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

pnut's Issues

Look at whether files are building correctly

It looks like there are calls to babel-runtime in the lib files which probably means that babel-runtime is required for the library to work. We aren't using any features that require babel-runtime so might need to update the babel config.

Animation discussion

@dxinteractive commented on Wed Jan 11 2017

Animation approaches

SVG animation / CSS transitions

  • Generally more smooth and render performant
  • Easier to implement
  • Without care they can tween data points to other unrelated data points. Data points need to be uniquely identifiable by the animator to tween correctly, and if that can be achieved then the tweens will look correct. Windowed pans through data should not be confused with interpolation of value changes.
  • Can't be used as-is to render a static representation of the transition or interpolated states (such as onion skins, or viewing the "4.5th" data point etc.) as the easing is outside of the component. Can be overcome with algorithm duplication.
  • Can't cope with dynamically changing transition speeds, such as scrubbing the playhead
  • Depending on the method used, make sure that animations can immediately override each other without jumping

Javascript / component based interpolation and re-rendering

  • Potentially a lot less render performant unless care is taken
  • Tweening data points correctly is easier as the animator can more easily receive data about which point is which by using some kind of unique id. There still exists the problem of ensuring there is rich enough data to be able to uniquely identify data points in the first place, and to cope with the transition to when those points no longer exist, hereby known as the "disappearing data point" problem.
  • More control over tween algorithms (i.e. can do any kind of interpolation, including all from D3, and even non-keyframe based interpolations like exponential moving averages if we want)
  • Ability to render a static representation of the transition and interpolated states (such as onion skins, or viewing the "4.5th" data point etc.) as the easing and playhead position is known to the component.
  • Can cope with dynamically changing transition speeds, such as scrubbing the playhead

The "disappearing data point" problem

  • Data in a data set that has info over time must be rich enough to be able to uniquely identify every data point, so that they can be interpolated correctly. Using the position of each data point in the array is not a valid method as data points may pop in and out of existence. Reason for that happening:
    1. The data set may omitting values that don't exist
    2. The view applying windowing / panning / zooming
  • Given this, there is still the issue of how to "tween away" data points that disappear over time. Where do they go and how do they animate there? Or do they just disappear?
    1. Possibly can have the class for data-over-time analyze the data set and zero-fill missing values
    2. This can be compensated for in the component, as it'll have knowledge of view limits and can therefore has the data required to calculate which data point is which, if the data set is rich enough.
  • ... more to come

Add scale() and unlerp()

scale(columns: string|array<string>, inputMin: number, inputMax: number, outputMin: number, outputMax: number): ChartData

unlerp(min: number, max: number, value: number): number

ChartData to support date objects as values

const rows = [
    {
        month: "2014-01-01",
        supply: 123605,
        demand: 28
    },
    {
        month: "2014-02-01",
        supply: 457959,
        demand: 72
    },
    {
        month: "2014-03-01",
        supply: 543558,
        demand: 96
    },
    {
        month: "2014-04-01",
        supply: 657625,
        demand: 107
    },
    {
        month: "2014-05-01",
        supply: 724687,
        demand: 116
    },
    {
        month: "2014-06-01",
        supply: 577673,
        demand: 93
    },
    {
        month: "2014-07-01",
        supply: 510476,
        demand: 85
    },
    {
        month: "2014-08-01",
        supply: 587977,
        demand: 104
    },
    {
        month: "2014-09-01",
        supply: 589351,
        demand: 121
    },
    {
        month: "2014-10-01",
        supply: 557710,
        demand: 138
    },
    {
        month: "2014-11-01",
        supply: 550750,
        demand: 139
    },
    {
        month: "2014-12-01",
        supply: 240661,
        demand: 95
    },
    {
        month: "2015-01-01",
        supply: 278804,
        demand: 87
    },
    {
        month: "2015-02-01",
        supply: 785962,
        demand: 141
    },
    {
        month: "2015-03-01",
        supply: 713841,
        demand: 129
    },
    {
        month: "2015-04-01",
        supply: 681580,
        demand: 132
    },
    {
        month: "2015-05-01",
        supply: 930395,
        demand: 139
    },
    {
        month: "2015-06-01",
        supply: 937566,
        demand: 109
    },
    {
        month: "2015-07-01",
        supply: 1011621,
        demand: 126
    },
    {
        month: "2015-08-01",
        supply: 1638135,
        demand: 154
    },
    {
        month: "2015-09-01",
        supply: 1209174,
        demand: 138
    },
    {
        month: "2015-10-01",
        supply: 1060541,
        demand: 137
    },
    {
        month: "2015-11-01",
        supply: 1236615,
        demand: 170
    },
    {
        month: "2015-12-01",
        supply: 629503,
        demand: 125
    },
    {
        month: "2016-01-01",
        supply: 678891,
        demand: 109
    },
    {
        month: "2016-02-01",
        supply: 1681174,
        demand: 163
    },
    {
        month: "2016-03-01",
        supply: 1209983,
        demand: 140
    },
    {
        month: "2016-04-01",
        supply: 1380393,
        demand: 149
    },
    {
        month: "2016-05-01",
        supply: 1267107,
        demand: 151
    },
    {
        month: "2016-06-01",
        supply: 1371218,
        demand: 154
    },
    {
        month: "2016-07-01",
        supply: 1652395,
        demand: 160
    },
    {
        month: "2016-08-01",
        supply: 1561521,
        demand: 181
    },
    {
        month: "2016-09-01",
        supply: 1896226,
        demand: 218
    },
    {
        month: "2016-10-01",
        supply: 1810362,
        demand: 227
    },
    {
        month: "2016-11-01",
        supply: 1877047,
        demand: 247
    },
    {
        month: "2016-12-01",
        supply: 770154,
        demand: 204
    }
];

Also possibly define "type" in columns?

Chart type support tracker

Below is a list of charts that pnut should be able to handle. We don't need to build all of these right now but do need to consider how the pnut API will support them. If you come across any others, comment below and I'll add it to the list.

New charting specs

@dxinteractive commented on Wed Nov 23 2016

Some examples to get your mind limber:
https://docs.google.com/spreadsheets/d/1mDViacD51PPNYiwZKCY5xavfKao7OxaLu2j_ZljsGvM/edit?usp=sharing

Proposed chart data stucture

To be turned into an Immutable record. Example:

{
	// provides knowledge of keys, associated labels, and implied order
	// if data values are numbers, this data is continuous, if not then its discrete

	columns: [
		{
			key: 'day',
			label: 'Day'
		{
			key: 'supply',
			label: 'Supply (houses)'
		},
		{
			key: 'demand',
			label: 'Demand (houses)'
		},
		{
			key: 'fruit',
			label: 'Random fruit'
		}
	],
	data: [
		{
			day: 1,
			supply: 34,
			demand: 99,
			fruit: "apple"
		},
		{
			day: 2,
			supply: 32,
			demand: 88,
			fruit: "apple"
		},
		{
			day: 3,
			supply: 13,
			demand: 55,
			fruit: "orange"
		},
		{
			day: 8,
			supply: 22,
			demand: 56,
			fruit: "peach"
		},
		{
			day: 19
			supply:  12
			demand:  4,
			fruit: "pear"
		}
	]
}

Possible methods

min(key) // returns minimum value of `key` in collection
max(key) // returns minimum value of `key` in collection
sum(key) // returns sum of values of `key` in collection
isContinuous(key) // returns true if the data is continuous.
// which can be determined by checking if the value of the given on the first valid non-null item in the data collection is a Number, where a valid item is a number, string or null. If false, the data is discrete

Methods that return the data point with the max or min of a value (as opposed to the max or min value) are not yet scoped, as the result may be more than one data point.

The idea of 'primary' columns / dimensions should not be reflected in the data structure,
its up to the charts to find out which column to use as primary based on their props.

Charts and Props

All charts will accept data, a chartdata object.

Line / area

  • data
  • x: string (primary column)
  • y: string|string[] or lines: string|string[] (value columns)
  • Plane props

Scatter / Bubble

(has no primary column as such, just has a point on the chart for every data point)

  • data
  • x: string (value column)
  • y: string (value column)
  • size: string (value column)
  • color: string (value column)
  • Plane props

Column

  • data
  • x (primary column)
  • y: string|string[] or columns: string|string[] (value columns)
  • Plane props

Bar

  • data
  • y (primary column)
  • x: string|string[] or bars: string|string[] (value columns)
  • Plane props

Pie / Strudel / Doughnut

  • data
  • slices: string (primary column)
  • size: string (value column)

"Plane"

Kinf of hocky thing that knows about the concept of a plane. Displays two spatial axis. Both axis are treated as continuous, and work best when given continuous data for both domain and range.

  • If non-continuous data is provided to the primary axis (the domain, usually the x axis then it must treat these as equally spaced. Think of a line chart with "red", "green", "blue" along its x axis. This situation usually means you should probably be using a bar chart, but it can happen.
  • If non-continuous data is provided to the secondary axis (the range, usually the y axis) then the charts behaviour is undefined. This is like trying to chart the colour of someones shirt over time using a line chart, it doesn't make any sense.

Abilities and commonalities

  • They may have grid lines and/or ticks on the domain and/or the range of the chart.
  • They may have the potential ability to zoom on the domain and/or the range?
  • Can have arbitrary shapes drawn on the plane, like a vertical line or benchmark line.
  • Scales? Like exp, log, sqrt etc.

Plane props

Automatically calculate these from the data when not explicitly passed in.

  • rangeMin
  • rangeMax
  • domainMin
  • domainMax

e.g. the values in the data that would place a point on any edge of the chart.

Special case Plane with only 1 continuous dimensions, like Bar and Column charts!

Displays a range, but there is no actual domain as the data on that axis is treated as discrete. It's ideal for displaying non-continuous data on the primary axis, and continuous data on the secondary axis.

  • If continuous data is provided to the primary axis (domain) then it must treat these as ordered and discrete. Pretty much just use integers (0, 1, 2, 3). The chart will have to pass numbers down to its renderer anyway, so you'll probably just need to use array indexes for positioning in all cases, and set the minDomain and maxDomain accordingly.

  • If non-continuous data is provided to the secondary (range) axis then the charts behaviour is undefined. This is like someone trying to chart "green" number of hats. It's dumb.

Warning!

Bar and column charts will render the same data set horizontally or vertically! This means that the x and y coords in the data could end up being vertical or horizontal after rendering.

Abilities and commonalities

  • They may have grid lines and/or ticks on the range of the chart.
  • They may have the potential ability to zoom on the range?

Desserts

Pies, strudels and doughnuts do not display on a plane.

  • If continuous data is provided to the primary axis (the column that contains the 'slices') then it must treat these as discrete (just use toString()).
  • If discrete data is provided to the secondary (range) axis then the charts behaviour is undefined.
  • These can only show one non-primary value at a time.

Example component hierarchy

<Line>
    Plane2D( // and / or Animation() or other hocky stuff
        <Chart>
            <LineCanvas />
            <AxisX />
            <AxisY />
        </Chart>
    )
</Line>

@dxinteractive commented on Mon Nov 28 2016

We'll need to decide on Option 1 or 2 for the chart data structure. I'm thinking that option 1 is more extensible.


@dancoates commented on Mon Nov 28 2016

Yeah I definitely prefer option 1

Work on consistency of renderer props

There is some inconsistency at the moment as to whether a renderer accepts configuration of rendered elements by the user passing an object, function, or component. If possible and if it doesn't introduce undue extra complexity for the end user it would be good to make these consistent.

Precalculate scale data better.

groups {
    x: {
        x: {
            time: scale, 
            year: scale,
            scale
        },
        fooBar: {
            time: scale, 
            year: scale,
            scale
        }
    },
    y: {
        y: {
            supply: scale,
            demand: scale,
            scale: scale
        }
    }
}

Use this data structure and calculate scales and data before ititally. Then when looping through children check if they redefine the scale and if true calculate just that scale again.

Build interpolation into Chart components by default (phase 2)

Say if we set a frame prop on a Chart, the Chart then internally uses chartData.frameAtIndexInterpolated() and its results are used for rendering values. However, default scales should continue to use the original chartData object so that the results of min and max are done on the whole data set and not just the current frame in time.

frameAtIndexInterpolated() accepts 3 args, which we should be able to get like:

  • frameColumn corresponds to a Chart's timeColumn or something like that
  • primaryColumn should be able to be automatic, hopefully, because most chart types intrinsically has a primary column (Line / column = x, bar = y, scatter doesn't have one so will need to be specified by the user somehow)
  • index can just use Chart's frame prop

Chart memoization might have a memory leak?

Not sure about this but just leaving this here as a reminder to check it out. The memoization cache in chart is created in the constructor and isn't cleared which might mean that the memos object grows very large over time.

Data Viz Exploration

Can we use pnut to render less row/column type data?

Bellcurves
SineSquare Waves
Trendlines

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.