Published
Edited
Sep 8, 2021
Importers
1 star
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
html`<svg viewBox="0 0 ${width} 280">${renderGroup(
new Group()
.addPath(libTestData)
.addPath(libTestData.translate(new Vector(libTestData.bounds.x, 0)))
.addPath(libTestData.translate(new Vector(libTestData.bounds.x * 4, 0)))

.rotate(4)
.translate(new Vector(25, 25))
)}</svg>`
Insert cell
Insert cell
Insert cell
class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}

add(otherVector) {
return new Vector(this.x + otherVector.x, this.y + otherVector.y);
}

scale(factor) {
return new Vector(this.x * factor, this.y * factor);
}

rotate(angle) {
const radians = (Math.PI / 180) * angle;
return new Vector(
this.x * Math.cos(radians) - this.y * Math.sin(radians),
this.y * Math.cos(radians) + this.x * Math.sin(radians)
);
}
}
Insert cell
class Path {
constructor() {
this.points = [];
}

get tail() {
return this.points[this.points.length - 1] || new Vector(0, 0);
}

get head() {
return this.points[0] || new Vector(0, 0);
}

addToTail(delta) {
const newPath = new Path();
const point = this.tail.add(delta);
newPath.points = [...this.points, point];
return newPath;
}

// returns a vector describing the relative size of the path...
// another way to describe would be the max delta
get bounds() {
const allX = this.points.map(p => p.x);
const allY = this.points.map(p => p.y);
const deltaX = Math.abs(Math.max(...allX) - Math.min(...allY));
const deltaY = Math.abs(Math.max(...allY) - Math.min(...allY));
return new Vector(deltaX, deltaY);
}

get minPoint() {
const allX = this.points.map(p => p.x);
const allY = this.points.map(p => p.y);
return new Vector(Math.min(...allX), Math.min(...allY));
}

get maxPoint() {
const allX = this.points.map(p => p.x);
const allY = this.points.map(p => p.y);
return new Vector(Math.max(...allX), Math.max(...allY));
}

get center() {
return this.minPoint.add(this.maxPoint).scale(0.5);
}

// adds vector to every one of the points
translate(vector) {
const newPath = new Path();
newPath.points = this.points.map(point => point.add(vector));
return newPath;
}

translateCenterToOrigin() {
return this.translate(this.center.scale(-1));
}

translateMinPointToOrigin() {
return this.translate(this.minPoint.scale(-1));
}

rotate(angle, about = this.center) {
const newPath = new Path();
newPath.points = this.points;

const adjustedToOrigin = newPath.translate(about.scale(-1));

adjustedToOrigin.points = adjustedToOrigin.points.map(point =>
point.rotate(angle)
);
return adjustedToOrigin.translate(about);
}

scale(factor, about = this.center) {
const newPath = new Path();
newPath.points = this.points.map(point => point.scale(factor));
return newPath.translate(about.scale(0.5));
}

join(otherPath, thisDirection = "tail", otherDirection = "head") {
let localPoints = this.points || [];
let otherPoints = otherPath.points || [];

if (thisDirection === "head") {
localPoints = localPoints.reverse();
}

if (otherDirection === "head") {
otherPoints = otherPoints.reverse();
}

const newPath = new Path();
newPath.points = [...localPoints, ...otherPoints];
return newPath;
}

toSimple() {
return this.points.map(vector => [vector.x, vector.y]);
}

static join(...paths) {
return paths.reduce((bigPath, littlePath) => {
return bigPath.join(littlePath);
}, new Path());
}

static translate(paths, vector) {
return paths.map(p => p.translate(vector));
}

static translateMinPointToOrigin(...paths) {
const macro = Path.join(...paths);
const offset = macro.minPoint.scale(-1);
return Path.translate(paths, offset);
}

static bounds(...paths) {
return Path.join(...paths).bounds;
}

static center(...paths) {
return Path.join(...paths).center;
}

static fromSimple(points) {
const newPath = new Path();
newPath.points = points.map(([x, y]) => new Vector(x, y));
return newPath;
}
}
Insert cell
class Group {
constructor() {
this.paths = [];
}

get _compoundPath() {
return Path.join(...this.paths);
}

get center() {
return this._compoundPath.center;
}
get maxPoint() {
return this._compoundPath.maxPoint;
}
get bounds() {
return this._compoundPath.bounds;
}
get minPoint() {
return this._compoundPath.minPoint;
}

_clone() {
const newGroup = new Group();
newGroup.paths = [...this.paths];
return newGroup;
}

_mapPaths(mapper) {
const newGroup = this._clone();
newGroup.paths = newGroup.paths.map(mapper);
return newGroup;
}

addPath(path) {
const newGroup = this._clone();
newGroup.paths.push(path);
return newGroup;
}

translate(vector) {
return this._mapPaths(path => path.translate(vector));
}

static merge(...groups) {
const newGroup = new Group();
for (const group of groups) {
newGroup.paths.push(...group.paths);
}
return newGroup;
}

translateMinPointToOrigin() {
const offset = this._compoundPath.minPoint.scale(-1);
return this.translate(offset);
}

scale(factor, about = this.center) {
return this._mapPaths(path => path.scale(factor, about));
}

rotate(angle, about = this.center) {
return this._mapPaths(path => path.rotate(angle, about));
}
}
Insert cell
function renderGroup(group) {
return `
<g>
${group.paths.map(path => `
<path d="${renderPath(path)}" fill="none" stroke="orange" stroke-width="1" stroke-miterlimit="1" />
`)}
</g>

`
}
Insert cell
Insert cell
libTestData = new Path()
.addToTail(new Vector(0, 0))
.addToTail(new Vector(1, 0))
.addToTail(new Vector(0, 1.5))
.addToTail(new Vector(-1, 0))
.addToTail(new Vector(0, -1.5))
.translate(new Vector(0.1, 0.1))
.scale(140)
Insert cell
scaledDown = libTestData.scale(0.5)
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more