-
Notifications
You must be signed in to change notification settings - Fork 198
density contours stroke & fill #948
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 14 commits
ff27647
0448962
9f34cda
39dad32
d97b954
282bc03
21d2b75
e0673aa
e231d38
175ed46
0d206c2
dcc9784
09c20da
d327591
abfd86e
452d148
b59e720
a932e4e
269a70a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,8 @@ | ||
| import {contourDensity, create, geoPath} from "d3"; | ||
| import {constant, maybeTuple} from "../options.js"; | ||
| import {constant, maybeTuple, maybeZ, valueof} from "../options.js"; | ||
| import {Mark} from "../plot.js"; | ||
| import {applyFrameAnchor, applyIndirectStyles, applyTransform} from "../style.js"; | ||
| import {applyFrameAnchor, applyGroupedChannelStyles, applyDirectStyles, applyIndirectStyles, applyTransform, groupZ} from "../style.js"; | ||
| import {initializer} from "../transforms/basic.js"; | ||
|
|
||
| const defaults = { | ||
| ariaLabel: "density", | ||
|
|
@@ -11,44 +12,113 @@ const defaults = { | |
| }; | ||
|
|
||
| export class Density extends Mark { | ||
| constructor(data, options = {}) { | ||
| const {x, y, bandwidth = 20, thresholds = 20} = options; | ||
| constructor(data, {x, y, z, weight, stroke, fill, bandwidth = 20, thresholds = 20, ...options} = {}) { | ||
| let f, s; | ||
| if (fill === "density") { fill = undefined; f = true; } | ||
| if (stroke === "density") { stroke = undefined; s = true; } | ||
| super( | ||
| data, | ||
| [ | ||
| {name: "x", value: x, scale: "x", optional: true}, | ||
| {name: "y", value: y, scale: "y", optional: true} | ||
| {name: "y", value: y, scale: "y", optional: true}, | ||
| {name: "weight", value: weight, optional: true}, | ||
| {name: "z", value: maybeZ({z, fill, stroke}), optional: true} | ||
| ], | ||
| options, | ||
| densityInitializer({...options, fill, stroke}, +bandwidth, +thresholds, f, s), | ||
| defaults | ||
| ); | ||
| this.bandwidth = +bandwidth; | ||
| this.thresholds = +thresholds; | ||
| this.z = z; | ||
| this.path = geoPath(); | ||
| } | ||
| filter(index) { | ||
| return index; | ||
| } | ||
| render(index, scales, channels, dimensions) { | ||
| const {x: X, y: Y} = channels; | ||
| const {bandwidth, thresholds} = this; | ||
| const [cx, cy] = applyFrameAnchor(this, dimensions); | ||
| const {width, height} = dimensions; | ||
| const {contours} = channels; | ||
| const {path} = this; | ||
| return create("svg:g") | ||
| .call(applyIndirectStyles, this, scales, dimensions) | ||
| .call(applyTransform, this, scales) | ||
| .call(g => g.selectAll("path") | ||
| .data(contourDensity() | ||
| .x(X ? i => X[i] : constant(cx)) | ||
| .y(Y ? i => Y[i] : constant(cy)) | ||
| .size([width, height]) | ||
| .bandwidth(bandwidth) | ||
| .thresholds(thresholds) | ||
| (index)) | ||
| .call(g => g.selectAll() | ||
| .data(Array.from(index, i => [i])) | ||
| .enter() | ||
| .append("path") | ||
| .attr("d", geoPath())) | ||
| .node(); | ||
| .call(applyDirectStyles, this) | ||
| .call(applyGroupedChannelStyles, this, channels) | ||
| .attr("d", ([i]) => path(contours[i]))) | ||
| .node(); | ||
| } | ||
| } | ||
|
|
||
| export function density(data, {x, y, ...options} = {}) { | ||
| ([x, y] = maybeTuple(x, y)); | ||
| return new Density(data, {...options, x, y}); | ||
| } | ||
|
|
||
| function densityInitializer(options, bandwidth, thresholds, f, s) { | ||
| return initializer(options, function(data, facets, channels, scales, dimensions) { | ||
| const X = channels.x?.scale ? valueof(channels.x.value, scales[channels.x.scale]) : channels.x?.value; | ||
| const Y = channels.y?.scale ? valueof(channels.y.value, scales[channels.y.scale]) : channels.y?.value; | ||
| const W = channels.weight?.value; | ||
| const Z = channels.z?.value; | ||
| const {z} = this; | ||
| const [cx, cy] = applyFrameAnchor(options, dimensions); | ||
|
||
| const {width, height} = dimensions; | ||
| const newFacets = []; | ||
| const contours = []; | ||
| const newChannels = Object.entries(channels).filter(([key]) => key !== "x" && key !== "y" && key !== "weight").map(([key, d]) => [key, {...d, value: []}]); | ||
| if (f) newChannels.push(["fill", {value: [], scale: "color"}]); | ||
| if (s) newChannels.push(["stroke", {value: [], scale: "color"}]); | ||
| let max = 0, maxn = 0; | ||
| const density = contourDensity() | ||
| .x(X ? i => X[i] : constant(cx)) | ||
| .y(Y ? i => Y[i] : constant(cy)) | ||
| .weight(W ? i => W[i] : 1) | ||
| .size([width, height]) | ||
| .bandwidth(bandwidth) | ||
| .thresholds(thresholds); | ||
|
|
||
| // First pass: seek the maximum density across all facets and series; memoize for performance. | ||
|
||
| const memo = []; | ||
| thresholds = []; | ||
| for (const [facetIndex, facet] of facets.entries()) { | ||
| newFacets.push([]); | ||
| for (const index of Z ? groupZ(facet, Z, z) : [facet]) { | ||
| const c = density(index); | ||
| const d = c[c.length - 1]; | ||
| if (d) { | ||
| if (d.value > max) { | ||
| max = d.value; | ||
| maxn = c.length; | ||
| thresholds = c.map(d => d.value); | ||
| } | ||
| memo.push({facetIndex, index, c, top: d.value}); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Second pass: generate contours with the thresholds derived above | ||
| density.thresholds(thresholds); | ||
| for (const {facetIndex, index, c: memoc, top} of memo) { | ||
| const c = top < max ? density(index) : memoc; | ||
|
||
| for (const contour of c) { | ||
| newFacets[facetIndex].push(contours.length); | ||
| contours.push(contour); | ||
| for (const [key, {value}] of newChannels) { | ||
| value.push( | ||
| (f && key === "fill") || (s && key === "stroke") ? contour.value | ||
| : channels[key].value[index[0]] | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| channels = {contours: {value: contours}, ...Object.fromEntries(newChannels)}; | ||
| // normalize colors to a thresholds scale | ||
| const m = max * (maxn + 1) / maxn; | ||
| if (f) channels.fill.value = channels.fill.value.map(v => v / m); | ||
| if (s) channels.stroke.value = channels.stroke.value.map(v => v / m); | ||
|
||
|
|
||
| return {data, facets: newFacets, channels}; | ||
| }); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we use
applyChannelStylesinstead ofapplyGroupedChannelStyles, we won’t need to do this. (I’ll make this change shortly.)