@hhenrichsen/motion-canvas-graphing
Version:
Some graphing components for Motion Canvas.
340 lines (330 loc) • 13.1 kB
JavaScript
import { initial, vector2Signal, canvasStyleSignal, signal, computed, Layout, resolveCanvasStyle, drawRect, parser, Line } from '@motion-canvas/2d';
import { Vector2, BBox, range, clamp, useLogger } from '@motion-canvas/core';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
/* global Reflect, Promise */
function __decorate(decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
}
class Plot extends Layout {
labelFormatterX;
labelFormatterY;
edgePadding() {
return this.labelSize()
.add(this.labelPadding())
.add(this.tickLabelSize().mul([Math.log10(this.max().y) + 1, 2]))
.add(this.tickOverflow())
.add(this.axisStrokeWidth());
}
constructor(props) {
super(props);
this.labelFormatterX = props.labelFormatterX ?? (x => x.toFixed(0));
this.labelFormatterY = props.labelFormatterY ?? (y => y.toFixed(0));
}
cacheBBox() {
return BBox.fromSizeCentered(this.size().add(this.edgePadding().mul(2)));
}
draw(context) {
const halfSize = this.computedSize().mul(-0.5);
for (let i = 0; i <= this.ticks().floored.x; i++) {
const startPosition = halfSize.add(this.computedSize().mul([i / this.ticks().x, 1]));
context.beginPath();
context.moveTo(startPosition.x, startPosition.y +
this.tickOverflow().x +
this.axisStrokeWidth().x / 2 +
this.axisStrokeWidth().x / 2);
context.lineTo(startPosition.x, halfSize.y);
context.strokeStyle = resolveCanvasStyle(this.axisColorX(), context);
context.lineWidth = this.gridStrokeWidth().x;
context.stroke();
context.fillStyle = resolveCanvasStyle(this.axisTextColorX(), context);
context.font = `${this.tickLabelSize().y}px sans-serif`;
context.textAlign = 'center';
context.textBaseline = 'top';
context.fillText(`${this.labelFormatterX(this.mapToX(i / this.ticks().x))}`, startPosition.x, startPosition.y +
this.axisStrokeWidth().x +
this.tickOverflow().x +
Math.floor(this.tickPadding().x / 2));
}
for (let i = 0; i <= this.ticks().floored.y; i++) {
const startPosition = halfSize.add(this.computedSize().mul([1, 1 - i / this.ticks().y]));
context.beginPath();
context.moveTo(startPosition.x, startPosition.y);
context.lineTo(halfSize.x - this.tickOverflow().y, startPosition.y);
context.strokeStyle = resolveCanvasStyle(this.axisColorY(), context);
context.lineWidth = this.gridStrokeWidth().y;
context.stroke();
context.fillStyle = resolveCanvasStyle(this.axisTextColorY(), context);
context.font = `${this.tickLabelSize().y}px ${this.fontFamily()}`;
context.textAlign = 'right';
context.textBaseline = 'middle';
context.fillText(`${this.labelFormatterY(this.mapToY(i / this.ticks().y))}`, halfSize.x -
this.axisStrokeWidth().y -
this.tickOverflow().y -
Math.floor(this.tickPadding().y / 2), startPosition.y);
}
context.beginPath();
const yAxisStartPoint = this.getPointFromPlotSpace([0, this.min().y]);
const yAxisEndPoint = this.getPointFromPlotSpace([0, this.max().y]);
context.moveTo(yAxisStartPoint.x - this.gridStrokeWidth().y / 2, yAxisStartPoint.y - this.gridStrokeWidth().y / 2);
context.lineTo(yAxisEndPoint.x - this.gridStrokeWidth().y / 2, yAxisEndPoint.y + this.gridStrokeWidth().y / 2);
context.strokeStyle = resolveCanvasStyle(this.axisColorX(), context);
context.lineWidth = this.axisStrokeWidth().x;
context.stroke();
context.beginPath();
const xAxisStartPoint = this.getPointFromPlotSpace([this.min().x, 0]);
const xAxisEndPoint = this.getPointFromPlotSpace([this.max().x, 0]);
context.moveTo(xAxisStartPoint.x - this.gridStrokeWidth().x / 2, xAxisStartPoint.y + this.gridStrokeWidth().x / 2);
context.lineTo(xAxisEndPoint.x + this.gridStrokeWidth().x / 2, xAxisEndPoint.y + this.gridStrokeWidth().x / 2);
context.strokeStyle = resolveCanvasStyle(this.axisColorY(), context);
context.lineWidth = this.axisStrokeWidth().y;
context.stroke();
// Draw X axis label
context.fillStyle = resolveCanvasStyle(this.axisTextColorX(), context);
context.font = `${this.labelSize().y}px ${this.fontFamily()}`;
context.textAlign = 'center';
context.textBaseline = 'alphabetic';
context.fillText(this.labelX(), 0, -halfSize.y +
this.axisStrokeWidth().x +
this.tickOverflow().x +
this.tickLabelSize().x +
this.tickPadding().x +
Math.floor(this.labelPadding().x) +
this.labelSize().x);
// Draw rotated Y axis label
context.fillStyle = resolveCanvasStyle(this.axisTextColorY(), context);
context.font = `${this.labelSize().y}px ${this.fontFamily()}`;
context.textAlign = 'center';
context.textBaseline = 'alphabetic';
context.save();
context.translate(halfSize.x -
this.axisStrokeWidth().y -
this.tickOverflow().y -
this.tickLabelSize().y -
this.tickPadding().y -
Math.floor(this.labelPadding().y / 2) -
this.labelSize().y, 0);
context.rotate(-Math.PI / 2);
context.fillText(this.labelY(), 0, 0);
context.restore();
if (this.clip()) {
context.clip(this.getPath());
}
this.drawChildren(context);
}
getPath() {
const path = new Path2D();
const box = BBox.fromSizeCentered(this.size());
drawRect(path, box);
return path;
}
getPointFromPlotSpace(point) {
const bottomLeft = this.computedSize().mul([-0.5, 0.5]);
return this.toRelativeGridSize(point)
.mul([1, -1])
.mul(this.computedSize())
.add(bottomLeft);
}
mapToX(value) {
return this.min().x + value * (this.max().x - this.min().x);
}
mapToY(value) {
return this.min().y + value * (this.max().y - this.min().y);
}
toRelativeGridSize(p) {
return new Vector2(p).sub(this.min()).div(this.max().sub(this.min()));
}
makeGraphData(resolution, f) {
return range(this.min().x, this.max().x + resolution, resolution).map(x => [
x,
f(x),
]);
}
}
__decorate([
initial(Vector2.zero),
vector2Signal('min')
], Plot.prototype, "min", void 0);
__decorate([
initial(Vector2.one.mul(100)),
vector2Signal('max')
], Plot.prototype, "max", void 0);
__decorate([
initial(Vector2.one.mul(10)),
vector2Signal('ticks')
], Plot.prototype, "ticks", void 0);
__decorate([
initial(Vector2.one.mul(30)),
vector2Signal('labelSize')
], Plot.prototype, "labelSize", void 0);
__decorate([
initial(Vector2.one.mul(5)),
vector2Signal('labelPadding')
], Plot.prototype, "labelPadding", void 0);
__decorate([
initial(Vector2.one.mul(10)),
vector2Signal('tickLabelSize')
], Plot.prototype, "tickLabelSize", void 0);
__decorate([
initial(Vector2.one.mul(5)),
vector2Signal('tickOverflow')
], Plot.prototype, "tickOverflow", void 0);
__decorate([
initial(Vector2.one.mul(6)),
vector2Signal('tickPadding')
], Plot.prototype, "tickPadding", void 0);
__decorate([
initial(Vector2.one.mul(1)),
vector2Signal('gridStrokeWidth')
], Plot.prototype, "gridStrokeWidth", void 0);
__decorate([
initial(Vector2.one.mul(2)),
vector2Signal('axisStrokeWidth')
], Plot.prototype, "axisStrokeWidth", void 0);
__decorate([
initial('white'),
canvasStyleSignal()
], Plot.prototype, "axisColorX", void 0);
__decorate([
initial('white'),
canvasStyleSignal()
], Plot.prototype, "axisTextColorX", void 0);
__decorate([
initial(''),
signal()
], Plot.prototype, "labelX", void 0);
__decorate([
initial('white'),
canvasStyleSignal()
], Plot.prototype, "axisColorY", void 0);
__decorate([
initial('white'),
canvasStyleSignal()
], Plot.prototype, "axisTextColorY", void 0);
__decorate([
initial(''),
signal()
], Plot.prototype, "labelY", void 0);
__decorate([
computed()
], Plot.prototype, "edgePadding", null);
class ScatterPlot extends Layout {
firstIndex() {
return Math.ceil(this.data().length * this.start() + 1);
}
firstPointProgress() {
return this.firstIndex() - this.start() * this.data().length;
}
lastIndex() {
return Math.floor(this.data().length * this.end() - 1);
}
pointProgress() {
return this.end() * this.data().length - this.lastIndex();
}
constructor(props) {
super({
...props,
});
}
draw(context) {
context.save();
context.fillStyle = resolveCanvasStyle(this.pointColor(), context);
const parent = this.parent();
if (!(parent instanceof Plot)) {
useLogger().warn('Using a ScatterPlot outside of a Plot does nothing');
return;
}
if (this.firstIndex() < this.lastIndex()) {
const firstPoint = this.data()[this.firstIndex() - 1];
const coord = parent.getPointFromPlotSpace(firstPoint);
context.beginPath();
context.arc(coord.x, coord.y, this.pointRadius() * this.firstPointProgress(), 0, Math.PI * 2);
context.fill();
}
const data = this.data();
data.slice(this.firstIndex(), this.lastIndex()).forEach(point => {
const coord = parent.getPointFromPlotSpace(point);
context.beginPath();
context.arc(coord.x, coord.y, this.pointRadius(), 0, Math.PI * 2);
context.fill();
});
if (this.lastIndex() > this.firstIndex()) {
const lastPoint = data[this.lastIndex()];
const lastCoord = parent.getPointFromPlotSpace(lastPoint);
context.beginPath();
context.arc(lastCoord.x, lastCoord.y, this.pointRadius() * this.pointProgress(), 0, Math.PI * 2);
context.fill();
}
context.restore();
}
}
__decorate([
initial(5),
signal()
], ScatterPlot.prototype, "pointRadius", void 0);
__decorate([
initial('white'),
canvasStyleSignal()
], ScatterPlot.prototype, "pointColor", void 0);
__decorate([
signal()
], ScatterPlot.prototype, "data", void 0);
__decorate([
initial(0),
parser((value) => clamp(0, 1, value)),
signal()
], ScatterPlot.prototype, "start", void 0);
__decorate([
initial(1),
parser((value) => clamp(0, 1, value)),
signal()
], ScatterPlot.prototype, "end", void 0);
__decorate([
computed()
], ScatterPlot.prototype, "firstIndex", null);
__decorate([
computed()
], ScatterPlot.prototype, "firstPointProgress", null);
__decorate([
computed()
], ScatterPlot.prototype, "lastIndex", null);
__decorate([
computed()
], ScatterPlot.prototype, "pointProgress", null);
class LinePlot extends Line {
constructor(props) {
super({
...props,
points: props.data,
});
}
parsedPoints() {
const parent = this.parent();
if (!(parent instanceof Plot)) {
useLogger().warn('Using a LinePlot outside of a Plot is the same as a Line');
return super.parsedPoints();
}
const data = this.data().map(point => parent.getPointFromPlotSpace(point));
return data;
}
childrenBBox() {
return BBox.fromPoints(...this.parsedPoints());
}
}
__decorate([
signal()
], LinePlot.prototype, "data", void 0);
export { LinePlot, Plot, ScatterPlot };