A single chart component to rule them all.
Description • Motivation • Example • Documentation • Run Locally • Stack • Goals
For developers who want to build a group of powerful D3 charts faster, 1Chart is a Proof of Concept project that explores the possibility to simplify the creation of charts while following a consitent and simple pattern.
Unlike D3 alone, where you need to spend time dealing with its sometimes complex syntax and organization, this PoC focuses on building powerful charts through the use of schemas.
There are plenty of use cases for charts and D3 has been the go-to for many years. But somehow it still misses that development experience that we would like to create a great set of graphics. Moreover, D3 is a so convenient tool that it lets you do a lot of other things as well and that advantage sometimes left no easy control over UIs dedicated to pure graphs.
The idea behind this PoC is to ease the creation of charts following a single pattern while giving the consistency required to have multiple yet different graphs. You can think of 1Chart as tool that helps creating your custom Charts Library by using D3 as wrapper.
Let's say you have the next data:
[
{
"year": 2012,
"value": 557948
},
{
"year": 2013,
"value": 127633
},
{
"year": 2014,
"value": 835987
},
{
"year": 2015,
"value": 958813
},
{
"year": 2016,
"value": 1325000
},
{
"year": 2017,
"value": 653300
},
{
"year": 2018,
"value": 228088
},
{
"year": 2019,
"value": 1020893
},
{
"year": 2020,
"value": 335443
},
{
"year": 2021,
"value": 653300
}
]
We can tell 1Chart to build a "Vertical Bars" chart with this simple schema:
import { format } from "d3";
export default {
id: "vertical-bars",
title: "Vertical Bars",
subtitle: "This example only shows vertical bars",
values: {
year: {
scale: "band",
},
value: {
scale: "linear",
domain: [0, 1400000],
},
},
components: [
{
type: "background-lines",
value: "year",
},
{
type: "bottom-axis",
value: "year",
height: 13,
},
{
type: "left-axis",
value: "value",
format: (val: number) => `$${format(".2s")(val)}`,
width: 50,
},
{
type: "vertical-bars",
value: ["year", "value"],
borderRadius: "4 4 0 0",
barWidth: 20,
},
],
};
Those values
come from your data naming so you're telling 1Chart to generate
scales for them.
All we need to do then is to pass both data and schema to the component:
import React from "react";
import schema from "../../examples/DevelopmentTimeline/DevelopmentTimeline.schema.ts";
import data from "../../examples/DevelopmentTimeline/DevelopmentTimeline.mockup.json";
import Chart from "./Chart";
// Note: the ChartsProvider is required to get the default Theme but wasn't added
// here for simplicity.
function DevelopmentTimeline() {
return <Chart data={data} isLoading={false} schema={schema} />;
}
The code above will render a chart similar to this:
This project is composed by 3 major exports:
<Chart>
- A component that renders a D3 chart by following schemas.<ChartsProvider>
- A component that provides a set of global variables to all children charts.useChart
- An optional hook that could be used to build other custom charts.
Prop | Type | Description |
---|---|---|
data |
TData |
The data that will be handled by the prop schema . |
schema |
ISchema |
A configuration for each chart. See schema for a comple list of available options. |
isLoading? |
boolean |
Show a placeholder loader instead of the chart. Default: false , |
inCard? |
boolean |
Shows chart inside a card or alone. Default true . |
A schema
is created as follows:
// @property {Object} schema - The schema configuration
export const schema = {
// @property {string} schema.id - A html id used to insert the svg components
id: "you-schema-id",
// @property {string=} schema.title - Optional title, displayed above the chart
title: "Schema Title",
// @property {string=} schema.subtitle - Optional subtitle, displayed below title
subtitle: "Schema Subtitle",
// @property {Object} schema.values - Specify data values and how should be treated
values: {
// @property {Object} schema.values[keyname] - Values that will be searched in data
example1: {
// @property {string} schema.values[keyname].scale - Type of scale to be used for data
scale: "band",
// @property {string=} schema.values[keyname].gap - Optional gap between values
gap: 0.2,
},
example2: {
scale: "linear",
// @property {[number, number]=} schema.values[keyname].domain - Range of min/max values to scale
domain: [0, 1000],
},
},
// @property {Array} schema.components - Elements that will be rendered at the svg Chart
components: [
{
// @property {string} schema.components[0].type - Name of the component to use
type: "background-lines",
// @property {string} schema.components[0].value - Name of value specified from "values" above
value: "example1",
},
{
type: "bottom-axis",
value: "example1",
// @property {string} schema.components[1].height - Optional height of element
height: 13,
},
{
type: "line",
// @property {string} schema.components[2].value - This component expects 2 type of values
value: ["example1", "example2"],
// @property {string} schema.components[2].legend - Used to render legends at card header
legend: "Line legend",
// @property {string} schema.components[2].enabledColor - Custom color taken from a theme provided
enabledColor: "primary500",
},
],
};
All
components
are rendered in the Array order, acting like layers.
Prop | Type | Description |
---|---|---|
children |
React.ReactNode |
It can be basically a ReactElement, a ReactFragment, a string, a number or an array of ReactNodes, or null, or undefined, or a boolean. |
components? |
IComponents |
Optional methods which extend the chart elements that are currently supported. |
scales? |
IScales |
Optional methods which extend the scales types that are currently supported. |
theme? |
ITheme |
Optional variables that extend the theme that is currently set. |
The available components
can be extended as follows:
// @property {Object} components - A set of methods that extend the components supported
export const components = {
/**
* @function
* @name my-component-name
* @description - These methods generate svg elements with D3. The arguments represent
* info that helps to create such elements (aka components).
* @param {Object} componentConfig - It passes whatever you decide to use as config
* for the component. As convention it should have at least a `value` property.
* @param {Object} chartConfig - Multiple properties (scales, dimensions,
* internalDimensions, schema, svg, theme and others) which help to create and render
* components in the right place.
* @param {Array} data - The data passed to a <Chart> component as prop.
* @returns {Selection} - As convention, should return a TSVGSelection from D3.
*/
["my-left-axis-example"](componentConfig, chartConfig, data) {
const { format, ticks, value } = componentConfig;
const { internalDimensions, scales, schema, svg } = chartConfig;
return svg
.append("g")
.attr("class", "my-left-axis-example")
.attr("transform", `translate(${internalDimensions.left}, 0)`)
.call(
axisLeft(scales[value] as any)
.tickFormat(format)
.tickValues(ticks || schema.values[value].domain)
.tickSize(0),
)
.call((g: any) => g.select(".domain").attr("stroke-width", 0))
.call((g: any) => g.selectAll("text").attr("class", "my-text-example"));
};
The scales
are limited to those that D3 support, but those can be extended with
specific needs:
// @property {Object} scales - A set of methods that extend the scales supported
export const scales = {
/**
* @function
* @name my-scale-name
* @description - These methods create custom scales required to enhaced values
* behavior over the chart components.
* @param {Object} scaleConfig - It passes whatever you decide to use as config
* for the sale. As convention it should have at least a `value` property.
* @param {Object} dimensions - These are internal dimensions which are limited
* to the final chart itself.
* @param {Array} data - The data passed to a <Chart> component as prop.
* @returns {Scale} - It must return at type of D3 Scale (band, linear, quantile,
* radial, sequential, threshold, time, etc).
*/
["my-band-example"](scaleConfig, dimensions, data) {
const { value, domain, range, size } = scaleConfig;
const { width } = dimensions;
return scaleBand()
.domain(domain ? domain : data.map((d) => (d as TValueName)[value]))
.range(range ? [range[0], range[1]] : [0, size || width]);
},
};
To run the project locally you need to install the dependencies first. 1Chart does not require Yarn but the lock file is shared. To install dependencies with Yarn just run:
yarn install
Since 1Chart uses Storybook as dev server, execute the following script to run it locally:
yarn dev
- Typescript
- React
- React Content Loader
- D3
- Styled Components
- Styled System
- Storybook
- Consistency
- Productivity
- Flexibility
- Maintainability
- Performance
- Data Agnostic
- Responsiveness
- Themeable