Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,25 @@ In addition to the [standard mark options](#marks), the following optional chann

By default, the data is assumed to represent a single series (a single value that varies over time, *e.g.*). If the **z** channel is specified, data is grouped by *z* to form separate series. Typically *z* is a categorical value such as a series name. If **z** is not specified, it defaults to **stroke** if a channel, or **fill** if a channel.

The **fill** defaults to none. The **stroke** defaults to currentColor if the fill is none, and to none otherwise. If both the stroke and fill are defined as channels, or if the *z* channel is also specified, it is possible for the stroke or fill to vary within a series; varying color within a series is not supported, however, so only the first channel value for each series is considered. This limitation also applies to the **fillOpacity**, **strokeOpacity**, **strokeWidth**, and **title** channels. The **strokeWidth** defaults to 1.5 and the **strokeMiterlimit** defaults to 1.
The **fill** defaults to none. The **stroke** defaults to currentColor if the fill is none, and to none otherwise.

The **strokeWidth** defaults to 1.5 and the **strokeMiterlimit** defaults to 1. The **stroke** defaults to currentColor. The **fill** defaults to none.

If both the stroke and fill are defined as channels, or if the *z* channel is also specified, it is possible for the stroke or fill to vary within a series; in that case the color for the mark is taken as the first channel value for each series. A different reducer can be applied by specifying {value, reduce}. The following reducers are available:

* *first* (default) - the first value
* *last* - the last value
* *count* - the number of values
* *distinct* - the number of distinct values
* *sum* - the sum of values
* *min* - the minimum value
* *max* - the maximum value
* *mean* - the mean (average) of values
* *median* - the median of values
* *mode* - the mode (most frequent occurrence) of values
* a function to be passed the array of values

This also applies to the **fillOpacity**, **strokeOpacity**, **strokeWidth** and **title** channels.

Points along the line are connected in input order. Likewise, if there are multiple series via the *z*, *fill*, or *stroke* channel, the series are drawn in input order such that the last series is drawn on top. Typically, the data is already in sorted order, such as chronological for time series; if sorting is needed, consider a [sort transform](#transforms).

Expand Down
3 changes: 2 additions & 1 deletion src/marks/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark} from "../plot.js";
import {indexOf, maybeZ} from "../options.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, maybeGroupedStyles} from "../style.js";
import {maybeIdentityX, maybeIdentityY} from "../transforms/identity.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

Expand All @@ -15,6 +15,7 @@ const defaults = {

export class Area extends Mark {
constructor(data, options = {}) {
options = maybeGroupedStyles(options);
const {x1, y1, x2, y2, curve, tension} = options;
super(
data,
Expand Down
3 changes: 2 additions & 1 deletion src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark} from "../plot.js";
import {indexOf, identity, maybeTuple, maybeZ} from "../options.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, offset} from "../style.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, maybeGroupedStyles, offset} from "../style.js";

const defaults = {
ariaLabel: "line",
Expand All @@ -15,6 +15,7 @@ const defaults = {

export class Line extends Mark {
constructor(data, options = {}) {
options = maybeGroupedStyles(options);
const {x, y, curve, tension} = options;
super(
data,
Expand Down
46 changes: 45 additions & 1 deletion src/style.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import {isoFormat, namespaces} from "d3";
import {nonempty} from "./defined.js";
import {formatNumber} from "./format.js";
import {string, number, maybeColorChannel, maybeNumberChannel, isTemporal, isNumeric} from "./options.js";
import {string, number, maybeColorChannel, maybeNumberChannel, maybeValue, isTemporal, isNumeric} from "./options.js";
import {max, min, mean, median, mode, sum, InternSet} from "d3";
import {map} from "./transforms/map.js";
import {identity} from "./options.js";

export const offset = typeof window !== "undefined" && window.devicePixelRatio > 1 ? 0 : 0.5;

Expand Down Expand Up @@ -257,3 +260,44 @@ export function applyFrameAnchor({frameAnchor}, {width, height, marginTop, margi
/^top/.test(frameAnchor) ? marginTop : /^bottom/.test(frameAnchor) ? height - marginBottom : (marginTop + height - marginBottom) / 2
];
}

export function maybeGroupedStyles(options = {}) {
const grouped = [];
for (const key of ["fill", "fillOpacity", "stroke", "strokeOpacity", "strokeWidth", "title", "ariaLabel"]) {
if (options[key] != null) {
let {value, reduce} = maybeValue(options[key]);
if (reduce) {
options[key] = value === undefined ? identity : value;
reduce = maybeReduce(reduce);
grouped.push([key, d => ({
uniform: true,
value: reduce(d)
})]);
}
}
}
return grouped.length > 0 ? map(Object.fromEntries(grouped), options) : options;
}

function maybeReduce(reduce) {
if (typeof reduce === "string") {
switch (reduce.toLowerCase()) {
case "first": return ([x]) => x;
case "last": return x => x[x.length - 1];
case "count": return x => x.length;
case "distinct": return d => new InternSet(d).size;
case "sum": return sum;
// proportion
// proportion-facet
// deviation
case "min": return min;
case "max": return max;
case "mean": return mean;
case "median": return median;
// variance
case "mode": return mode;
}
}
if (typeof reduce !== "function") throw new Error("invalid reduce");
return reduce;
}
8 changes: 6 additions & 2 deletions src/transforms/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,12 @@ function mapFunction(f) {
return {
map(I, S, T) {
const M = f(take(S, I));
if (M.length !== I.length) throw new Error("mismatched length");
for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M[i];
if (M.uniform) {
for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M.value;
} else {
if (M.length !== I.length) throw new Error("mismatched length");
for (let i = 0, n = I.length; i < n; ++i) T[I[i]] = M[i];
}
}
};
}
Expand Down