/ Chart

Preact based charting library written with d3-maths

Preact based charting library written with d3-maths

preact-charts

preact based charting library. Written with d3-maths, and TypeScript!

Install via npm using: npm install preact-charts.

The goal of this project is to be as light as possible adding some easy chart support to the preact ecosystem.
I have tried my best to limit the required dependencies to only the sections of d3 that are needed, while also not attempting to replicate already implemented code.

I hope that this library of components will be as developer friendly as possible, including easy ways for developers to style the charts as they wish.

The below documentation will include some TypeScript syntax. If you are new to TypeScript or don't use it, optional props are declared with a ?:.

Features / RoadMap

Version 1.0

  • [x] Basic charting.
  • [x] Implemented css-in-js solution
  • [x] Switch from typestyle to goober
  • [ ] Implement build using microbundle
  • [ ] Transition from hard-typed colour array to procedurally generated colours
  • [ ] Further componentize charts to prevent code duplication

Current Charts

Difference Chart

Will display a vertical bar chart with a y-axis that is centered at 0 on the x-axis.

Props:

interface DiffBarProps {
    name: string;
    data: Array<{name: string, value: number}>;
    height?: number; // default 800
    width?: number; // default 600
    margin?: Margin; // default {top: 25, right: 25, bottom: 50, left: 25}
    ticks?: number; // default 8
}

Usage:

import { h, Component } from 'preact';
import { DifferenceChart } from 'preact-charts';
const data: Array<{name: string, value: number}> = [...];
...
<DifferenceChart name='differenceChart' data={data}/>

Grouped Bar Chart

Props:

interface GroupedBarProps {
    name: string;
    data: GroupedDataObject;
    groups: string[];
    legendReference: {[key: string]: string};
    height?: number; // default 500
    width?: number; // default 500
    margin?: Margin; // default {top: 25, right: 25, bottom: 75, left: 50}
    ticks?: number; // default 6
}

Where:

interface GroupedDataObject {
    [key: string]: Array<{name: number | string, value: number}>;
}

Prop legendReference is an object where the keys are the same as the keys of the input data. The value is what will be displayed on the chart legend.

Usage:

import { h, Component } from 'preact';
import { GroupedBar, GroupedDataObject } from 'preact-charts';
const data: GroupedDataObject = {...};
...
<GroupedBar name='groupedBar1' data={data} groups={Object.keys(data)} legendReference={chartLegendReference}/>

Horizontal Bar Chart

This chart is the same as the GroupedBar, but is displayed with horizontal bars.

Props:

interface HorizontalBarProps {
    name: string;
    data: GroupedDataObject;
    groups: string[];
    legendReference: {[key: string]: string};

    height?: number; // default 800
    width?: number; // default 600
    margin?: Margin; // default {top: 25, right: 25, bottom: 25, left: 150}
    ticks?: number; // default 6
}

Usage:

import { h, Component } from 'preact';
import { HorizontalBar, GroupedDataObject } from 'preact-charts';
const data: GroupedDataObject = {...};
...
<HorizontalBar name='hozBar' data={data} groups={Object.keys(data)} legendReference={chartLegendReference}/>

Histogram

Props:

interface HistogramProps {
    name: string;
    x: string;
    data: DataArray;
    height?: number; // default 250
    width?: number; // default 350
    margin?: Margin; // default {top: 25, right: 25, bottom: 75, left: 50}
    ticks?: number; // default 8
    barColour?: string; // default steelblue
    barOutline?: string; // default black
}

x should be a key in data. Will display a histogram of the data with that key.

Usage:

import { h, Component } from 'preact';
import { Histogram, DataArray } from 'preact-charts';
const data: DataArray = [...];
...
<Histogram name='histogram' x={Object.keys(data[0])[0]} data={data}/>

LineScatter

Props:

interface LineScatterProps {
    name: string;
    x: string;
    y: string;
    data: DataArray[];
    height?: number; // default 500
    width?: number; // default 500
    margin?: Margin; // default {top: 25, right: 25, bottom: 75, left: 75}
    radius?: number; // default 5
    labels?: boolean; // default false
    legendReference?: string[];
}

Input data is an array of data arrays. Each inner array will be displayed as a group of dots connected by a line.
legendReference is an array where legendReference[index] === data[index]. The index of legendReference relate to the index of the data.

LineScatter has a built in zoom functionality. Allowing user to select a portion of the chart and the axis will scale to that selection.
Double clicking the chart will cause the zoom to reset.

Usage:

import { h, Component } from 'preact';
import { LineScatter, DataArray } from 'preact-charts';
const data: DataArray[] = [...];
...
<LineScatter name='linescatter' data={data} x='dataKey1' y='dataKey2' labels={true} legendReference={legendRef}/>

ScatterPlot

Props:

interface ScatterPlotProps {
    name: string;
    x: string;
    y: string;
    data: DataArray;
    height?: number; // default 500
    width?: number; // default 500
    margin?: Margin; // default {top: 25, right: 25, bottom: 75, left: 75}
    radius?: number; // default 5
    labels?: boolean; // default false
    dotFill?: string; // default steelblue
    dotBorder?: string; // default whitesmoke
}

The radius of the displayed circles can be changed. labels is a boolean determining if axis labels should be presented.

ScatterPlot also has a built in zoom functionality. Allowing user to select a portion of the chart and the axis will scale to that selection.
Double clicking the chart will cause the zoom to reset.

Usage:

import { h, Component } from 'preact';
import { ScatterPlot, DataArray } from 'preact-charts';
const data: DataArray = [...];
...
<ScatterPlot name='scatter' data={data} x='dataKey1' y='dataKey2' labels={true}/>

RangeChart

Props:

interface RangeChartProps {
    name: string;
    y: string;
    data: TimestampArray;
    height?: number; // default 200
    width?: number; // default 1000
    margin?: Margin; // default {top: 25, right: 25, bottom: 75, left: 75}
    lineColour?: string; // default steelblue
    fillColour?: string; // default steelblue
    onBrush?: (extent: Date[]) => void;
    brushColour?: string; // default darkgoldenrod
}

Input data requires a key called timestamp which is a parsed date. onBrush is called when the chart has been brushed.
This allows for data filtering based on returned time extents. onBrush gets the argument of extent which is an array of length 2, which is [start, end]

Usage:

import { h, Component } from 'preact';
import { RangeChart, TimestampArray } from 'preact-charts';
const data: TimestampArray = [...];
...
<RangeChart name='rangechart' y={yKey} data={data} onBrush={this.handleRangeBrush}/>

this.handleRangeBrush = (extent: Date[]) => {
    const brushStart = extent[0];
    const brushEnd = extent[1];
    // do something with brush extents
}

TrendChart

Props:

interface TrendChartProps {
    name: string;
    x: string;
    y: string;
    data: TimestampArray;
    height?: number; // default 200
    width?: number; // default 1000
    margin?: Margin; // default {top: 25, right: 25, bottom: 75, left: 75}
    lineColour?: string; // default lightblue
    extent?: Date[];
    tooltip?: boolean; // default true
    axisControl?: boolean; // default true
    contolColour?: string; //default goldenrod
}

Allows you to pass an extent, as generated by the RangeChart onBrush to filter the presented data. Allows choice of x-axis key, which must still be a date.
Line colour is customizable.
Tooltip being on will display a circle with the related data displayed when hovering over the chart. The chosen point is the closet to the current mouse location.
Axis control being true will display plus and minus buttons at the top and bottom of the y-axis. Allowing the users to have control over the y-axis.

TrendChart is also allowed to have <Flag /> child components, which will be displayed on the chart and can be interacted with.

Usage:

import { h, Component } from 'preact';
import { TrendChart, Flag, TimestampArray } from 'preact-charts';
const data: TimestampArray = [...];
const flags: EventsData[] = [...];
...
<TrendChart name='trend1' data={data} y={yKey} x='timestamp' lineColour='cyan' extent={state.rangeExtent}>
    {
        // optional flag addition
        flags.map((flag) => <Flag isClicked={isFlagClicked} {...flag} onClick={this.handleFlagClick}/>)
    }
</TrendChart>

this.handleFlagClick = (e: number) => {
    // e is the flagID number
}

Other Components

Flag

Props:

interface FlagProps extends EventsData {
    onClick: (flagID: number) => void;
    isClicked: boolean;
}

These are the flag props required from the user, the rest are passed through in TrendChart.
onClick is a function when the flag is clicked.
The flags do not manage their own state and should be told when they should stay in the clicked state. Clicked flags will have a green topper.
Flags will be displayed as a rectangle with over the extent of [start, end]

Types Reference

export interface Margin {
    top: number;
    right: number;
    bottom: number;
    left: number;
}

export interface NumberObject {
    [key: string]: number;
}

export type DataArray = NumberObject[];

export interface GroupedDataObject {
    [key: string]: Array<{name: number | string, value: number}>;
}

export interface DataObject {
    [key: string]: number | string | Date;
}

export interface TimestampData extends DataObject {
    timestamp: Date;
}

export type TimestampArray = TimestampData[];

export interface EventsData {
    idx: number;
    event: number;
    start: string | Date;
    end: string | Date;
    type: string;
    sub_type: string | null;
    comment: string | null;
}

Features

This is a note on some features that are included:

I use resize observer to ensure that the charts will resize on window resize. This requires a browser that implements this feature or a polyfill.
This also means that the charts will always fill their parent container. So placing them in sized divs is important.

Chart names should be unique, they are used for things like chart clipPath IDs.

GitHub