Skip to content

Commit ee9aed7

Browse files
committed
add linearRegression
1 parent 890e727 commit ee9aed7

12 files changed

+1004
-23
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"@observablehq/plot": "./src/index.js"
3030
},
3131
"devDependencies": {
32-
"d3": "^6.3.1",
32+
"d3": "^6.4.0",
3333
"eslint": "^7.12.1",
3434
"esm": "^3.2.25",
3535
"js-beautify": "^1.13.0",

src/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {group, groupX, groupY} from "./marks/group.js";
1010
export {Line, line, lineX, lineY} from "./marks/line.js";
1111
export {Link, link} from "./marks/link.js";
1212
export {Rect, rect, rectX, rectY} from "./marks/rect.js";
13+
export {linearRegression} from "./marks/regression.js";
1314
export {RuleX, RuleY, ruleX, ruleY} from "./marks/rule.js";
1415
export {stackAreaX, stackAreaY, stackBarX, stackBarY} from "./marks/stack.js";
1516
export {Text, text, textX, textY} from "./marks/text.js";

src/mark.js

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class Mark {
3232
let index, data;
3333
if (this.data !== undefined) {
3434
if (this.transform === identity) { // optimized common case
35-
data = this.data, index = facets !== undefined ? facets : range(data);
35+
data = this.data, index = facets !== undefined ? facets : range(data.length);
3636
} else if (this.transform.length === 2) { // facet-aware transform
3737
({index, data} = this.transform(this.data, facets));
3838
data = arrayify(data);
@@ -46,7 +46,7 @@ export class Mark {
4646
index = [], data = [];
4747
for (const facet of facets) {
4848
const facetData = arrayify(this.transform(take(this.data, facet)), Array);
49-
const facetIndex = facetData === undefined ? undefined : offsetRange(facetData, k);
49+
const facetIndex = facetData === undefined ? undefined : range(k, k + facetData.length);
5050
k += facetData.length;
5151
index.push(facetIndex);
5252
data.push(facetData);
@@ -65,7 +65,7 @@ export class Mark {
6565
}
6666
} else { // basic transform, non-faceted
6767
data = arrayify(this.transform(this.data));
68-
index = data === undefined ? undefined : range(data);
68+
index = data === undefined ? undefined : range(data.length);
6969
}
7070
}
7171
return {
@@ -192,15 +192,15 @@ export function titleGroup(L) {
192192
.text(([i]) => L[i]) : () => {};
193193
}
194194

195-
// Returns a Uint32Array with elements [0, 1, 2, … data.length - 1].
196-
export function range(data) {
197-
return Uint32Array.from(data, indexOf);
198-
}
199-
200-
// Returns a Uint32Array with elements [k, k + 1, … k + data.length - 1].
201-
export function offsetRange(data, k) {
202-
k = Math.floor(k);
203-
return Uint32Array.from(data, (_, i) => i + k);
195+
// Returns a Uint32Array with elements [start, start + 1, start + 2, … stop - 1].
196+
export function range(start, stop) {
197+
if (stop === undefined) stop = start, start = 0;
198+
start = Math.floor(start), stop = Math.floor(stop);
199+
if (!(stop >= start)) throw new Error("invalid range");
200+
const n = stop - start;
201+
const range = new Uint32Array(n);
202+
for (let i = 0; i < n; ++i) range[i] = i + start;
203+
return range;
204204
}
205205

206206
// Returns an array [values[index[0]], values[index[1]], …].

src/marks/regression.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {extent} from "d3-array";
2+
import {defined} from "../defined.js";
3+
import {group} from "../group.js";
4+
import {maybeColor, range, valueof} from "../mark.js";
5+
import {link} from "./link.js";
6+
7+
export function linearRegression(data, {stroke, x, y, z, ...options}) {
8+
let [vstroke, cstroke] = maybeColor(stroke, "currentColor");
9+
if (z === undefined && vstroke != null) z = vstroke;
10+
const X1 = [], Y1 = [], X2 = [], Y2 = [], S = vstroke ? [] : undefined; // lazily populated
11+
return link(data, {
12+
...options,
13+
transform: (data, facets) => {
14+
x = valueof(data, x);
15+
y = valueof(data, y);
16+
z = z !== undefined ? valueof(data, z) : undefined;
17+
if (vstroke !== undefined) vstroke = valueof(data, vstroke);
18+
const [x1, x2] = extent(x);
19+
const index = [];
20+
let offset = 0;
21+
// TODO it’d be nice if faceting didn’t make this complicated
22+
for (let facet of facets === undefined ? [range(data.length)] : facets) {
23+
facet = facet.filter(i => defined(x[i]) && defined(y[i]));
24+
let n = 0;
25+
for (const index of z ? group(facet, z) : [facet]) {
26+
const f = linearRegressionLine(index, x, y);
27+
X1.push(x1), Y1.push(f(x1)), X2.push(x2), Y2.push(f(x2));
28+
if (S) S.push(vstroke[index[0]]);
29+
++n;
30+
}
31+
index.push(range(offset, offset + n));
32+
offset += n;
33+
}
34+
return {index: facets === undefined ? index[0] : index};
35+
},
36+
x1: X1,
37+
y1: Y1,
38+
x2: X2,
39+
y2: Y2,
40+
stroke: cstroke ? cstroke : vstroke ? S : undefined
41+
});
42+
}
43+
44+
function linearRegressionLine(I, X, Y) {
45+
const n = I.length;
46+
if (n === 1) return () => Y[I[0]];
47+
let sx = 0, sy = 0, sxx = 0, sxy = 0;
48+
for (const i of I) {
49+
const x = X[i];
50+
const y = Y[i];
51+
sx += x;
52+
sy += y;
53+
sxx += x * x;
54+
sxy += x * y;
55+
}
56+
const m = (n * sxy - sx * sy) / (n * sxx - sx * sx);
57+
const b = (sy - m * sx) / n;
58+
return x => b + m * x;
59+
}

src/transforms/bin.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {bin as binner, cross} from "d3-array";
2-
import {valueof, first, second, maybeValue, range, offsetRange} from "../mark.js";
2+
import {valueof, first, second, maybeValue, range} from "../mark.js";
33

44
export function bin1(options = {}) {
55
let {value, domain, thresholds, cumulative} = maybeValue(options);
@@ -34,7 +34,7 @@ function binof({value, domain, thresholds}) {
3434
const bin = binner().value(i => values[i]);
3535
if (domain !== undefined) bin.domain(domain);
3636
if (thresholds !== undefined) bin.thresholds(thresholds);
37-
return bin(range(data));
37+
return bin(range(data.length));
3838
};
3939
}
4040

@@ -43,7 +43,7 @@ function rebin(bins, facets, subset, cumulative) {
4343
if (facets === undefined) {
4444
if (cumulative) bins = accumulate(cumulative < 0 ? bins.reverse() : bins);
4545
bins = bins.filter(nonempty);
46-
return {index: range(bins), data: bins};
46+
return {index: range(bins.length), data: bins};
4747
}
4848
const index = [];
4949
const data = [];
@@ -52,7 +52,7 @@ function rebin(bins, facets, subset, cumulative) {
5252
let b = bins.map(facet);
5353
if (cumulative) b = accumulate(cumulative < 0 ? b.reverse() : b);
5454
b = b.filter(nonempty);
55-
index.push(offsetRange(b, k));
55+
index.push(range(k, k + b.length));
5656
data.push(b);
5757
k += b.length;
5858
}

src/transforms/group.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import {groups} from "d3-array";
22
import {defined} from "../defined.js";
3-
import {valueof, maybeValue, range, offsetRange} from "../mark.js";
3+
import {valueof, maybeValue, range} from "../mark.js";
44

55
export function group1(x) {
66
const {value} = maybeValue({value: x});
77
return (data, facets) => {
88
const values = valueof(data, value);
9-
let g = groups(range(data), i => values[i]).filter(defined1);
9+
let g = groups(range(data.length), i => values[i]).filter(defined1);
1010
return regroup(g, facets);
1111
};
1212
}
@@ -17,21 +17,21 @@ export function group2(vx, vy) {
1717
return (data, facets) => {
1818
const valuesX = valueof(data, x);
1919
const valuesY = valueof(data, y);
20-
let g = groups(range(data), i => valuesX[i], i => valuesY[i]).filter(defined1);
20+
let g = groups(range(data.length), i => valuesX[i], i => valuesY[i]).filter(defined1);
2121
g = g.flatMap(([x, xgroup]) => xgroup.filter(defined1).map(([y, ygroup]) => [x, y, ygroup]));
2222
return regroup(g, facets);
2323
};
2424
}
2525

2626
// When faceting, subdivides the given groups according to the facet indexes.
2727
function regroup(groups, facets) {
28-
if (facets === undefined) return {index: range(groups), data: groups};
28+
if (facets === undefined) return {index: range(groups.length), data: groups};
2929
const index = [];
3030
const data = [];
3131
let k = 0;
3232
for (const facet of facets.map(subset)) {
3333
let g = groups.map(facet).filter(nonempty1);
34-
index.push(offsetRange(g, k));
34+
index.push(range(k, k + g.length));
3535
data.push(g);
3636
k += g.length;
3737
}

0 commit comments

Comments
 (0)