Created
April 6, 2020 18:16
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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__": "[email protected]" | |
}) | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment