class Plot {
constructor(config = {}) {
if (!config.dimensions) config.dimensions = {};
this.dimensions = {
width: config.dimensions.width || 600,
height: config.dimensions.height || 375
};
if (!config.margin) config.margin = {};
this.margin = {
top: config.margin.top || config.margin.y || 30,
right: config.margin.right || config.margin.x || 30,
bottom: config.margin.bottom || config.margin.y || 30,
left: config.margin.left || config.margin.x || 30
};
this.dimensions.innerWidth =
this.dimensions.width - this.margin.left - this.margin.right;
this.dimensions.innerHeight =
this.dimensions.height - this.margin.top - this.margin.bottom;
if (!config.domain || !config.domain.x || !config.domain.y)
throw new Error('Pass the domain in the config!');
this.domain = config.domain;
if (!config.titles) config.titles = {};
this.titles = config.titles;
this.initWrapper(config.outputId);
this.initSvg();
this.initScales();
// "this.properties" are not available...!
// Example: Instead of `svg.call(this.drawAxes);` do this:
this.initAxes();
this.initTitle();
return this;
}
show() {
return this.wrapper.node();
}
initWrapper(outputId) {
if (outputId) {
this.wrapper = d3
.select(outputId)
.selectAll("*")
.remove();
} else {
this.wrapper = d3.create("div");
}
this.wrapper
.style("position", "relative")
.style("width", `${this.dimensions.width}px`)
.style("height", `${this.dimensions.height}px`);
}
initSvg() {
this.svg = d3
.create("svg")
.attr("viewBox", [0, 0, this.dimensions.width, this.dimensions.height])
.style("width", `${100}%`)
.style("height", `${100}%`)
.style("position", "absolute")
.style("left", 0)
.style("top", 0)
.style("z-index", '1000');
this.wrapper.node().appendChild(this.svg.node());
}
initScales() {
this.scales = {
x: d3
.scaleLinear()
.domain(this.domain.x)
.range([this.margin.left, this.dimensions.width - this.margin.right]),
y: d3
.scaleLinear()
.domain(this.domain.y)
.range([this.dimensions.height - this.margin.bottom, this.margin.top]),
color: d3
.scaleLinear()
.range(["#000000", "#ffffff"])
.domain([0, 1])
};
}
initTitle() {
this.svg
.append("g")
.attr(
"transform",
`translate(${this.margin.left + this.dimensions.innerWidth / 2}, 25)`
)
.attr("font-family", "sans-serif")
.attr("font-size", 18)
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(this.titles.figure);
}
initAxes(selection) {
this.svg
.append("g")
.attr(
"transform",
`translate(0,${this.dimensions.height - this.margin.bottom})`
)
.call(d3.axisBottom(this.scales.x))
.append("g")
.attr(
"transform",
`translate(${this.margin.left + this.dimensions.innerWidth / 2}, 25)`
)
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(this.titles.x);
this.svg
.append("g")
.attr("transform", `translate(${this.margin.left},0)`)
.call(d3.axisLeft(this.scales.y))
.append("g")
.attr(
"transform",
`rotate(-90 0 0)
translate(-${this.margin.top + this.dimensions.innerHeight / 2}, -25)`
)
.append("text")
.attr("x", 0)
.attr("y", 0)
.attr("text-anchor", "middle")
.attr("fill", "black")
.text(this.titles.y);
}
scatter(x, y, config) {
var data;
var color;
if (!config.color) {
data = d3.zip(x, y);
color = r => "red";
} else if (typeof config.color === 'string') {
data = d3.zip(x, y);
color = r => config.color;
} else if (typeof config.color === 'array') {
data = d3.zip(x, y, config.color);
color = r => r[2];
} else if (typeof config.color === 'object') {
data = d3.zip(x, y, config.color);
color = r => r[2];
} else if (isFunction(config.color)) {
data = d3.zip(x, y);
color = config.color;
} else {
console.error("Not supported:", typeof config.color);
return;
}
if (!config.strokeColor) config.strokeColor = "red";
this.svg
.append("g")
.selectAll("circle")
.data(data)
.join("circle")
.attr("cx", r => this.scales.x(parseFloat(r[0])))
.attr("cy", r => this.scales.y(parseFloat(r[1])))
.attr("fill", r => color(r))
.attr("stroke", config.strokeColor)
.attr("stroke-width", config.strokeWidth || 3)
.attr("r", config.size || 3);
}
func(f, config) {
if (!config) {
config = {};
}
if (!isFunction(f)) {
console.error("Not supported:", typeof f);
return;
}
var self = this;
const x_begin = this.scales.x.domain()[0],
x_end = this.scales.x.domain()[1] - 0.0001,
dx = (x_begin + x_end) / 100,
x_borders = d3.range(x_begin, x_end + dx, dx);
this.svg
.append("path")
.datum(x_borders)
.attr("fill", "none")
.attr("stroke", config.color || "black")
.attr("stroke-width", config.strokeWidth || 3)
.attr(
"d",
d3
.line()
.x(function(d) {
return self.scales.x(d);
})
.y(function(d) {
return self.scales.y(f(d));
})
);
}
heatmap(linearMap) {
var self = this,
selection = this.svg;
const // Initialize the variables
width = this.dimensions.width - this.margin.left - this.margin.right,
height = this.dimensions.height - this.margin.top - this.margin.bottom,
nx = 50,
ny = Math.floor(nx * (height / width)),
x_begin = this.scales.x.domain()[0],
x_end = this.scales.x.domain()[1],
y_begin = this.scales.y.domain()[0],
y_end = this.scales.y.domain()[1],
dx = (x_begin + x_end) / nx,
dy = (y_begin + y_end) / ny,
x_borders = d3.range(x_begin, x_end, dx),
y_borders = d3.range(y_end, y_begin, -dy);
// Compute the pixel colors; scaled by CSS.
// source: https://github.com/tensorflow/playground -> heatmap.js
let canvas = this.wrapper
.append("canvas")
.attr("width", nx)
.attr("height", ny)
.style(
"width",
`${(this.dimensions.innerWidth / this.dimensions.width) * 100}%`
)
.style(
"height",
`${(this.dimensions.innerHeight / this.dimensions.height) * 100}%`
)
.style("position", "absolute")
.style("top", `${(this.margin.top / this.dimensions.height) * 100}%`)
.style("left", `${(this.margin.left / this.dimensions.width) * 100}%`)
.style("z-index", '999');
let context = canvas.node().getContext("2d");
let image = context.createImageData(nx, ny);
let pointer = -1;
y_borders.forEach(function(y) {
x_borders.forEach(function(x) {
let color = d3.rgb(self.scales.color(linearMap(x, y)));
image.data[++pointer] = color.r;
image.data[++pointer] = color.g;
image.data[++pointer] = color.b;
image.data[++pointer] = (color.opacity * 255) / 2;
});
});
context.putImageData(image, 0, 0);
}
treemap(tree_input) {
var self = this,
selection = this.svg,
tree = new Tree(tree_input);
const // Initialize the variables
width = this.dimensions.width - this.margin.left - this.margin.right,
height = this.dimensions.height - this.margin.top - this.margin.bottom,
nx = this.dimensions.innerWidth,
ny = this.dimensions.innerHeight,
x_begin = this.scales.x.domain()[0],
x_end = this.scales.x.domain()[1] - 0.0001,
y_begin = this.scales.y.domain()[0],
y_end = this.scales.y.domain()[1] - 0.0001,
dx = (x_begin + x_end) / nx,
dy = (y_begin + y_end) / ny,
x_borders = d3.range(x_begin, x_end, dx),
y_borders = d3.range(y_end, y_begin, -dy);
// Compute the pixel colors; scaled by CSS.
// source: https://github.com/tensorflow/playground -> heatmap.js
let canvas = this.wrapper
.append("canvas")
.attr("width", nx)
.attr("height", ny)
.style(
"width",
`${(this.dimensions.innerWidth / this.dimensions.width) * 100}%`
)
.style(
"height",
`${(this.dimensions.innerHeight / this.dimensions.height) * 100}%`
)
.style("position", "absolute")
.style("top", `${(this.margin.top / this.dimensions.height) * 100}%`)
.style("left", `${(this.margin.left / this.dimensions.width) * 100}%`)
.style("z-index", '999');
let context = canvas.node().getContext("2d");
let image = context.createImageData(nx, ny);
let pointer = -1;
y_borders.forEach(function(y) {
x_borders.forEach(function(x) {
let color = d3.rgb(self.scales.color(tree.predict(x, y)));
image.data[++pointer] = color.r;
image.data[++pointer] = color.g;
image.data[++pointer] = color.b;
image.data[++pointer] = (color.opacity * 255) / 2;
});
});
context.putImageData(image, 0, 0);
}
}