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
18 changes: 17 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,23 @@ 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 **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; 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 **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
8 changes: 4 additions & 4 deletions src/marks/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {area as shapeArea, create, group} from "d3";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, maybeZ} from "../mark.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, maybeGroupedStyles} from "../style.js";
import {maybeStackX, maybeStackY} from "../transforms/stack.js";

const defaults = {
Expand Down Expand Up @@ -49,13 +49,13 @@ export class Area extends Mark {
}

export function area(data, options) {
return new Area(data, options);
return new Area(data, maybeGroupedStyles(options));
}

export function areaX(data, {y = indexOf, ...options} = {}) {
return new Area(data, maybeStackX({...options, y1: y, y2: undefined}));
return new Area(data, maybeStackX(maybeGroupedStyles({...options, y1: y, y2: undefined})));
}

export function areaY(data, {x = indexOf, ...options} = {}) {
return new Area(data, maybeStackY({...options, x1: x, x2: undefined}));
return new Area(data, maybeStackY(maybeGroupedStyles({...options, x1: x, x2: undefined})));
}
8 changes: 4 additions & 4 deletions src/marks/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {create, group, line as shapeLine} from "d3";
import {Curve} from "../curve.js";
import {defined} from "../defined.js";
import {Mark, indexOf, identity, maybeTuple, maybeZ} from "../mark.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles} from "../style.js";
import {applyDirectStyles, applyIndirectStyles, applyTransform, applyGroupedChannelStyles, maybeGroupedStyles} from "../style.js";

const defaults = {
fill: "none",
Expand Down Expand Up @@ -47,13 +47,13 @@ export class Line extends Mark {

export function line(data, {x, y, ...options} = {}) {
([x, y] = maybeTuple(x, y));
return new Line(data, {...options, x, y});
return new Line(data, maybeGroupedStyles({...options, x, y}));
}

export function lineX(data, {x = identity, y = indexOf, ...options} = {}) {
return new Line(data, {...options, x, y});
return new Line(data, maybeGroupedStyles({...options, x, y}));
}

export function lineY(data, {x = indexOf, y = identity, ...options} = {}) {
return new Line(data, {...options, x, y});
return new Line(data, maybeGroupedStyles({...options, x, y}));
}
48 changes: 47 additions & 1 deletion src/style.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {string, number, maybeColor, maybeNumber, title, titleGroup} from "./mark.js";
import {string, number, maybeColor, maybeNumber, maybeValue, title, titleGroup} from "./mark.js";
import {filter} from "./defined.js";
import {max, min, mean, median, mode, sum, InternSet} from "d3";
import {map} from "./transforms/map.js";

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

Expand Down Expand Up @@ -153,3 +155,47 @@ export function filterStyles(index, {fill: F, fillOpacity: FO, stroke: S, stroke
function none(color) {
return color == null || color === "none";
}

export function maybeGroupedStyles(options = {}) {
let {z} = options;
if (z !== undefined) {
const maps = [];
for (const key of ["fill", "fillOpacity", "stroke", "strokeOpacity", "strokeWidth", "title"]) {
if (options[key] != null) {
let {value, reduce} = maybeValue(options[key]);
options[key] = value;
if (reduce) {
reduce = maybeReduce(reduce);
maps.push([key, d => (d[0] = reduce(d), d)]);
}
}
}
if (maps.length > 0) {
options = map(Object.fromEntries(maps), options);
}
}
return 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;
}