@jsmlt/jsmlt
Version:
JavaScript Machine Learning
543 lines (462 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports["default"] = void 0;
var _datapoint = _interopRequireDefault(require("./datapoint"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; }
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _slicedToArray(arr, i) { return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest(); }
function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance"); }
function _iterableToArrayLimit(arr, i) { if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) { return; } 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"] != null) _i["return"](); } finally { if (_d) throw _e; } } return _arr; }
function _arrayWithHoles(arr) { if (Array.isArray(arr)) return arr; }
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; }
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a 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); } }
function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }
/**
* 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 =
/*#__PURE__*/
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 = _objectSpread({}, 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 = {}; // 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 = new 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(void 0, 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 _datapoint["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 _this2$transformAbsol = _this2.transformAbsolutePositionToRelativePosition(e.clientX, e.clientY);
var _this2$transformAbsol2 = _slicedToArray(_this2$transformAbsol, 2);
_this2.mouseX = _this2$transformAbsol2[0];
_this2.mouseY = _this2$transformAbsol2[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) {
// Handle screen resizing for obtaining correct coordinates
this.resize(); // 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 _this$convertCanvasCo = this.convertCanvasCoordinatesToFeatures(clickX, clickY),
_this$convertCanvasCo2 = _slicedToArray(_this$convertCanvasCo, 2),
px = _this$convertCanvasCo2[0],
py = _this$convertCanvasCo2[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();
}
/**
* 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) {
// 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 _this$convertFeatures = this.convertFeaturesToCanvasCoordinates(0, 0),
_this$convertFeatures2 = _slicedToArray(_this$convertFeatures, 2),
originX = _this$convertFeatures2[0],
originY = _this$convertFeatures2[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 _this4$convertFeature = _this4.convertFeaturesToCanvasCoordinates.apply(_this4, _toConsumableArray(_this4.convertBoundaryCoordinatesToFeatures(boundaryPoint[0], boundaryPoint[1]))),
_this4$convertFeature2 = _slicedToArray(_this4$convertFeature, 2),
xx = _this4$convertFeature2[0],
yy = _this4$convertFeature2[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 classIndex === null ? '#DDDDDD' : 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;