@jsmlt/jsmlt
Version:
JavaScript Machine Learning
592 lines (480 loc) • 19.4 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); // Internal dependencies
var _datapoint = require('./datapoint');
var _datapoint2 = _interopRequireDefault(_datapoint);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* UI canvas for displaying machine learning results.
*
* Listeners:
* This class supports event listeners, meaning that the outside world can bind functions to events
* triggered explicitly by this class. Listeners can be added using `addListener` and removed by
* `removeListener`. The `emit` method is not intended for use by the outside world, and is used by
* this class to emit an event to the listeners bound to it.
*/
var Canvas = function () {
/**
* Contructor. Load DOM element and user options.
*
* @param {Object} el DOM Canvas element
* @param {Object} [optionsUser] - User-defined options for the canvas
* @param {boolean} [optionsUser.continuousClick = false] - Whether the "click" callback should
* be called any time the mouse is down (true) or only at the moment the mouse button is first
* pressed (false). If true, a click event is emitted every `continuousClickInterval`
* milliseconds when the left mouse button is down
* @param {number} [optionsUser.continuousClickInterval = 50] - Number of milliseconds between
* emitting each click event when `continuousClick` is enabled
* @param {number} [optionsUser.x1 = -2.5] - Left bound of coordinate system for canvas
* @param {number} [optionsUser.y1 = -2.5] - Bottom bound of coordinate system for canvas
* @param {number} [optionsUser.x2 = 2.5] - Right bound of coordinate system for canvas
* @param {number} [optionsUser.y2 = 2.5] - Top bound of coordinate system for canvas
*/
function Canvas(el, optionsUser) {
var _this = this;
_classCallCheck(this, Canvas);
// Options
var optionsDefault = {
continuousClick: false,
continuousClickInterval: 50,
x1: -2.5,
y1: -2.5,
x2: 2.5,
y2: 2.5
};
this.options = _extends({}, optionsDefault, optionsUser);
// Settings for canvas
this.canvas = {
element: el,
context: el.getContext('2d')
};
// Handle canvas resize on window resize
window.addEventListener('resize', function () {
return _this.resize();
});
this.resize();
// Event listeners bound to the canvas
this.listeners = new Map();
// Canvas elements to be drawn
this.elements = [];
// Class boundaries
this.classesBoundaries = {};
// Weights of classifiers
this.weights = null;
this.multiWeights = null;
// Initialization
this.handleMouseEvents();
// Animation
window.requestAnimationFrame(function () {
return _this.refresh();
});
// Temporary properties
this.tmp = {};
this.tmp.predFeatures = [];
this.tmp.predLabels = [];
}
/**
* Add an event listener for events of some type emitted from this object.
*
* @param {string} label - Event identifier
* @param {function} callback - Callback function for when the event is emitted
*/
_createClass(Canvas, [{
key: 'addListener',
value: function addListener(label, callback) {
if (!this.listeners.has(label)) {
this.listeners.set(label, []);
}
this.listeners.get(label).push(callback);
}
/**
* Remove a previously added event listener for events of some type emitted from this object.
*
* @param {string} label - Event identifier
* @param {function} callback - Callback function to remove from event
*/
}, {
key: 'removeListener',
value: function removeListener(label, callback) {
var listeners = this.listeners.get(label);
if (listeners) {
this.listeners.set(label, listeners.filter(function (x) {
return !(typeof x === 'function' && x === callback);
}));
}
}
/**
* Emit an event, which triggers the listener callback functions bound to it.
*
* @param {string} label - Event identifier
* @param {...mixed} args - Remaining arguments contain arguments that should be passed to the
* callback functions
* @return {boolean} Whether any listener callback functions were executed
*/
}, {
key: 'emit',
value: function emit(label) {
for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
args[_key - 1] = arguments[_key];
}
var listeners = this.listeners.get(label);
if (listeners) {
listeners.forEach(function (listener) {
listener.apply(undefined, args);
});
return true;
}
return false;
}
/**
* Add a data point element to the canvas, using a dataset datapoint as its model.
*
* @param {jsmlt.Dataset.Datapoint} datapoint - Dataset datapoint (model)
*/
}, {
key: 'addDatapoint',
value: function addDatapoint(datapoint) {
this.elements.push(new _datapoint2.default(this, datapoint));
}
/**
* Handle mouse events on the canvas, e.g. for adding data points.
*/
}, {
key: 'handleMouseEvents',
value: function handleMouseEvents() {
var _this2 = this;
if (this.options.continuousClick) {
this.mouseStatus = 0;
this.mouseX = 0;
this.mouseY = 0;
this.canvas.element.addEventListener('mousedown', function () {
_this2.mouseStatus = 1;
_this2.continuousClickIntervalId = setInterval(function () {
return _this2.click();
}, _this2.options.continuousClickInterval);
});
document.addEventListener('mouseup', function () {
_this2.mouseStatus = 0;
clearInterval(_this2.continuousClickIntervalId);
});
document.addEventListener('mousemove', function (e) {
var _transformAbsolutePos = _this2.transformAbsolutePositionToRelativePosition(e.clientX, e.clientY);
var _transformAbsolutePos2 = _slicedToArray(_transformAbsolutePos, 2);
_this2.mouseX = _transformAbsolutePos2[0];
_this2.mouseY = _transformAbsolutePos2[1];
});
}
this.canvas.element.addEventListener('mousedown', function (e) {
_this2.click.apply(_this2, _toConsumableArray(_this2.transformAbsolutePositionToRelativePosition(e.clientX, e.clientY)));
});
}
/**
* Transform the absolute position of the mouse in the viewport to the mouse position relative
* to the top-left point of the canvas.
*
* @param {number} x - Absolute mouse x-coordinate within viewport
* @param {number} y - Absolute mouse y-coordinate within viewport
* @return {Array.<number>} Two-dimensional array consisting of relative x- and y-coordinate
*/
}, {
key: 'transformAbsolutePositionToRelativePosition',
value: function transformAbsolutePositionToRelativePosition(x, y) {
// Properties used for calculating mouse position
var el = this.canvas.element;
var rect = el.getBoundingClientRect();
return [x - rect.left, y - rect.top];
}
/**
* Trigger a click at some position in the canvas.
*
* @param {number} [x = -1] - X-coordinate of the click. Defaults to stored mouse position from
* mousemove event
* @param {number} [y = -1] - Y-coordinate of the click. Defaults to stored mouse position from
* mousemove event
*/
}, {
key: 'click',
value: function click() {
var x = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : -1;
var y = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : -1;
// Get click coordinates
var clickX = x;
var clickY = y;
if (x === -1) {
clickX = this.mouseX;
clickY = this.mouseY;
}
// Calculate normalized coordinates with origin in canvas center
var _convertCanvasCoordin = this.convertCanvasCoordinatesToFeatures(clickX, clickY),
_convertCanvasCoordin2 = _slicedToArray(_convertCanvasCoordin, 2),
px = _convertCanvasCoordin2[0],
py = _convertCanvasCoordin2[1];
this.emit('click', px, py);
}
/**
* Clear the canvas.
*/
}, {
key: 'clear',
value: function clear() {
this.canvas.context.clearRect(0, 0, this.canvas.width, this.canvas.height);
}
/**
* Handle the canvas size for different device pixel ratios and on window resizes.
*/
}, {
key: 'resize',
value: function resize() {
this.canvas.element.style.width = '100%';
this.canvas.element.style.height = '100%';
this.canvas.element.width = this.canvas.element.offsetWidth * window.devicePixelRatio;
this.canvas.element.height = this.canvas.element.offsetHeight * window.devicePixelRatio;
this.canvas.width = this.canvas.element.offsetWidth;
this.canvas.height = this.canvas.element.offsetHeight;
this.canvas.context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
/**
* Redraw the canvas, clearing it and drawing all elements on it.
*/
}, {
key: 'redraw',
value: function redraw() {
var _this3 = this;
// Clear canvas
this.clear();
// Basic canvas elements
this.drawGrid();
this.drawAxes();
// Draw dynamic canvas elements
this.elements.forEach(function (element) {
element.draw();
});
// Class boundaries
this.drawClassBoundaries();
// Refresh again
window.requestAnimationFrame(function () {
return _this3.refresh();
});
}
/**
* Refresh (i.e. redraw) everything on the canvas.
*/
}, {
key: 'refresh',
value: function refresh() {
// Dynamic canvas elements
this.elements.forEach(function (element) {
element.update();
});
this.redraw();
}
}, {
key: 'setWeightVector',
value: function setWeightVector(weights) {
this.weights = weights;
}
/**
* Set the class boundaries used for drawing the decision regions on the canvas.
*
* @param {Object<string, Array.<Array.<Array.<number>>>>} classesBoundaries - Class boundaries
* per class label
*/
}, {
key: 'setClassBoundaries',
value: function setClassBoundaries(classesBoundaries) {
this.classesBoundaries = classesBoundaries;
}
/**
* Calculate normalized canvas coordinates, i.e. transform mouse coordinates (relative to the
* canvas origin = top left) to feature space for both x and y. The feature subspace shape is
* determined by the x1, y1, x2, and y2 parameters in the class options (see constructor).
*
* @param {number} x - x-coordinate in canvas
* @param {number} y - y-coordinate in canvas
* @return {Array.<number>} Corresponding point in feature space (first element corresponds to x,
* second element corresponds to y)
*/
}, {
key: 'convertCanvasCoordinatesToFeatures',
value: function convertCanvasCoordinatesToFeatures(x, y) {
// Handle screen resizing for obtaining correct coordinates
this.resize();
// Mouse x- and y-position on [0,1] interval
var f1 = x / this.canvas.width;
var f2 = y / this.canvas.height;
// Convert to [-1,1] interval
f1 = this.options.x1 + f1 * (this.options.x2 - this.options.x1);
f2 = this.options.y1 + (1 - f2) * (this.options.y2 - this.options.y1);
return [f1, f2];
}
/**
* Convert coordinates on a centered, double unit square (i.e., a square from (-1, -1) to (1, 1))
* to feature space.
*
* @param {number} bx - Input x-coordinate in input space
* @param {number} by - Input y-coordinate in input space
* @return {Array.<number>} Corresponding point in feature space (first element corresponds to x,
* second element corresponds to y)
*/
}, {
key: 'convertBoundaryCoordinatesToFeatures',
value: function convertBoundaryCoordinatesToFeatures(bx, by) {
var f1 = this.options.x1 + (bx + 1) / 2 * (this.options.x2 - this.options.x1);
var f2 = this.options.y1 + (by + 1) / 2 * (this.options.y2 - this.options.y1);
return [f1, f2];
}
/**
* Calculate canvas coordinates (origin at (0,0)) for a 2-dimensional data point's features
*
* @param {number} f1 First feature
* @param {number} f2 Second feature
* @return {Array.<number>} Corresponding point in the canvas (first element corresponds to x,
* second element corresponds to y)
*/
}, {
key: 'convertFeaturesToCanvasCoordinates',
value: function convertFeaturesToCanvasCoordinates(f1, f2) {
var x = (f1 - this.options.x1) / (this.options.x2 - this.options.x1);
var y = 1 - (f2 - this.options.y1) / (this.options.y2 - this.options.y1);
return [x * this.canvas.width, y * this.canvas.height];
}
/**
* Draw a grid on the canvas
*/
}, {
key: 'drawGrid',
value: function drawGrid() {
var canvas = this.canvas;
var context = canvas.context;
// Loop over all line offsets
for (var i = 1; i < 10; i += 1) {
// Horizontal
context.beginPath();
context.moveTo(0, i / 10 * canvas.height);
context.lineTo(canvas.width, i / 10 * canvas.height);
context.lineWidth = 1;
context.strokeStyle = '#EAEAEA';
context.stroke();
// Vertical
context.beginPath();
context.moveTo(i / 10 * canvas.width, 0);
context.lineTo(i / 10 * canvas.width, canvas.height);
context.lineWidth = 1;
context.strokeStyle = '#EAEAEA';
context.stroke();
}
}
/**
* Draw the axes on the canvas
*/
}, {
key: 'drawAxes',
value: function drawAxes() {
var canvas = this.canvas;
var context = canvas.context;
// Origin coordinates
var _convertFeaturesToCan = this.convertFeaturesToCanvasCoordinates(0, 0),
_convertFeaturesToCan2 = _slicedToArray(_convertFeaturesToCan, 2),
originX = _convertFeaturesToCan2[0],
originY = _convertFeaturesToCan2[1];
// Horizontal
context.beginPath();
context.moveTo(0, originY);
context.lineTo(canvas.width, originY);
context.lineWidth = 2;
context.strokeStyle = '#CCC';
context.stroke();
// Vertical
context.beginPath();
context.moveTo(originX, 0);
context.lineTo(originX, canvas.height);
context.lineWidth = 2;
context.strokeStyle = '#CCC';
context.stroke();
}
/**
* Draw class boundaries
*/
}, {
key: 'drawClassBoundaries',
value: function drawClassBoundaries() {
var _this4 = this;
var context = this.canvas.context;
Object.keys(this.classesBoundaries).forEach(function (classLabel) {
var classBoundaries = _this4.classesBoundaries[classLabel];
// The path delineates the decision region for this class
context.beginPath();
classBoundaries.forEach(function (classBoundary) {
var firstpoint = true;
classBoundary.forEach(function (boundaryPoint) {
var _convertFeaturesToCan3 = _this4.convertFeaturesToCanvasCoordinates.apply(_this4, _toConsumableArray(_this4.convertBoundaryCoordinatesToFeatures(boundaryPoint[0], boundaryPoint[1]))),
_convertFeaturesToCan4 = _slicedToArray(_convertFeaturesToCan3, 2),
xx = _convertFeaturesToCan4[0],
yy = _convertFeaturesToCan4[1];
if (firstpoint) {
firstpoint = false;
context.moveTo(xx, yy);
} else {
context.lineTo(xx, yy);
}
if (Math.abs(boundaryPoint[0]) !== 1 && Math.abs(boundaryPoint[1]) !== 1) {
context.fillStyle = _this4.getClassColor(classLabel);
context.fillStyle = '#000';
context.globalAlpha = 0.25;
context.globalAlpha = 1;
}
});
context.closePath();
});
context.fillStyle = '#5DA5DA';
context.strokeStyle = '#5DA5DA';
context.fillStyle = _this4.getClassColor(classLabel);
context.strokeStyle = _this4.getClassColor(classLabel);
context.globalAlpha = 0.5;
context.fill();
context.globalAlpha = 1;
});
}
/**
* Get drawing color for a class index.
*
* @param {number} classIndex - Class index
* @return {string} Color in HEX with '#' prefix
*/
}, {
key: 'getClassColor',
value: function getClassColor(classIndex) {
var colors = this.getColors();
return colors[Object.keys(colors)[parseInt(classIndex, 10)]];
}
/**
* Get available drawing colors.
*
* @return <Array.{string}> Colors in HEX with '#' prefix; array keys are color names
*/
}, {
key: 'getColors',
value: function getColors() {
return {
blue: '#5DA5DA',
orange: '#FAA43A',
green: '#60BD68',
pink: '#F17CB0',
brown: '#B2912F',
purple: '#B276B2',
yellow: '#DECF3F',
red: '#F15854',
gray: '#4D4D4D'
};
}
}]);
return Canvas;
}();
exports.default = Canvas;
module.exports = exports['default'];