Published
Edited
Jul 6, 2020
1 fork
Importers
Insert cell
Insert cell
{
var // Create an array for x and y components and the colors
x = points.map(r => r.x),
y = points.map(r => r.y),
colors = points.map(r => (r.z == 0 ? "red" : "blue"));

// Create a new plot instance
var plot = new Plot({
margin: {
x: 142.5,
y: 30
},
domain: { x: [0, 10], y: [0, 10] },
titles: {
figure: 'Plot title',
x: 'x axis title',
y: 'y axis title'
}
});

plot.scales.color = d3
.scaleLinear()
.range(["#ff0000", "#ffffff", "#0000ff"])
.domain([0, 1, 2]);

// Add data points to the plot as scatter
plot.scatter(x, y, {
// Colors can be added as string, array or function
//color: "green",
//color: ["red", "green"], // only the first two points will be added
color: colors,
//color: r => (r[0] < 5 ? "green" : "yellow"),
// The radius of the dots in px (of the svg) (default: 3)
size: 10,
strokeColor: r => (r[0] > 5 ? "darkred" : "black"),
strokeWidth: 5
});

plot.func(
function(x) {
return (x * x) / 10;
},
{ color: "black", strokeWidth: 3 }
);

// Draw a heatmap on the visible surface of the plot
plot.heatmap(function(x, y) {
return 0.1 * x + 0.1 * y + 0;
});

// Return the wrapper node
return plot.show();
}
Insert cell
Insert cell
{
// Create a new plot instance
var plot = new Plot({
margin: {
x: 142.5,
y: 30
},
domain: { x: [0, 10], y: [0, 10] }
});

plot.scales.color = d3
.scaleLinear()
.range(["#ff0000", "#00ff00"])
.domain([1, 0]);

// Draw a heatmap on the visible surface of the plot
plot.treemap({
depth: 3,
domain: { x: [0, 10], y: [0, 10] },
data: [[0, 0, 0, 0], [1, 0, 1, 1], [0, 0, 0, 0], [0, 0, 1, 0]]
});

// Return the wrapper node
return plot.show();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class Tree {
constructor(tree) {
this.tree = tree || {
depth: 1,
domain: {
x: [0, 10],
y: [0, 10]
},
data: [[0]]
};
this.errors = { train: NaN, test: NaN };

//return this;
}

getTree() {
return this.tree;
}

predict(x, y) {
var dx =
(this.tree.domain.x[1] - this.tree.domain.x[0]) /
Math.pow(2, this.tree.depth - 1);
var dy =
(this.tree.domain.y[1] - this.tree.domain.y[0]) /
Math.pow(2, this.tree.depth - 1);

var datax = Math.floor(x / dx);
var datay = Math.floor(y / dy);

return this.tree.data[datay][datax];
}

fit(train, test) {
var self = this,
n = Math.pow(2, self.tree.depth - 1),
dx = (self.tree.domain.x[1] - self.tree.domain.x[0]) / n,
dy = (self.tree.domain.y[1] - self.tree.domain.y[0]) / n;

var negative = Array(n)
.fill()
.map(() => Array(n).fill(0));
var positive = Array(n)
.fill()
.map(() => Array(n).fill(0));
this.tree.data = Array(n)
.fill()
.map(() => Array(n).fill(0));

train.forEach(function(r) {
var datax = Math.floor(r.x / dx);
var datay = Math.floor(r.y / dy);

if (r.color == -1) negative[datay][datax] += 1;
else positive[datay][datax] += 1;
});

for (var i = 0; i < n; i++) {
for (var j = 0; j < n; j++) {
if (positive[j][i] == 0 && negative[j][i] == 0)
this.tree.data[j][i] = 0;
else if (positive[j][i] == negative[j][i]) this.tree.data[j][i] = 0;
else if (positive[j][i] > negative[j][i]) this.tree.data[j][i] = 1;
else if (positive[j][i] < negative[j][i]) this.tree.data[j][i] = -1;
}
}

// Calculate the Training Error
var counter = 0;
this.errors.train = 0;

train.forEach(function(r) {
var prediction = self.predict(r.x, r.y) > 0 ? 1 : -1;
if (prediction != r.color) self.errors.train += 1;
counter++;
});

this.errors.train /= counter;
this.errors.train = this.errors.train.toFixed(3);

return this;
}

calculateTestError(test) {
var self = this,
n = Math.pow(2, self.tree.depth - 1),
dx = (self.tree.domain.x[1] - self.tree.domain.x[0]) / n,
dy = (self.tree.domain.y[1] - self.tree.domain.y[0]) / n,
counter = 0;
this.errors.test = 0;

test.forEach(function(r) {
var prediction = self.predict(r.x, r.y) > 0 ? 1 : -1;
if (prediction != r.color) self.errors.test += 1;
counter++;
});

this.errors.test /= counter;
this.errors.test = this.errors.test.toFixed(3);

return this;
}

getErrors() {
return this.errors;
}
}
Insert cell
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;

// TODO: Add a auto-configurability of the domain
// Maybe with d3.extent?
// Problem of redrawing once data is added.
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();

// WARNING: when a function is called with svg.call(this.functionName)
// "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);
}
}
Insert cell
function isFunction(obj) {
// source: https://stackoverflow.com/a/6000016
return !!(obj && obj.constructor && obj.call && obj.apply);
}
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