Skip to content

Commit

Permalink
api tweaks; documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
mbostock committed Aug 1, 2023
1 parent cadd4c8 commit 67fbf48
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 45 deletions.
1 change: 1 addition & 0 deletions docs/.vitepress/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export default defineConfig({
{text: "Auto", link: "/marks/auto"},
{text: "Axis", link: "/marks/axis"},
{text: "Bar", link: "/marks/bar"},
{text: "Bollinger", link: "/marks/bollinger"},
{text: "Box", link: "/marks/box"},
{text: "Cell", link: "/marks/cell"},
{text: "Contour", link: "/marks/contour"},
Expand Down
115 changes: 115 additions & 0 deletions docs/marks/bollinger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<script setup>

import * as Plot from "@observablehq/plot";
import * as d3 from "d3";
import {ref} from "vue";
import aapl from "../data/aapl.ts";

const n = ref(20);
const k = ref(2);

</script>

# Bollinger mark

The **bollinger mark** is a [composite mark](../features/marks.md#marks) consisting of a [line](./line.md) representing a moving average, and an [area](./area.md) representing volatility as a band; the band thickness is proportional to the deviation of nearby values. The bollinger mark is often used to analyze the price of financial instruments such as stocks.

For example, the chart below shows the price of Apple stock from 2013–2018, with window size *n* of {{n}} days and radius *k* of {{k}} standard deviations.

<p>
<label class="label-input">
<span>Window size (n):</span>
<input type="range" v-model.number="n" min="1" max="100" step="1" />
<span style="font-variant-numeric: tabular-nums;">{{n.toLocaleString("en-US")}}</span>
</label>
<label class="label-input">
<span>Radius (k):</span>
<input type="range" v-model.number="k" min="0" max="10" step="0.1" />
<span style="font-variant-numeric: tabular-nums;">{{k.toLocaleString("en-US")}}</span>
</label>
</p>

:::plot hidden
```js
Plot.bollingerY(aapl, {x: "Date", y: "Close", n, k}).plot()
```
:::

```js-vue
Plot.bollingerY(aapl, {x: "Date", y: "Close", n: {{n}}, k: {{k}}}).plot()
```

For more control, you can also use the [bollinger map method](#bollinger) directly with the [map transform](../transforms/map.md).

:::plot
```js
Plot.plot({
marks: [
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, -2), {x: "Date", y: "Close", stroke: "red"})),
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, 2), {x: "Date", y: "Close", stroke: "green"})),
Plot.lineY(aapl, Plot.mapY(Plot.bollinger(20, 0), {x: "Date", y: "Close"}))
]
})
```
:::

The bollinger mark has two constructors: the common [bollingerY](#bollingerY) for when time goes right→ (or ←left); and the rare [bollingerX](#bollingerX) for when time goes up↑ (or down↓).

:::plot
```js
Plot.bollingerX(aapl, {y: "Date", x: "Close"}).plot()
```
:::

As [shorthand](../features/shorthand.md), you can pass an array of numbers as data. Below, the *x* axis represents the zero-based index into the data (*i.e.*, trading days since May 13, 2013).

:::plot
```js
Plot.bollingerY(aapl.map((d) => d.Close)).plot()
```
:::

## Bollinger options

The bollinger mark is a [composite mark](../features/marks.md#marks) consisting of two marks:

* an [area](../marks/area.md) representing volatility as a band, and
* a [line](../marks/line.md) representing a moving average

In addition to the standard mark options which are passed through to the underlying area and line, the bollinger mark supports the following options:

* **n** - the window size (corresponding to the window transform’s **k** option), an integer
* **k** - the band radius, a number representing a multiple of standard deviations
* **color** - the fill color of the area, and the stroke color of the line; defaults to *currentColor*
* **opacity** - the fill opacity of the area; defaults to 0.2
* **fill** - the fill color of the area; defaults to **color**
* **fillOpacity** - the fill opacity of the area; defaults to **paocity**
* **stroke** - the stroke color of the line; defaults to **color**
* **strokeOpacity** - the stroke opacity of the line; defaults to 1
* **strokeWidth** - the stroke width of the line in pixels; defaults to 1.5

## bollingerX(*data*, *options*) {#bollingerX}

```js
Plot.bollingerX(aapl, {y: "Date", x: "Close"})
```

Returns a bollinger mark for when time goes up↑ (or down↓). If the **x** option is not specified, it defaults to the identity function, as when *data* is an array of numbers [*x₀*, *x₁*, *x₂*, …]. If the **y** option is not specified, it defaults to [0, 1, 2, …].

## bollingerY(*data*, *options*) {#bollingerY}

```js
Plot.bollingerY(aapl, {x: "Date", y: "Close"})
```

Returns a bollinger mark for when time goes right→ (or ←left). If the **y** option is not specified, it defaults to the identity function, as when *data* is an array of numbers [*y₀*, *y₁*, *y₂*, …]. If the **x** option is not specified, it defaults to [0, 1, 2, …].

TODO Describe the **interval** option inherited from line/area.

## bollinger(*n*, *k*) {#bollinger}

```js
Plot.lineY(data, Plot.map({y: Plot.bollinger(20, 0)}, {x: "Date", y: "Close"}))
```

Returns a bollinger map method for use with the [map transform](../transforms/map.md).
2 changes: 1 addition & 1 deletion src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export {Arrow, arrow} from "./marks/arrow.js";
export {auto, autoSpec} from "./marks/auto.js";
export {axisX, axisY, axisFx, axisFy, gridX, gridY, gridFx, gridFy} from "./marks/axis.js";
export {BarX, BarY, barX, barY} from "./marks/bar.js";
export {bollinger, bollingerX, bollingerY, bollingerBandX, bollingerBandY} from "./marks/bollinger.js";
export {bollinger, bollingerX, bollingerY} from "./marks/bollinger.js";
export {boxX, boxY} from "./marks/box.js";
export {Cell, cell, cellX, cellY} from "./marks/cell.js";
export {Contour, contour} from "./marks/contour.js";
Expand Down
11 changes: 2 additions & 9 deletions src/marks/bollinger.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type {CompoundMark, Data, MarkOptions} from "../mark.js";
import type {Transformed} from "../transforms/basic.js";
import type {Map} from "../transforms/map.js";
import type {AreaXOptions, AreaYOptions} from "./area.js";
import type {LineXOptions, LineYOptions} from "./line.js";
Expand All @@ -26,16 +25,10 @@ export type BollingerXOptions = BollingerOptions & AreaXOptions & LineXOptions;
export type BollingerYOptions = BollingerOptions & AreaYOptions & LineYOptions;

/** TODO */
export function bollingerBandX(data?: Data, options?: BollingerXOptions): CompoundMark;
export function bollingerX(data?: Data, options?: BollingerXOptions): CompoundMark;

/** TODO */
export function bollingerBandY(data?: Data, options?: BollingerYOptions): CompoundMark;

/** TODO */
export function bollingerX<T>(n: number, k: number, options?: T): Transformed<T>;

/** TODO */
export function bollingerY<T>(n: number, k: number, options?: T): Transformed<T>;
export function bollingerY(data?: Data, options?: BollingerYOptions): CompoundMark;

/** TODO */
export function bollinger(n: number, k: number): Map;
46 changes: 14 additions & 32 deletions src/marks/bollinger.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,70 +6,52 @@ import {areaX, areaY} from "./area.js";
import {lineX, lineY} from "./line.js";
import {identity} from "../options.js";

export function bollingerBandX(
export function bollingerX(
data,
{
x,
x = identity,
y,
n = 20,
k = 2,
color = "currentColor",
opacity = 0.2,
fill = color,
fillOpacity = 0.2,
stroke,
fillOpacity = opacity,
stroke = color,
strokeOpacity,
strokeWidth,
...options
} = {}
) {
return marks(
areaX(data, bollingerX(n, k, {x, y1: y, y2: y, fill, fillOpacity, ...options})),
lineX(data, bollingerX(n, 0, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
areaX(data, map({x1: bollinger(n, -k), x2: bollinger(n, k)}, {x1: x, x2: x, y, fill, fillOpacity, ...options})),
lineX(data, map({x: bollinger(n, 0)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
);
}

export function bollingerBandY(
export function bollingerY(
data,
{
x,
y,
y = identity,
n = 20,
k = 2,
color = "currentColor",
opacity = 0.2,
fill = color,
fillOpacity = 0.2,
fillOpacity = opacity,
stroke = color,
strokeOpacity,
strokeWidth,
...options
} = {}
) {
return marks(
areaY(data, bollingerY(n, k, {x, y1: y, y2: y, fill, fillOpacity, ...options})),
lineY(data, bollingerY(n, 0, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
areaY(data, map({y1: bollinger(n, -k), y2: bollinger(n, k)}, {x, y1: y, y2: y, fill, fillOpacity, ...options})),
lineY(data, map({y: bollinger(n, 0)}, {x, y, stroke, strokeOpacity, strokeWidth, ...options}))
);
}

export function bollingerX(n, k, options) {
let {x, x1, x2} = options;
if (x === undefined && x1 === undefined && x2 === undefined) options = {...options, x: (x = identity)};
const outputs = {};
if (x != null) outputs.x = bollinger(n, k);
if (x1 != null) outputs.x1 = bollinger(n, -k);
if (x2 != null) outputs.x2 = bollinger(n, k);
return map(outputs, options);
}

export function bollingerY(n, k, options) {
let {y, y1, y2} = options;
if (y === undefined && y1 === undefined && y2 === undefined) options = {...options, y: (y = identity)};
const outputs = {};
if (y != null) outputs.y = bollinger(n, k);
if (y1 != null) outputs.y1 = bollinger(n, -k);
if (y2 != null) outputs.y2 = bollinger(n, k);
return map(outputs, options);
}

export function bollinger(n, k) {
return window({k: n, reduce: (Y) => mean(Y) + k * deviation(Y), strict: true, anchor: "end"});
return window({k: n, reduce: (Y) => mean(Y) + k * (deviation(Y) || 0), strict: true, anchor: "end"});
}
6 changes: 3 additions & 3 deletions test/plots/aapl-bollinger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export async function aaplBollinger() {
grid: true
},
marks: [
Plot.bollingerBandY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
]
});
Expand All @@ -25,7 +25,7 @@ export async function aaplBollingerGridInterval() {
Plot.gridX({tickSpacing: 40, stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
Plot.gridX({tickSpacing: 80, stroke: "#fff", strokeOpacity: 1}),
Plot.axisX({tickSpacing: 80}),
Plot.bollingerBandY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
]
});
Expand All @@ -42,7 +42,7 @@ export async function aaplBollingerGridSpacing() {
Plot.gridX({interval: "3 months", stroke: "#fff", strokeOpacity: 1, strokeWidth: 0.5}),
Plot.gridX({interval: "1 year", stroke: "#fff", strokeOpacity: 1}),
Plot.axisX({interval: "1 year"}),
Plot.bollingerBandY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.bollingerY(AAPL, {x: "Date", y: "Close", stroke: "blue"}),
Plot.line(AAPL, {x: "Date", y: "Close", strokeWidth: 1})
]
});
Expand Down

0 comments on commit 67fbf48

Please sign in to comment.