npm package: @sis-cc/dotstatsuite-d3-charts
Description: Set of configurable charts based on d3.
Author: OECD
Latest version: 10.3.0

# rcw-charts

[![oecd](https://img.shields.io/badge/organisation-OECD-11629A.svg?style=flat-square)]()
[![test](https://img.shields.io/badge/component-test-red.svg?style=flat-square)]()
[![lint](https://img.shields.io/badge/component-lint-red.svg?style=flat-square)]()

Set of configurable charts based on [d3](https://d3js.org/).

## Setup

```
npm install rcw-charts
```

## Usage

```javascript
import { BarChart } from 'rcw-charts';

const chart = new BarChart(el, options, data);
chart.update(el, otherOptions, otherData);
chart.destroy(el);
```

## Available charts

* bar
* row
* scatter
* line
* vertical symbol
* horizontal symbol
* timeline

## API (out of the box usage)

### methods

#### `constructor(el, options, data)`

Setup DOM elements and call `update`.

#### `update(el, options, data)`

Draw the chart using the lifecycle of d3 (join, enter, update, exit).

#### `destroy(el)`

Destroy all d3 related elements (selections, events) from `el` including `el`.

### props

#### `el`

> any valid DOM element (JSDOM elements are valid, React's virtual DOM elements are not)

```javascript
const el = document.getElementById('root');
```

#### `options`

```javascript
const axisOptions = {
  step: 1,
  format: { proc: null, pattern: '.0f', isTime: false },
  color: 'black',
  thickness: 1
};

const options = {
  base: {
    width: 920,
    height: 455,
    margin: 0,
  },
  axis: {
    x: axisOptions,
    y: {
      ...axisOptions,
      step: 2
    }
  }
};
```

| name                                 | default                                  | type/domain                                                | description                                          |
| ------------------------------------ | ---------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------- |
| `base`                               | -                                        | `{}`                                                       | base options                                         |
| `base.width`                         | `920`                                    | `<Integer>`                                                | width of the chart                                   |
| `base.height`                        | `455`                                    | `<Integer>`                                                | height of the chart                                  |
| `base.margin`                        | `0`                                      | `<Integer>`                                                | margins (top, right, bottom, left) of the chart (#1) |
| `base.isAnnotated`                   | `false`                                  | `<Boolean>`                                                | serie annotations                                    |
| `base.padding`                       | `{top: 0, right: 0, bottom: 0, left: 0}` | `{}`                                                       | padding around base                                  |
| `base.innerPadding`                  | `{top: 0, right: 0, bottom: 0, left: 0}` | `{}`                                                       | padding around series                                |
| `axis`                               | -                                        | `{}`                                                       | axis options                                         |
| `axis.{x\|y}`                        | -                                        | `{}`                                                       | x or y axis options                                  |
| `axis.{x\|y}.color`                  | `black`                                  | CSS                                                        | color of the axis (line only)                        |
| `axis.{x\|y}.thickness`              | `1`                                      | `<Integer>`                                                | thickness of the axis (line only)                    |
| `axis.{x\|y}.orient`                 | `left`                                   | `[top|right|bottom|left]`                                  | orientation of the axis                              |
| `axis.{y\}.padding`                  | `0`                                      | `<Integer>`                                                | padding of the axis (#4)                             |
| `axis.{x\|y}.tick`                   | -                                        | `{}`                                                       | tick options                                         |
| `axis.{x\|y}.tick.size`              | `6`                                      | `<Integer>`                                                | length (width for y, height for x) of a tick         |
| `axis.{x\|y}.tick.minorSize`         | `0`                                      | `<Integer>`                                                | length (width for y, height for x) of a minor tick         |
| `axis.{x\|y}.tick.color`             | `black`                                  | CSS                                                        | color of a tick                                      |
| `axis.{x\|y}.tick.thickness`         | `1`                                      | `<Integer>`                                                | thickness of a tick                                  |
| `axis.{x\|y}.tick.minorThickness`    | `1`                                      | `<Integer>`                                                | thickness of a minor tick                            |
| `axis.{x\|y}.font`                   | -                                        | -                                                          | font options                                         |
| `axis.{x\|y}.font.family`            | `sans-serif`                             | CSS                                                        | font family                                          |
| `axis.{x\|y}.font.size`              | `10`                                     | `<Integer>`                                                | font size                                            |
| `axis.{x\|y}.font.color`             | `black`                                  | CSS                                                        | font color                                           |
| `axis.{x\|y}.font.weight`            | `normal`                                 | CSS                                                        | font weight                                          |
| `axis.{x\|y}.font.rotation`          | `-45`                                    | `<Integer>`                                                | font rotation (#3)                                   |
| `axis.{x\|y}.format`                 | -                                        | -                                                          | format options                                       |
| `axis.{x\|y}.format.proc`            | `null`                                   | `<function>`                                               | proc to manually compute a format (#2)               |
| `axis.{x\|y}.format.pattern`         | `.0f`                                    | D3                                                         | pattern injected in `d3.format`                      |
| `axis.{x\|y}.format.isTime`          | `false`                                  | `<Boolean>`                                                | use `d3.time.format`                                 |
| `axis.{x\|y}.format.maxWidth`        | `null`                                   | `<Integer>`                                                | value to set a maximum to the width of the text (in pixels) |
| `axis.{x\|y}.ordinal.gap`            | `.3`                                     | `[0 -> 1]`                                                 | ratio for band gap                                   |
| `axis.{x\|y}.ordinal.padding`        | `.3`                                     | `[0 -> 1]`                                                 | space ratio around bands                             |
| `axis.{x\|y}.ordinal.minDisplaySize` | `300`                                    | `<Integer>`                                                | minimal number of pixels required to display the axis (width for horizontal, height for vertical)    |
| `axis.{x\|y}.linear.min`             | `0`                                      | `<Integer>`                                                | min value (#7)                                       |
| `axis.{x\|y}.linear.minDisplaySize`  | `100`                                    | `<Integer>`                                                | minimal number of pixels required to display the axis (width for horizontal, height for vertical)    |
| `axis.{x\|y}.linear.minTickSizeFactor`| `2`                                    | `<Integer>`                                                 | minimal ratio of a tick label offset (width for horizontal, height for vertical) for step size   |
| `axis.{x\|y}.linear.minTickSizeFactor`| `5`                                    | `<Integer>`                                                 | maximal ratio of a tick label offset (width for horizontal, height for vertical) for step size   |
| `axis.{x\|y}.linear.max`             | `0`                                      | `<Integer>`                                                | max value (#7)                                       |
| `axis.{x\|y}.linear.step`            | `0`                                      | `<Integer>`                                                | step that defines tick count (#8)                    |
| `axis.{x\|y}.linear.frequency`       | `year`                                   | `[year|month|week|day|hour|minute|second]`                 | frequency used along with a step for timelines (#10) |
| `axis.{x\|y}.linear.pivot`           | -                                        | `{}`                                                       | pivot options                                        |
| `axis.{x\|y}.linear.pivot.value`     | `null`                                   | `<Integer>`                                                | pivot value, if null computed from min, max or data  |
| `axis.{x\|y}.linear.pivot.color`     | `black`                                  | CSS                                                        | color of pivot line                                  |
| `axis.{x\|y}.linear.pivot.thickness` | `2`                                      | `<Integer>`                                                | width of pivot line                                  |
| `axis.{x\|y}.grid.color`             | `black`                                  | CSS                                                        | color of a line                                      |
| `axis.{x\|y}.grid.thickness`         | `1`                                      | `<Integer>`                                                | thickness of a line                                  |
| `axis.{x\|y}.grid.baselines`         | `[]`                                     | `Array of <Integer>`                                       | grid lines considered as pivot line (#9)             |
| `serie`                              | -                                        | `{}`                                                       | serie options                                        |
| `serie.colors`                       | `['gray']`                               | `[CSS]`                                                    | colors of a serie                                    |
| `serie.overColors`                   | `['green']`                              | `[CSS]`                                                    | colors of a hovered serie                            |
| `serie.highlightColors`              | `['blue']`                               | `[CSS]`                                                    | colors of highlighted series                         |
| `serie.baselineColors`               | `['#263238']`                            | `[CSS]`                                                    | colors of baseline series                            |
| `serie.annotation`                   | -                                        | `{}`                                                       | annotation options                                   |
| `serie.annotation.font.family`       | `sans-serif`                             | CSS                                                        | font family                                          |
| `serie.annotation.font.size`         | `10`                                     | `<Integer>`                                                | font size                                            |
| `serie.annotation.font.weight`       | `normal`                                 | CSS                                                        | font weight                                          |
| `serie.annotation.margin`            | `2`                                      | `<Integer>`                                                | margin around annotation                             |
| `serie.annotation.format`            | -                                        | `{}`                                                       | annotation format (cf axis.format)                   |
| `serie.annotation.display`           | `focus`                                  | `[never|always|highlight|baseline|focus]`                  | annotation display mode (#6)                         |
| `serie.scatter`                      | -                                        | `{}`                                                       | scatter options                                      |
| `serie.scatter.markerRadius`         | `7`                                      | `<Integer>`                                                | radius of a marker                                   |
| `serie.scatter.areaIndex`            | `.1`                                     | `[0 -> 1]`                                                 | ratio that control annotation position on edges      |
| `serie.line.shadow`                  | `10`                                     | `<Integer>`                                                | shadow line for hovering more easily                 |
| `serie.line.thickness`               | `1`                                      | `<Integer>`                                                | thickness of a line                                  |
| `serie.line.marker`                  | -                                        | `{}`                                                       | marker options for a line                            |
| `serie.line.marker.shadow`           | `2`                                      | `<Integer>`                                                | shadow border of a marker                            |
| `serie.line.marker.shape`            | `circle`                                 | `[circle|cross|diamond|square|triangle-up, triangle-down]` | shape of a marker                                    |
| `serie.line.focused`                 | -                                        | `{}`                                                       | focused line options                                 |
| `serie.line.focused.thickness`       | `2`                                      | `<Integer>`                                                | thickness of a focused line                          |
| `serie.line.densityRatio`            | `32`                                     | `<Integer>`                                                | ratio for visibility of markers and annotations      |
| `serie.tooltip`                      | -                                        | `{}`                                                       | scatter options                                      |
| `serie.tooltip.display`              | `over`                                   | `[never|over]`                                             | tooltip display mode                                 |
| `serie.tooltip.layout`               | `<proc>`                                 | `<function>`                                               | proc that render an html string (#5)                 |

* (#1) compliant with the [d3 standard](https://bl.ocks.org/mbostock/3019563)
* (#2) `proc(datum) => datum.x%2 === 0 ? datum.x : 'odd values are odd...'`
* (#3) only used for x if labels are too long
* (#4) correlated to orient (left, right), only for y
* (#5) ``proc(serie, datum, color) => `<div style="${style}">X: ${datum.x}<br />Y: ${datum.y}</div>` ``
* (#6) focus is the aggregation of all focus modes (highlight, baseline)
* (#7) min and max values are overriden by data to avoid clamping and misleading charts
* (#8) if min is 0 and max is 100 with 20 step, ticks will be [0,20,40,60,80,100], a 0 step (default) is considered invalid and will be overriden by data
* (#9) pivot has an impact on all the computations, baselines have no impact. They are declarative, if they are drawn they have the same style as a pivot line.
* (#10) a time axis has the same behavior as a linear axis except that ticks are handle by the combination of a frequency and a step (ie every quarter is `month` and `3`)

#### `data`

```javascript
{
  label: 'my serie',
  datapoints: [
    {x, y, z, highlightIndex, baselineIndex, label},
  ]
}
```

`x, y, z` can be `<Integer>` or `<String>`

## API (advanced usage)

> Advanced usage should be used when options are not enough to fit the needs.

### The Scatterplot into Bubble usecase

The difference between the scatterplot and the bubble 
is the data used to compute the size of the plots.

Here is the source of the bubble:
```javascript
function plotSizeAccessor(target) {
  target.accessors = {
    ...target.accessors,
    plotSize: (options, datum) => target.accessors.z(datum) > 0 ? target.accessors.z(datum) : 0
  };
}

@plotSizeAccessor
export default class Bubble extends ScatterPlot {}
```

The scatterplot engine is used to draw a bubble with a slight difference,
the plotSize accessor has been changed:

```javascript
// in Scatterplot
function plotSizeAccessor(target) {
  target.accessors = {
    ...target.accessors,
    plotSize: (options, datum) => options.plotSize
  };
}
```

*note: bubble needs a little bit more like displaying smaller bubbles on top but to keep the usecase
simple for the documentation, only the size aspect has been kept.*

This usecase is very simple but shows that:
* tweaking a chart is not as complex as creating a chart
* tweaking is not risky and has no side-effect on the inherited chart
* tweaking is handy, the context is provided (`(options, datum) => options.plotSize`):
  * datum is not used in scatterplot
  * datum is required for bubble
  * datum represents a plot data (`{x,y,z}`)
  * the accessor is executed somewhere in the flow engine where the datum is injected
  * plotSizes are computed on the fly by the engine (data-driven approach)

### Available hooks (decorators)

| name        | description                                                                       |
| ------------| --------------------------------------------------------------------------------- |
| `selectors` | define css classes associated with charts DOM elements                            |
| `accessors` | define how the engine access data or options (x dimension, annotation label, ...) |
| `computors` | define computations used in the chart engine flow (ranges, scales, offsets, ...)  |
| `eauors`    | stands for 'enter and update -ors', define how to draw elements (with d3)         |

Each chart defines `decorators` and a flow engine.  
Decorators are inherited and can overriden anywhere in the inheritance chain:

1. `Base` defines how to compute the x axis
1. `Base::Scatterplot` do nothing (ie reuse the x axis computation inherited from Base)
1. `Scatterplot::Bubble` can redefine how to compute the x axis

```javascript
// default axis computation
export default function computors(target) {
  target.computors = {
    ...target.computors,
    xAxis: (scale, options) => computeAxis(scale, options)
  }
}

// bar needs an ordinal axis
export default function computors(target) {
  target.computors = {
    ...target.computors,
    xAxis: (scale, options) => computeOrdinalAxis(scale, options)
  }
}
```

### Thoughts

Hooks are a way for tweaking charts and handle usecases that options do not cover.  
Some hooks may be permanent, others can introduce features that may become native 
(ie accessible throught options).

### Tooltips

Tooltip is a transversal concept that is be generic except the UI element on which it is attached
and the event that trigger it.  
That is why tooltip handlers need to be declared for each charts since a chart is the only one to know its structure.

Handlers:
* `showTooltip(_selector, accessors, options, scales, datum)`: show the tooltip
* `hideTooltip(_selector, partial)`: hide the tooltip

These handlers manage the visibility of the tooltip and its placement.  
They also use the layout (provided in options) to display the tooltip as expected.

```javascript
// in eauors
d3.select(/*<selector>*/)
  .on('mousemove', _.curry(showTooltip)(selectors.tooltip, accessors, options, scales))
  .on('mouseleave', _.curry(hideTooltip)(selectors.tooltip));
```

It is not necessary to understand currying here, choose the UI part on which tooltip should appear
and copy/paste the handler partial executions.  
Recommended events are `mousemove` and `mouseleave` but others can be used.

Layout sample:
```javascript
function layout(datum, color) {
  const style = `
    font-size: 10px;
    font-family: sans-serif;
    color: ${color};
    padding: 5px;
    border: 1px solid ${color};
    background: white;
    opacity: .8;
  `;
  return `<div style="${style}">X: ${datum.x}<br />Y: ${datum.y}</div>`;
}
```

Inline style is accepted and allow a good customization.  
`datum` holds all the data relative to the point, bar, etc and `color` is the color used for highlighting.