UNPKG

@jsmlt/jsmlt

Version:

JavaScript Machine Learning

592 lines (480 loc) 19.4 kB
'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'];