@wq/chart is a @wq/app plugin providing reusable charts powered by the excellent d3.js library. Some basic chart types (scatter
, timeSeries
, boxplot
) are included, as well as the ability to create new chart types. Any data source can be used, as long as enough information is provided to understand the structure of the data.
Installation
wq.app for PyPI
python3 -m venv venv # create virtual env (if needed)
. venv/bin/activate # activate virtual env
python3 -m pip install wq # install wq framework (wq.app, wq.db, etc.)
# pip install wq.app # install wq.app only
@wq/app for npm
npm install @wq/chart
API
When used as a @wq/app plugin, @wq/chart detects <svg>
s containing data-wq-*
attributes and automatically generates appropriate charts. Alternatively, the @wq/chart
API can also be used directly, by specifying a dataset object and a d3
selection. Note that the import path is slighly different in each case.
wq.app for PyPI
// @wq/app plugin
define(['wq/app', 'wq/chartapp'], function(app, chartapp) {
app.use(chartapp);
app.init(...);
});
// direct API
define(['wq/chart', 'd3', ...], function(chart, d3, ...) {
var dataset = { ... }; // See below
var svg = d3.select('svg#chart');
var plot = chart.timeSeries()
.width(800)
.height(300);
svg.datum([dataset]).call(plot);
});
@wq/app for npm
// @wq/app plugin
import chartapp from '@wq/chart';
app.use(chartapp);
app.init(...);
// direct API
import { chart } from '@wq/chart';
import * as d3 from 'd3';
const dataset = { ... }; // See below
const svg = d3.select('svg#chart');
const plot = chart.timeSeries()
.width(800)
.height(300);
svg.datum([dataset]).call(plot);
The direct API functions each return a configurable function that can be called on a d3 selection that already has data bound to it. By convention, this generated chart function is referred to as plot
to differentiate it from the chart module. However, any variable name can be used.
The dataset
in the example above would typically be a JavaScript object of the form:
{
'id': 'temp-data',
'label': 'Temperature',
'units': 'C'
'list': [
{'date': '2013-09-26', 'value': 26},
{'date': '2013-09-27', 'value': 23},
// ...
]
}
When used as a @wq/app plugin, @wq/chart automatically loads objects in this format via CSV files generated by Django REST Pandas. To use the plugin, the URL and other chart options should be specified on an otherwise empty <svg>
in the page template.
<svg data-wq-url="/api/data/timeseries.csv"
data-wq-type="timeSeries"
data-wq-point-cutoff="100"
data-wq-time-format="%Y-%m-%d"
data-wq-width="800"
data-wq-height="600"
data-wq-x="date"
data-wq-y="value"
data-wq-label-template=""
data-wq-point-label-template="Value on " >
</svg>
Chart Options
The core chart generator (chart.base()) includes a number of setup routines that are utilized by each of the built-in chart types. All chart types inherit the base chart options, and some include additional options unique to each chart. All options have reasonable defaults, most of which can be re-configured using d3-style getter/setter functions.
Options
These options control basic chart formatting and layout.
Option | Default | Purpose |
---|---|---|
plot.width(val) |
700 |
Sets the drawing width in pixels (including margins). The <svg> object should generally have the same dimensions and/or viewport. |
plot.height(val) |
300 |
Sets the drawing height in pixels (including margins). |
plot.outerFill(val) |
#f3f3f3 |
Background color for the entire chart (including axes). |
plot.innerFill(val) |
#eee |
Background color for the actual plot area. |
plot.viewBox(val) |
auto | SVG viewBox attribute. Computed based on plot width and height by default. Set to false to disable entirely. Useful for helping ensure chart scales appropriately on different screen sizes. |
plot.legend(obj) |
auto | Legend size and position ('right' or 'bottom' ). If position is 'right' , size refers to the width of the legend, while if position is 'bottom' , size refers to the height. The default is to place the legend on the bottom if there are 5 or fewer datasets, and on the right if there are more. |
plot.xscale(obj) |
auto | Domain of x values in the dataset. If unset, will be automatically determined from the data and optionally “niced” to a round range. If set, should be an object of the form {'xmin': val, 'xmax': val} . |
plot.xscalefn(fn) |
d3.scale.linear |
Actual d3 function to use to generate the scale. |
plot.xnice(fn) |
null |
Function to use to generate a nice scale. |
plot.xticks(val) |
null |
Explicitly set the number of ticks to use for the x axis. |
plot.yscales(obj) |
auto | Domain(s) of y values in the dataset. If unset, will be automatically determined from the data and niced to a round range. If there are multiple datasets with different units, a separate yscale will be computed for each unit. If set, should be an object of the form {unit1: {'ymin': val, 'ymax': val}, unit2: {'ymin': val, 'ymax': val} |
plot.yscalefn(fn) |
d3.scale.linear |
Actual d3 function to use to generate the y scale(s) |
plot.cscale(fn) |
d3.scale.category20() |
Color scale to use (one color for each dataset) |
Margins
There is also a special method, plot.setMargin(name, margin)
, that can be used to “reserve” named spaces at the margins of the chart. Unlike other options, setMargin
is not a getter/setter function. Instead, it takes two arguments: a unique margin name
, and a margin
object which should have at least one of left
, right
, top
, or bottom
set. The values should be distances measured in pixels. All of the set margins are aggregated by plot.getMargins()
to determine the final margins for the chart.
// Reserve 30px at the top for a custom header
plot.setMargin("myheader", {'top': 30});
var allMargins = plot.getMargins();
// allMargins.top == 5 + 30 == 35
Margins for the legend and axes are set automatically using the setMargin()
mechanism. The margin names padding
, xaxis
, yaxis
, and legend
are reserved for this purpose.
Accessors
Accessors control how the data object is parsed, i.e. how data properties are accessed. Accessors are simple functions that take an object and return a value. Overriding the default accessor makes it possible to chart data structures that are not of the format shown above.
Dataset Accessors
Option | Default | Purpose |
---|---|---|
plot.datasets(fn(rootObj)) |
rootObj.data or rootObj |
Returns the array of datasets within rootObj . If rootObj is already an array, it can be used directly. |
plot.id(fn(dataset)) |
dataset.id |
Accesses the unique identifier for the dataset as a whole. |
plot.label(fn(dataset)) |
dataset.label |
Accesses the label to be shown in the legend for this dataset. |
plot.items(fn(dataset)) |
dataset.list |
Accessor for the actual data values to be plotted. |
plot.yunits(fn(dataset)) |
dataset.units |
Units for the dataset y values (determines which y scale will be used). |
plot.xunits(fn(dataset)) |
unset | Units for the dataset x values. Defined differently by each chart type. |
plot.xmin(fn(dataset)) |
d3.min(items(dataset),xvalue) |
Function to determine minimum x value of the dataset. |
plot.xmax(fn(dataset)) |
d3.max(items(dataset),xvalue) |
Function to determine maximum x value of the dataset. |
plot.ymin(fn(dataset)) |
d3.min(items(dataset),yvalue) |
Function to determine minimum y value of the dataset. |
plot.ymax(fn(dataset)) |
d3.max(items(dataset),yvalue) |
Function to determine maximum y value of the dataset. |
plot.xset(fn(rootObj)) |
all unique x values | Function to access an array containing all unique x values across all datasets in rootObj . Not meant to be overridden. |
Legend Accessors
Option | Default | Purpose |
---|---|---|
plot.legendItems(fn(rootObj)) |
datasets(rootObj) |
Returns an array of legend items. Can be overridden if there are fewer (or more) legend items than datasets for some reason. |
plot.legendItemId(fn(legendItem)) |
id(legendItem) |
Returns the unique identifier for a legend item. Can be overridden if this is not the same as the dataset id. |
plot.legendItemLabel(fn(legendItem)) |
label(legendItem) |
Returns the label for a legend item. Can be overridden if this is not the same as the dataset label. |
plot.legendItemShape(fn(legItemId)) |
"rect" |
The name of an SVG tag to use for legend items. |
plot.legendItemStyle(fn(legItemId)(sel)) |
plot.rectStyle |
Returns a function for the given legend item id (legItemId ) that can set the appropriate attributes necessary to style the provided d3 selection (sel ) which will be an SVG tag of the type specified by legendItemShape . |
Accessors for Individual Values
Option | Default | Purpose |
---|---|---|
plot.xvalue(fn(d)) |
unset | Accessor for x values of individual data points. Defined differently by each chart type. |
plot.xscaled(fn(d)) |
xscale(xvalue(d)) |
Convenience function to access an x value and return its scaled equivalent. Not meant to be overridden. |
plot.yvalue(fn(d)) |
unset | Accessor for y values of individual data points. Defined differently by each chart type. |
plot.yscaled(fn(scaleid)(d)) |
yscales[scaleid](yvalue(d)) |
Convenience function to access a function that can take a y value and return its scaled equivalent. (The nested function is needed since there may be more than one y axis). Not meant to be overridden. |
plot.translate(fn(scaleid)(d)) |
"translate(x,y)" |
Returns a function that can generate a translate() string (for use as a SVG transform value), containing the xscaled and yscaled values for a given data point. |
plot.itemid(fn(d)) |
xvalue(d)+'='+yvalue(d) |
Accessor for uniquely identifying individual data values. |
Scatter plots
chart.scatter() returns a function useful for drawing basic x-y scatter plots and line charts. One or more datasets containing x and y values should be provided. All datasets should have the same units for x values, but can can have different y units if needed. Alternating left and right-side y axes will be created for each unique y unit (so it’s best to have no more than two).
chart.scatter()
can be used with Django REST Pandas’ PandasScatterSerializer.
Default Overrides
chart.scatter()
overrides the following base chart defaults:
Option | scatter Default |
---|---|
plot.xvalue(fn(d)) |
d.x |
plot.xunits(fn(dataset)) |
dataset.xunits |
plot.yvalue(fn(d)) |
d.y |
plot.yunits(fn(dataset)) |
dataset.yunits |
plot.legendItemShape(fn(legItemId)) |
Same as pointShape() (see below) |
plot.legendItemStyle(fn(legItemId)(sel)) |
Same as pointStyle() (see below) |
Additional Options
chart.scatter()
defines these additional options:
Option | Default | Purpose |
---|---|---|
plot.drawPointsIf(fn(dataset)) |
Up to 50 items | Whether to draw points for the given dataset. Can be a function returning true if you always want points drawn. |
plot.drawLinesIf(fn(dataset)) |
> 50 items | Whether to draw lines for the given dataset. Can be a function returning true if you always want lines drawn. Specified separately from drawPointsIf() in case you want to have both lines and points for some reason. |
plot.lineStyle(fn(datasetId)(sel)) |
Sets stroke with plot.cscale |
Used when the drawLinesIf() function returns true. Returns a function for the given dataset id that can the appropriate attributes necessary to style the provided d3 selection (sel ) which will be an SVG <path> . |
plot.pointShape(datasetId) |
"circle" |
Used when the drawPointsIf() function returns true. Specifies the shape to use when rendering points for each dataset. This value will also be used for legend items for consistency. |
plot.pointStyle(fn(datasetId)(sel)) |
plot.circleStyle |
Returns a function for the given dataset id that can set the appropriate attributes necessary to style the provided d3 selection (sel ) which will be an SVG tag of the type specified by pointShape . |
plot.pointover(fn(datasetId)(d)) |
Adds highlight | Returns a function for the given dataset id that will be called with the data for a point whenever the point is hovered over. |
plot.pointout(fn(datasetId)(d)) |
Removes highlight | ” “ the point is no longer being hovered over. |
plot.pointLabel(fn(datasetId)(d)) |
"{datasetId} at {d.x}: {d.y}" |
Returns a function for the given dataset id that can generate tooltip labels for data points. These will be added via an SVG <title> tag. Note that the tooltip is distinct from the pointover() functionality even though both appear at the same time. |
Time series plots
chart.timeSeries() is a simple extension to chart.scatter()
that assumes the x values are times or dates.
chart.timeSeries()
can be used with Django REST Pandas’ PandasUnstackedSerializer.
Default Overrides
chart.timeSeries()
overrides the following scatter
chart defaults:
Option | timeSeries Default |
---|---|
plot.xvalue(fn(d)) |
timeFormat.parse(d.date) |
plot.yvalue(fn(d)) |
d.value |
plot.xscalefn(fn) |
d3.time.scale |
plot.xnice(fn) |
d3.time.year |
Additional Options
chart.timeSeries()
defines one additional option:
Option | Default | Purpose |
---|---|---|
plot.timeFormat(val) |
"%Y-%m-%d" |
Format string to use to parse time values. |
Box & Whisker plots
chart.boxplot() returns a function useful for rendering simple box-and-whisker plots. The quartile data for each box should be precomputed.
The default implementation of chart.boxplot()
assumes a dataset with roughly the following structure:
{
'id': 'temp-data',
'label': 'Temperature',
'units': 'C'
'list': [
{
'year': "2013",
'value-whislo': 3,
'value-q1', 8
'value-median': 17,
'value-q3': 20,
'value-whishi': 25
}
// ...
]
}
The x value (year
in the above example) is used to define an ordinal scale where each item on the x axis corresponds to a box. Thus, any text or numeric attribute can be defined as the x value, provided that the xvalue
accessor is defined.
var plot = chart.boxplot().xvalue(function(d) { return d.year });
chart.boxplot()
can be customized by overriding the following accessor methods:
Accessors for Individual Values
Option | Default | Purpose |
---|---|---|
plot.prefix(val) |
"value-" |
Prefix for boxplot value names |
plot.whislo(fn(d)) |
prefix + "whislo" |
Accessor for the low whisker value |
plot.q1(fn(d)) |
prefix + "q1" |
Accessor for the 25% quartile |
plot.median(fn(d)) |
prefix + "median" |
Accessor for the median |
plot.q3(fn(d)) |
prefix + "q3" |
Accessor for the 25% quartile |
plot.whishi(fn(d)) |
prefix + "whislo" |
Accessor for the high whisker value |
chart.boxplot()
can be used with Django REST Pandas’ PandasBoxplotSerializer.
Custom Charts
The base chart provides “hooks” that allow for specifying the chart rendering process before, during, and after each dataset is rendered. Each of the chart types above defines one or more of these functions. They can also be used if you want to define a new chart type or significantly alter the behavior of one of the existing types.
Option | Purpose |
---|---|
plot.init(fn(datasets)) |
Initial chart configuration. If defined, the init function will be passed an array of all of the datasets. |
plot.renderBackground(fn(dataset)) |
Render a background layer for each dataset |
plot.render(fn(dataset)) |
Render the primary layer for each dataset. |
plot.wrapup(fn(datasets, opts)) |
Wrapup routine, useful for drawing e.g. legends. opts will be an object containing computed widths and heights for the actual chart inner and outer drawing areas. |
plot.rectStyle(dsid)(sel) |
Returns a function for the given dataset id (dsid ) that can set the appropriate attributes necessary to style the provided d3 selection (sel ), which would normally be an SVG <rect> tag. |
plot.circleStyle(dsid)(sel) |
” “ <circle> tag. |
To define your own chart function generator, you could do something like the following:
function myChart() {
var plot = chart.base()
.render(render);
function render(dataset) {
var items = plot.items()(dataset);
d3.select(this)
.selectAll('g.data')
.data(items)
.append('g').attr('class', 'data')
/* do something cool with d3 */
}
return plot;
}
var plot = myChart();
svg.datum([dataset]).call(plot);