Skip to content

Instantly share code, notes, and snippets.

@Tythos
Created April 6, 2020 18:16
Show Gist options
  • Save Tythos/92b7d2c60b3f928e40765e264a510332 to your computer and use it in GitHub Desktop.
Save Tythos/92b7d2c60b3f928e40765e264a510332 to your computer and use it in GitHub Desktop.
Single-file JavaScript module: MATLAB- (or matplotlib-) like plotting capabilities, wrapping d3 for easy and reusable charts
/* https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.plot
Once figure/axis handle is initialized, the dependency tree for new series
and handle property modifications is, order of update required:
#. <svg/> dimensions
#. margin
#. title, xlabel, ylabel positions
#. x/y scales
#. x/y axes
#. point positions
#. line positions
*/
define(function(require, exports, module) {
let d3 = require("lib/d3-v4.2.6");
exports.Figure = class {
constructor(svg) {
/* Constructs a new fxp figure/axis/plot object around a given SVG element.
*/
this.svg = d3.select(svg);
this.body = this.svg.append("g");
this.margin = { top: 0.05, right: 0.05, bottom: 0.30, left: 0.15 };
this.series = [];
let w = parseInt(this.svg.attr("width"));
if (!w) {
w = 640;
this.svg.attr("width", w + "px");
}
let h = parseInt(this.svg.attr("height"));
if (!h) {
h = 480;
this.svg.attr("height", h + "px");
}
let width = w * (1 - this.margin.left - this.margin.right);
let height = h * (1 - this.margin.top - this.margin.bottom);
this.xScale = d3.scaleLinear().domain([0, 1]).range([0, width]);
this.yScale = d3.scaleLinear().domain([0, 1]).range([height, 0]);
let gLeft = w * this.margin.left;
let gTop = h * this.margin.top;
this.body.attr("transform", "translate(" + gLeft + "," + gTop + ")");
// Add title
this.svg.append("text")
.classed("fxpTitle", true)
.text("The Title")
.attr("transform", "translate(" + (0.5 * w) + "," + (0.5 * h * this.margin.top) + ")")
.attr("dominant-baseline", "center")
.attr("text-anchor", "middle");
this.svg.append("text")
.classed("fxpXlabel", true)
.text("The X Axis")
.attr("transform", "translate(" + (this.margin.left * w + 0.5 * width) + "," + (this.margin.top * h + height + this.margin.bottom * 0.5 * h) + ")")
.attr("dominant-baseline", "center")
.attr("text-anchor", "middle");
this.svg.append("text")
.classed("fxpYlabel", true)
.text("The Y Axis")
.attr("transform", "translate(" + (0.5 * this.margin.left * w) + "," + (this.margin.top * h + 0.5 * height) + ") rotate(-90)")
.attr("dominant-baseline", "center")
.attr("text-anchor", "middle");
// Define axes
this.xAxis = this.body.append("g")
.attr("class", "axis axis--x")
.attr("transform", "translate(0," + height + ")");
this.yAxis = this.body.append("g")
.attr("class", "axis axis-y");
return this;
}
updateScales(newData) {
/* Updates the xScale and yScale domains based on the data in existing
selections and the new dataset provided here. Also updates plot properties
that depend upon the scales, such as axes.
*/
let xMin = d3.min(newData, function(d) { return d[0]; });
let xMax = d3.max(newData, function(d) { return d[0]; });
let yMin = d3.min(newData, function(d) { return d[1]; });
let yMax = d3.max(newData, function(d) { return d[1]; });
let xLim = this.series.length > 0 ? this.xScale.domain() : [xMin,xMax];
let yLim = this.series.length > 0 ? this.yScale.domain() : [yMin,yMax];
if (xMin < xLim[0]) {
xLim[0] = xMin;
}
if (xLim[1] < xMax) {
xLim[1] = xMax;
}
if (yMin < yLim[0]) {
yLim[0] = yMin;
}
if (yLim[1] < yMax) {
yLim[1] = yMax;
}
this.xScale.domain(xLim);
this.yScale.domain(yLim);
this.xAxis.call(d3.axisBottom(this.xScale).ticks(7));
this.yAxis.call(d3.axisLeft(this.yScale).ticks(5));
}
moveSeries() {
/* Recomputes position of each item in each series based on scales that have
likely changed.
*/
this.series.forEach(function(series) {
if (series.classed("PointSeries")) {
series.selectAll("circle")
.attr("cx", function(d) { return this.xScale(d[0]); }.bind(this))
.attr("cy", function(d) { return this.yScale(d[1]); }.bind(this));
} else if (series.classed("LineSeries")) {
let line = d3.line()
.x(function(d) { return this.xScale(d[0]); }.bind(this))
.y(function(d) { return this.yScale(d[1]); }.bind(this));
//.curve(d3.curveBasis); // easy eay to implement curved line series
series.select("path").attr("d", line);
} else {
console.warn("Unrecognized series class, ignoring for move");
}
}, this);
}
scatter(data) {
/* Adds a point series to the figure. Returns the d3 selection of all points
(circles) for any additional modification.
*/
this.updateScales(data);
this.moveSeries();
let series = this.body.append("g")
.attr("class", "PointSeries");
let points = series.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("cx", function(d) { return this.xScale(d[0]); }.bind(this))
.attr("cy", function(d) { return this.yScale(d[1]); }.bind(this))
.attr("r", 4);
this.series.push(series);
return series;
}
plot(x, y) {
/* Adds a line series to the figure. Returns the d3 eelection of all line
segments for any additional modification. Some default styling is included
to make sure it isn't rendered as closed path.
*/
if (x.length != y.length) { console.error("Size of X and Y datasets must be identical"); }
let data = x.map(function(v, i) { return [v, y[i]]; });
this.updateScales(data);
this.moveSeries();
let series = this.body.append("g")
.attr("class", "LineSeries")
.attr("fill", "none")
.attr("stroke", "black");
let line = d3.line()
.x(function(d) { return this.xScale(d[0]); }.bind(this))
.y(function(d) { return this.yScale(d[1]); }.bind(this));
//.curve(d3.curveBasis); // easy way to implement curved line series
let path = series.append("path")
.datum(data)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("stroke-width", 1.5)
.attr("d", line);
this.series.push(series);
return series;
}
title(value) {
/* Accesssor for text value of figure title.
*/
let text = this.svg.select(".fxpTitle");
if (value) {
text.text(value);
return this;
} else {
return text.text();
}
}
xLabel(value) {
/* Accesssor for text value of x axis label.
*/
let text = this.svg.select(".fxpXlabel");
if (value) {
text.text(value);
return this;
} else {
return text.text();
}
}
yLabel(value) {
/* Accesssor for text value of y axis label.
*/
let text = this.svg.select(".fxpYlabel");
if (value) {
text.text(value);
return this;
} else {
return text.text();
}
}
}
return Object.assign(exports, {
"__uni__": "com.github.tythos.fxp",
"__semver__": "1.1.0",
"__author__": "code@tythos.net"
})
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment