diff --git a/nengo_gui/static/components/2d_axes.ts b/nengo_gui/static/components/2d_axes.ts index 25fe1990..960d325b 100644 --- a/nengo_gui/static/components/2d_axes.ts +++ b/nengo_gui/static/components/2d_axes.ts @@ -14,102 +14,102 @@ import * as d3 from "d3"; export default class Axes2D { -constructor(parent, args) { - var self = this; - - this.max_y_width = 100; - - // Draw the plot as an SVG - this.svg = d3.select(parent).append('svg') - .attr('width', '100%') - .attr('height', '100%'); - - // Scales for mapping x and y values to pixels - this.scale_x = d3.scale.linear(); - this.scale_y = d3.scale.linear(); - this.scale_y.domain([args.min_value, args.max_value]); - - // Spacing between the graph and the outside edges (in pixels) - this.set_axes_geometry(args.width, args.height); - - // Define the x-axis - this.axis_x = d3.svg.axis() - .scale(this.scale_x) - .orient("bottom") - .ticks(2); - - this.axis_x_g = this.svg.append("g") - .attr("class", "axis axis_x unselectable") - .call(this.axis_x); - - // Define the y-axis - this.axis_y = d3.svg.axis() - .scale(this.scale_y) - .orient("left") - .tickValues([args.min_value, args.max_value]); - - this.axis_y_g = this.svg.append("g") - .attr("class", "axis axis_y unselectable") - .call(this.axis_y); -}; - -set_axes_geometry(width, height) { - var scale = parseFloat($('#main').css('font-size')); - this.width = width; - this.height = height; - this.ax_left = this.max_y_width; - this.ax_right = width - 1.75 * scale; - this.ax_bottom = height - 1.75 * scale; - this.ax_top = 1.75 * scale; - - this.tick_size = 0.4 * scale; - this.tick_padding = 0.2 * scale; -}; + constructor(parent, args) { + var self = this; + + this.max_y_width = 100; + + // Draw the plot as an SVG + this.svg = d3.select(parent).append('svg') + .attr('width', '100%') + .attr('height', '100%'); + + // Scales for mapping x and y values to pixels + this.scale_x = d3.scale.linear(); + this.scale_y = d3.scale.linear(); + this.scale_y.domain([args.min_value, args.max_value]); + + // Spacing between the graph and the outside edges (in pixels) + this.set_axes_geometry(args.width, args.height); + + // Define the x-axis + this.axis_x = d3.svg.axis() + .scale(this.scale_x) + .orient("bottom") + .ticks(2); + + this.axis_x_g = this.svg.append("g") + .attr("class", "axis axis_x unselectable") + .call(this.axis_x); + + // Define the y-axis + this.axis_y = d3.svg.axis() + .scale(this.scale_y) + .orient("left") + .tickValues([args.min_value, args.max_value]); + + this.axis_y_g = this.svg.append("g") + .attr("class", "axis axis_y unselectable") + .call(this.axis_y); + }; -/** - * Adjust the graph layout due to changed size - */ -on_resize(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; + set_axes_geometry(width, height) { + var scale = parseFloat($('#main').css('font-size')); + this.width = width; + this.height = height; + this.ax_left = this.max_y_width; + this.ax_right = width - 1.75 * scale; + this.ax_bottom = height - 1.75 * scale; + this.ax_top = 1.75 * scale; + + this.tick_size = 0.4 * scale; + this.tick_padding = 0.2 * scale; }; - this.set_axes_geometry(width, height); - - this.scale_x.range([this.ax_left, this.ax_right]); - this.scale_y.range([this.ax_bottom, this.ax_top]); - - // Adjust positions of x axis on resize - this.axis_x - .tickPadding(this.tick_padding) - .outerTickSize(this.tick_size, this.tick_size); - this.axis_y - .tickPadding(this.tick_padding) - .outerTickSize(this.tick_size, this.tick_size); - - this.axis_x_g.attr("transform", "translate(0," + this.ax_bottom + ")"); - this.axis_x_g.call(this.axis_x); - this.axis_y_g.attr("transform", "translate(" + this.ax_left + ", 0)"); - this.axis_y_g.call(this.axis_y); -}; - -fit_ticks(parent) { - var self = this; - setTimeout(function() { - var ticks = $(parent.div).find('.tick'); - var max_w = 0; - for (var i = 0; i < ticks.length; i++) { - var w = ticks[i].getBBox().width; - if (w > max_w) { - max_w = w; - } + + /** + * Adjust the graph layout due to changed size + */ + on_resize(width, height) { + if (width < this.minWidth) { + width = this.minWidth; } - self.max_y_width = max_w; - self.set_axes_geometry(); - self.on_resize(parent.width, parent.height); - }, 1); -}; + if (height < this.minHeight) { + height = this.minHeight; + }; + this.set_axes_geometry(width, height); + + this.scale_x.range([this.ax_left, this.ax_right]); + this.scale_y.range([this.ax_bottom, this.ax_top]); + + // Adjust positions of x axis on resize + this.axis_x + .tickPadding(this.tick_padding) + .outerTickSize(this.tick_size, this.tick_size); + this.axis_y + .tickPadding(this.tick_padding) + .outerTickSize(this.tick_size, this.tick_size); + + this.axis_x_g.attr("transform", "translate(0," + this.ax_bottom + ")"); + this.axis_x_g.call(this.axis_x); + this.axis_y_g.attr("transform", "translate(" + this.ax_left + ", 0)"); + this.axis_y_g.call(this.axis_y); + }; + + fit_ticks(parent) { + var self = this; + setTimeout(function() { + var ticks = $(parent.div).find('.tick'); + var max_w = 0; + for (var i = 0; i < ticks.length; i++) { + var w = ticks[i].getBBox().width; + if (w > max_w) { + max_w = w; + } + } + self.max_y_width = max_w; + self.set_axes_geometry(); + self.on_resize(parent.width, parent.height); + }, 1); + }; } diff --git a/nengo_gui/static/components/component.ts b/nengo_gui/static/components/component.ts index 41126b1e..d486afc5 100644 --- a/nengo_gui/static/components/component.ts +++ b/nengo_gui/static/components/component.ts @@ -33,287 +33,287 @@ export function save_all_components() { export class Component { -constructor(parent, viewport, args) { - var self = this; - - this.viewport = viewport; - - // Create the div for the component and position it - this.div = document.createElement('div'); - - // Prevent interact from messing up cursor - interact(this.div).styleCursor(true); - - this.x = args.x; - this.y = args.y; - this.w = args.width; - this.h = args.height; - - this.redraw_size(); - this.redraw_pos(); - - this.div.style.position = 'absolute'; - this.div.classList.add('graph'); - parent.appendChild(this.div); - this.parent = parent; - - this.label = document.createElement('div'); - this.label.classList.add('label', 'unselectable'); - this.label.innerHTML = args.label.replace('<', '<').replace('>', '>'); - this.label.style.position = 'fixed'; - this.label.style.width = args.width; - this.label.style.height = '2em'; - this.label_visible = true; - this.div.appendChild(this.label); - if (args.label_visible === false) { - this.hide_label(); - } + constructor(parent, viewport, args) { + var self = this; - self.minWidth = 2; - self.minHeight = 2; + this.viewport = viewport; - // Move element to be drawn on top when clicked on + // Create the div for the component and position it + this.div = document.createElement('div'); - this.div.onmousedown = function() { - this.style.zIndex = utils.next_zindex(); - }; + // Prevent interact from messing up cursor + interact(this.div).styleCursor(true); - this.div.ontouchstart = this.div.onmousedown; + this.x = args.x; + this.y = args.y; + this.w = args.width; + this.h = args.height; - // Allow element to be dragged - interact(this.div) - .draggable({ - inertia: true, - onstart: function() { - menu.hide_any(); - }, - onmove: function(event) { - var target = event.target; + this.redraw_size(); + this.redraw_pos(); - self.x = self.x + event.dx / - (self.viewport.w * self.viewport.scale); - self.y = self.y + event.dy / - (self.viewport.h * self.viewport.scale); + this.div.style.position = 'absolute'; + this.div.classList.add('graph'); + parent.appendChild(this.div); + this.parent = parent; - self.redraw_pos(); + this.label = document.createElement('div'); + this.label.classList.add('label', 'unselectable'); + this.label.innerHTML = args.label.replace('<', '<').replace('>', '>'); + this.label.style.position = 'fixed'; + this.label.style.width = args.width; + this.label.style.height = '2em'; + this.label_visible = true; + this.div.appendChild(this.label); + if (args.label_visible === false) { + this.hide_label(); + } - }, - onend: function(event) { - self.save_layout(); - } - }); + self.minWidth = 2; + self.minHeight = 2; - // Allow element to be resized - interact(this.div) - .resizable({ - edges: {left: true, top: true, right: true, bottom: true} - }) - .on('resizestart', function(event) { - menu.hide_any(); - }) - .on('resizemove', function(event) { - var target = event.target; - var newWidth = event.rect.width; - var newHeight = event.rect.height; - var dx = event.deltaRect.left ; - var dy = event.deltaRect.top ; - var dz = event.deltaRect.right; - var da = event.deltaRect.bottom; - - self.x += (dx + dz) / 2 / (viewport.w * viewport.scale); - self.y += (dy + da) / 2 / (viewport.h * viewport.scale); - - self.w = newWidth / (viewport.w * viewport.scale) / 2; - self.h = newHeight / (viewport.h * viewport.scale) / 2; - - self.on_resize(newWidth, newHeight); - self.redraw_size(); - self.redraw_pos(); - }) - .on('resizeend', function(event) { - self.save_layout(); - }); + // Move element to be drawn on top when clicked on - // Open a WebSocket to the server - this.uid = args.uid; - if (this.uid != undefined) { - this.ws = utils.create_websocket(this.uid); - this.ws.onmessage = function(event) { - self.on_message(event); + this.div.onmousedown = function() { + this.style.zIndex = utils.next_zindex(); }; - } - // Flag whether there is a scheduled update that hasn't happened yet - this.pending_update = false; + this.div.ontouchstart = this.div.onmousedown; - this.menu = new menu.Menu(self.parent); - interact(this.div) - .on('hold', function(event) { // Change to 'tap' for right click - if (event.button == 0) { - if (self.menu.visible_any()) { + // Allow element to be dragged + interact(this.div) + .draggable({ + inertia: true, + onstart: function() { menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); + }, + onmove: function(event) { + var target = event.target; + + self.x = self.x + event.dx / + (self.viewport.w * self.viewport.scale); + self.y = self.y + event.dy / + (self.viewport.h * self.viewport.scale); + + self.redraw_pos(); + + }, + onend: function(event) { + self.save_layout(); } - event.stopPropagation(); - } - }) - .on('tap', function(event) { // Get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - menu.hide_any(); + }); + + // Allow element to be resized + interact(this.div) + .resizable({ + edges: {left: true, top: true, right: true, bottom: true} + }) + .on('resizestart', function(event) { + menu.hide_any(); + }) + .on('resizemove', function(event) { + var target = event.target; + var newWidth = event.rect.width; + var newHeight = event.rect.height; + var dx = event.deltaRect.left ; + var dy = event.deltaRect.top ; + var dz = event.deltaRect.right; + var da = event.deltaRect.bottom; + + self.x += (dx + dz) / 2 / (viewport.w * viewport.scale); + self.y += (dy + da) / 2 / (viewport.h * viewport.scale); + + self.w = newWidth / (viewport.w * viewport.scale) / 2; + self.h = newHeight / (viewport.h * viewport.scale) / 2; + + self.on_resize(newWidth, newHeight); + self.redraw_size(); + self.redraw_pos(); + }) + .on('resizeend', function(event) { + self.save_layout(); + }); + + // Open a WebSocket to the server + this.uid = args.uid; + if (this.uid != undefined) { + this.ws = utils.create_websocket(this.uid); + this.ws.onmessage = function(event) { + self.on_message(event); + }; + } + + // Flag whether there is a scheduled update that hasn't happened yet + this.pending_update = false; + + this.menu = new menu.Menu(self.parent); + interact(this.div) + .on('hold', function(event) { // Change to 'tap' for right click + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); + } else { + self.menu.show(event.clientX, event.clientY, + self.generate_menu()); + } + event.stopPropagation(); + } + }) + .on('tap', function(event) { // Get rid of menus when clicking off + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); + } } + }); + $(this.div).bind('contextmenu', function(event) { + event.preventDefault(); + event.stopPropagation(); + if (self.menu.visible_any()) { + menu.hide_any(); + } else { + self.menu.show(event.clientX, event.clientY, self.generate_menu()); } }); - $(this.div).bind('contextmenu', function(event) { - event.preventDefault(); - event.stopPropagation(); - if (self.menu.visible_any()) { - menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, self.generate_menu()); - } - }); - - all_components.push(this); -}; -/** - * Method to be called when Component is resized. - */ -on_resize(width, height) {}; + all_components.push(this); + }; -/** - * Method to be called when Component received a WebSocket message. - */ -on_message(event) {}; - -generate_menu() { - var self = this; - var items = []; - if (this.label_visible) { - items.push(['Hide label', function() { - self.hide_label(); - self.save_layout(); - }]); - } else { - items.push(['Show label', function() { - self.show_label(); - self.save_layout(); - }]); - } - items.push(['Remove', function() {self.remove();}]); - return items; -}; + /** + * Method to be called when Component is resized. + */ + on_resize(width, height) {}; -remove(undo_flag, notify_server) { - undo_flag = typeof undo_flag !== 'undefined' ? undo_flag : false; - notify_server = typeof notify_server !== 'undefined' ? notify_server : true; + /** + * Method to be called when Component received a WebSocket message. + */ + on_message(event) {}; - if (notify_server) { - if (undo_flag === true) { - this.ws.send('remove_undo'); + generate_menu() { + var self = this; + var items = []; + if (this.label_visible) { + items.push(['Hide label', function() { + self.hide_label(); + self.save_layout(); + }]); } else { - this.ws.send('remove'); + items.push(['Show label', function() { + self.show_label(); + self.save_layout(); + }]); } - } - this.parent.removeChild(this.div); - var index = all_components.indexOf(this); - all_components.splice(index, 1); -}; + items.push(['Remove', function() {self.remove();}]); + return items; + }; -/** - * Schedule update() to be called in the near future. + remove(undo_flag, notify_server) { + undo_flag = typeof undo_flag !== 'undefined' ? undo_flag : false; + notify_server = typeof notify_server !== 'undefined' ? notify_server : true; - * If update() is already scheduled, then do nothing. This is meant to limit - * how fast update() is called in the case that we are changing the data faster - * than whatever processing is needed in update(). - */ -schedule_update(event) { - if (this.pending_update == false) { - this.pending_update = true; - var self = this; - window.setTimeout( - function() { - self.pending_update = false; - self.update(); - }, 10); - } -}; + if (notify_server) { + if (undo_flag === true) { + this.ws.send('remove_undo'); + } else { + this.ws.send('remove'); + } + } + this.parent.removeChild(this.div); + var index = all_components.indexOf(this); + all_components.splice(index, 1); + }; -/** - * Do any visual updating that is needed due to changes in the underlying data. - */ -update(event) {}; + /** + * Schedule update() to be called in the near future. + + * If update() is already scheduled, then do nothing. This is meant to limit + * how fast update() is called in the case that we are changing the data faster + * than whatever processing is needed in update(). + */ + schedule_update(event) { + if (this.pending_update == false) { + this.pending_update = true; + var self = this; + window.setTimeout( + function() { + self.pending_update = false; + self.update(); + }, 10); + } + }; -hide_label(event) { - if (this.label_visible) { - this.label.style.display = 'none'; - this.label_visible = false; - } -}; + /** + * Do any visual updating that is needed due to changes in the underlying data. + */ + update(event) {}; -show_label(event) { - if (!this.label_visible) { - this.label.style.display = 'inline'; - this.label_visible = true; - } -}; + hide_label(event) { + if (this.label_visible) { + this.label.style.display = 'none'; + this.label_visible = false; + } + }; -layout_info() { - var info = {}; - info.x = this.x; - info.y = this.y; - info.width = this.w; - info.height = this.h; - info.label_visible = this.label_visible; - return info; -}; + show_label(event) { + if (!this.label_visible) { + this.label.style.display = 'inline'; + this.label_visible = true; + } + }; -save_layout() { - var info = this.layout_info(); - this.ws.send('config:' + JSON.stringify(info)); -}; + layout_info() { + var info = {}; + info.x = this.x; + info.y = this.y; + info.width = this.w; + info.height = this.h; + info.label_visible = this.label_visible; + return info; + }; -update_layout(config) { - this.w = config.width; - this.h = config.height; - this.x = config.x; - this.y = config.y; + save_layout() { + var info = this.layout_info(); + this.ws.send('config:' + JSON.stringify(info)); + }; - this.redraw_size(); - this.redraw_pos(); - this.on_resize(this.get_screen_width(), this.get_screen_height()); + update_layout(config) { + this.w = config.width; + this.h = config.height; + this.x = config.x; + this.y = config.y; - if (config.label_visible === true) { - this.show_label(); - } else { - this.hide_label(); - } -}; + this.redraw_size(); + this.redraw_pos(); + this.on_resize(this.get_screen_width(), this.get_screen_height()); -redraw_size() { - this.width = this.viewport.w * this.w * this.viewport.scale * 2; - this.height = this.viewport.h * this.h * this.viewport.scale * 2; - this.div.style.width = this.width; - this.div.style.height = this.height; -}; + if (config.label_visible === true) { + this.show_label(); + } else { + this.hide_label(); + } + }; -redraw_pos() { - var x = (this.x + this.viewport.x - this.w) * - this.viewport.w * this.viewport.scale; - var y = (this.y + this.viewport.y - this.h) * - this.viewport.h * this.viewport.scale; - utils.set_transform(this.div, x, y); -}; + redraw_size() { + this.width = this.viewport.w * this.w * this.viewport.scale * 2; + this.height = this.viewport.h * this.h * this.viewport.scale * 2; + this.div.style.width = this.width; + this.div.style.height = this.height; + }; -get_screen_width() { - return this.viewport.w * this.w * this.viewport.scale * 2; -}; + redraw_pos() { + var x = (this.x + this.viewport.x - this.w) * + this.viewport.w * this.viewport.scale; + var y = (this.y + this.viewport.y - this.h) * + this.viewport.h * this.viewport.scale; + utils.set_transform(this.div, x, y); + }; -get_screen_height() { - return this.viewport.h * this.h * this.viewport.scale * 2; -}; + get_screen_width() { + return this.viewport.w * this.w * this.viewport.scale * 2; + }; + + get_screen_height() { + return this.viewport.h * this.h * this.viewport.scale * 2; + }; } diff --git a/nengo_gui/static/components/htmlview.ts b/nengo_gui/static/components/htmlview.ts index 68e8acc5..383ccd85 100644 --- a/nengo_gui/static/components/htmlview.ts +++ b/nengo_gui/static/components/htmlview.ts @@ -14,76 +14,76 @@ import * as utils from "../utils"; export default class HTMLView extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - var self = this; + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + var self = this; - this.sim = sim; + this.sim = sim; - this.pdiv = document.createElement('div'); - this.pdiv.style.width = '100%'; - this.pdiv.style.height = '100%'; - utils.set_transform(this.pdiv, 0, 0); - this.pdiv.style.position = 'fixed'; - this.pdiv.classList.add('htmlview'); - this.div.appendChild(this.pdiv); + this.pdiv = document.createElement('div'); + this.pdiv.style.width = '100%'; + this.pdiv.style.height = '100%'; + utils.set_transform(this.pdiv, 0, 0); + this.pdiv.style.position = 'fixed'; + this.pdiv.classList.add('htmlview'); + this.div.appendChild(this.pdiv); - // For storing the accumulated data. - this.data_store = new DataStore(1, this.sim, 0); + // For storing the accumulated data. + this.data_store = new DataStore(1, this.sim, 0); - // Call schedule_update whenever the time is adjusted in the SimControl - this.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); + // Call schedule_update whenever the time is adjusted in the SimControl + this.sim.div.addEventListener('adjust_time', + function(e) {self.schedule_update();}, false); - this.on_resize(this.get_screen_width(), this.get_screen_height()); -}; - -/** - * Receive new line data from the server - */ -on_message(event) { - var data = event.data.split(" ", 1); - var time = parseFloat(data[0]); + this.on_resize(this.get_screen_width(), this.get_screen_height()); + }; - var msg = event.data.substring(data[0].length + 1); + /** + * Receive new line data from the server + */ + on_message(event) { + var data = event.data.split(" ", 1); + var time = parseFloat(data[0]); - this.data_store.push([time, msg]); - this.schedule_update(); -}; + var msg = event.data.substring(data[0].length + 1); -/** - * Redraw the lines and axis due to changed data - */ -update() { - // Let the data store clear out old values - this.data_store.update(); + this.data_store.push([time, msg]); + this.schedule_update(); + }; - var data = this.data_store.get_last_data()[0]; + /** + * Redraw the lines and axis due to changed data + */ + update() { + // Let the data store clear out old values + this.data_store.update(); - if (data === undefined) { - data = ''; - } + var data = this.data_store.get_last_data()[0]; - this.pdiv.innerHTML = data; + if (data === undefined) { + data = ''; + } -}; + this.pdiv.innerHTML = data; -/** - * Adjust the graph layout due to changed size - */ -on_resize(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; }; - this.width = width; - this.height = height; - this.label.style.width = width; - - this.update(); -}; + /** + * Adjust the graph layout due to changed size + */ + on_resize(width, height) { + if (width < this.minWidth) { + width = this.minWidth; + } + if (height < this.minHeight) { + height = this.minHeight; + }; + + this.width = width; + this.height = height; + this.label.style.width = width; + + this.update(); + }; } diff --git a/nengo_gui/static/components/image.ts b/nengo_gui/static/components/image.ts index f7c024b5..09009d28 100644 --- a/nengo_gui/static/components/image.ts +++ b/nengo_gui/static/components/image.ts @@ -16,115 +16,115 @@ import { DataStore } from "../datastore"; export default class Image extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - - var self = this; - self.sim = sim; - self.display_time = args.display_time; - self.pixels_x = args.pixels_x; - self.pixels_y = args.pixels_y; - self.n_pixels = self.pixels_x * self.pixels_y; - - // For storing the accumulated data - self.data_store = new DataStore(self.n_pixels, self.sim, 0); - - // Draw the plot as an SVG - self.svg = d3.select(self.div).append('svg') - .attr('width', '100%') - .attr('height', '100%') - .attr('style', [ - 'padding-top:', '2em', - ].join("")); - - // Call schedule_update whenever the time is adjusted in the SimControl - self.sim.div.addEventListener('adjust_time', - function(e) {self.schedule_update();}, false); - - // Create the image - self.image = self.svg.append("image") - .attr("x", 0) - .attr("y", 0) - .attr("width", "100%") - .attr("height", "100%") - .attr("style", [ - "image-rendering: -webkit-optimize-contrast;", - "image-rendering: -moz-crisp-edges;", - "image-rendering: pixelated;" - ].join("")); - - self.canvas = document.createElement('CANVAS'); - self.canvas.width = self.pixels_x; - self.canvas.height = self.pixels_y; - - self.on_resize(this.get_screen_width(), this.get_screen_height()); - -}; + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + + var self = this; + self.sim = sim; + self.display_time = args.display_time; + self.pixels_x = args.pixels_x; + self.pixels_y = args.pixels_y; + self.n_pixels = self.pixels_x * self.pixels_y; + + // For storing the accumulated data + self.data_store = new DataStore(self.n_pixels, self.sim, 0); + + // Draw the plot as an SVG + self.svg = d3.select(self.div).append('svg') + .attr('width', '100%') + .attr('height', '100%') + .attr('style', [ + 'padding-top:', '2em', + ].join("")); + + // Call schedule_update whenever the time is adjusted in the SimControl + self.sim.div.addEventListener('adjust_time', + function(e) {self.schedule_update();}, false); + + // Create the image + self.image = self.svg.append("image") + .attr("x", 0) + .attr("y", 0) + .attr("width", "100%") + .attr("height", "100%") + .attr("style", [ + "image-rendering: -webkit-optimize-contrast;", + "image-rendering: -moz-crisp-edges;", + "image-rendering: pixelated;" + ].join("")); + + self.canvas = document.createElement('CANVAS'); + self.canvas.width = self.pixels_x; + self.canvas.height = self.pixels_y; + + self.on_resize(this.get_screen_width(), this.get_screen_height()); -/** - * Receive new line data from the server - */ -on_message(event) { - var data = new Uint8Array(event.data); - var msg_size = this.n_pixels + 4; - - for (var i = 0; i < data.length; i += msg_size) { - var time_data = new Float32Array(event.data.slice(i, i + 4)); - data = Array.prototype.slice.call(data, i + 3, i + msg_size); - data[0] = time_data[0]; - this.data_store.push(data); - } - this.schedule_update(); -}; - -/** - * Redraw the lines and axis due to changed data - */ -update() { - var self = this; - - // Let the data store clear out old values - self.data_store.update(); - - var data = self.data_store.get_last_data(); - var ctx = self.canvas.getContext("2d"); - var imgData = ctx.getImageData(0, 0, self.pixels_x, self.pixels_y); - for (var i = 0; i < self.n_pixels; i++) { - imgData.data[4*i + 0] = data[i]; - imgData.data[4*i + 1] = data[i]; - imgData.data[4*i + 2] = data[i]; - imgData.data[4*i + 3] = 255; - } - ctx.putImageData(imgData, 0, 0); - var dataURL = self.canvas.toDataURL("image/png"); - - self.image.attr("xlink:href", dataURL); -}; - -/** - * Adjust the graph layout due to changed size - */ -on_resize(width, height) { - var self = this; - if (width < self.minWidth) { - width = self.minWidth; - } - if (height < self.minHeight) { - height = self.minHeight; }; - self.svg - .attr("width", width) - .attr("height", height); - - self.update(); + /** + * Receive new line data from the server + */ + on_message(event) { + var data = new Uint8Array(event.data); + var msg_size = this.n_pixels + 4; + + for (var i = 0; i < data.length; i += msg_size) { + var time_data = new Float32Array(event.data.slice(i, i + 4)); + data = Array.prototype.slice.call(data, i + 3, i + msg_size); + data[0] = time_data[0]; + this.data_store.push(data); + } + this.schedule_update(); + }; - self.label.style.width = width; + /** + * Redraw the lines and axis due to changed data + */ + update() { + var self = this; + + // Let the data store clear out old values + self.data_store.update(); + + var data = self.data_store.get_last_data(); + var ctx = self.canvas.getContext("2d"); + var imgData = ctx.getImageData(0, 0, self.pixels_x, self.pixels_y); + for (var i = 0; i < self.n_pixels; i++) { + imgData.data[4*i + 0] = data[i]; + imgData.data[4*i + 1] = data[i]; + imgData.data[4*i + 2] = data[i]; + imgData.data[4*i + 3] = 255; + } + ctx.putImageData(imgData, 0, 0); + var dataURL = self.canvas.toDataURL("image/png"); + + self.image.attr("xlink:href", dataURL); + }; - self.width = width; - self.height = height; - self.div.style.width = width; - self.div.style.height = height; -}; + /** + * Adjust the graph layout due to changed size + */ + on_resize(width, height) { + var self = this; + if (width < self.minWidth) { + width = self.minWidth; + } + if (height < self.minHeight) { + height = self.minHeight; + }; + + self.svg + .attr("width", width) + .attr("height", height); + + self.update(); + + self.label.style.width = width; + + self.width = width; + self.height = height; + self.div.style.width = width; + self.div.style.height = height; + }; } diff --git a/nengo_gui/static/components/netgraph.ts b/nengo_gui/static/components/netgraph.ts index a267f42d..e2520cfe 100644 --- a/nengo_gui/static/components/netgraph.ts +++ b/nengo_gui/static/components/netgraph.ts @@ -22,726 +22,726 @@ import Viewport from "../viewport"; export default class NetGraph { -constructor(parent, config, args) { - var self = this; - this.config = config; - this.viewport = new Viewport(this); - - if (args.uid[0] === '<') { - console.log("invalid uid for NetGraph: " + args.uid); - } - this.offsetX = 0; // Global x,y pan offset - this.offsetY = 0; - - var scale = 1.0; - Object.defineProperty(this, 'scale', { - // Global scaling factor - get: function() { - return scale; - }, - set: function(val) { - if (val === scale) { - return; - } - scale = val; - this.update_fonts(); - this.redraw(); + constructor(parent, config, args) { + var self = this; + this.config = config; + this.viewport = new Viewport(this); - self.viewport.scale = scale; - self.viewport.redraw_all(); - } - }); - - Object.defineProperty(this, 'zoom_fonts', { - // Scale fonts when zooming - get: function() { - return self.config.zoom_fonts; - }, - set: function(val) { - if (val === this.zoom_fonts) { - return; - } - self.config.zoom_fonts = val; - this.update_fonts(); + if (args.uid[0] === '<') { + console.log("invalid uid for NetGraph: " + args.uid); } - }); - - Object.defineProperty(this, 'aspect_resize', { - // Preserve aspect ratios on window resize - get: function() { - return self.config.aspect_resize; - }, - set: function(val) { - if (val === this.aspect_resize) { - return; - } - self.config.aspect_resize = val; + this.offsetX = 0; // Global x,y pan offset + this.offsetY = 0; + + var scale = 1.0; + Object.defineProperty(this, 'scale', { + // Global scaling factor + get: function() { + return scale; + }, + set: function(val) { + if (val === scale) { + return; + } + scale = val; + this.update_fonts(); + this.redraw(); - } - }); - - Object.defineProperty(this, 'font_size', { - get: function() { - return self.config.font_size; - }, - set: function(val) { - if (val === this.font_size) { - return; - } - self.config.font_size = val; - this.update_fonts(); - } - }); - - // Do networks have transparent backgrounds? - Object.defineProperty(this, 'transparent_nets', { - get: function() { - return self.config.transparent_nets; - }, - set: function(val) { - if (val === this.transparent_nets) { - return; + self.viewport.scale = scale; + self.viewport.redraw_all(); } - self.config.transparent_nets = val; - for (var key in this.svg_objects) { - var ngi = this.svg_objects[key]; - ngi.compute_fill(); - if (ngi.type === 'net' && ngi.expanded) { - ngi.shape.style["fill-opacity"] = val ? 0.0 : 1.0; + }); + + Object.defineProperty(this, 'zoom_fonts', { + // Scale fonts when zooming + get: function() { + return self.config.zoom_fonts; + }, + set: function(val) { + if (val === this.zoom_fonts) { + return; } + self.config.zoom_fonts = val; + this.update_fonts(); } + }); - } - }); - - this.svg_objects = {}; // Dict of all NetGraphItems, by uid - this.svg_conns = {}; // Dict of all NetGraphConnections, by uid - this.minimap_objects = {}; - this.minimap_conns = {}; - - this.mm_min_x = 0; - this.mm_max_x = 0; - this.mm_min_y = 0; - this.mm_max_y = 0; - - this.mm_scale = .1; - - this.in_zoom_delay = false; - - // Since connections may go to items that do not exist yet (since they - // are inside a collapsed network), this dictionary keeps a list of - // connections to be notified when a particular item appears. The - // key in the dictionary is the uid of the nonexistent item, and the - // value is a list of NetGraphConnections that should be notified - // when that item appears. - this.collapsed_conns = {}; - - // Create the master SVG element - this.svg = this.createSVGElement('svg'); - this.svg.classList.add('netgraph'); - this.svg.style.width = '100%'; - this.svg.id = 'netgraph'; - this.svg.style.height = '100%'; - this.svg.style.position = 'absolute'; - - interact(this.svg).styleCursor(false); - - parent.appendChild(this.svg); - this.parent = parent; - - this.width = $(this.svg).width(); - this.height = $(this.svg).height(); - - this.tool_height = $(toolbar.toolbar).height(); - - // Three separate layers, so that expanded networks are at the back, - // then connection lines, and then other items (nodes, ensembles, and - // collapsed networks) are drawn on top. - this.g_networks = this.createSVGElement('g'); - this.svg.appendChild(this.g_networks); - this.g_conns = this.createSVGElement('g'); - this.svg.appendChild(this.g_conns); - this.g_items = this.createSVGElement('g'); - this.svg.appendChild(this.g_items); - - // Reading netgraph.css file as text and embedding it within def tags; - // this is needed for saving the SVG plot to disk. - - // Load contents of the CSS file as string - var css = require('!!css-loader!./netgraph.css').toString(); - // Embed CSS code into SVG tag - var s = document.createElement('style'); - s.setAttribute('type', 'text/css'); - s.innerHTML = ""; - - var defs = document.createElement('defs'); - defs.appendChild(s); - - this.svg.insertBefore(defs, this.svg.firstChild); - - // Connect to server - this.ws = utils.create_websocket(args.uid); - this.ws.onmessage = function(event) { - self.on_message(event); - }; + Object.defineProperty(this, 'aspect_resize', { + // Preserve aspect ratios on window resize + get: function() { + return self.config.aspect_resize; + }, + set: function(val) { + if (val === this.aspect_resize) { + return; + } + self.config.aspect_resize = val; + + } + }); - // Respond to resize events - this.svg.addEventListener("resize", function() { - self.on_resize(); - }); - window.addEventListener("resize", function() { - self.on_resize(); - }); - - // Dragging the background pans the full area by changing offsetX,Y - var self = this; - - // Define cursor behaviour for background - interact(this.svg) - .on('mousedown', function() { - var cursor = document.documentElement.getAttribute('style'); - if (cursor !== null) { - if (cursor.match(/resize/) == null) { - // Don't change resize cursor - document.documentElement.setAttribute( - 'style', 'cursor:move;'); + Object.defineProperty(this, 'font_size', { + get: function() { + return self.config.font_size; + }, + set: function(val) { + if (val === this.font_size) { + return; } + self.config.font_size = val; + this.update_fonts(); } - }) - .on('mouseup', function() { - document.documentElement.setAttribute('style', 'cursor:default;'); }); - interact(this.svg) - .draggable({ - onstart: function() { - menu.hide_any(); + // Do networks have transparent backgrounds? + Object.defineProperty(this, 'transparent_nets', { + get: function() { + return self.config.transparent_nets; }, - onmove: function(event) { - self.offsetX += event.dx / self.get_scaled_width(); - self.offsetY += event.dy / self.get_scaled_height(); - for (var key in self.svg_objects) { - self.svg_objects[key].redraw_position(); - if (self.mm_display) { - self.minimap_objects[key].redraw_position(); + set: function(val) { + if (val === this.transparent_nets) { + return; + } + self.config.transparent_nets = val; + for (var key in this.svg_objects) { + var ngi = this.svg_objects[key]; + ngi.compute_fill(); + if (ngi.type === 'net' && ngi.expanded) { + ngi.shape.style["fill-opacity"] = val ? 0.0 : 1.0; } } - for (var key in self.svg_conns) { - self.svg_conns[key].redraw(); + + } + }); + + this.svg_objects = {}; // Dict of all NetGraphItems, by uid + this.svg_conns = {}; // Dict of all NetGraphConnections, by uid + this.minimap_objects = {}; + this.minimap_conns = {}; + + this.mm_min_x = 0; + this.mm_max_x = 0; + this.mm_min_y = 0; + this.mm_max_y = 0; + + this.mm_scale = .1; + + this.in_zoom_delay = false; + + // Since connections may go to items that do not exist yet (since they + // are inside a collapsed network), this dictionary keeps a list of + // connections to be notified when a particular item appears. The + // key in the dictionary is the uid of the nonexistent item, and the + // value is a list of NetGraphConnections that should be notified + // when that item appears. + this.collapsed_conns = {}; + + // Create the master SVG element + this.svg = this.createSVGElement('svg'); + this.svg.classList.add('netgraph'); + this.svg.style.width = '100%'; + this.svg.id = 'netgraph'; + this.svg.style.height = '100%'; + this.svg.style.position = 'absolute'; + + interact(this.svg).styleCursor(false); + + parent.appendChild(this.svg); + this.parent = parent; + + this.width = $(this.svg).width(); + this.height = $(this.svg).height(); + + this.tool_height = $(toolbar.toolbar).height(); + + // Three separate layers, so that expanded networks are at the back, + // then connection lines, and then other items (nodes, ensembles, and + // collapsed networks) are drawn on top. + this.g_networks = this.createSVGElement('g'); + this.svg.appendChild(this.g_networks); + this.g_conns = this.createSVGElement('g'); + this.svg.appendChild(this.g_conns); + this.g_items = this.createSVGElement('g'); + this.svg.appendChild(this.g_items); + + // Reading netgraph.css file as text and embedding it within def tags; + // this is needed for saving the SVG plot to disk. + + // Load contents of the CSS file as string + var css = require('!!css-loader!./netgraph.css').toString(); + // Embed CSS code into SVG tag + var s = document.createElement('style'); + s.setAttribute('type', 'text/css'); + s.innerHTML = ""; + + var defs = document.createElement('defs'); + defs.appendChild(s); + + this.svg.insertBefore(defs, this.svg.firstChild); + + // Connect to server + this.ws = utils.create_websocket(args.uid); + this.ws.onmessage = function(event) { + self.on_message(event); + }; + + // Respond to resize events + this.svg.addEventListener("resize", function() { + self.on_resize(); + }); + window.addEventListener("resize", function() { + self.on_resize(); + }); + + // Dragging the background pans the full area by changing offsetX,Y + var self = this; + + // Define cursor behaviour for background + interact(this.svg) + .on('mousedown', function() { + var cursor = document.documentElement.getAttribute('style'); + if (cursor !== null) { + if (cursor.match(/resize/) == null) { + // Don't change resize cursor + document.documentElement.setAttribute( + 'style', 'cursor:move;'); + } } + }) + .on('mouseup', function() { + document.documentElement.setAttribute('style', 'cursor:default;'); + }); - self.viewport.x = self.offsetX; - self.viewport.y = self.offsetY; - self.viewport.redraw_all(); + interact(this.svg) + .draggable({ + onstart: function() { + menu.hide_any(); + }, + onmove: function(event) { + self.offsetX += event.dx / self.get_scaled_width(); + self.offsetY += event.dy / self.get_scaled_height(); + for (var key in self.svg_objects) { + self.svg_objects[key].redraw_position(); + if (self.mm_display) { + self.minimap_objects[key].redraw_position(); + } + } + for (var key in self.svg_conns) { + self.svg_conns[key].redraw(); + } - self.scaleMiniMapViewBox(); + self.viewport.x = self.offsetX; + self.viewport.y = self.offsetY; + self.viewport.redraw_all(); - }, - onend: function(event) { - // Let the server know what happened - self.notify({act: "pan", x: self.offsetX, y: self.offsetY}); - }}); - - // Scrollwheel on background zooms the full area by changing scale. - // Note that offsetX,Y are also changed to zoom into a particular - // point in the space - interact(document.getElementById('main')) - .on('click', function(event) { - $('.ace_text-input').blur(); - }) - .on('wheel', function(event) { - event.preventDefault(); + self.scaleMiniMapViewBox(); - menu.hide_any(); - var x = (event.clientX) / self.width; - var y = (event.clientY - self.tool_height) / self.height; + }, + onend: function(event) { + // Let the server know what happened + self.notify({act: "pan", x: self.offsetX, y: self.offsetY}); + }}); - if (event.deltaMode === 1) { - // DOM_DELTA_LINE - if (event.deltaY != 0) { - var delta = Math.log(1. + Math.abs(event.deltaY)) * 60; - if (event.deltaY < 0) { - delta *= -1; + // Scrollwheel on background zooms the full area by changing scale. + // Note that offsetX,Y are also changed to zoom into a particular + // point in the space + interact(document.getElementById('main')) + .on('click', function(event) { + $('.ace_text-input').blur(); + }) + .on('wheel', function(event) { + event.preventDefault(); + + menu.hide_any(); + var x = (event.clientX) / self.width; + var y = (event.clientY - self.tool_height) / self.height; + + if (event.deltaMode === 1) { + // DOM_DELTA_LINE + if (event.deltaY != 0) { + var delta = Math.log(1. + Math.abs(event.deltaY)) * 60; + if (event.deltaY < 0) { + delta *= -1; + } + } else { + var delta = 0; } - } else { + } else if (event.deltaMode === 2) { + // DOM_DELTA_PAGE + // No idea what device would generate scrolling by a page var delta = 0; + } else { + // DOM_DELTA_PIXEL + var delta = event.deltaY; } - } else if (event.deltaMode === 2) { - // DOM_DELTA_PAGE - // No idea what device would generate scrolling by a page - var delta = 0; - } else { - // DOM_DELTA_PIXEL - var delta = event.deltaY; - } - var scale = 1. + Math.abs(delta) / 600.; - if (delta > 0) { - scale = 1. / scale; - } + var scale = 1. + Math.abs(delta) / 600.; + if (delta > 0) { + scale = 1. / scale; + } - comp.save_all_components(); + comp.save_all_components(); - var xx = x / self.scale - self.offsetX; - var yy = y / self.scale - self.offsetY; - self.offsetX = (self.offsetX + xx) / scale - xx; - self.offsetY = (self.offsetY + yy) / scale - yy; + var xx = x / self.scale - self.offsetX; + var yy = y / self.scale - self.offsetY; + self.offsetX = (self.offsetX + xx) / scale - xx; + self.offsetY = (self.offsetY + yy) / scale - yy; - self.scale = scale * self.scale; - self.viewport.x = self.offsetX; - self.viewport.y = self.offsetY; - self.viewport.redraw_all(); + self.scale = scale * self.scale; + self.viewport.x = self.offsetX; + self.viewport.y = self.offsetY; + self.viewport.redraw_all(); - self.scaleMiniMapViewBox(); + self.scaleMiniMapViewBox(); - self.redraw(); + self.redraw(); - // Let the server know what happened - self.notify({ - act: "zoom", scale: self.scale, x: self.offsetX, y: self.offsetY + // Let the server know what happened + self.notify({ + act: "zoom", scale: self.scale, x: self.offsetX, y: self.offsetY + }); }); - }); - this.menu = new menu.Menu(self.parent); - - // Determine when to pull up the menu - interact(this.svg) - .on('hold', function(event) { // Change to 'tap' for right click - if (event.button == 0) { - if (self.menu.visible_any()) { - menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); + this.menu = new menu.Menu(self.parent); + + // Determine when to pull up the menu + interact(this.svg) + .on('hold', function(event) { // Change to 'tap' for right click + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); + } else { + self.menu.show(event.clientX, event.clientY, + self.generate_menu()); + } + event.stopPropagation(); } - event.stopPropagation(); - } - }) - .on('tap', function(event) { // Get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - menu.hide_any(); + }) + .on('tap', function(event) { // Get rid of menus when clicking off + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); + } } + }); + + $(this.svg).bind('contextmenu', function(event) { + event.preventDefault(); + if (self.menu.visible_any()) { + menu.hide_any(); + } else { + self.menu.show(event.clientX, event.clientY, self.generate_menu()); } }); - $(this.svg).bind('contextmenu', function(event) { - event.preventDefault(); - if (self.menu.visible_any()) { - menu.hide_any(); - } else { - self.menu.show(event.clientX, event.clientY, self.generate_menu()); - } - }); - - this.create_minimap(); - this.update_fonts(); -}; - -generate_menu() { - var self = this; - var items = []; - items.push(['Auto-layout', function() { - self.notify({act: "feedforward_layout", uid: null}); - }]); - return items; -}; - -/** - * Event handler for received WebSocket messages - */ -on_message(event) { - var data = JSON.parse(event.data); - if (data.type === 'net') { - this.create_object(data); - } else if (data.type === 'ens') { - this.create_object(data); - } else if (data.type === 'node') { - this.create_object(data); - } else if (data.type === 'conn') { - this.create_connection(data); - } else if (data.type === 'pan') { - this.set_offset(data.pan[0], data.pan[1]); - } else if (data.type === 'zoom') { - this.scale = data.zoom; - } else if (data.type === 'expand') { - var item = this.svg_objects[data.uid]; - item.expand(true, true); - } else if (data.type === 'collapse') { - var item = this.svg_objects[data.uid]; - item.collapse(true, true); - } else if (data.type === 'pos_size') { - var item = this.svg_objects[data.uid]; - item.x = data.pos[0]; - item.y = data.pos[1]; - item.width = data.size[0]; - item.height = data.size[1]; - - item.redraw(); + this.create_minimap(); + this.update_fonts(); + }; - this.scaleMiniMap(); + generate_menu() { + var self = this; + var items = []; + items.push(['Auto-layout', function() { + self.notify({act: "feedforward_layout", uid: null}); + }]); + return items; + }; - } else if (data.type === 'config') { - // Anything about the config of a component has changed - var uid = data.uid; - for (var i = 0; i < comp.all_components.length; i++) { - if (comp.all_components[i].uid === uid) { - comp.all_components[i].update_layout(data.config); - break; + /** + * Event handler for received WebSocket messages + */ + on_message(event) { + var data = JSON.parse(event.data); + if (data.type === 'net') { + this.create_object(data); + } else if (data.type === 'ens') { + this.create_object(data); + } else if (data.type === 'node') { + this.create_object(data); + } else if (data.type === 'conn') { + this.create_connection(data); + } else if (data.type === 'pan') { + this.set_offset(data.pan[0], data.pan[1]); + } else if (data.type === 'zoom') { + this.scale = data.zoom; + } else if (data.type === 'expand') { + var item = this.svg_objects[data.uid]; + item.expand(true, true); + } else if (data.type === 'collapse') { + var item = this.svg_objects[data.uid]; + item.collapse(true, true); + } else if (data.type === 'pos_size') { + var item = this.svg_objects[data.uid]; + item.x = data.pos[0]; + item.y = data.pos[1]; + item.width = data.size[0]; + item.height = data.size[1]; + + item.redraw(); + + this.scaleMiniMap(); + + } else if (data.type === 'config') { + // Anything about the config of a component has changed + var uid = data.uid; + for (var i = 0; i < comp.all_components.length; i++) { + if (comp.all_components[i].uid === uid) { + comp.all_components[i].update_layout(data.config); + break; + } + } + } else if (data.type === 'js') { + eval(data.code); + } else if (data.type === 'rename') { + var item = this.svg_objects[data.uid]; + item.set_label(data.name); + + } else if (data.type === 'remove') { + var item = this.svg_objects[data.uid]; + if (item === undefined) { + item = this.svg_conns[data.uid]; } - } - } else if (data.type === 'js') { - eval(data.code); - } else if (data.type === 'rename') { - var item = this.svg_objects[data.uid]; - item.set_label(data.name); - - } else if (data.type === 'remove') { - var item = this.svg_objects[data.uid]; - if (item === undefined) { - item = this.svg_conns[data.uid]; - } - item.remove(); - - } else if (data.type === 'reconnect') { - var conn = this.svg_conns[data.uid]; - conn.set_pres(data.pres); - conn.set_posts(data.posts); - conn.set_recurrent(data.pres[0] === data.posts[0]); - conn.redraw(); - - } else if (data.type === 'delete_graph') { - var uid = data.uid; - for (var i = 0; i < comp.all_components.length; i++) { - if (comp.all_components[i].uid === uid) { - comp.all_components[i].remove(true, data.notify_server); - break; + item.remove(); + + } else if (data.type === 'reconnect') { + var conn = this.svg_conns[data.uid]; + conn.set_pres(data.pres); + conn.set_posts(data.posts); + conn.set_recurrent(data.pres[0] === data.posts[0]); + conn.redraw(); + + } else if (data.type === 'delete_graph') { + var uid = data.uid; + for (var i = 0; i < comp.all_components.length; i++) { + if (comp.all_components[i].uid === uid) { + comp.all_components[i].remove(true, data.notify_server); + break; + } } + } else { + console.log('invalid message'); + console.log(data); } - } else { - console.log('invalid message'); - console.log(data); - } -}; + }; -/** - * Report an event back to the server - */ -notify(info) { - this.ws.send(JSON.stringify(info)); -}; + /** + * Report an event back to the server + */ + notify(info) { + this.ws.send(JSON.stringify(info)); + }; -/** - * Pan the screen (and redraw accordingly) - */ -set_offset(x, y) { - this.offsetX = x; - this.offsetY = y; - this.redraw(); - - this.viewport.x = x; - this.viewport.y = y; - this.viewport.redraw_all(); -}; - -update_fonts() { - if (this.zoom_fonts) { - $('#main').css('font-size', 3 * this.scale * this.font_size/100 + 'em'); - } else { - $('#main').css('font-size', this.font_size/100 + 'em'); - } -}; + /** + * Pan the screen (and redraw accordingly) + */ + set_offset(x, y) { + this.offsetX = x; + this.offsetY = y; + this.redraw(); + + this.viewport.x = x; + this.viewport.y = y; + this.viewport.redraw_all(); + }; -/** - * Redraw all elements - */ -redraw() { - for (var key in this.svg_objects) { - this.svg_objects[key].redraw(); - } - for (var key in this.svg_conns) { - this.svg_conns[key].redraw(); - } -}; + update_fonts() { + if (this.zoom_fonts) { + $('#main').css('font-size', 3 * this.scale * this.font_size/100 + 'em'); + } else { + $('#main').css('font-size', this.font_size/100 + 'em'); + } + }; -/** - * Helper function for correctly creating SVG elements. - */ -createSVGElement(tag) { - return document.createElementNS("http://www.w3.org/2000/svg", tag); -}; + /** + * Redraw all elements + */ + redraw() { + for (var key in this.svg_objects) { + this.svg_objects[key].redraw(); + } + for (var key in this.svg_conns) { + this.svg_conns[key].redraw(); + } + }; -/** - * Create a new NetGraphItem. - * - * If an existing NetGraphConnection is looking for this item, it will be - * notified - */ -create_object(info) { - var item_mini = new NetGraphItem(this, info, true); - this.minimap_objects[info.uid] = item_mini; + /** + * Helper function for correctly creating SVG elements. + */ + createSVGElement(tag) { + return document.createElementNS("http://www.w3.org/2000/svg", tag); + }; - var item = new NetGraphItem(this, info, false, item_mini); - this.svg_objects[info.uid] = item; + /** + * Create a new NetGraphItem. + * + * If an existing NetGraphConnection is looking for this item, it will be + * notified + */ + create_object(info) { + var item_mini = new NetGraphItem(this, info, true); + this.minimap_objects[info.uid] = item_mini; - this.detect_collapsed_conns(item.uid); - this.detect_collapsed_conns(item_mini.uid); + var item = new NetGraphItem(this, info, false, item_mini); + this.svg_objects[info.uid] = item; - this.scaleMiniMap(); -}; + this.detect_collapsed_conns(item.uid); + this.detect_collapsed_conns(item_mini.uid); -/** - * Create a new NetGraphConnection. - */ -create_connection(info) { - var conn_mini = new NetGraphConnection(this, info, true); - this.minimap_conns[info.uid] = conn_mini; + this.scaleMiniMap(); + }; - var conn = new NetGraphConnection(this, info, false, conn_mini); - this.svg_conns[info.uid] = conn; -}; + /** + * Create a new NetGraphConnection. + */ + create_connection(info) { + var conn_mini = new NetGraphConnection(this, info, true); + this.minimap_conns[info.uid] = conn_mini; -/** - * Handler for resizing the full SVG. - */ -on_resize(event) { - var width = $(this.svg).width(); - var height = $(this.svg).height(); + var conn = new NetGraphConnection(this, info, false, conn_mini); + this.svg_conns[info.uid] = conn; + }; - if (this.aspect_resize) { - for (var key in this.svg_objects) { - var item = this.svg_objects[key]; - if (item.depth == 1) { - var new_width = item.get_screen_width() / this.scale; - var new_height = item.get_screen_height() / this.scale; - item.width = new_width/(2*width); - item.height = new_height/(2*height); + /** + * Handler for resizing the full SVG. + */ + on_resize(event) { + var width = $(this.svg).width(); + var height = $(this.svg).height(); + + if (this.aspect_resize) { + for (var key in this.svg_objects) { + var item = this.svg_objects[key]; + if (item.depth == 1) { + var new_width = item.get_screen_width() / this.scale; + var new_height = item.get_screen_height() / this.scale; + item.width = new_width/(2*width); + item.height = new_height/(2*height); + } } } - } - this.width = width; - this.height = height; - this.mm_width = $(this.minimap).width(); - this.mm_height = $(this.minimap).height(); + this.width = width; + this.height = height; + this.mm_width = $(this.minimap).width(); + this.mm_height = $(this.minimap).height(); - this.redraw(); -}; + this.redraw(); + }; -/** - * Return the pixel width of the SVG times the current scale factor. - */ -get_scaled_width() { - return this.width * this.scale; -}; + /** + * Return the pixel width of the SVG times the current scale factor. + */ + get_scaled_width() { + return this.width * this.scale; + }; -/** - * Return the pixel height of the SVG times the current scale factor. - */ -get_scaled_height() { - return this.height * this.scale; -}; + /** + * Return the pixel height of the SVG times the current scale factor. + */ + get_scaled_height() { + return this.height * this.scale; + }; -/** - * Expand or collapse a network. - */ -toggle_network(uid) { - var item = this.svg_objects[uid]; - if (item.expanded) { - item.collapse(true); - } else { - item.expand(); - } -}; + /** + * Expand or collapse a network. + */ + toggle_network(uid) { + var item = this.svg_objects[uid]; + if (item.expanded) { + item.collapse(true); + } else { + item.expand(); + } + }; -/** - * Register a NetGraphConnection with a target item that it is looking for. - * - * This is a NetGraphItem that does not exist yet, because it is inside a - * collapsed network. When it does appear, NetGraph.detect_collapsed will - * handle notifying the NetGraphConnection. - */ -register_conn(conn, target) { - if (this.collapsed_conns[target] === undefined) { - this.collapsed_conns[target] = [conn]; - } else { - var index = this.collapsed_conns[target].indexOf(conn); - if (index === -1) { - this.collapsed_conns[target].push(conn); + /** + * Register a NetGraphConnection with a target item that it is looking for. + * + * This is a NetGraphItem that does not exist yet, because it is inside a + * collapsed network. When it does appear, NetGraph.detect_collapsed will + * handle notifying the NetGraphConnection. + */ + register_conn(conn, target) { + if (this.collapsed_conns[target] === undefined) { + this.collapsed_conns[target] = [conn]; + } else { + var index = this.collapsed_conns[target].indexOf(conn); + if (index === -1) { + this.collapsed_conns[target].push(conn); + } } - } -}; + }; -/** - * Manage collapsed_conns dictionary. - * - * If a NetGraphConnection is looking for an item with a particular uid, - * but that item does not exist yet (due to it being inside a collapsed - * network), then it is added to the collapsed_conns dictionary. When - * an item is created, this function is used to see if any - * NetGraphConnections are waiting for it, and notifies them. - */ -detect_collapsed_conns(uid) { - var conns = this.collapsed_conns[uid]; - if (conns !== undefined) { - delete this.collapsed_conns[uid]; - for (var i in conns) { - var conn = conns[i]; - // Make sure the NetGraphConnection hasn't been removed since - // it started listening. - if (!conn.removed) { - conn.set_pre(conn.find_pre()); - conn.set_post(conn.find_post()); - conn.redraw(); + /** + * Manage collapsed_conns dictionary. + * + * If a NetGraphConnection is looking for an item with a particular uid, + * but that item does not exist yet (due to it being inside a collapsed + * network), then it is added to the collapsed_conns dictionary. When + * an item is created, this function is used to see if any + * NetGraphConnections are waiting for it, and notifies them. + */ + detect_collapsed_conns(uid) { + var conns = this.collapsed_conns[uid]; + if (conns !== undefined) { + delete this.collapsed_conns[uid]; + for (var i in conns) { + var conn = conns[i]; + // Make sure the NetGraphConnection hasn't been removed since + // it started listening. + if (!conn.removed) { + conn.set_pre(conn.find_pre()); + conn.set_post(conn.find_post()); + conn.redraw(); + } } } - } -}; + }; -/** - * Create a minimap. - */ -create_minimap() { - var self = this; - - this.minimap_div = document.createElement('div'); - this.minimap_div.className = 'minimap'; - this.parent.appendChild(this.minimap_div); - - this.minimap = this.createSVGElement('svg'); - this.minimap.classList.add('minimap'); - this.minimap.id = 'minimap'; - this.minimap_div.appendChild(this.minimap); - - // Box to show current view - this.view = this.createSVGElement('rect'); - this.view.classList.add('view'); - this.minimap.appendChild(this.view); - - this.g_networks_mini = this.createSVGElement('g'); - this.g_conns_mini = this.createSVGElement('g'); - this.g_items_mini = this.createSVGElement('g'); - // Order these are appended is important for layering - this.minimap.appendChild(this.g_networks_mini); - this.minimap.appendChild(this.g_conns_mini); - this.minimap.appendChild(this.g_items_mini); - - this.mm_width = $(this.minimap).width(); - this.mm_height = $(this.minimap).height(); - - // Default display minimap - this.mm_display = true; - this.toggleMiniMap(); -}; - -toggleMiniMap() { - if (this.mm_display == true) { - $('.minimap')[0].style.visibility = 'hidden'; - this.g_conns_mini.style.opacity = 0; - this.mm_display = false; - } else { - $('.minimap')[0].style.visibility = 'visible'; - this.g_conns_mini.style.opacity = 1; - this.mm_display = true ; - this.scaleMiniMap(); - } -}; + /** + * Create a minimap. + */ + create_minimap() { + var self = this; + + this.minimap_div = document.createElement('div'); + this.minimap_div.className = 'minimap'; + this.parent.appendChild(this.minimap_div); + + this.minimap = this.createSVGElement('svg'); + this.minimap.classList.add('minimap'); + this.minimap.id = 'minimap'; + this.minimap_div.appendChild(this.minimap); + + // Box to show current view + this.view = this.createSVGElement('rect'); + this.view.classList.add('view'); + this.minimap.appendChild(this.view); + + this.g_networks_mini = this.createSVGElement('g'); + this.g_conns_mini = this.createSVGElement('g'); + this.g_items_mini = this.createSVGElement('g'); + // Order these are appended is important for layering + this.minimap.appendChild(this.g_networks_mini); + this.minimap.appendChild(this.g_conns_mini); + this.minimap.appendChild(this.g_items_mini); + + this.mm_width = $(this.minimap).width(); + this.mm_height = $(this.minimap).height(); + + // Default display minimap + this.mm_display = true; + this.toggleMiniMap(); + }; -/** - * Calculate the minimap position offsets and scaling. - */ -scaleMiniMap() { - if (!this.mm_display) { - return; - } - - var keys = Object.keys(this.svg_objects); - if (keys.length === 0) { - return; - } - - // TODO: Could also store the items at the four min max values - // and only compare against those, or check against all items - // in the lists when they move. Might be important for larger - // networks. - var first_item = true; - for (var key in this.svg_objects) { - var item = this.svg_objects[key]; - // Ignore anything inside a subnetwork - if (item.depth > 1) { - continue; + toggleMiniMap() { + if (this.mm_display == true) { + $('.minimap')[0].style.visibility = 'hidden'; + this.g_conns_mini.style.opacity = 0; + this.mm_display = false; + } else { + $('.minimap')[0].style.visibility = 'visible'; + this.g_conns_mini.style.opacity = 1; + this.mm_display = true ; + this.scaleMiniMap(); } + }; - var minmax_xy = item.getMinMaxXY(); - if (first_item == true) { - this.mm_min_x = minmax_xy[0]; - this.mm_max_x = minmax_xy[1]; - this.mm_min_y = minmax_xy[2]; - this.mm_max_y = minmax_xy[3]; - first_item = false; - continue; + /** + * Calculate the minimap position offsets and scaling. + */ + scaleMiniMap() { + if (!this.mm_display) { + return; } - if (this.mm_min_x > minmax_xy[0]) { - this.mm_min_x = minmax_xy[0]; - } - if (this.mm_max_x < minmax_xy[1]) { - this.mm_max_x = minmax_xy[1]; + var keys = Object.keys(this.svg_objects); + if (keys.length === 0) { + return; } - if (this.mm_min_y > minmax_xy[2]) { - this.mm_min_y = minmax_xy[2]; + + // TODO: Could also store the items at the four min max values + // and only compare against those, or check against all items + // in the lists when they move. Might be important for larger + // networks. + var first_item = true; + for (var key in this.svg_objects) { + var item = this.svg_objects[key]; + // Ignore anything inside a subnetwork + if (item.depth > 1) { + continue; + } + + var minmax_xy = item.getMinMaxXY(); + if (first_item == true) { + this.mm_min_x = minmax_xy[0]; + this.mm_max_x = minmax_xy[1]; + this.mm_min_y = minmax_xy[2]; + this.mm_max_y = minmax_xy[3]; + first_item = false; + continue; + } + + if (this.mm_min_x > minmax_xy[0]) { + this.mm_min_x = minmax_xy[0]; + } + if (this.mm_max_x < minmax_xy[1]) { + this.mm_max_x = minmax_xy[1]; + } + if (this.mm_min_y > minmax_xy[2]) { + this.mm_min_y = minmax_xy[2]; + } + if (this.mm_max_y < minmax_xy[3]) { + this.mm_max_y = minmax_xy[3]; + } } - if (this.mm_max_y < minmax_xy[3]) { - this.mm_max_y = minmax_xy[3]; + + this.mm_scale = 1 / + Math.max(this.mm_max_x - this.mm_min_x, this.mm_max_y - this.mm_min_y); + + // Give a bit of a border + this.mm_min_x -= this.mm_scale * .05; + this.mm_max_x += this.mm_scale * .05; + this.mm_min_y -= this.mm_scale * .05; + this.mm_max_y += this.mm_scale * .05; + // TODO: there is a better way to do this than recalculate + this.mm_scale = 1 / + Math.max(this.mm_max_x - this.mm_min_x, this.mm_max_y - this.mm_min_y); + + this.redraw(); + this.scaleMiniMapViewBox(); + }; + + /** + * Scale the viewbox in the minimap. + * + * Calculate which part of the map is being displayed on the + * main viewport and scale the viewbox to reflect that. + */ + scaleMiniMapViewBox() { + if (!this.mm_display) { + return; } - } - this.mm_scale = 1 / - Math.max(this.mm_max_x - this.mm_min_x, this.mm_max_y - this.mm_min_y); + var mm_w = this.mm_width; + var mm_h = this.mm_height; - // Give a bit of a border - this.mm_min_x -= this.mm_scale * .05; - this.mm_max_x += this.mm_scale * .05; - this.mm_min_y -= this.mm_scale * .05; - this.mm_max_y += this.mm_scale * .05; - // TODO: there is a better way to do this than recalculate - this.mm_scale = 1 / - Math.max(this.mm_max_x - this.mm_min_x, this.mm_max_y - this.mm_min_y); + var w = mm_w * this.mm_scale; + var h = mm_h * this.mm_scale; - this.redraw(); - this.scaleMiniMapViewBox(); -}; + var disp_w = (this.mm_max_x - this.mm_min_x) * w; + var disp_h = (this.mm_max_y - this.mm_min_y) * h; -/** - * Scale the viewbox in the minimap. - * - * Calculate which part of the map is being displayed on the - * main viewport and scale the viewbox to reflect that. - */ -scaleMiniMapViewBox() { - if (!this.mm_display) { - return; - } - - var mm_w = this.mm_width; - var mm_h = this.mm_height; - - var w = mm_w * this.mm_scale; - var h = mm_h * this.mm_scale; - - var disp_w = (this.mm_max_x - this.mm_min_x) * w; - var disp_h = (this.mm_max_y - this.mm_min_y) * h; - - var view_offsetX = -(this.mm_min_x + this.offsetX) * - w + (mm_w - disp_w) / 2.; - var view_offsetY = -(this.mm_min_y + this.offsetY) * - h + (mm_h - disp_h) / 2.; - - this.view.setAttributeNS(null, 'x', view_offsetX); - this.view.setAttributeNS(null, 'y', view_offsetY); - this.view.setAttribute('width', w / this.scale); - this.view.setAttribute('height', h / this.scale); -}; + var view_offsetX = -(this.mm_min_x + this.offsetX) * + w + (mm_w - disp_w) / 2.; + var view_offsetY = -(this.mm_min_y + this.offsetY) * + h + (mm_h - disp_h) / 2.; + + this.view.setAttributeNS(null, 'x', view_offsetX); + this.view.setAttributeNS(null, 'y', view_offsetY); + this.view.setAttribute('width', w / this.scale); + this.view.setAttribute('height', h / this.scale); + }; } diff --git a/nengo_gui/static/components/netgraph_conn.ts b/nengo_gui/static/components/netgraph_conn.ts index 37e9c156..9b78a0aa 100644 --- a/nengo_gui/static/components/netgraph_conn.ts +++ b/nengo_gui/static/components/netgraph_conn.ts @@ -12,388 +12,388 @@ export default class NetGraphConnection { -constructor(ng, info, minimap, mini_conn) { - this.ng = ng; - this.uid = info.uid; - - // Flag to indicate this Connection has been deleted - this.removed = false; - - // The actual NetGraphItem currently connected to/from - this.pre = null; - this.post = null; - - this.minimap = minimap; - this.mini_conn = mini_conn; - if (!minimap) { - this.g_conns = ng.g_conns; - this.objects = ng.svg_objects; - } else { - this.g_conns = ng.g_conns_mini; - this.objects = ng.minimap_objects; - } - - // The uids for the pre and post items in the connection. + constructor(ng, info, minimap, mini_conn) { + this.ng = ng; + this.uid = info.uid; - // The lists start with the ideal target item, followed by the parent - // of that item, and its parent, and so on. If the first item on the - // this does not exist (due to it being inside a collapsed network), - // the connection will look for the next item on the list, and so on - // until it finds one that does exist. - this.pres = info.pre; - this.posts = info.post; + // Flag to indicate this Connection has been deleted + this.removed = false; - this.recurrent = this.pres[0] === this.posts[0]; + // The actual NetGraphItem currently connected to/from + this.pre = null; + this.post = null; - // Figure out the best available items to connect to - this.set_pre(this.find_pre()); - this.set_post(this.find_post()); - - // Determine parent and add to parent's children list - if (info.parent === null) { - this.parent = null; - } else { - this.parent = this.objects[info.parent]; + this.minimap = minimap; + this.mini_conn = mini_conn; if (!minimap) { - this.parent.child_connections.push(this); + this.g_conns = ng.g_conns; + this.objects = ng.svg_objects; + } else { + this.g_conns = ng.g_conns_mini; + this.objects = ng.minimap_objects; } - } - // Create the line and its arrowhead marker - this.g = ng.createSVGElement('g'); + // The uids for the pre and post items in the connection. - this.create_line(); + // The lists start with the ideal target item, followed by the parent + // of that item, and its parent, and so on. If the first item on the + // this does not exist (due to it being inside a collapsed network), + // the connection will look for the next item on the list, and so on + // until it finds one that does exist. + this.pres = info.pre; + this.posts = info.post; - this.redraw(); + this.recurrent = this.pres[0] === this.posts[0]; - this.g_conns.appendChild(this.g); -} + // Figure out the best available items to connect to + this.set_pre(this.find_pre()); + this.set_post(this.find_post()); -set_recurrent(recurrent) { - if (this.recurrent === recurrent) { - return; - } - this.remove_line(); - this.recurrent = recurrent; - this.create_line(); -}; - -create_line() { - if (this.recurrent) { - this.recurrent_ellipse = this.ng.createSVGElement('path'); - this.recurrent_ellipse.setAttribute( - 'd', - "M6.451,28.748C2.448,26.041,0,22.413,0,18.425C0, " + - "10.051,10.801,3.262,24.125,3.262 " + - "S48.25,10.051,48.25,18.425c0," + - "6.453-6.412,11.964-15.45,14.153"); - this.recurrent_ellipse.setAttribute('class', 'recur'); - this.g.appendChild(this.recurrent_ellipse); - - this.marker = this.ng.createSVGElement('path'); - this.g.appendChild(this.marker); - - if (this.minimap == false) { - this.marker.setAttribute('d', "M 6.5 0 L 0 5.0 L 7.5 8.0 z"); + // Determine parent and add to parent's children list + if (info.parent === null) { + this.parent = null; } else { - this.marker.setAttribute('d', "M 4 0 L 0 2 L 4 4 z"); - } - } else { - this.line = this.ng.createSVGElement('line'); - this.g.appendChild(this.line); - this.marker = this.ng.createSVGElement('path'); - if (this.minimap == false) { - this.marker.setAttribute('d', "M 10 0 L -5 -5 L -5 5 z"); - } else { - this.marker.setAttribute('d', "M 3 0 L -2.5 -2.5 L -2.5 2.5 z"); + this.parent = this.objects[info.parent]; + if (!minimap) { + this.parent.child_connections.push(this); + } } - this.g.appendChild(this.marker); - } -}; - -remove_line() { - if (this.recurrent) { - this.g.removeChild(this.recurrent_ellipse); - this.g.removeChild(this.marker); - this.recurrent_ellipse = undefined; - this.marker = undefined; - } else { - this.g.removeChild(this.line); - this.g.removeChild(this.marker); - this.line = undefined; - this.marker = undefined; - } -}; -/** - * Set the item connecting from. - */ -set_pre(pre) { - if (this.pre !== null) { - // If we're currently connected, disconnect - var index = this.pre.conn_out.indexOf(this); - if (index === -1) { - console.log('error removing in set_pre'); - } - this.pre.conn_out.splice(index, 1); - } - this.pre = pre; - if (this.pre !== null) { - // Add myself to pre's output connections list - this.pre.conn_out.push(this); + // Create the line and its arrowhead marker + this.g = ng.createSVGElement('g'); + + this.create_line(); + + this.redraw(); + + this.g_conns.appendChild(this.g); } -}; -/** - * Set the item connecting to. - */ -set_post(post) { - if (this.post !== null) { - // If we're currently connected, disconnect - var index = this.post.conn_in.indexOf(this); - if (index === -1) { - console.log('error removing in set_pre'); + set_recurrent(recurrent) { + if (this.recurrent === recurrent) { + return; } - this.post.conn_in.splice(index, 1); - } - this.post = post; - if (this.post !== null) { - // Add myself to post's input connections list - this.post.conn_in.push(this); - } -}; + this.remove_line(); + this.recurrent = recurrent; + this.create_line(); + }; + + create_line() { + if (this.recurrent) { + this.recurrent_ellipse = this.ng.createSVGElement('path'); + this.recurrent_ellipse.setAttribute( + 'd', + "M6.451,28.748C2.448,26.041,0,22.413,0,18.425C0, " + + "10.051,10.801,3.262,24.125,3.262 " + + "S48.25,10.051,48.25,18.425c0," + + "6.453-6.412,11.964-15.45,14.153"); + this.recurrent_ellipse.setAttribute('class', 'recur'); + this.g.appendChild(this.recurrent_ellipse); -/** - * Determine the best available item to connect from. - */ -find_pre() { - for (var i in this.pres) { - var pre = this.objects[this.pres[i]]; - if (pre !== undefined) { - return pre; + this.marker = this.ng.createSVGElement('path'); + this.g.appendChild(this.marker); + + if (this.minimap == false) { + this.marker.setAttribute('d', "M 6.5 0 L 0 5.0 L 7.5 8.0 z"); + } else { + this.marker.setAttribute('d', "M 4 0 L 0 2 L 4 4 z"); + } } else { - // Register to be notified if a better match occurs - this.ng.register_conn(this, this.pres[i]); + this.line = this.ng.createSVGElement('line'); + this.g.appendChild(this.line); + this.marker = this.ng.createSVGElement('path'); + if (this.minimap == false) { + this.marker.setAttribute('d', "M 10 0 L -5 -5 L -5 5 z"); + } else { + this.marker.setAttribute('d', "M 3 0 L -2.5 -2.5 L -2.5 2.5 z"); + } + this.g.appendChild(this.marker); } - } - return null; -}; - -/** - * Determine the best available item to connect to. - */ -find_post() { - for (var i in this.posts) { - var post = this.objects[this.posts[i]]; - if (post !== undefined) { - return post; + }; + + remove_line() { + if (this.recurrent) { + this.g.removeChild(this.recurrent_ellipse); + this.g.removeChild(this.marker); + this.recurrent_ellipse = undefined; + this.marker = undefined; } else { - // Register to be notified if a better match occurs - this.ng.register_conn(this, this.posts[i]); + this.g.removeChild(this.line); + this.g.removeChild(this.marker); + this.line = undefined; + this.marker = undefined; } - } - return null; -}; - -set_pres(pres) { - this.pres = pres; - this.set_pre(this.find_pre()); + }; + + /** + * Set the item connecting from. + */ + set_pre(pre) { + if (this.pre !== null) { + // If we're currently connected, disconnect + var index = this.pre.conn_out.indexOf(this); + if (index === -1) { + console.log('error removing in set_pre'); + } + this.pre.conn_out.splice(index, 1); + } + this.pre = pre; + if (this.pre !== null) { + // Add myself to pre's output connections list + this.pre.conn_out.push(this); + } + }; + + /** + * Set the item connecting to. + */ + set_post(post) { + if (this.post !== null) { + // If we're currently connected, disconnect + var index = this.post.conn_in.indexOf(this); + if (index === -1) { + console.log('error removing in set_pre'); + } + this.post.conn_in.splice(index, 1); + } + this.post = post; + if (this.post !== null) { + // Add myself to post's input connections list + this.post.conn_in.push(this); + } + }; + + /** + * Determine the best available item to connect from. + */ + find_pre() { + for (var i in this.pres) { + var pre = this.objects[this.pres[i]]; + if (pre !== undefined) { + return pre; + } else { + // Register to be notified if a better match occurs + this.ng.register_conn(this, this.pres[i]); + } + } + return null; + }; + + /** + * Determine the best available item to connect to. + */ + find_post() { + for (var i in this.posts) { + var post = this.objects[this.posts[i]]; + if (post !== undefined) { + return post; + } else { + // Register to be notified if a better match occurs + this.ng.register_conn(this, this.posts[i]); + } + } + return null; + }; - if (!this.minimap) { - this.mini_conn.set_pres(pres); - } -}; + set_pres(pres) { + this.pres = pres; + this.set_pre(this.find_pre()); -set_posts(posts) { - this.posts = posts; - this.set_post(this.find_post()); + if (!this.minimap) { + this.mini_conn.set_pres(pres); + } + }; - if (!this.minimap) { - this.mini_conn.set_posts(posts); - } -}; + set_posts(posts) { + this.posts = posts; + this.set_post(this.find_post()); -/** - * Remove this connection. - */ -remove() { - if (!this.minimap && this.parent !== null) { - var index = this.parent.child_connections.indexOf(this); - if (index === -1) { - console.log('error removing in remove'); + if (!this.minimap) { + this.mini_conn.set_posts(posts); } - this.parent.child_connections.splice(index, 1); - } - - if (this.pre != null) { - var index = this.pre.conn_out.indexOf(this); - if (index === -1) { - console.log('error removing from conn_out'); + }; + + /** + * Remove this connection. + */ + remove() { + if (!this.minimap && this.parent !== null) { + var index = this.parent.child_connections.indexOf(this); + if (index === -1) { + console.log('error removing in remove'); + } + this.parent.child_connections.splice(index, 1); } - this.pre.conn_out.splice(index, 1); - } - if (this.post != null) { - var index = this.post.conn_in.indexOf(this); - if (index === -1) { - console.log('error removing from conn_in'); + if (this.pre != null) { + var index = this.pre.conn_out.indexOf(this); + if (index === -1) { + console.log('error removing from conn_out'); + } + this.pre.conn_out.splice(index, 1); } - this.post.conn_in.splice(index, 1); - } - this.g_conns.removeChild(this.g); - this.removed = true; + if (this.post != null) { + var index = this.post.conn_in.indexOf(this); + if (index === -1) { + console.log('error removing from conn_in'); + } + this.post.conn_in.splice(index, 1); + } - delete this.ng.svg_conns[this.uid]; + this.g_conns.removeChild(this.g); + this.removed = true; - if (!this.minimap) { - this.mini_conn.remove(); - } -}; + delete this.ng.svg_conns[this.uid]; -/** - * Redraw the connection. - */ -redraw() { - if (this.pre === null || this.post === null) { - if (this.line !== undefined) { - this.line.setAttribute('visibility', 'hidden'); + if (!this.minimap) { + this.mini_conn.remove(); } - this.marker.setAttribute('visibility', 'hidden'); - return; - } else { - if (this.line !== undefined) { - this.line.setAttribute('visibility', 'visible'); - } - this.marker.setAttribute('visibility', 'visible'); - } - var pre_pos = this.pre.get_screen_location(); - - if (this.recurrent) { - var item = this.objects[this.pres[0]]; - if (item === undefined) { + }; + + /** + * Redraw the connection. + */ + redraw() { + if (this.pre === null || this.post === null) { + if (this.line !== undefined) { + this.line.setAttribute('visibility', 'hidden'); + } this.marker.setAttribute('visibility', 'hidden'); - this.recurrent_ellipse.setAttribute('visibility', 'hidden'); + return; } else { + if (this.line !== undefined) { + this.line.setAttribute('visibility', 'visible'); + } this.marker.setAttribute('visibility', 'visible'); - this.recurrent_ellipse.setAttribute('visibility', 'visible'); - var width = item.get_displayed_size()[0]; - var height = item.get_displayed_size()[1]; - - var scale = item.shape.getAttribute('transform'); - var scale_value = parseFloat(scale.split(/[()]+/)[1]); + } + var pre_pos = this.pre.get_screen_location(); - if (this.minimap == false) { - this.recurrent_ellipse.setAttribute( - 'style', 'stroke-width:' + 2/scale_value + ';'); + if (this.recurrent) { + var item = this.objects[this.pres[0]]; + if (item === undefined) { + this.marker.setAttribute('visibility', 'hidden'); + this.recurrent_ellipse.setAttribute('visibility', 'hidden'); } else { - this.recurrent_ellipse.setAttribute( - 'style', 'stroke-width:' + 1/scale_value + ';'); - } + this.marker.setAttribute('visibility', 'visible'); + this.recurrent_ellipse.setAttribute('visibility', 'visible'); + var width = item.get_displayed_size()[0]; + var height = item.get_displayed_size()[1]; - var ex = pre_pos[0] - scale_value*17.5; - var ey = pre_pos[1] - height - scale_value*36; + var scale = item.shape.getAttribute('transform'); + var scale_value = parseFloat(scale.split(/[()]+/)[1]); - this.recurrent_ellipse.setAttribute( - 'transform', 'translate(' + ex + ',' + ey + ')' + scale); + if (this.minimap == false) { + this.recurrent_ellipse.setAttribute( + 'style', 'stroke-width:' + 2/scale_value + ';'); + } else { + this.recurrent_ellipse.setAttribute( + 'style', 'stroke-width:' + 1/scale_value + ';'); + } - var mx = pre_pos[0] - 1; - if (this.minimap == false) { - var my = pre_pos[1] - height - scale_value*32.15 - 5; - } else { - var my = pre_pos[1] - height - scale_value*32 - 2; + var ex = pre_pos[0] - scale_value*17.5; + var ey = pre_pos[1] - height - scale_value*36; + + this.recurrent_ellipse.setAttribute( + 'transform', 'translate(' + ex + ',' + ey + ')' + scale); + + var mx = pre_pos[0] - 1; + if (this.minimap == false) { + var my = pre_pos[1] - height - scale_value*32.15 - 5; + } else { + var my = pre_pos[1] - height - scale_value*32 - 2; + } + this.marker.setAttribute( + 'transform', 'translate(' + mx + ',' + my + ')'); + } + } else { + var post_pos = this.post.get_screen_location(); + this.line.setAttribute('x1', pre_pos[0]); + this.line.setAttribute('y1', pre_pos[1]); + this.line.setAttribute('x2', post_pos[0]); + this.line.setAttribute('y2', post_pos[1]); + + // Angle between objects + var angle = Math.atan2( + post_pos[1] - pre_pos[1], post_pos[0] - pre_pos[0]); + + var w1 = this.pre.get_screen_width(); + var h1 = this.pre.get_screen_height(); + var w2 = this.post.get_screen_width(); + var h2 = this.post.get_screen_height(); + + var a1 = Math.atan2(h1, w1); + var a2 = Math.atan2(h2, w2); + + var pre_length = this.intersect_length(angle, a1, w1, h1); + var post_to_pre_angle = angle - Math.PI; + if (post_to_pre_angle < -Math.PI) { + post_to_pre_angle += 2*Math.PI; } + var post_length = this.intersect_length(post_to_pre_angle, a2, w2, h2); + + var mx = (pre_pos[0] + pre_length[0]) * 0.4 + + (post_pos[0] + post_length[0]) * 0.6; + var my = (pre_pos[1] + pre_length[1]) * 0.4 + + (post_pos[1] + post_length[1]) * 0.6; + + // Check to make sure the marker doesn't go past either endpoint + var vec1 = [post_pos[0] - pre_pos[0], post_pos[1] - pre_pos[1]]; + var vec2 = [mx - pre_pos[0], my - pre_pos[1]]; + var dot_prod = (vec1[0]*vec2[0] + vec1[1]*vec2[1]) / + (vec1[0]*vec1[0] + vec1[1]*vec1[1]); + + if (dot_prod < 0) { + mx = pre_pos[0]; + my = pre_pos[1]; + } else if (dot_prod > 1) { + mx = post_pos[0]; + my = post_pos[1]; + } + angle = 180 / Math.PI * angle; this.marker.setAttribute( - 'transform', 'translate(' + mx + ',' + my + ')'); - } - } else { - var post_pos = this.post.get_screen_location(); - this.line.setAttribute('x1', pre_pos[0]); - this.line.setAttribute('y1', pre_pos[1]); - this.line.setAttribute('x2', post_pos[0]); - this.line.setAttribute('y2', post_pos[1]); - - // Angle between objects - var angle = Math.atan2( - post_pos[1] - pre_pos[1], post_pos[0] - pre_pos[0]); - - var w1 = this.pre.get_screen_width(); - var h1 = this.pre.get_screen_height(); - var w2 = this.post.get_screen_width(); - var h2 = this.post.get_screen_height(); - - var a1 = Math.atan2(h1, w1); - var a2 = Math.atan2(h2, w2); - - var pre_length = this.intersect_length(angle, a1, w1, h1); - var post_to_pre_angle = angle - Math.PI; - if (post_to_pre_angle < -Math.PI) { - post_to_pre_angle += 2*Math.PI; - } - var post_length = this.intersect_length(post_to_pre_angle, a2, w2, h2); - - var mx = (pre_pos[0] + pre_length[0]) * 0.4 + - (post_pos[0] + post_length[0]) * 0.6; - var my = (pre_pos[1] + pre_length[1]) * 0.4 + - (post_pos[1] + post_length[1]) * 0.6; - - // Check to make sure the marker doesn't go past either endpoint - var vec1 = [post_pos[0] - pre_pos[0], post_pos[1] - pre_pos[1]]; - var vec2 = [mx - pre_pos[0], my - pre_pos[1]]; - var dot_prod = (vec1[0]*vec2[0] + vec1[1]*vec2[1]) / - (vec1[0]*vec1[0] + vec1[1]*vec1[1]); - - if (dot_prod < 0) { - mx = pre_pos[0]; - my = pre_pos[1]; - } else if (dot_prod > 1) { - mx = post_pos[0]; - my = post_pos[1]; + 'transform', 'translate(' + mx + ',' + my + ')' + + ' rotate(' + angle + ')'); } - angle = 180 / Math.PI * angle; - this.marker.setAttribute( - 'transform', 'translate(' + mx + ',' + my + ')' + - ' rotate(' + angle + ')'); - } - - if (!this.minimap && this.ng.mm_display) { - this.mini_conn.redraw(); - } -}; -/** - * Determine the length of an intersection line through a rectangle. - * - * @param {number} theta - the angle of the line - * @param {number} alpha - the angle between zero and the top right corner - * of the object - **/ -intersect_length( + if (!this.minimap && this.ng.mm_display) { + this.mini_conn.redraw(); + } + }; + + /** + * Determine the length of an intersection line through a rectangle. + * + * @param {number} theta - the angle of the line + * @param {number} alpha - the angle between zero and the top right corner + * of the object + **/ + intersect_length( theta, alpha, width, height) { - var quad = 0; - var beta = 2 * (Math.PI/2 - alpha); // Angle between top corners - var h2 = (height/2) * (height/2); - var w2 = (width/2) * (width/2); - - if (theta >= -alpha && theta < alpha) { - // 1st quadrant - var x = width/2; - var y = width/2 * Math.tan(theta); - } else if (theta >= alpha && theta < alpha + beta) { - // 2nd quadrant - var x = (height/2) / Math.tan(theta); - var y = height/2; - } else if (theta >= alpha + beta || theta < -(alpha + beta)) { - // 3rd quadrant - var x = -width/2; - var y = -width/2 * Math.tan(theta); - } else { - // 4th quadrant - var x = -(height/2) / Math.tan(theta); - var y = -height/2; - } + var quad = 0; + var beta = 2 * (Math.PI/2 - alpha); // Angle between top corners + var h2 = (height/2) * (height/2); + var w2 = (width/2) * (width/2); + + if (theta >= -alpha && theta < alpha) { + // 1st quadrant + var x = width/2; + var y = width/2 * Math.tan(theta); + } else if (theta >= alpha && theta < alpha + beta) { + // 2nd quadrant + var x = (height/2) / Math.tan(theta); + var y = height/2; + } else if (theta >= alpha + beta || theta < -(alpha + beta)) { + // 3rd quadrant + var x = -width/2; + var y = -width/2 * Math.tan(theta); + } else { + // 4th quadrant + var x = -(height/2) / Math.tan(theta); + var y = -height/2; + } - return [x, y]; -}; + return [x, y]; + }; } diff --git a/nengo_gui/static/components/netgraph_item.ts b/nengo_gui/static/components/netgraph_item.ts index 7ad44e00..a3fecd5f 100644 --- a/nengo_gui/static/components/netgraph_item.ts +++ b/nengo_gui/static/components/netgraph_item.ts @@ -16,948 +16,948 @@ import * as menu from "../menu"; export default class NetGraphItem { -constructor(ng, info, minimap, mini_item) { - var self = this; - - this.ng = ng; - this.type = info.type; - this.uid = info.uid; - this.sp_targets = info.sp_targets; - this.default_output = info.default_output; - this.passthrough = info.passthrough; - this.fixed_width = null; - this.fixed_height = null; - this.dimensions = info.dimensions; - this.minimap = minimap; - this.html_node = info.html; - if (minimap == false) { - this.g_networks = this.ng.g_networks; - this.g_items = this.ng.g_items; - this.mini_item = mini_item; - } else { - this.g_networks = this.ng.g_networks_mini; - this.g_items = this.ng.g_items_mini; - } - - var width = info.size[0]; - Object.defineProperty(this, 'width', { - get: function() { - return width; - }, - set: function(val) { - width = val; - - if (!this.minimap) { - this.mini_item.width = val; - } + constructor(ng, info, minimap, mini_item) { + var self = this; + + this.ng = ng; + this.type = info.type; + this.uid = info.uid; + this.sp_targets = info.sp_targets; + this.default_output = info.default_output; + this.passthrough = info.passthrough; + this.fixed_width = null; + this.fixed_height = null; + this.dimensions = info.dimensions; + this.minimap = minimap; + this.html_node = info.html; + if (minimap == false) { + this.g_networks = this.ng.g_networks; + this.g_items = this.ng.g_items; + this.mini_item = mini_item; + } else { + this.g_networks = this.ng.g_networks_mini; + this.g_items = this.ng.g_items_mini; } - }); - var height = info.size[1]; - Object.defineProperty(this, 'height', { - get: function() { - return height; - }, - set: function(val) { - height = val; - if (!this.minimap) { - this.mini_item.height = val; + var width = info.size[0]; + Object.defineProperty(this, 'width', { + get: function() { + return width; + }, + set: function(val) { + width = val; + + if (!this.minimap) { + this.mini_item.width = val; + } } - } - }); - var x = info.pos[0]; - Object.defineProperty(this, 'x', { - get: function() { - return x; - }, - set: function(val) { - x = val; + }); + var height = info.size[1]; + Object.defineProperty(this, 'height', { + get: function() { + return height; + }, + set: function(val) { + height = val; - if (!this.minimap) { - this.mini_item.x = val; + if (!this.minimap) { + this.mini_item.height = val; + } } - } - }); - var y = info.pos[1]; - Object.defineProperty(this, 'y', { - get: function() { - return y; - }, - set: function(val) { - y = val; + }); + var x = info.pos[0]; + Object.defineProperty(this, 'x', { + get: function() { + return x; + }, + set: function(val) { + x = val; - if (!this.minimap) { - this.mini_item.y = val; + if (!this.minimap) { + this.mini_item.x = val; + } } - } - }); - - // If this is a network, the children list is the set of NetGraphItems - // and NetGraphConnections that are inside this network. - this.children = []; - this.child_connections = []; - - // NetGraphConnections leading into and out of this item - this.conn_out = []; - this.conn_in = []; - - // Minimum and maximum drawn size, in pixels - this.minWidth = 5; - this.minHeight = 5; - this.aspect = null; - - this.expanded = false; - - // Determine the parent NetGraphItem (if any) and the nested depth - // of this item. - if (info.parent === null) { - this.parent = null; - this.depth = 1; - } else { - this.parent = self.ng.svg_objects[info.parent]; - this.depth = this.parent.depth + 1; - if (!minimap) { - this.parent.children.push(this); - } - } - - // Create the SVG group to hold this item - var g = this.ng.createSVGElement('g'); - this.g = g; - this.g_items.appendChild(g); - g.classList.add(this.type); - - this.area = this.ng.createSVGElement('rect'); - this.area.style.fill = 'transparent'; - - this.menu = new menu.Menu(this.ng.parent); - - // Different types use different SVG elements for display - if (info.type === 'node') { - if (this.passthrough) { - this.shape = this.ng.createSVGElement('ellipse'); - if (this.minimap == false) { - this.fixed_width = 10; - this.fixed_height = 10; - } else { - this.fixed_width = 3; - this.fixed_height = 3; + }); + var y = info.pos[1]; + Object.defineProperty(this, 'y', { + get: function() { + return y; + }, + set: function(val) { + y = val; + + if (!this.minimap) { + this.mini_item.y = val; + } } - this.g.classList.add('passthrough'); + }); + + // If this is a network, the children list is the set of NetGraphItems + // and NetGraphConnections that are inside this network. + this.children = []; + this.child_connections = []; + + // NetGraphConnections leading into and out of this item + this.conn_out = []; + this.conn_in = []; + + // Minimum and maximum drawn size, in pixels + this.minWidth = 5; + this.minHeight = 5; + this.aspect = null; + + this.expanded = false; + + // Determine the parent NetGraphItem (if any) and the nested depth + // of this item. + if (info.parent === null) { + this.parent = null; + this.depth = 1; } else { - this.shape = this.ng.createSVGElement('rect'); + this.parent = self.ng.svg_objects[info.parent]; + this.depth = this.parent.depth + 1; + if (!minimap) { + this.parent.children.push(this); + } } - } else if (info.type === 'net') { - this.shape = this.ng.createSVGElement('rect'); - } else if (info.type === 'ens') { - this.aspect = 1.; - this.shape = this.ensemble_svg(); - } else { - console.log("Unknown NetGraphItem type"); - console.log(info.type); - } - - this.compute_fill(); - - if (this.minimap == false) { - var label = this.ng.createSVGElement('text'); - this.label = label; - label.innerHTML = info.label; - g.appendChild(label); - }; - - g.appendChild(this.shape); - g.appendChild(this.area); - this.redraw(); + // Create the SVG group to hold this item + var g = this.ng.createSVGElement('g'); + this.g = g; + this.g_items.appendChild(g); + g.classList.add(this.type); - interact.margin(10); + this.area = this.ng.createSVGElement('rect'); + this.area.style.fill = 'transparent'; - if (!this.minimap) { - // Dragging an item to change its position - var uid = this.uid; - interact(g).draggable({ - onstart: function() { - menu.hide_any(); - self.move_to_front(); - }, - onmove: function(event) { - var w = self.ng.get_scaled_width(); - var h = self.ng.get_scaled_height(); - var item = self.ng.svg_objects[uid]; - var parent = item.parent; - while (parent !== null) { - w = w * parent.width * 2; - h = h * parent.height * 2; - parent = parent.parent; - } - item.x += event.dx / w; - item.y += event.dy / h; - item.redraw(); + this.menu = new menu.Menu(this.ng.parent); - if (self.depth === 1) { - self.ng.scaleMiniMap(); + // Different types use different SVG elements for display + if (info.type === 'node') { + if (this.passthrough) { + this.shape = this.ng.createSVGElement('ellipse'); + if (this.minimap == false) { + this.fixed_width = 10; + this.fixed_height = 10; + } else { + this.fixed_width = 3; + this.fixed_height = 3; } - }, - onend: function(event) { - var item = self.ng.svg_objects[uid]; - item.constrain_position(); - self.ng.notify({ - act: "pos", uid: uid, x: item.x, y: item.y - }); - - item.redraw(); + this.g.classList.add('passthrough'); + } else { + this.shape = this.ng.createSVGElement('rect'); } - }); + } else if (info.type === 'net') { + this.shape = this.ng.createSVGElement('rect'); + } else if (info.type === 'ens') { + this.aspect = 1.; + this.shape = this.ensemble_svg(); + } else { + console.log("Unknown NetGraphItem type"); + console.log(info.type); + } - if (!this.passthrough) { - // Dragging the edge of item to change its size - var tmp = this.shape; - if (info.type === 'ens') { - tmp = $(this.shape.getElementsByClassName('mainCircle'))[0]; - } - interact(this.area).resizable({ - edges: {left: true, right: true, bottom: true, top: true}, - invert: this.type == 'ens' ? 'reposition' : 'none' - }).on('resizestart', function(event) { - menu.hide_any(); - }).on('resizemove', function(event) { - var item = self.ng.svg_objects[uid]; - var pos = item.get_screen_location(); - var h_scale = self.ng.get_scaled_width(); - var v_scale = self.ng.get_scaled_height(); - var parent = item.parent; - while (parent !== null) { - h_scale = h_scale * parent.width * 2; - v_scale = v_scale * parent.height * 2; - parent = parent.parent; - } + this.compute_fill(); - if (self.aspect !== null) { - self.constrain_aspect(); + if (this.minimap == false) { + var label = this.ng.createSVGElement('text'); + this.label = label; + label.innerHTML = info.label; + g.appendChild(label); + }; - var vertical_resize = - event.edges.bottom || event.edges.top; - var horizontal_resize = - event.edges.left || event.edges.right; + g.appendChild(this.shape); + g.appendChild(this.area); - var w = pos[0] - event.clientX + self.ng.offsetX; - var h = pos[1] - event.clientY + self.ng.offsetY; + this.redraw(); - if (event.edges.right) { - w *= -1; - } - if (event.edges.bottom) { - h *= -1; - } - if (w < 0) { - w = 1; + interact.margin(10); + + if (!this.minimap) { + // Dragging an item to change its position + var uid = this.uid; + interact(g).draggable({ + onstart: function() { + menu.hide_any(); + self.move_to_front(); + }, + onmove: function(event) { + var w = self.ng.get_scaled_width(); + var h = self.ng.get_scaled_height(); + var item = self.ng.svg_objects[uid]; + var parent = item.parent; + while (parent !== null) { + w = w * parent.width * 2; + h = h * parent.height * 2; + parent = parent.parent; } - if (h < 0) { - h = 1; + item.x += event.dx / w; + item.y += event.dy / h; + item.redraw(); + + if (self.depth === 1) { + self.ng.scaleMiniMap(); } + }, + onend: function(event) { + var item = self.ng.svg_objects[uid]; + item.constrain_position(); + self.ng.notify({ + act: "pos", uid: uid, x: item.x, y: item.y + }); + + item.redraw(); + } + }); - var screen_w = item.width * h_scale; - var screen_h = item.height * v_scale; - - if (horizontal_resize && vertical_resize) { - var p = (screen_w * w + screen_h * h) / Math.sqrt( - screen_w * screen_w + screen_h * screen_h); - var norm = Math.sqrt(self.aspect * self.aspect + 1); - h = p / (self.aspect / norm); - w = p * (self.aspect / norm); - } else if (horizontal_resize) { - h = w / self.aspect; - } else { - w = h * self.aspect; + if (!this.passthrough) { + // Dragging the edge of item to change its size + var tmp = this.shape; + if (info.type === 'ens') { + tmp = $(this.shape.getElementsByClassName('mainCircle'))[0]; + } + interact(this.area).resizable({ + edges: {left: true, right: true, bottom: true, top: true}, + invert: this.type == 'ens' ? 'reposition' : 'none' + }).on('resizestart', function(event) { + menu.hide_any(); + }).on('resizemove', function(event) { + var item = self.ng.svg_objects[uid]; + var pos = item.get_screen_location(); + var h_scale = self.ng.get_scaled_width(); + var v_scale = self.ng.get_scaled_height(); + var parent = item.parent; + while (parent !== null) { + h_scale = h_scale * parent.width * 2; + v_scale = v_scale * parent.height * 2; + parent = parent.parent; } - var scaled_w = w / h_scale; - var scaled_h = h / v_scale; + if (self.aspect !== null) { + self.constrain_aspect(); - item.width = scaled_w; - item.height = scaled_h; - } else { - var dw = event.deltaRect.width / h_scale / 2; - var dh = event.deltaRect.height / v_scale / 2; - var offset_x = dw + event.deltaRect.left / h_scale; - var offset_y = dh + event.deltaRect.top / v_scale; - - item.width += dw; - item.height += dh; - item.x += offset_x; - item.y += offset_y; - } + var vertical_resize = + event.edges.bottom || event.edges.top; + var horizontal_resize = + event.edges.left || event.edges.right; - item.redraw(); + var w = pos[0] - event.clientX + self.ng.offsetX; + var h = pos[1] - event.clientY + self.ng.offsetY; - if (self.depth === 1) { - self.ng.scaleMiniMap(); - } - }).on('resizeend', function(event) { - var item = self.ng.svg_objects[uid]; - item.constrain_position(); - item.redraw(); - self.ng.notify({act: "pos_size", uid: uid, - x: item.x, y: item.y, - width: item.width, height: item.height}); - }); - } + if (event.edges.right) { + w *= -1; + } + if (event.edges.bottom) { + h *= -1; + } + if (w < 0) { + w = 1; + } + if (h < 0) { + h = 1; + } + + var screen_w = item.width * h_scale; + var screen_h = item.height * v_scale; + + if (horizontal_resize && vertical_resize) { + var p = (screen_w * w + screen_h * h) / Math.sqrt( + screen_w * screen_w + screen_h * screen_h); + var norm = Math.sqrt(self.aspect * self.aspect + 1); + h = p / (self.aspect / norm); + w = p * (self.aspect / norm); + } else if (horizontal_resize) { + h = w / self.aspect; + } else { + w = h * self.aspect; + } + + var scaled_w = w / h_scale; + var scaled_h = h / v_scale; - // Determine when to pull up the menu - interact(this.g) - .on('hold', function(event) { - // Change to 'tap' for right click - if (event.button == 0) { - if (self.menu.visible_any()) { - menu.hide_any(); + item.width = scaled_w; + item.height = scaled_h; } else { - self.menu.show(event.clientX, - event.clientY, - self.generate_menu()); + var dw = event.deltaRect.width / h_scale / 2; + var dh = event.deltaRect.height / v_scale / 2; + var offset_x = dw + event.deltaRect.left / h_scale; + var offset_y = dh + event.deltaRect.top / v_scale; + + item.width += dw; + item.height += dh; + item.x += offset_x; + item.y += offset_y; } - event.stopPropagation(); - } - }) - .on('tap', function(event) { - // Get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - menu.hide_any(); + + item.redraw(); + + if (self.depth === 1) { + self.ng.scaleMiniMap(); } - } - }) - .on('doubletap', function(event) { - // Get rid of menus when clicking off - if (event.button == 0) { - if (self.menu.visible_any()) { - menu.hide_any(); - } else if (self.type === 'net') { - if (self.expanded) { - self.collapse(true); + }).on('resizeend', function(event) { + var item = self.ng.svg_objects[uid]; + item.constrain_position(); + item.redraw(); + self.ng.notify({act: "pos_size", uid: uid, + x: item.x, y: item.y, + width: item.width, height: item.height}); + }); + } + + // Determine when to pull up the menu + interact(this.g) + .on('hold', function(event) { + // Change to 'tap' for right click + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); } else { - self.expand(); + self.menu.show(event.clientX, + event.clientY, + self.generate_menu()); + } + event.stopPropagation(); + } + }) + .on('tap', function(event) { + // Get rid of menus when clicking off + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); + } + } + }) + .on('doubletap', function(event) { + // Get rid of menus when clicking off + if (event.button == 0) { + if (self.menu.visible_any()) { + menu.hide_any(); + } else if (self.type === 'net') { + if (self.expanded) { + self.collapse(true); + } else { + self.expand(); + } } } + }); + $(this.g).bind('contextmenu', function(event) { + event.preventDefault(); + event.stopPropagation(); + if (self.menu.visible_any()) { + menu.hide_any(); + } else { + self.menu.show( + event.clientX, event.clientY, self.generate_menu()); } }); - $(this.g).bind('contextmenu', function(event) { - event.preventDefault(); - event.stopPropagation(); - if (self.menu.visible_any()) { - menu.hide_any(); - } else { - self.menu.show( - event.clientX, event.clientY, self.generate_menu()); - } - }); - if (info.type === 'net') { - // If a network is flagged to expand on creation, then expand it - if (info.expanded) { - // Report to server but do not add to the undo stack - this.expand(true, true); + if (info.type === 'net') { + // If a network is flagged to expand on creation, then expand it + if (info.expanded) { + // Report to server but do not add to the undo stack + this.expand(true, true); + } } - } + }; }; -}; -set_label(label) { - this.label.innerHTML = label; -}; + set_label(label) { + this.label.innerHTML = label; + }; -move_to_front() { - this.g.parentNode.appendChild(this.g); + move_to_front() { + this.g.parentNode.appendChild(this.g); - for (var item in this.children) { - this.children[item].move_to_front(); - } -}; + for (var item in this.children) { + this.children[item].move_to_front(); + } + }; -generate_menu() { - var self = this; - var items = []; - if (this.type === 'net') { - if (this.expanded) { - items.push(['Collapse network', function() { - self.collapse(true); + generate_menu() { + var self = this; + var items = []; + if (this.type === 'net') { + if (this.expanded) { + items.push(['Collapse network', function() { + self.collapse(true); + }]); + items.push(['Auto-layout', function() { + self.request_feedforward_layout(); + }]); + } else { + items.push(['Expand network', function() { + self.expand(); + }]); + } + if (this.default_output && this.sp_targets.length == 0) { + items.push(['Output Value', function() { + self.create_graph('Value'); + }]); + } + } + if (this.type == 'ens') { + items.push(['Value', function() { + self.create_graph('Value'); }]); - items.push(['Auto-layout', function() { - self.request_feedforward_layout(); + if (this.dimensions > 1) { + items.push(['XY-value', function() { + self.create_graph('XYValue'); + }]); + } + items.push(['Spikes', function() { + self.create_graph('Raster'); }]); - } else { - items.push(['Expand network', function() { - self.expand(); + items.push(['Voltages', function() { + self.create_graph('Voltage'); + }]); + items.push(['Firing pattern', function() { + self.create_graph('SpikeGrid'); }]); } - if (this.default_output && this.sp_targets.length == 0) { - items.push(['Output Value', function() { - self.create_graph('Value'); + if (this.type == 'node') { + items.push(['Slider', function() { + self.create_graph('Slider'); }]); + if (this.dimensions > 0) { + items.push(['Value', function() { + self.create_graph('Value'); + }]); + } + if (this.dimensions > 1) { + items.push(['XY-value', function() { + self.create_graph('XYValue'); + }]); + } + if (this.html_node) { + items.push(['HTML', function() { + self.create_graph('HTMLView'); + }]); + } } - } - if (this.type == 'ens') { - items.push(['Value', function() { - self.create_graph('Value'); - }]); - if (this.dimensions > 1) { - items.push(['XY-value', function() { - self.create_graph('XYValue'); + if (this.sp_targets.length > 0) { + items.push(['Semantic pointer cloud', function() { + self.create_graph('Pointer', self.sp_targets[0]); + }]); + items.push(['Semantic pointer plot', function() { + self.create_graph('SpaSimilarity', self.sp_targets[0]); }]); } - items.push(['Spikes', function() { - self.create_graph('Raster'); - }]); - items.push(['Voltages', function() { - self.create_graph('Voltage'); + // TODO: Enable input and output value plots for basal ganglia network + items.push(['Details ...', function() { + self.create_modal(); }]); - items.push(['Firing pattern', function() { - self.create_graph('SpikeGrid'); - }]); - } - if (this.type == 'node') { - items.push(['Slider', function() { - self.create_graph('Slider'); - }]); - if (this.dimensions > 0) { - items.push(['Value', function() { - self.create_graph('Value'); - }]); + return items; + }; + + create_graph(type, args) { + var info = {}; + info.act = 'create_graph'; + info.type = type; + var w = this.get_nested_width(); + var h = this.get_nested_height(); + + var pos = this.get_screen_location(); + + info.x = pos[0] / (this.ng.viewport.w * this.ng.viewport.scale) - + this.ng.viewport.x + w; + info.y = pos[1] / (this.ng.viewport.h * this.ng.viewport.scale) - + this.ng.viewport.y + h; + + info.width = 100 / (this.ng.viewport.w * this.ng.viewport.scale); + info.height = 100 / (this.ng.viewport.h * this.ng.viewport.scale); + + if (info.type == 'Slider') { + info.width /= 2; } - if (this.dimensions > 1) { - items.push(['XY-value', function() { - self.create_graph('XYValue'); - }]); + + info.uid = this.uid; + if (typeof(args) != 'undefined') { + info.args = args; } - if (this.html_node) { - items.push(['HTML', function() { - self.create_graph('HTMLView'); - }]); + this.ng.notify(info); + }; + + create_modal() { + var info = {}; + info.act = 'create_modal'; + info.uid = this.uid; + info.conn_in_uids = this.conn_in.map(function(c) { + return c.uid; + }); + info.conn_out_uids = this.conn_out.map(function(c) { + return c.uid; + }); + this.ng.notify(info); + }; + + request_feedforward_layout() { + this.ng.notify({act: "feedforward_layout", uid: this.uid}); + }; + + /** + * Expand a collapsed network. + */ + expand(rts, auto) { + // Default to true if no parameter is specified + rts = typeof rts !== 'undefined' ? rts : true; + auto = typeof auto !== 'undefined' ? auto : false; + + this.g.classList.add('expanded'); + + if (!this.expanded) { + this.expanded = true; + if (this.ng.transparent_nets) { + this.shape.style["fill-opacity"] = 0.0; + } + this.g_items.removeChild(this.g); + this.g_networks.appendChild(this.g); + if (!this.minimap) { + this.mini_item.expand(rts, auto); + } + } else { + console.log("expanded a network that was already expanded"); + console.log(this); } - } - if (this.sp_targets.length > 0) { - items.push(['Semantic pointer cloud', function() { - self.create_graph('Pointer', self.sp_targets[0]); - }]); - items.push(['Semantic pointer plot', function() { - self.create_graph('SpaSimilarity', self.sp_targets[0]); - }]); - } - // TODO: Enable input and output value plots for basal ganglia network - items.push(['Details ...', function() { - self.create_modal(); - }]); - return items; -}; - -create_graph(type, args) { - var info = {}; - info.act = 'create_graph'; - info.type = type; - var w = this.get_nested_width(); - var h = this.get_nested_height(); - - var pos = this.get_screen_location(); - - info.x = pos[0] / (this.ng.viewport.w * this.ng.viewport.scale) - - this.ng.viewport.x + w; - info.y = pos[1] / (this.ng.viewport.h * this.ng.viewport.scale) - - this.ng.viewport.y + h; - - info.width = 100 / (this.ng.viewport.w * this.ng.viewport.scale); - info.height = 100 / (this.ng.viewport.h * this.ng.viewport.scale); - - if (info.type == 'Slider') { - info.width /= 2; - } - - info.uid = this.uid; - if (typeof(args) != 'undefined') { - info.args = args; - } - this.ng.notify(info); -}; - -create_modal() { - var info = {}; - info.act = 'create_modal'; - info.uid = this.uid; - info.conn_in_uids = this.conn_in.map(function(c) { - return c.uid; - }); - info.conn_out_uids = this.conn_out.map(function(c) { - return c.uid; - }); - this.ng.notify(info); -}; - -request_feedforward_layout() { - this.ng.notify({act: "feedforward_layout", uid: this.uid}); -}; -/** - * Expand a collapsed network. - */ -expand(rts, auto) { - // Default to true if no parameter is specified - rts = typeof rts !== 'undefined' ? rts : true; - auto = typeof auto !== 'undefined' ? auto : false; + if (rts) { + if (auto) { + // Update the server, but do not place on the undo stack + this.ng.notify({act: "auto_expand", uid: this.uid}); + } else { + this.ng.notify({act: "expand", uid: this.uid}); + } + } + }; - this.g.classList.add('expanded'); + set_label_below(flag) { + if (flag && !this.label_below) { + var screen_h = this.get_screen_height(); + this.label.setAttribute( + 'transform', 'translate(0, ' + (screen_h / 2) + ')'); + } else if (!flag && this.label_below) { + this.label.setAttribute('transform', ''); + } + }; + + /** + * Collapse an expanded network. + */ + collapse(report_to_server, auto) { + auto = typeof auto !== 'undefined' ? auto : false; + this.g.classList.remove('expanded'); + + // Remove child NetGraphItems and NetGraphConnections + while (this.child_connections.length > 0) { + this.child_connections[0].remove(); + } + while (this.children.length > 0) { + this.children[0].remove(); + } + + if (this.expanded) { + this.expanded = false; + if (this.ng.transparent_nets) { + this.shape.style["fill-opacity"] = 1.0; + } + this.g_networks.removeChild(this.g); + this.g_items.appendChild(this.g); + if (!this.minimap) { + this.mini_item.collapse(report_to_server, auto); + } + } else { + console.log("collapsed a network that was already collapsed"); + console.log(this); + } - if (!this.expanded) { - this.expanded = true; - if (this.ng.transparent_nets) { - this.shape.style["fill-opacity"] = 0.0; + if (report_to_server) { + if (auto) { + // Update the server, but do not place on the undo stack + this.ng.notify({act: "auto_collapse", uid: this.uid}); + } else { + this.ng.notify({act: "collapse", uid: this.uid}); + } } + }; + + /** + * Determine the fill color based on the depth. + */ + compute_fill() { + var depth = this.ng.transparent_nets ? 1 : this.depth; + + if (!this.passthrough) { + var fill = Math.round(255 * Math.pow(0.8, depth)); + this.shape.style.fill = 'rgb(' + fill + ',' + fill + ',' + fill + ')'; + var stroke = Math.round(255 * Math.pow(0.8, depth + 2)); + this.shape.style.stroke = + 'rgb(' + stroke + ',' + stroke + ',' + stroke + ')'; + } + }; + + /** + * Remove the item from the graph. + */ + remove() { + if (this.expanded) { + // Collapse the item, but don't tell the server since that would + // update the server's config + this.collapse(false); + } + + // Remove the item from the parent's children list + if (!this.minimap && this.parent !== null) { + var index = this.parent.children.indexOf(this); + this.parent.children.splice(index, 1); + } + + delete this.ng.svg_objects[this.uid]; + + // Update any connections into or out of this item + var conn_in = this.conn_in.slice(); + for (var i in conn_in) { + var conn = conn_in[i]; + conn.set_post(conn.find_post()); + conn.redraw(); + } + var conn_out = this.conn_out.slice(); + for (var i in conn_out) { + var conn = conn_out[i]; + conn.set_pre(conn.find_pre()); + conn.redraw(); + } + + // Remove from the SVG this.g_items.removeChild(this.g); - this.g_networks.appendChild(this.g); + if (this.depth == 1) { + this.ng.scaleMiniMap(); + } + if (!this.minimap) { - this.mini_item.expand(rts, auto); + this.mini_item.remove(); } - } else { - console.log("expanded a network that was already expanded"); - console.log(this); - } + }; + + constrain_aspect() { + this.size = this.get_displayed_size(); + }; + + get_displayed_size() { + if (this.aspect !== null) { + var h_scale = this.ng.get_scaled_width(); + var v_scale = this.ng.get_scaled_height(); + var w = this.get_nested_width() * h_scale; + var h = this.get_nested_height() * v_scale; + + if (h * this.aspect < w) { + w = h * this.aspect; + } else if (w / this.aspect < h) { + h = w / this.aspect; + } - if (rts) { - if (auto) { - // Update the server, but do not place on the undo stack - this.ng.notify({act: "auto_expand", uid: this.uid}); + return [w / h_scale, h / v_scale]; } else { - this.ng.notify({act: "expand", uid: this.uid}); + return [this.width, this.height]; + } + }; + + constrain_position() { + this.constrain_aspect(); + + if (this.parent !== null) { + this.width = Math.min(0.5, this.width); + this.height = Math.min(0.5, this.height); + + this.x = Math.min(this.x, 1.0 - this.width); + this.x = Math.max(this.x, this.width); + + this.y = Math.min(this.y, 1.0 - this.height); + this.y = Math.max(this.y, this.height); } - } -}; + }; -set_label_below(flag) { - if (flag && !this.label_below) { + redraw_position() { + var screen = this.get_screen_location(); + + // Update my position + this.g.setAttribute('transform', 'translate(' + screen[0] + ', ' + + screen[1] + ')'); + }; + + redraw_children() { + // Update any children's positions + for (var i in this.children) { + var item = this.children[i]; + item.redraw(); + } + }; + + redraw_child_connections() { + // Update any children's positions + for (var i in this.child_connections) { + var item = this.child_connections[i]; + item.redraw(); + } + }; + + redraw_connections() { + // Update any connections into and out of this + for (var i in this.conn_in) { + var item = this.conn_in[i]; + item.redraw(); + } + for (var i in this.conn_out) { + var item = this.conn_out[i]; + item.redraw(); + } + }; + + /** + * Return the width of the item, taking into account parent widths. + */ + get_nested_width() { + var w = this.width; + var parent = this.parent; + while (parent !== null) { + w *= parent.width * 2; + parent = parent.parent; + } + return w; + }; + + /** + * Return the height of the item, taking into account parent heights. + */ + get_nested_height() { + var h = this.height; + var parent = this.parent; + while (parent !== null) { + h *= parent.height * 2; + parent = parent.parent; + } + return h; + }; + + redraw_size() { + var screen_w = this.get_screen_width(); var screen_h = this.get_screen_height(); - this.label.setAttribute( - 'transform', 'translate(0, ' + (screen_h / 2) + ')'); - } else if (!flag && this.label_below) { - this.label.setAttribute('transform', ''); - } -}; -/** - * Collapse an expanded network. - */ -collapse(report_to_server, auto) { - auto = typeof auto !== 'undefined' ? auto : false; - this.g.classList.remove('expanded'); - - // Remove child NetGraphItems and NetGraphConnections - while (this.child_connections.length > 0) { - this.child_connections[0].remove(); - } - while (this.children.length > 0) { - this.children[0].remove(); - } - - if (this.expanded) { - this.expanded = false; - if (this.ng.transparent_nets) { - this.shape.style["fill-opacity"] = 1.0; + if (this.aspect !== null) { + if (screen_h * this.aspect < screen_w) { + screen_w = screen_h * this.aspect; + } else if (screen_w / this.aspect < screen_h) { + screen_h = screen_w / this.aspect; + } + } + + // The circle pattern isn't perfectly square, so make its area smaller + var area_w = this.type === 'ens' ? screen_w * 0.97 : screen_w; + var area_h = screen_h; + this.area.setAttribute('transform', + 'translate(-' + (area_w / 2) + ', -' + (area_h / 2) + ')'); + this.area.setAttribute('width', area_w); + this.area.setAttribute('height', area_h); + + if (this.type === 'ens') { + var scale = Math.sqrt(screen_h * screen_h + screen_w * screen_w) / + Math.sqrt(2); + var r = 17.8; // TODO: Don't hardcode the size of the ensemble + this.shape.setAttribute('transform', 'scale(' + scale / 2 / r + ')'); + this.shape.style.setProperty('stroke-width', 20/scale); + } else if (this.passthrough) { + this.shape.setAttribute('rx', screen_w / 2); + this.shape.setAttribute('ry', screen_h / 2); + } else { + this.shape.setAttribute( + 'transform', + 'translate(-' + (screen_w / 2) + ', -' + (screen_h / 2) + ')'); + this.shape.setAttribute('width', screen_w); + this.shape.setAttribute('height', screen_h); + if (this.type === 'node') { + var radius = Math.min(screen_w, screen_h); + // TODO: Don't hardcode .1 as the corner radius scale + this.shape.setAttribute('rx', radius*.1); + this.shape.setAttribute('ry', radius*.1); + } } - this.g_networks.removeChild(this.g); - this.g_items.appendChild(this.g); + if (!this.minimap) { - this.mini_item.collapse(report_to_server, auto); + this.label.setAttribute( + 'transform', 'translate(0, ' + (screen_h / 2) + ')'); + }; + }; + + get_screen_width() { + if (this.minimap && !this.ng.mm_display) { + return 1; + } + + if (this.fixed_width !== null) { + return this.fixed_width; } - } else { - console.log("collapsed a network that was already collapsed"); - console.log(this); - } - if (report_to_server) { - if (auto) { - // Update the server, but do not place on the undo stack - this.ng.notify({act: "auto_collapse", uid: this.uid}); + if (!this.minimap) { + var w = this.ng.width; + var screen_w = this.get_nested_width() * w * this.ng.scale; } else { - this.ng.notify({act: "collapse", uid: this.uid}); + var w = this.ng.mm_width; + var screen_w = this.get_nested_width() * w * this.ng.mm_scale; + }; + + if (screen_w < this.minWidth) { + screen_w = this.minWidth; } - } -}; -/** - * Determine the fill color based on the depth. - */ -compute_fill() { - var depth = this.ng.transparent_nets ? 1 : this.depth; - - if (!this.passthrough) { - var fill = Math.round(255 * Math.pow(0.8, depth)); - this.shape.style.fill = 'rgb(' + fill + ',' + fill + ',' + fill + ')'; - var stroke = Math.round(255 * Math.pow(0.8, depth + 2)); - this.shape.style.stroke = - 'rgb(' + stroke + ',' + stroke + ',' + stroke + ')'; - } -}; + return screen_w * 2; + }; -/** - * Remove the item from the graph. - */ -remove() { - if (this.expanded) { - // Collapse the item, but don't tell the server since that would - // update the server's config - this.collapse(false); - } - - // Remove the item from the parent's children list - if (!this.minimap && this.parent !== null) { - var index = this.parent.children.indexOf(this); - this.parent.children.splice(index, 1); - } - - delete this.ng.svg_objects[this.uid]; - - // Update any connections into or out of this item - var conn_in = this.conn_in.slice(); - for (var i in conn_in) { - var conn = conn_in[i]; - conn.set_post(conn.find_post()); - conn.redraw(); - } - var conn_out = this.conn_out.slice(); - for (var i in conn_out) { - var conn = conn_out[i]; - conn.set_pre(conn.find_pre()); - conn.redraw(); - } - - // Remove from the SVG - this.g_items.removeChild(this.g); - if (this.depth == 1) { - this.ng.scaleMiniMap(); - } - - if (!this.minimap) { - this.mini_item.remove(); - } -}; - -constrain_aspect() { - this.size = this.get_displayed_size(); -}; - -get_displayed_size() { - if (this.aspect !== null) { - var h_scale = this.ng.get_scaled_width(); - var v_scale = this.ng.get_scaled_height(); - var w = this.get_nested_width() * h_scale; - var h = this.get_nested_height() * v_scale; - - if (h * this.aspect < w) { - w = h * this.aspect; - } else if (w / this.aspect < h) { - h = w / this.aspect; - } - - return [w / h_scale, h / v_scale]; - } else { - return [this.width, this.height]; - } -}; - -constrain_position() { - this.constrain_aspect(); - - if (this.parent !== null) { - this.width = Math.min(0.5, this.width); - this.height = Math.min(0.5, this.height); - - this.x = Math.min(this.x, 1.0 - this.width); - this.x = Math.max(this.x, this.width); - - this.y = Math.min(this.y, 1.0 - this.height); - this.y = Math.max(this.y, this.height); - } -}; - -redraw_position() { - var screen = this.get_screen_location(); - - // Update my position - this.g.setAttribute('transform', 'translate(' + screen[0] + ', ' + - screen[1] + ')'); -}; - -redraw_children() { - // Update any children's positions - for (var i in this.children) { - var item = this.children[i]; - item.redraw(); - } -}; - -redraw_child_connections() { - // Update any children's positions - for (var i in this.child_connections) { - var item = this.child_connections[i]; - item.redraw(); - } -}; - -redraw_connections() { - // Update any connections into and out of this - for (var i in this.conn_in) { - var item = this.conn_in[i]; - item.redraw(); - } - for (var i in this.conn_out) { - var item = this.conn_out[i]; - item.redraw(); - } -}; + get_screen_height() { + if (this.minimap && !this.ng.mm_display) { + return 1; + } -/** - * Return the width of the item, taking into account parent widths. - */ -get_nested_width() { - var w = this.width; - var parent = this.parent; - while (parent !== null) { - w *= parent.width * 2; - parent = parent.parent; - } - return w; -}; + if (this.fixed_height !== null) { + return this.fixed_height; + } -/** - * Return the height of the item, taking into account parent heights. - */ -get_nested_height() { - var h = this.height; - var parent = this.parent; - while (parent !== null) { - h *= parent.height * 2; - parent = parent.parent; - } - return h; -}; - -redraw_size() { - var screen_w = this.get_screen_width(); - var screen_h = this.get_screen_height(); - - if (this.aspect !== null) { - if (screen_h * this.aspect < screen_w) { - screen_w = screen_h * this.aspect; - } else if (screen_w / this.aspect < screen_h) { - screen_h = screen_w / this.aspect; - } - } - - // The circle pattern isn't perfectly square, so make its area smaller - var area_w = this.type === 'ens' ? screen_w * 0.97 : screen_w; - var area_h = screen_h; - this.area.setAttribute('transform', - 'translate(-' + (area_w / 2) + ', -' + (area_h / 2) + ')'); - this.area.setAttribute('width', area_w); - this.area.setAttribute('height', area_h); - - if (this.type === 'ens') { - var scale = Math.sqrt(screen_h * screen_h + screen_w * screen_w) / - Math.sqrt(2); - var r = 17.8; // TODO: Don't hardcode the size of the ensemble - this.shape.setAttribute('transform', 'scale(' + scale / 2 / r + ')'); - this.shape.style.setProperty('stroke-width', 20/scale); - } else if (this.passthrough) { - this.shape.setAttribute('rx', screen_w / 2); - this.shape.setAttribute('ry', screen_h / 2); - } else { - this.shape.setAttribute( - 'transform', - 'translate(-' + (screen_w / 2) + ', -' + (screen_h / 2) + ')'); - this.shape.setAttribute('width', screen_w); - this.shape.setAttribute('height', screen_h); - if (this.type === 'node') { - var radius = Math.min(screen_w, screen_h); - // TODO: Don't hardcode .1 as the corner radius scale - this.shape.setAttribute('rx', radius*.1); - this.shape.setAttribute('ry', radius*.1); - } - } - - if (!this.minimap) { - this.label.setAttribute( - 'transform', 'translate(0, ' + (screen_h / 2) + ')'); + if (this.minimap == false) { + var h = this.ng.height; + var screen_h = this.get_nested_height() * h * this.ng.scale; + } else { + var h = this.ng.mm_height; + var screen_h = this.get_nested_height() * h * this.ng.mm_scale; + }; + + if (screen_h < this.minHeight) { + screen_h = this.minHeight; + } + + return screen_h * 2; }; -}; - -get_screen_width() { - if (this.minimap && !this.ng.mm_display) { - return 1; - } - - if (this.fixed_width !== null) { - return this.fixed_width; - } - - if (!this.minimap) { - var w = this.ng.width; - var screen_w = this.get_nested_width() * w * this.ng.scale; - } else { - var w = this.ng.mm_width; - var screen_w = this.get_nested_width() * w * this.ng.mm_scale; + + /** + * Force a redraw of the item. + */ + redraw() { + this.redraw_position(); + this.redraw_size(); + this.redraw_children(); + this.redraw_child_connections(); + this.redraw_connections(); + + if (!this.minimap && this.ng.mm_display) { + this.mini_item.redraw(); + } }; - if (screen_w < this.minWidth) { - screen_w = this.minWidth; - } + /** + * Determine the pixel location of the centre of the item. + */ + get_screen_location() { + // FIXME: this should probably use this.ng.get_scaled_width + // and this.ng.get_scaled_height + if (this.minimap && !this.ng.mm_display) { + return [1, 1]; + } - return screen_w * 2; -}; + if (this.minimap == false) { + var w = this.ng.width * this.ng.scale; + var h = this.ng.height * this.ng.scale; -get_screen_height() { - if (this.minimap && !this.ng.mm_display) { - return 1; - } + var offsetX = this.ng.offsetX * w; + var offsetY = this.ng.offsetY * h; + } else { + var mm_w = this.ng.mm_width; + var mm_h = this.ng.mm_height; - if (this.fixed_height !== null) { - return this.fixed_height; - } + var w = mm_w * this.ng.mm_scale; + var h = mm_h * this.ng.mm_scale; - if (this.minimap == false) { - var h = this.ng.height; - var screen_h = this.get_nested_height() * h * this.ng.scale; - } else { - var h = this.ng.mm_height; - var screen_h = this.get_nested_height() * h * this.ng.mm_scale; - }; + var disp_w = (this.ng.mm_max_x - this.ng.mm_min_x) * w; + var disp_h = (this.ng.mm_max_y - this.ng.mm_min_y) * h; - if (screen_h < this.minHeight) { - screen_h = this.minHeight; - } + var offsetX = -this.ng.mm_min_x * w + (mm_w - disp_w) / 2.; + var offsetY = -this.ng.mm_min_y * h + (mm_h - disp_h) / 2.; + }; - return screen_h * 2; -}; + var dx = 0; + var dy = 0; + var parent = this.parent; + while (parent !== null) { + dx *= parent.width * 2; + dy *= parent.height * 2; -/** - * Force a redraw of the item. - */ -redraw() { - this.redraw_position(); - this.redraw_size(); - this.redraw_children(); - this.redraw_child_connections(); - this.redraw_connections(); - - if (!this.minimap && this.ng.mm_display) { - this.mini_item.redraw(); - } -}; + dx += (parent.x - parent.width); + dy += (parent.y - parent.height); + parent = parent.parent; + } + dx *= w; + dy *= h; + + var ww = w; + var hh = h; + if (this.parent !== null) { + ww *= this.parent.get_nested_width() * 2; + hh *= this.parent.get_nested_height() * 2; + } -/** - * Determine the pixel location of the centre of the item. - */ -get_screen_location() { - // FIXME: this should probably use this.ng.get_scaled_width - // and this.ng.get_scaled_height - if (this.minimap && !this.ng.mm_display) { - return [1, 1]; - } - - if (this.minimap == false) { - var w = this.ng.width * this.ng.scale; - var h = this.ng.height * this.ng.scale; - - var offsetX = this.ng.offsetX * w; - var offsetY = this.ng.offsetY * h; - } else { - var mm_w = this.ng.mm_width; - var mm_h = this.ng.mm_height; - - var w = mm_w * this.ng.mm_scale; - var h = mm_h * this.ng.mm_scale; - - var disp_w = (this.ng.mm_max_x - this.ng.mm_min_x) * w; - var disp_h = (this.ng.mm_max_y - this.ng.mm_min_y) * h; - - var offsetX = -this.ng.mm_min_x * w + (mm_w - disp_w) / 2.; - var offsetY = -this.ng.mm_min_y * h + (mm_h - disp_h) / 2.; + return [this.x * ww + dx + offsetX, + this.y * hh + dy + offsetY]; }; - var dx = 0; - var dy = 0; - var parent = this.parent; - while (parent !== null) { - dx *= parent.width * 2; - dy *= parent.height * 2; - - dx += (parent.x - parent.width); - dy += (parent.y - parent.height); - parent = parent.parent; - } - dx *= w; - dy *= h; - - var ww = w; - var hh = h; - if (this.parent !== null) { - ww *= this.parent.get_nested_width() * 2; - hh *= this.parent.get_nested_height() * 2; - } - - return [this.x * ww + dx + offsetX, - this.y * hh + dy + offsetY]; -}; + /** + * Function for drawing ensemble svg. + */ + ensemble_svg() { + var shape = this.ng.createSVGElement('g'); + shape.setAttribute('class', 'ensemble'); + + var dx = -1.25; + var dy = 0.25; + + var circle = this.ng.createSVGElement('circle'); + this.setAttributes( + circle, {'cx': -11.157 + dx, 'cy': -7.481 + dy, 'r': '4.843'}); + shape.appendChild(circle); + var circle = this.ng.createSVGElement('circle'); + this.setAttributes( + circle, {'cx': 0.186 + dx, 'cy': -0.127 + dy, 'r': '4.843'}); + shape.appendChild(circle); + var circle = this.ng.createSVGElement('circle'); + this.setAttributes( + circle, {'cx': 5.012 + dx, 'cy': 12.56 + dy, 'r': '4.843'}); + shape.appendChild(circle); + var circle = this.ng.createSVGElement('circle'); + this.setAttributes( + circle, {'cx': 13.704 + dx, 'cy': -0.771 + dy, 'r': '4.843'}); + shape.appendChild(circle); + var circle = this.ng.createSVGElement('circle'); + this.setAttributes( + circle, {'cx': -10.353 + dx, 'cy': 8.413 + dy, 'r': '4.843'}); + shape.appendChild(circle); + var circle = this.ng.createSVGElement('circle'); + this.setAttributes( + circle, {'cx': 3.894 + dx, 'cy': -13.158 + dy, 'r': '4.843'}); + shape.appendChild(circle); + + return shape; + }; -/** - * Function for drawing ensemble svg. - */ -ensemble_svg() { - var shape = this.ng.createSVGElement('g'); - shape.setAttribute('class', 'ensemble'); - - var dx = -1.25; - var dy = 0.25; - - var circle = this.ng.createSVGElement('circle'); - this.setAttributes( - circle, {'cx': -11.157 + dx, 'cy': -7.481 + dy, 'r': '4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes( - circle, {'cx': 0.186 + dx, 'cy': -0.127 + dy, 'r': '4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes( - circle, {'cx': 5.012 + dx, 'cy': 12.56 + dy, 'r': '4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes( - circle, {'cx': 13.704 + dx, 'cy': -0.771 + dy, 'r': '4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes( - circle, {'cx': -10.353 + dx, 'cy': 8.413 + dy, 'r': '4.843'}); - shape.appendChild(circle); - var circle = this.ng.createSVGElement('circle'); - this.setAttributes( - circle, {'cx': 3.894 + dx, 'cy': -13.158 + dy, 'r': '4.843'}); - shape.appendChild(circle); - - return shape; -}; + /** + * Helper function for setting attributes. + */ + setAttributes(el, attrs) { + for (var key in attrs) { + el.setAttribute(key, attrs[key]); + } + }; -/** - * Helper function for setting attributes. - */ -setAttributes(el, attrs) { - for (var key in attrs) { - el.setAttribute(key, attrs[key]); - } -}; - -getMinMaxXY() { - var min_x = this.x - this.width; - var max_x = this.x + this.width; - var min_y = this.y - this.height; - var max_y = this.y + this.height; - return [min_x, max_x, min_y, max_y]; -}; + getMinMaxXY() { + var min_x = this.x - this.width; + var max_x = this.x + this.width; + var min_y = this.y - this.height; + var max_y = this.y + this.height; + return [min_x, max_x, min_y, max_y]; + }; } diff --git a/nengo_gui/static/components/pointer.ts b/nengo_gui/static/components/pointer.ts index c3d91984..c19876d7 100644 --- a/nengo_gui/static/components/pointer.ts +++ b/nengo_gui/static/components/pointer.ts @@ -18,237 +18,237 @@ import * as utils from "../utils"; export default class Pointer extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - var self = this; + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + var self = this; - this.sim = sim; - this.pointer_status = false; + this.sim = sim; + this.pointer_status = false; - this.pdiv = document.createElement('div'); - this.pdiv.style.width = args.width; - this.pdiv.style.height = args.height; - utils.set_transform(this.pdiv, 0, 25); - this.pdiv.style.position = 'fixed'; - this.pdiv.classList.add('pointer'); - this.div.appendChild(this.pdiv); + this.pdiv = document.createElement('div'); + this.pdiv.style.width = args.width; + this.pdiv.style.height = args.height; + utils.set_transform(this.pdiv, 0, 25); + this.pdiv.style.position = 'fixed'; + this.pdiv.classList.add('pointer'); + this.div.appendChild(this.pdiv); - this.show_pairs = args.show_pairs; + this.show_pairs = args.show_pairs; - // For storing the accumulated data - this.data_store = new DataStore(1, this.sim, 0); + // For storing the accumulated data + this.data_store = new DataStore(1, this.sim, 0); - // Call schedule_update whenever the time is adjusted in the SimControl - this.sim.div.addEventListener('adjust_time', function(e) { - self.schedule_update(); - }, false); + // Call schedule_update whenever the time is adjusted in the SimControl + this.sim.div.addEventListener('adjust_time', function(e) { + self.schedule_update(); + }, false); - // Call reset whenever the simulation is reset - this.sim.div.addEventListener('sim_reset', function(e) { - self.reset(); - }, false); + // Call reset whenever the simulation is reset + this.sim.div.addEventListener('sim_reset', function(e) { + self.reset(); + }, false); - this.on_resize(this.get_screen_width(), this.get_screen_height()); + this.on_resize(this.get_screen_width(), this.get_screen_height()); - this.fixed_value = ''; + this.fixed_value = ''; - this.div.addEventListener("mouseup", function(event) { - // For some reason 'tap' doesn't seem to work here while the - // simulation is running, so I'm doing the timing myself - var now = new Date().getTime() / 1000; - if (now - self.mouse_down_time > 0.1) { - return; - } - if (event.button == 0) { - if (self.menu.visible) { - self.menu.hide(); - } else { - self.menu.show(event.clientX, event.clientY, - self.generate_menu()); + this.div.addEventListener("mouseup", function(event) { + // For some reason 'tap' doesn't seem to work here while the + // simulation is running, so I'm doing the timing myself + var now = new Date().getTime() / 1000; + if (now - self.mouse_down_time > 0.1) { + return; } - } - }); - - this.div.addEventListener("mousedown", function(event) { - self.mouse_down_time = new Date().getTime() / 1000; - }); -}; - -generate_menu() { - var self = this; - var items = []; - items.push(['Set value...', function() { - self.set_value(); - }]); - if (this.show_pairs) { - items.push(['Hide pairs', function() { - self.set_show_pairs(false); - }]); - } else { - items.push(['Show pairs', function() { - self.set_show_pairs(true); + if (event.button == 0) { + if (self.menu.visible) { + self.menu.hide(); + } else { + self.menu.show(event.clientX, event.clientY, + self.generate_menu()); + } + } + }); + + this.div.addEventListener("mousedown", function(event) { + self.mouse_down_time = new Date().getTime() / 1000; + }); + }; + + generate_menu() { + var self = this; + var items = []; + items.push(['Set value...', function() { + self.set_value(); }]); - } - - // Add the parent's menu items to this - // TODO: is this really the best way to call the parent's generate_menu()? - return $.merge(items, Component.prototype.generate_menu.call(this)); -}; - -set_show_pairs(value) { - if (this.show_pairs !== value) { - this.show_pairs = value; - this.save_layout(); - } -}; - -set_value() { - var self = this; - self.sim.modal.title('Enter a Semantic Pointer value...'); - self.sim.modal.single_input_body('Pointer', 'New value'); - self.sim.modal.footer('ok_cancel', function(e) { - var value = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; + if (this.show_pairs) { + items.push(['Hide pairs', function() { + self.set_show_pairs(false); + }]); + } else { + items.push(['Show pairs', function() { + self.set_show_pairs(true); + }]); } - if ((value === null) || (value === '')) { - value = ':empty:'; + + // Add the parent's menu items to this + // TODO: is this really the best way to call the parent's generate_menu()? + return $.merge(items, Component.prototype.generate_menu.call(this)); + }; + + set_show_pairs(value) { + if (this.show_pairs !== value) { + this.show_pairs = value; + this.save_layout(); } - self.fixed_value = value; - self.ws.send(value); - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var ptr = $item.val(); - if (ptr === null) { - ptr = ''; + }; + + set_value() { + var self = this; + self.sim.modal.title('Enter a Semantic Pointer value...'); + self.sim.modal.single_input_body('Pointer', 'New value'); + self.sim.modal.footer('ok_cancel', function(e) { + var value = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; + } + if ((value === null) || (value === '')) { + value = ':empty:'; + } + self.fixed_value = value; + self.ws.send(value); + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var ptr = $item.val(); + if (ptr === null) { + ptr = ''; + } + self.ws.send(':check only:' + ptr); + return self.pointer_status; } - self.ws.send(':check only:' + ptr); - return self.pointer_status; } - } - }); + }); - $('#singleInput').attr('data-error', 'Invalid semantic ' + - 'pointer expression. Semantic pointers themselves must start with ' + - 'a capital letter. Expressions can include mathematical operators ' + - 'such as +, * (circular convolution), and ~ (pseudo-inverse). ' + - 'E.g., (A+~(B*C)*2)*0.5 would be a valid semantic pointer expression.'); + $('#singleInput').attr('data-error', 'Invalid semantic ' + + 'pointer expression. Semantic pointers themselves must start with ' + + 'a capital letter. Expressions can include mathematical operators ' + + 'such as +, * (circular convolution), and ~ (pseudo-inverse). ' + + 'E.g., (A+~(B*C)*2)*0.5 would be a valid semantic pointer expression.'); - self.sim.modal.show(); -}; + self.sim.modal.show(); + }; -/** - * Receive new line data from the server. - */ -on_message(event) { - var data = event.data.split(" "); + /** + * Receive new line data from the server. + */ + on_message(event) { + var data = event.data.split(" "); - if (data[0].substring(0, 11) == "bad_pointer") { - this.pointer_status = false; - return; - } else if (data[0].substring(0, 12) == "good_pointer") { - this.pointer_status = true; - return; - } + if (data[0].substring(0, 11) == "bad_pointer") { + this.pointer_status = false; + return; + } else if (data[0].substring(0, 12) == "good_pointer") { + this.pointer_status = true; + return; + } - var time = parseFloat(data[0]); + var time = parseFloat(data[0]); - var items = data[1].split(";"); - this.data_store.push([time, items]); - this.schedule_update(); -}; + var items = data[1].split(";"); + this.data_store.push([time, items]); + this.schedule_update(); + }; -/** - * Redraw the lines and axis due to changed data. - */ -update() { - // Let the data store clear out old values - this.data_store.update(); - - var data = this.data_store.get_last_data()[0]; - - while (this.pdiv.firstChild) { - this.pdiv.removeChild(this.pdiv.firstChild); - } - this.pdiv.style.width = this.width; - this.pdiv.style.height = this.height; - - if (data === undefined) { - return; - } - - var total_size = 0; - - var items = []; - - // Display the text in proportion to similarity - for (var i = 0; i < data.length; i++) { - var size = parseFloat(data[i].substring(0, 4)); - var span = document.createElement('span'); - span.innerHTML = data[i].substring(4); - this.pdiv.appendChild(span); - total_size += size; - var c = Math.floor(255 - 255 * size); - // TODO: Use clip - if (c < 0) { - c = 0; + /** + * Redraw the lines and axis due to changed data. + */ + update() { + // Let the data store clear out old values + this.data_store.update(); + + var data = this.data_store.get_last_data()[0]; + + while (this.pdiv.firstChild) { + this.pdiv.removeChild(this.pdiv.firstChild); } - if (c > 255) { - c = 255; + this.pdiv.style.width = this.width; + this.pdiv.style.height = this.height; + + if (data === undefined) { + return; } - span.style.color = 'rgb(' + c + ',' + c + ',' + c + ')'; - items.push(span); - } - var scale = this.height / total_size * 0.6; + var total_size = 0; + + var items = []; + + // Display the text in proportion to similarity + for (var i = 0; i < data.length; i++) { + var size = parseFloat(data[i].substring(0, 4)); + var span = document.createElement('span'); + span.innerHTML = data[i].substring(4); + this.pdiv.appendChild(span); + total_size += size; + var c = Math.floor(255 - 255 * size); + // TODO: Use clip + if (c < 0) { + c = 0; + } + if (c > 255) { + c = 255; + } + span.style.color = 'rgb(' + c + ',' + c + ',' + c + ')'; + items.push(span); + } - for (var i = 0; i < data.length; i++) { - var size = parseFloat(data[i].substring(0, 4)); - items[i].style.fontSize = '' + (size * scale) + 'px'; - } -}; + var scale = this.height / total_size * 0.6; -/** - * Adjust the graph layout due to changed size. - */ -on_resize(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; + for (var i = 0; i < data.length; i++) { + var size = parseFloat(data[i].substring(0, 4)); + items[i].style.fontSize = '' + (size * scale) + 'px'; + } }; - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height = height; + /** + * Adjust the graph layout due to changed size. + */ + on_resize(width, height) { + if (width < this.minWidth) { + width = this.minWidth; + } + if (height < this.minHeight) { + height = this.minHeight; + }; - this.label.style.width = width; + this.width = width; + this.height = height; + this.div.style.width = width; + this.div.style.height = height; - this.update(); -}; + this.label.style.width = width; -layout_info() { - var info = Component.prototype.layout_info.call(this); - info.show_pairs = this.show_pairs; - return info; -}; + this.update(); + }; -update_layout(config) { - this.show_pairs = config.show_pairs; - Component.prototype.update_layout.call(this, config); -}; + layout_info() { + var info = Component.prototype.layout_info.call(this); + info.show_pairs = this.show_pairs; + return info; + }; -reset(event) { - this.data_store.reset(); - this.schedule_update(); -}; + update_layout(config) { + this.show_pairs = config.show_pairs; + Component.prototype.update_layout.call(this, config); + }; + + reset(event) { + this.data_store.reset(); + this.schedule_update(); + }; } diff --git a/nengo_gui/static/components/raster.ts b/nengo_gui/static/components/raster.ts index 74cfbcaa..a3db1507 100644 --- a/nengo_gui/static/components/raster.ts +++ b/nengo_gui/static/components/raster.ts @@ -22,178 +22,178 @@ import * as utils from "../utils"; export default class Raster extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - var self = this; - this.n_neurons = args.n_neurons || 1; - this.sim = sim; - - // For storing the accumulated data - this.data_store = new DataStore(1, this.sim, 0); - - this.axes2d = new TimeAxes(this.div, args); - this.axes2d.scale_y.domain([0, args.n_neurons]); - - // Call schedule_update whenever the time is adjusted in the SimControl - this.sim.div.addEventListener('adjust_time', function(e) { - self.schedule_update(); - }, false); - - // Call reset whenever the simulation is reset - this.sim.div.addEventListener('sim_reset', function(e) { - self.reset(); - }, false); - - // Create the lines on the plots - var line = d3.svg.line() - .x(function(d, i) { - return self.axes2d.scale_x(this.data_store.times[i]); - }) - .y(function(d) { - return self.axes2d.scale_y(d); - }); + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + var self = this; + this.n_neurons = args.n_neurons || 1; + this.sim = sim; + + // For storing the accumulated data + this.data_store = new DataStore(1, this.sim, 0); + + this.axes2d = new TimeAxes(this.div, args); + this.axes2d.scale_y.domain([0, args.n_neurons]); + + // Call schedule_update whenever the time is adjusted in the SimControl + this.sim.div.addEventListener('adjust_time', function(e) { + self.schedule_update(); + }, false); + + // Call reset whenever the simulation is reset + this.sim.div.addEventListener('sim_reset', function(e) { + self.reset(); + }, false); + + // Create the lines on the plots + var line = d3.svg.line() + .x(function(d, i) { + return self.axes2d.scale_x(this.data_store.times[i]); + }) + .y(function(d) { + return self.axes2d.scale_y(d); + }); + + this.path = this.axes2d.svg.append("g") + .selectAll('path') + .data(this.data_store.data); + + this.path.enter().append('path') + .attr('class', 'line') + .style('stroke', utils.make_colors(1)); + + this.update(); + this.on_resize(this.get_screen_width(), this.get_screen_height()); + this.axes2d.axis_y.tickValues([0, args.n_neurons]); + this.axes2d.fit_ticks(this); + }; - this.path = this.axes2d.svg.append("g") - .selectAll('path') - .data(this.data_store.data); + /** + * Receive new line data from the server. + */ + on_message(event) { + var time = new Float32Array(event.data, 0, 1); + var data = new Int16Array(event.data, 4); + this.data_store.push([time[0], data]); + this.schedule_update(); + }; - this.path.enter().append('path') - .attr('class', 'line') - .style('stroke', utils.make_colors(1)); + set_n_neurons(n_neurons) { + this.n_neurons = n_neurons; + this.axes2d.scale_y.domain([0, n_neurons]); + this.axes2d.axis_y.tickValues([0, n_neurons]); + this.ws.send('n_neurons:' + n_neurons); + }; - this.update(); - this.on_resize(this.get_screen_width(), this.get_screen_height()); - this.axes2d.axis_y.tickValues([0, args.n_neurons]); - this.axes2d.fit_ticks(this); -}; + /** + * Redraw the lines and axis due to changed data. + */ + update() { + // Let the data store clear out old values + this.data_store.update(); -/** - * Receive new line data from the server. - */ -on_message(event) { - var time = new Float32Array(event.data, 0, 1); - var data = new Int16Array(event.data, 4); - this.data_store.push([time[0], data]); - this.schedule_update(); -}; - -set_n_neurons(n_neurons) { - this.n_neurons = n_neurons; - this.axes2d.scale_y.domain([0, n_neurons]); - this.axes2d.axis_y.tickValues([0, n_neurons]); - this.ws.send('n_neurons:' + n_neurons); -}; - -/** - * Redraw the lines and axis due to changed data. - */ -update() { - // Let the data store clear out old values - this.data_store.update(); + // Determine visible range from the SimControl + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; - // Determine visible range from the SimControl - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; + this.axes2d.set_time_range(t1, t2); - this.axes2d.set_time_range(t1, t2); + // Update the lines + var shown_data = this.data_store.get_shown_data(); - // Update the lines - var shown_data = this.data_store.get_shown_data(); + var path = []; + for (var i = 0; i < shown_data[0].length; i++) { + var t = this.axes2d.scale_x( + this.data_store.times[ + this.data_store.first_shown_index + i]); - var path = []; - for (var i = 0; i < shown_data[0].length; i++) { - var t = this.axes2d.scale_x( - this.data_store.times[ - this.data_store.first_shown_index + i]); + for (var j = 0; j < shown_data[0][i].length; j++) { + var y1 = this.axes2d.scale_y(shown_data[0][i][j]); + var y2 = this.axes2d.scale_y(shown_data[0][i][j] + 1); + path.push('M ' + t + ' ' + y1 + 'V' + y2); + } + } + this.path.attr("d", path.join("")); + }; - for (var j = 0; j < shown_data[0][i].length; j++) { - var y1 = this.axes2d.scale_y(shown_data[0][i][j]); - var y2 = this.axes2d.scale_y(shown_data[0][i][j] + 1); - path.push('M ' + t + ' ' + y1 + 'V' + y2); + /** + * Adjust the graph layout due to changed size. + */ + on_resize(width, height) { + if (width < this.minWidth) { + width = this.minWidth; } - } - this.path.attr("d", path.join("")); -}; + if (height < this.minHeight) { + height = this.minHeight; + }; -/** - * Adjust the graph layout due to changed size. - */ -on_resize(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; + this.axes2d.on_resize(width, height); + + this.update(); + + this.label.style.width = width; + + this.width = width; + this.height = height; + this.div.style.width = width; + this.div.style.height = height; }; - this.axes2d.on_resize(width, height); - - this.update(); - - this.label.style.width = width; - - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height = height; -}; - -reset(event) { - this.data_store.reset(); - this.schedule_update(); -}; - -generate_menu() { - var self = this; - var items = []; - items.push(['Set # neurons...', function() {self.set_neuron_count();}]); - - return $.merge(items, Component.prototype.generate_menu.call(this)); -}; - -set_neuron_count() { - var count = this.n_neurons; - var self = this; - self.sim.modal.title('Set number of neurons...'); - self.sim.modal.single_input_body(count, 'Number of neurons'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_count = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_count !== null) { - new_count = parseInt(new_count); - self.set_n_neurons(new_count); - self.axes2d.fit_ticks(self); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var num = $item.val(); - var valid = false; - if ($.isNumeric(num)) { - num = Number(num); - if (num >= 0 && Number.isInteger(num)) { - valid = true; + reset(event) { + this.data_store.reset(); + this.schedule_update(); + }; + + generate_menu() { + var self = this; + var items = []; + items.push(['Set # neurons...', function() {self.set_neuron_count();}]); + + return $.merge(items, Component.prototype.generate_menu.call(this)); + }; + + set_neuron_count() { + var count = this.n_neurons; + var self = this; + self.sim.modal.title('Set number of neurons...'); + self.sim.modal.single_input_body(count, 'Number of neurons'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_count = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; + } + if (new_count !== null) { + new_count = parseInt(new_count); + self.set_n_neurons(new_count); + self.axes2d.fit_ticks(self); + } + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var num = $item.val(); + var valid = false; + if ($.isNumeric(num)) { + num = Number(num); + if (num >= 0 && Number.isInteger(num)) { + valid = true; + } } + return valid; } - return valid; - } - }, - }); - - $('#singleInput').attr('data-error', 'Input should be a positive integer'); - - self.sim.modal.show(); - $('#OK').on('click', function() { - var w = $(self.div).width(); - var h = $(self.div).height(); - self.on_resize(w, h); - }); -}; + }, + }); + + $('#singleInput').attr('data-error', 'Input should be a positive integer'); + + self.sim.modal.show(); + $('#OK').on('click', function() { + var w = $(self.div).width(); + var h = $(self.div).height(); + self.on_resize(w, h); + }); + }; } diff --git a/nengo_gui/static/components/slider.ts b/nengo_gui/static/components/slider.ts index 830569c4..e7367309 100644 --- a/nengo_gui/static/components/slider.ts +++ b/nengo_gui/static/components/slider.ts @@ -20,347 +20,347 @@ import SliderControl from "./slidercontrol"; export default class Slider extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - var self = this; - this.sim = sim; - - // Check if user is filling in a number into a slider - this.filling_slider_value = false; - this.n_sliders = args.n_sliders; - - this.data_store = null; - - this.notify_msgs = []; - // TODO: get rid of the immediate parameter once the websocket delay - // fix is merged in (#160) - this.immediate_notify = true; - - this.set_axes_geometry(this.width, this.height); - - this.minHeight = 40; - - this.group = document.createElement('div'); - this.group.style.height = this.slider_height; - this.group.style.marginTop = this.ax_top; - this.group.style.whiteSpace = 'nowrap'; - this.group.position = 'relative'; - this.div.appendChild(this.group); - - // Make the sliders - // The value to use when releasing from user control - this.reset_value = args.start_value; - // The value to use when restarting the simulation from beginning - this.start_value = args.start_value; - - this.sliders = []; - for (var i = 0; i < args.n_sliders; i++) { - // Creating a SliderControl object for every slider handle required - var slider = new SliderControl(args.min_value, args.max_value); - slider.container.style.width = (100 / args.n_sliders) + '%'; - slider.display_value(args.start_value[i]); - slider.index = i; - slider.fixed = false; - - slider.on('change', function(event) { - event.target.fixed = true; - self.send_value(event.target.index, event.value); - }).on('changestart', function(event) { - menu.hide_any(); - for (var i in this.sliders) { - if (this.sliders[i] !== event.target) { - this.sliders[i].deactivate_type_mode(); + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + var self = this; + this.sim = sim; + + // Check if user is filling in a number into a slider + this.filling_slider_value = false; + this.n_sliders = args.n_sliders; + + this.data_store = null; + + this.notify_msgs = []; + // TODO: get rid of the immediate parameter once the websocket delay + // fix is merged in (#160) + this.immediate_notify = true; + + this.set_axes_geometry(this.width, this.height); + + this.minHeight = 40; + + this.group = document.createElement('div'); + this.group.style.height = this.slider_height; + this.group.style.marginTop = this.ax_top; + this.group.style.whiteSpace = 'nowrap'; + this.group.position = 'relative'; + this.div.appendChild(this.group); + + // Make the sliders + // The value to use when releasing from user control + this.reset_value = args.start_value; + // The value to use when restarting the simulation from beginning + this.start_value = args.start_value; + + this.sliders = []; + for (var i = 0; i < args.n_sliders; i++) { + // Creating a SliderControl object for every slider handle required + var slider = new SliderControl(args.min_value, args.max_value); + slider.container.style.width = (100 / args.n_sliders) + '%'; + slider.display_value(args.start_value[i]); + slider.index = i; + slider.fixed = false; + + slider.on('change', function(event) { + event.target.fixed = true; + self.send_value(event.target.index, event.value); + }).on('changestart', function(event) { + menu.hide_any(); + for (var i in this.sliders) { + if (this.sliders[i] !== event.target) { + this.sliders[i].deactivate_type_mode(); + } } - } - }); + }); - this.group.appendChild(slider.container); - this.sliders.push(slider); - } - - // Call schedule_update whenever the time is adjusted in the SimControl - this.sim.div.addEventListener('adjust_time', function(e) { - self.schedule_update(); - }, false); - - this.sim.div.addEventListener('sim_reset', function(e) { - self.on_sim_reset(); - }, false); - - this.on_resize(this.get_screen_width(), this.get_screen_height()); -}; - -set_axes_geometry(width, height) { - this.width = width; - this.height = height; - var scale = parseFloat($('#main').css('font-size')); - this.border_size = 1; - this.ax_top = 1.75 * scale; - this.slider_height = this.height - this.ax_top; -}; - -send_value(slider_index, value) { - console.assert(typeof slider_index == 'number'); - console.assert(typeof value == 'number'); - - if (this.immediate_notify) { - this.ws.send(slider_index + ',' + value); - } else { - this.notify(slider_index + ',' + value); - } - this.sim.time_slider.jump_to_end(); -}; - -on_sim_reset(event) { - // Release slider position and reset it - for (var i = 0; i < this.sliders.length; i++) { - this.notify('' + i + ',reset'); - this.sliders[i].display_value(this.start_value[i]); - this.sliders[i].fixed = false; - } -}; + this.group.appendChild(slider.container); + this.sliders.push(slider); + } -/** - * Receive new line data from the server. - */ -on_message(event) { - var data = new Float32Array(event.data); - if (this.data_store === null) { - this.data_store = new DataStore(this.sliders.length, this.sim, 0); - } - this.reset_value = []; - for (var i = 0; i < this.sliders.length; i++) { - this.reset_value.push(data[i + 1]); - - if (this.sliders[i].fixed) { - data[i + 1] = this.sliders[i].value; + // Call schedule_update whenever the time is adjusted in the SimControl + this.sim.div.addEventListener('adjust_time', function(e) { + self.schedule_update(); + }, false); + + this.sim.div.addEventListener('sim_reset', function(e) { + self.on_sim_reset(); + }, false); + + this.on_resize(this.get_screen_width(), this.get_screen_height()); + }; + + set_axes_geometry(width, height) { + this.width = width; + this.height = height; + var scale = parseFloat($('#main').css('font-size')); + this.border_size = 1; + this.ax_top = 1.75 * scale; + this.slider_height = this.height - this.ax_top; + }; + + send_value(slider_index, value) { + console.assert(typeof slider_index == 'number'); + console.assert(typeof value == 'number'); + + if (this.immediate_notify) { + this.ws.send(slider_index + ',' + value); + } else { + this.notify(slider_index + ',' + value); } - } - this.data_store.push(data); + this.sim.time_slider.jump_to_end(); + }; - this.schedule_update(); -}; + on_sim_reset(event) { + // Release slider position and reset it + for (var i = 0; i < this.sliders.length; i++) { + this.notify('' + i + ',reset'); + this.sliders[i].display_value(this.start_value[i]); + this.sliders[i].fixed = false; + } + }; -/** - * Update visual display based when component is resized. - */ -on_resize(width, height) { - console.assert(typeof width == 'number'); - console.assert(typeof height == 'number'); - - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; + /** + * Receive new line data from the server. + */ + on_message(event) { + var data = new Float32Array(event.data); + if (this.data_store === null) { + this.data_store = new DataStore(this.sliders.length, this.sim, 0); + } + this.reset_value = []; + for (var i = 0; i < this.sliders.length; i++) { + this.reset_value.push(data[i + 1]); + + if (this.sliders[i].fixed) { + data[i + 1] = this.sliders[i].value; + } + } + this.data_store.push(data); + + this.schedule_update(); }; - this.set_axes_geometry(width, height); - - this.group.style.height = height - this.ax_top - 2 * this.border_size; - this.group.style.marginTop = this.ax_top; - - var N = this.sliders.length; - for (var i in this.sliders) { - this.sliders[i].on_resize(); - } - - this.label.style.width = this.width; - this.div.style.width = this.width; - this.div.style.height = this.height; -}; - -generate_menu() { - var self = this; - var items = []; - items.push(['Set range...', function() { - self.set_range(); - }]); - items.push(['Set value...', function() { - self.user_value(); - }]); - items.push(['Reset value', function() { - self.user_reset_value(); - }]); - - // Add the parent's menu items to this - // TODO: is this really the best way to call the parent's generate_menu()? - return $.merge(items, Component.prototype.generate_menu.call(this)); -}; + /** + * Update visual display based when component is resized. + */ + on_resize(width, height) { + console.assert(typeof width == 'number'); + console.assert(typeof height == 'number'); -/** - * Report an event back to the server. - */ -notify(info) { - this.notify_msgs.push(info); + if (width < this.minWidth) { + width = this.minWidth; + } + if (height < this.minHeight) { + height = this.minHeight; + }; - // Only send one message at a time - // TODO: find a better way to figure out when it's safe to send - // another message, rather than just waiting 1ms.... - if (this.notify_msgs.length == 1) { - var self = this; - window.setTimeout(function() { - self.send_notify_msg(); - }, 50); - } -}; + this.set_axes_geometry(width, height); -/** - * Send exactly one message back to server. - * - * Also schedule the next message to be sent, if any. - */ -send_notify_msg() { - var msg = this.notify_msgs[0]; - this.ws.send(msg); - if (this.notify_msgs.length > 1) { - var self = this; - window.setTimeout(function() { - self.send_notify_msg(); - }, 50); - } - this.notify_msgs.splice(0, 1); -}; + this.group.style.height = height - this.ax_top - 2 * this.border_size; + this.group.style.marginTop = this.ax_top; + + var N = this.sliders.length; + for (var i in this.sliders) { + this.sliders[i].on_resize(); + } -update() { - // Let the data store clear out old values - if (this.data_store !== null) { - this.data_store.update(); + this.label.style.width = this.width; + this.div.style.width = this.width; + this.div.style.height = this.height; + }; - var data = this.data_store.get_last_data(); + generate_menu() { + var self = this; + var items = []; + items.push(['Set range...', function() { + self.set_range(); + }]); + items.push(['Set value...', function() { + self.user_value(); + }]); + items.push(['Reset value', function() { + self.user_reset_value(); + }]); + + // Add the parent's menu items to this + // TODO: is this really the best way to call the parent's generate_menu()? + return $.merge(items, Component.prototype.generate_menu.call(this)); + }; - for (var i = 0; i < this.sliders.length; i++) { - if (!this.data_store.is_at_end() || !this.sliders[i].fixed) { - this.sliders[i].display_value(data[i]); - } + /** + * Report an event back to the server. + */ + notify(info) { + this.notify_msgs.push(info); + + // Only send one message at a time + // TODO: find a better way to figure out when it's safe to send + // another message, rather than just waiting 1ms.... + if (this.notify_msgs.length == 1) { + var self = this; + window.setTimeout(function() { + self.send_notify_msg(); + }, 50); } - } -}; - -user_value() { - var self = this; - - // First build the prompt string - var prompt_string = ''; - for (var i = 0; i < this.sliders.length; i++) { - prompt_string = prompt_string + this.sliders[i].value.toFixed(2); - if (i != this.sliders.length - 1) { - prompt_string = prompt_string + ", "; + }; + + /** + * Send exactly one message back to server. + * + * Also schedule the next message to be sent, if any. + */ + send_notify_msg() { + var msg = this.notify_msgs[0]; + this.ws.send(msg); + if (this.notify_msgs.length > 1) { + var self = this; + window.setTimeout(function() { + self.send_notify_msg(); + }, 50); } - } - self.sim.modal.title('Set slider value(s)...'); - self.sim.modal.single_input_body(prompt_string, 'New value(s)'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_value = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; + this.notify_msgs.splice(0, 1); + }; + + update() { + // Let the data store clear out old values + if (this.data_store !== null) { + this.data_store.update(); + + var data = this.data_store.get_last_data(); + + for (var i = 0; i < this.sliders.length; i++) { + if (!this.data_store.is_at_end() || !this.sliders[i].fixed) { + this.sliders[i].display_value(data[i]); + } + } } - self.immediate_notify = false; - if (new_value !== null) { - new_value = new_value.split(','); - // Update the sliders one at a time - for (var i = 0; i < self.sliders.length; i++) { - self.sliders[i].fixed = true; - self.sliders[i].set_value(parseFloat(new_value[i])); + }; + + user_value() { + var self = this; + + // First build the prompt string + var prompt_string = ''; + for (var i = 0; i < this.sliders.length; i++) { + prompt_string = prompt_string + this.sliders[i].value.toFixed(2); + if (i != this.sliders.length - 1) { + prompt_string = prompt_string + ", "; } } - self.immediate_notify = true; - $('#OK').attr('data-dismiss', 'modal'); - }); - - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - if (nums.length != self.sliders.length) { - return false; + self.sim.modal.title('Set slider value(s)...'); + self.sim.modal.single_input_body(prompt_string, 'New value(s)'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_value = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; + } + self.immediate_notify = false; + if (new_value !== null) { + new_value = new_value.split(','); + // Update the sliders one at a time + for (var i = 0; i < self.sliders.length; i++) { + self.sliders[i].fixed = true; + self.sliders[i].set_value(parseFloat(new_value[i])); } - for (var i = 0; i < nums.length; i++) { - if (!$.isNumeric(nums[i])) { + } + self.immediate_notify = true; + $('#OK').attr('data-dismiss', 'modal'); + }); + + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var nums = $item.val().split(','); + if (nums.length != self.sliders.length) { return false; } + for (var i = 0; i < nums.length; i++) { + if (!$.isNumeric(nums[i])) { + return false; + } + } + return true; } - return true; - } - }, - }); - - $('#singleInput').attr('data-error', 'Input should be one ' + - 'comma-separated numerical value for each slider.'); - self.sim.modal.show(); -}; - -user_reset_value() { - for (var i = 0; i < this.sliders.length; i++) { - this.notify('' + i + ',reset'); - - this.sliders[i].set_value(this.reset_value[i]); - this.sliders[i].fixed = false; - } -}; - -set_range() { - var range = this.sliders[0].scale.domain(); - var self = this; - self.sim.modal.title('Set slider range...'); - self.sim.modal.single_input_body([range[1], range[0]], 'New range'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_range = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; + }, + }); + + $('#singleInput').attr('data-error', 'Input should be one ' + + 'comma-separated numerical value for each slider.'); + self.sim.modal.show(); + }; + + user_reset_value() { + for (var i = 0; i < this.sliders.length; i++) { + this.notify('' + i + ',reset'); + + this.sliders[i].set_value(this.reset_value[i]); + this.sliders[i].fixed = false; } - if (new_range !== null) { - new_range = new_range.split(','); - var min = parseFloat(new_range[0]); - var max = parseFloat(new_range[1]); - for (var i in self.sliders) { - self.sliders[i].set_range(min, max); + }; + + set_range() { + var range = this.sliders[0].scale.domain(); + var self = this; + self.sim.modal.title('Set slider range...'); + self.sim.modal.single_input_body([range[1], range[0]], 'New range'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_range = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; } - self.save_layout(); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - var valid = false; - if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { - if (Number(nums[0]) < Number(nums[1])) { - // Two numbers, 1st less than 2nd - valid = true; - } + if (new_range !== null) { + new_range = new_range.split(','); + var min = parseFloat(new_range[0]); + var max = parseFloat(new_range[1]); + for (var i in self.sliders) { + self.sliders[i].set_range(min, max); } - return (nums.length == 2 && valid); + self.save_layout(); } - }, - }); - - $('#singleInput').attr('data-error', 'Input should be in the ' + - 'form ",".'); - self.sim.modal.show(); -}; - -layout_info() { - var info = Component.prototype.layout_info.call(this); - info.width = info.width; - info.min_value = this.sliders[0].scale.domain()[1]; - info.max_value = this.sliders[0].scale.domain()[0]; - return info; -}; - -update_layout(config) { - // FIXME: this has to be backwards to work. Something fishy must be going on - for (var i in this.sliders) { - this.sliders[i].set_range(config.min_value, config.max_value); - } - Component.prototype.update_layout.call(this, config); -}; + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var nums = $item.val().split(','); + var valid = false; + if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { + if (Number(nums[0]) < Number(nums[1])) { + // Two numbers, 1st less than 2nd + valid = true; + } + } + return (nums.length == 2 && valid); + } + }, + }); + + $('#singleInput').attr('data-error', 'Input should be in the ' + + 'form ",".'); + self.sim.modal.show(); + }; + + layout_info() { + var info = Component.prototype.layout_info.call(this); + info.width = info.width; + info.min_value = this.sliders[0].scale.domain()[1]; + info.max_value = this.sliders[0].scale.domain()[0]; + return info; + }; + + update_layout(config) { + // FIXME: this has to be backwards to work. Something fishy must be going on + for (var i in this.sliders) { + this.sliders[i].set_range(config.min_value, config.max_value); + } + Component.prototype.update_layout.call(this, config); + }; } diff --git a/nengo_gui/static/components/slidercontrol.ts b/nengo_gui/static/components/slidercontrol.ts index 0eaa79d7..d43ee93b 100644 --- a/nengo_gui/static/components/slidercontrol.ts +++ b/nengo_gui/static/components/slidercontrol.ts @@ -17,205 +17,205 @@ import * as utils from "../utils"; export default class SliderControl { -constructor(min, max) { - var self = this; - - this.min = min; - this.max = max; - - this.value = 0; - this.type_mode = false; - - this.border_width = 1; - - this.scale = d3.scale.linear(); - this.scale.domain([max, min]); - - // TODO: move CSS to CSS file - this.container = document.createElement('div'); - this.container.style.display = 'inline-block'; - this.container.style.position = 'relative'; - this.container.style.height = '100%'; - this.container.style.padding = '0.75em 0'; - - this.guideline = document.createElement('div'); - this.guideline.classList.add('guideline'); - this.guideline.style.width = '0.5em'; - this.guideline.style.height = '100%'; - this.guideline.style.margin = 'auto'; - $(this.guideline).on('mousedown', function(event) { - self.set_value(self.value); - }); - this.container.appendChild(this.guideline); - - this.handle = document.createElement('div'); - this.handle.classList.add('btn'); - this.handle.classList.add('btn-default'); - this.handle.innerHTML = 'n/a'; - this.handle.style.position = 'absolute'; - this.handle.style.height = '1.5em'; - this.handle.style.marginTop = '0.75em'; - this.handle.style.width = '95%'; - this.handle.style.fontSize = 'inherit'; - this.handle.style.padding = '0.1em 0'; - this.handle.style.borderWidth = this.border_width + 'px'; - this.handle.style.borderColor = '#666'; - this.handle.style.left = '2.5%'; - this.handle.style.transform = 'translate(0, -50%)'; - this.update_handle_pos(0); - this.container.appendChild(this.handle); - - interact(this.handle).draggable({ - onstart: function() { - self.dispatch('changestart', {'target': this}); - self.deactivate_type_mode(); - self._drag_y = self.get_handle_pos(); - }, - onmove: function(event) { - var target = event.target; - self._drag_y += event.dy; - - self.scale.range([0, self.guideline.clientHeight]); - self.set_value(self.scale.invert(self._drag_y)); - }, - onend: function(event) { - self.dispatch('changeend', {'target': this}); + constructor(min, max) { + var self = this; + + this.min = min; + this.max = max; + + this.value = 0; + this.type_mode = false; + + this.border_width = 1; + + this.scale = d3.scale.linear(); + this.scale.domain([max, min]); + + // TODO: move CSS to CSS file + this.container = document.createElement('div'); + this.container.style.display = 'inline-block'; + this.container.style.position = 'relative'; + this.container.style.height = '100%'; + this.container.style.padding = '0.75em 0'; + + this.guideline = document.createElement('div'); + this.guideline.classList.add('guideline'); + this.guideline.style.width = '0.5em'; + this.guideline.style.height = '100%'; + this.guideline.style.margin = 'auto'; + $(this.guideline).on('mousedown', function(event) { + self.set_value(self.value); + }); + this.container.appendChild(this.guideline); + + this.handle = document.createElement('div'); + this.handle.classList.add('btn'); + this.handle.classList.add('btn-default'); + this.handle.innerHTML = 'n/a'; + this.handle.style.position = 'absolute'; + this.handle.style.height = '1.5em'; + this.handle.style.marginTop = '0.75em'; + this.handle.style.width = '95%'; + this.handle.style.fontSize = 'inherit'; + this.handle.style.padding = '0.1em 0'; + this.handle.style.borderWidth = this.border_width + 'px'; + this.handle.style.borderColor = '#666'; + this.handle.style.left = '2.5%'; + this.handle.style.transform = 'translate(0, -50%)'; + this.update_handle_pos(0); + this.container.appendChild(this.handle); + + interact(this.handle).draggable({ + onstart: function() { + self.dispatch('changestart', {'target': this}); + self.deactivate_type_mode(); + self._drag_y = self.get_handle_pos(); + }, + onmove: function(event) { + var target = event.target; + self._drag_y += event.dy; + + self.scale.range([0, self.guideline.clientHeight]); + self.set_value(self.scale.invert(self._drag_y)); + }, + onend: function(event) { + self.dispatch('changeend', {'target': this}); + } + }); + + interact(this.handle).on('tap', function(event) { + self.activate_type_mode(); + event.stopPropagation(); + }).on('keydown', function(event) { + self.handle_keypress(event); + }); + + this.listeners = {}; + }; + + on(type, fn) { + this.listeners[type] = fn; + return this; + }; + + dispatch(type, ev) { + if (type in this.listeners) { + this.listeners[type].call(this, ev); + } + }; + + set_range(min, max) { + this.min = min; + this.max = max; + this.scale.domain([max, min]); + this.set_value(this.value); + this.on_resize(); + }; + + display_value(value) { + if (value < this.min) { + value = this.min; } - }); - - interact(this.handle).on('tap', function(event) { - self.activate_type_mode(); - event.stopPropagation(); - }).on('keydown', function(event) { - self.handle_keypress(event); - }); - - this.listeners = {}; -}; - -on(type, fn) { - this.listeners[type] = fn; - return this; -}; - -dispatch(type, ev) { - if (type in this.listeners) { - this.listeners[type].call(this, ev); - } -}; - -set_range(min, max) { - this.min = min; - this.max = max; - this.scale.domain([max, min]); - this.set_value(this.value); - this.on_resize(); -}; - -display_value(value) { - if (value < this.min) { - value = this.min; - } - if (value > this.max) { - value = this.max; - } - - this.value = value; - - this.update_handle_pos(value); - this.update_value_text(value); -}; - -set_value(value) { - var old_value = this.value; - this.display_value(value); - this.dispatch('change', {'target': this, 'value': this.value}); -}; - -activate_type_mode() { - if (this.type_mode) { - return; - } - - var self = this; - - this.dispatch('changestart', {'target': this}); - - this.type_mode = true; - - this.handle.innerHTML = - ''; - var elem = this.handle.querySelector('#value_in_field'); - elem.value = this.format_value(this.value); - elem.focus(); - elem.select(); - elem.style.width = '100%'; - elem.style.textAlign = 'center'; - elem.style.backgroundColor = 'transparent'; - $(elem).on('input', function(event) { - if (utils.is_num(elem.value)) { - self.handle.style.backgroundColor = ''; - } else { - self.handle.style.backgroundColor = 'salmon'; + if (value > this.max) { + value = this.max; } - }).on('blur', function(event) { - self.deactivate_type_mode(); - }); -}; -deactivate_type_mode(event) { - if (!this.type_mode) { - return; - } + this.value = value; + + this.update_handle_pos(value); + this.update_value_text(value); + }; + + set_value(value) { + var old_value = this.value; + this.display_value(value); + this.dispatch('change', {'target': this, 'value': this.value}); + }; - this.dispatch('changeend', {'target': this}); + activate_type_mode() { + if (this.type_mode) { + return; + } + + var self = this; + + this.dispatch('changestart', {'target': this}); + + this.type_mode = true; + + this.handle.innerHTML = + ''; + var elem = this.handle.querySelector('#value_in_field'); + elem.value = this.format_value(this.value); + elem.focus(); + elem.select(); + elem.style.width = '100%'; + elem.style.textAlign = 'center'; + elem.style.backgroundColor = 'transparent'; + $(elem).on('input', function(event) { + if (utils.is_num(elem.value)) { + self.handle.style.backgroundColor = ''; + } else { + self.handle.style.backgroundColor = 'salmon'; + } + }).on('blur', function(event) { + self.deactivate_type_mode(); + }); + }; - this.type_mode = false; + deactivate_type_mode(event) { + if (!this.type_mode) { + return; + } - $(this.handle).off('keydown'); - this.handle.style.backgroundColor = ''; - this.handle.innerHTML = this.format_value(this.value); -}; + this.dispatch('changeend', {'target': this}); -handle_keypress(event) { - if (!this.type_mode) { - return; - } + this.type_mode = false; - var enter_keycode = 13; - var esc_keycode = 27; - var key = event.which; + $(this.handle).off('keydown'); + this.handle.style.backgroundColor = ''; + this.handle.innerHTML = this.format_value(this.value); + }; - if (key == enter_keycode) { - var input = this.handle.querySelector('#value_in_field').value; - if (utils.is_num(input)) { + handle_keypress(event) { + if (!this.type_mode) { + return; + } + + var enter_keycode = 13; + var esc_keycode = 27; + var key = event.which; + + if (key == enter_keycode) { + var input = this.handle.querySelector('#value_in_field').value; + if (utils.is_num(input)) { + this.deactivate_type_mode(); + this.set_value(parseFloat(input)); + } + } else if (key == esc_keycode) { this.deactivate_type_mode(); - this.set_value(parseFloat(input)); } - } else if (key == esc_keycode) { - this.deactivate_type_mode(); - } -}; - -update_handle_pos(value) { - this.handle.style.top = this.scale(value) + this.border_width; -}; - -get_handle_pos() { - return parseFloat(this.handle.style.top) - this.border_width; -}; - -update_value_text(value) { - this.handle.innerHTML = this.format_value(value); -}; - -format_value(value) { - return value.toFixed(2); -}; - -on_resize() { - this.scale.range([0, this.guideline.clientHeight]); - this.update_handle_pos(this.value); -}; + }; + + update_handle_pos(value) { + this.handle.style.top = this.scale(value) + this.border_width; + }; + + get_handle_pos() { + return parseFloat(this.handle.style.top) - this.border_width; + }; + + update_value_text(value) { + this.handle.innerHTML = this.format_value(value); + }; + + format_value(value) { + return value.toFixed(2); + }; + + on_resize() { + this.scale.range([0, this.guideline.clientHeight]); + this.update_handle_pos(this.value); + }; } diff --git a/nengo_gui/static/components/spa_similarity.ts b/nengo_gui/static/components/spa_similarity.ts index d9c3443b..94463cb4 100644 --- a/nengo_gui/static/components/spa_similarity.ts +++ b/nengo_gui/static/components/spa_similarity.ts @@ -18,239 +18,239 @@ import Value from "./value"; export default class SpaSimilarity extends Value { -constructor(parent, viewport, sim, args) { - super(parent, viewport, sim, args); + constructor(parent, viewport, sim, args) { + super(parent, viewport, sim, args); - this.synapse = args.synapse; - this.data_store = - new GrowableDataStore(this.n_lines, this.sim, this.synapse); - this.show_pairs = false; + this.synapse = args.synapse; + this.data_store = + new GrowableDataStore(this.n_lines, this.sim, this.synapse); + this.show_pairs = false; - var self = this; + var self = this; - this.colors = utils.make_colors(6); - this.color_func = function(d, i) { - return self.colors[i % 6]; - }; + this.colors = utils.make_colors(6); + this.color_func = function(d, i) { + return self.colors[i % 6]; + }; - this.line.defined(function(d) { - return !isNaN(d); - }); - - // Create the legend from label args - this.legend_labels = args.pointer_labels; - this.legend = document.createElement('div'); - this.legend.classList.add('legend', 'unselectable'); - this.div.appendChild(this.legend); - this.legend_svg = utils.draw_legend( - this.legend, args.pointer_labels, this.color_func, this.uid); -}; - -reset_legend_and_data(new_labels) { - // Clear the database and create a new one since dimensions have changed - this.data_store = - new GrowableDataStore(new_labels.length, this.sim, this.synapse); - - // Delete the legend's children - while (this.legend.lastChild) { - this.legend.removeChild(this.legend.lastChild); - } - this.legend_svg = d3.select(this.legend) - .append("svg") - .attr("id", "legend" + this.uid); - - // Redraw all the legends if they exist - this.legend_labels = []; - if (new_labels[0] != "") { - this.update_legend(new_labels); - } - - this.update(); -}; - -data_msg(push_data) { - var data_dims = push_data.length - 1; - - // TODO: Move this check inside datastore? - if (data_dims > this.data_store.dims) { - this.data_store.dims = data_dims; - this.n_lines = data_dims; - } - - this.data_store.push(push_data); - this.schedule_update(); -}; - -update_legend(new_labels) { - var self = this; - this.legend_labels = this.legend_labels.concat(new_labels); - - // Expand the height of the svg, where 20 is around the height of the font - this.legend_svg.attr("height", 20 * this.legend_labels.length); - - // Data join - var recs = this.legend_svg.selectAll("rect") - .data(this.legend_labels); - var legend_labels = this.legend_svg.selectAll(".legend-label") - .data(this.legend_labels); - var val_texts = this.legend_svg.selectAll(".val").data(this.legend_labels); - // Enter to append remaining lines - recs.enter() - .append("rect") - .attr("x", 0) - .attr("y", function(d, i) { - return i * 20; - }) - .attr("width", 10) - .attr("height", 10) - .style("fill", this.color_func); - - legend_labels.enter().append("text") - .attr("x", 15) - .attr("y", function(d, i) { - return i * 20 + 9; - }) - .attr("class", "legend-label") - .html(function(d, i) { - return self.legend_labels[i]; + this.line.defined(function(d) { + return !isNaN(d); }); - // Expand the width of the svg of the longest string - var label_list = $("#legend" + this.uid + " .legend-label").toArray(); - var longest_label = Math.max.apply(Math, label_list.map(function(o) { - return o.getBBox().width; - })); - // 50 is for the similarity measure that is around three characters wide - var svg_right_edge = longest_label + 50; - this.legend_svg.attr("width", svg_right_edge); - - val_texts.attr("x", svg_right_edge) - .attr("y", function(d, i) { - return i * 20 + 9; - }); - val_texts.enter().append("text") - .attr("x", svg_right_edge) - .attr("y", function(d, i) { - return i * 20 + 9; - }) - .attr("text-anchor", "end") - .attr("class", "val"); -}; + // Create the legend from label args + this.legend_labels = args.pointer_labels; + this.legend = document.createElement('div'); + this.legend.classList.add('legend', 'unselectable'); + this.div.appendChild(this.legend); + this.legend_svg = utils.draw_legend( + this.legend, args.pointer_labels, this.color_func, this.uid); + }; -/** - * Handle websocket messages. - * - * There are three types of messages that can be received: - * - a legend needs to be updated - * - the data has been updated - * - show_pairs has been toggledn - * This calls the method associated to handling the type of message. - */ -on_message(event) { - var data = JSON.parse(event.data); - var func_name = data.shift(); - this[func_name](data); -}; + reset_legend_and_data(new_labels) { + // Clear the database and create a new one since dimensions have changed + this.data_store = + new GrowableDataStore(new_labels.length, this.sim, this.synapse); -/** - * Redraw the lines and axis due to changed data. - */ -update() { - // Let the data store clear out old values - this.data_store.update(); - - // Determine visible range from the SimControl - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - this.axes2d.set_time_range(t1, t2); - - // Update the lines - var self = this; - var shown_data = this.data_store.get_shown_data(); - // Data join - this.path = this.axes2d.svg.selectAll(".line").data(shown_data); - // Update - this.path.attr('d', self.line); - // Enter to append remaining lines - this.path.enter() - .append('path') - .attr('class', 'line') - .style('stroke', this.color_func) - .attr('d', self.line); - // Remove any lines that aren't needed anymore - this.path.exit().remove(); - - // Update the legend text - if (this.legend_svg && shown_data[0].length !== 0) { - // Get the most recent similarity - var latest_simi = []; - for (var i = 0; i < shown_data.length; i++) { - latest_simi.push(shown_data[i][shown_data[i].length - 1]); + // Delete the legend's children + while (this.legend.lastChild) { + this.legend.removeChild(this.legend.lastChild); + } + this.legend_svg = d3.select(this.legend) + .append("svg") + .attr("id", "legend" + this.uid); + + // Redraw all the legends if they exist + this.legend_labels = []; + if (new_labels[0] != "") { + this.update_legend(new_labels); + } + + this.update(); + }; + + data_msg(push_data) { + var data_dims = push_data.length - 1; + + // TODO: Move this check inside datastore? + if (data_dims > this.data_store.dims) { + this.data_store.dims = data_dims; + this.n_lines = data_dims; } - // Update the text in the legend - var texts = this.legend_svg.selectAll(".val").data(this.legend_labels); + this.data_store.push(push_data); + this.schedule_update(); + }; + + update_legend(new_labels) { + var self = this; + this.legend_labels = this.legend_labels.concat(new_labels); + + // Expand the height of the svg, where 20 is around the height of the font + this.legend_svg.attr("height", 20 * this.legend_labels.length); + + // Data join + var recs = this.legend_svg.selectAll("rect") + .data(this.legend_labels); + var legend_labels = this.legend_svg.selectAll(".legend-label") + .data(this.legend_labels); + var val_texts = this.legend_svg.selectAll(".val").data(this.legend_labels); + // Enter to append remaining lines + recs.enter() + .append("rect") + .attr("x", 0) + .attr("y", function(d, i) { + return i * 20; + }) + .attr("width", 10) + .attr("height", 10) + .style("fill", this.color_func); + + legend_labels.enter().append("text") + .attr("x", 15) + .attr("y", function(d, i) { + return i * 20 + 9; + }) + .attr("class", "legend-label") + .html(function(d, i) { + return self.legend_labels[i]; + }); + + // Expand the width of the svg of the longest string + var label_list = $("#legend" + this.uid + " .legend-label").toArray(); + var longest_label = Math.max.apply(Math, label_list.map(function(o) { + return o.getBBox().width; + })); + // 50 is for the similarity measure that is around three characters wide + var svg_right_edge = longest_label + 50; + this.legend_svg.attr("width", svg_right_edge); + + val_texts.attr("x", svg_right_edge) + .attr("y", function(d, i) { + return i * 20 + 9; + }); + val_texts.enter().append("text") + .attr("x", svg_right_edge) + .attr("y", function(d, i) { + return i * 20 + 9; + }) + .attr("text-anchor", "end") + .attr("class", "val"); + }; - texts.html(function(d, i) { - var sign = ''; - if (latest_simi[i] < 0) { - sign = "−"; + /** + * Handle websocket messages. + * + * There are three types of messages that can be received: + * - a legend needs to be updated + * - the data has been updated + * - show_pairs has been toggledn + * This calls the method associated to handling the type of message. + */ + on_message(event) { + var data = JSON.parse(event.data); + var func_name = data.shift(); + this[func_name](data); + }; + + /** + * Redraw the lines and axis due to changed data. + */ + update() { + // Let the data store clear out old values + this.data_store.update(); + + // Determine visible range from the SimControl + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; + + this.axes2d.set_time_range(t1, t2); + + // Update the lines + var self = this; + var shown_data = this.data_store.get_shown_data(); + // Data join + this.path = this.axes2d.svg.selectAll(".line").data(shown_data); + // Update + this.path.attr('d', self.line); + // Enter to append remaining lines + this.path.enter() + .append('path') + .attr('class', 'line') + .style('stroke', this.color_func) + .attr('d', self.line); + // Remove any lines that aren't needed anymore + this.path.exit().remove(); + + // Update the legend text + if (this.legend_svg && shown_data[0].length !== 0) { + // Get the most recent similarity + var latest_simi = []; + for (var i = 0; i < shown_data.length; i++) { + latest_simi.push(shown_data[i][shown_data[i].length - 1]); } - return sign + Math.abs(latest_simi[i]).toFixed(2); - }); - } -}; - -generate_menu() { - var self = this; - var items = []; - items.push(['Set range...', function() { - self.set_range(); - }]); - - if (this.show_pairs) { - items.push(['Hide pairs', function() { - self.set_show_pairs(false); - }]); - } else { - items.push(['Show pairs', function() { - self.set_show_pairs(true); + + // Update the text in the legend + var texts = this.legend_svg.selectAll(".val").data(this.legend_labels); + + texts.html(function(d, i) { + var sign = ''; + if (latest_simi[i] < 0) { + sign = "−"; + } + return sign + Math.abs(latest_simi[i]).toFixed(2); + }); + } + }; + + generate_menu() { + var self = this; + var items = []; + items.push(['Set range...', function() { + self.set_range(); }]); - } - - // Add the parent's menu items to this - return $.merge(items, Component.prototype.generate_menu.call(this)); -}; - -set_show_pairs(value) { - if (this.show_pairs !== value) { - this.show_pairs = value; - this.save_layout(); - this.ws.send(value); - } -}; - -layout_info() { - var info = Component.prototype.layout_info.call(this); - info.show_pairs = this.show_pairs; - info.min_value = this.axes2d.scale_y.domain()[0]; - info.max_value = this.axes2d.scale_y.domain()[1]; - return info; -}; - -update_layout(config) { - this.update_range(config.min_value, config.max_value); - this.show_pairs = config.show_pairs; - Component.prototype.update_layout.call(this, config); -}; - -reset() { - // Ask for a legend update - this.ws.send("reset_legend"); -}; - -// TODO: should I remove the ability to set range? -// Or limit it to something intuitive + + if (this.show_pairs) { + items.push(['Hide pairs', function() { + self.set_show_pairs(false); + }]); + } else { + items.push(['Show pairs', function() { + self.set_show_pairs(true); + }]); + } + + // Add the parent's menu items to this + return $.merge(items, Component.prototype.generate_menu.call(this)); + }; + + set_show_pairs(value) { + if (this.show_pairs !== value) { + this.show_pairs = value; + this.save_layout(); + this.ws.send(value); + } + }; + + layout_info() { + var info = Component.prototype.layout_info.call(this); + info.show_pairs = this.show_pairs; + info.min_value = this.axes2d.scale_y.domain()[0]; + info.max_value = this.axes2d.scale_y.domain()[1]; + return info; + }; + + update_layout(config) { + this.update_range(config.min_value, config.max_value); + this.show_pairs = config.show_pairs; + Component.prototype.update_layout.call(this, config); + }; + + reset() { + // Ask for a legend update + this.ws.send("reset_legend"); + }; + + // TODO: should I remove the ability to set range? + // Or limit it to something intuitive } diff --git a/nengo_gui/static/components/time_axes.ts b/nengo_gui/static/components/time_axes.ts index 354e8aa9..7028708a 100644 --- a/nengo_gui/static/components/time_axes.ts +++ b/nengo_gui/static/components/time_axes.ts @@ -13,50 +13,50 @@ import Axes2D from "./2d_axes"; export default class TimeAxes extends Axes2D { -constructor(parent, args) { - super(parent, args); - var self = this; - this.display_time = args.display_time; - - this.axis_x.ticks(0); - - this.axis_time_end = this.svg.append("text") - .text("Time: NULL") - .attr('class', 'graph_text unselectable')[0][0]; - this.axis_time_start = this.svg.append("text") - .text("Time: NULL") - .attr('class', 'graph_text unselectable')[0][0]; - - if (this.display_time == false) { - this.axis_time_start.style.display = 'none'; - this.axis_time_end.style.display = 'none'; - } -}; - -set_time_range(start, end) { - this.scale_x.domain([start, end]); - this.axis_time_start.textContent = start.toFixed(3); - this.axis_time_end.textContent = end.toFixed(3); - this.axis_x_g.call(this.axis_x); -}; - -on_resize(width, height) { - Axes2D.prototype.on_resize.call(this, width, height); - - var scale = parseFloat($('#main').css('font-size')); - var suppression_width = 6 * scale; - var text_offset = 1.2 * scale; - - if (width < suppression_width || this.display_time == false) { - this.axis_time_start.style.display = 'none'; - } else { - this.axis_time_start.style.display = 'block'; - } - - this.axis_time_start.setAttribute('y', this.ax_bottom + text_offset); - this.axis_time_start.setAttribute('x', this.ax_left - text_offset); - this.axis_time_end.setAttribute('y', this.ax_bottom + text_offset); - this.axis_time_end.setAttribute('x', this.ax_right - text_offset); -}; + constructor(parent, args) { + super(parent, args); + var self = this; + this.display_time = args.display_time; + + this.axis_x.ticks(0); + + this.axis_time_end = this.svg.append("text") + .text("Time: NULL") + .attr('class', 'graph_text unselectable')[0][0]; + this.axis_time_start = this.svg.append("text") + .text("Time: NULL") + .attr('class', 'graph_text unselectable')[0][0]; + + if (this.display_time == false) { + this.axis_time_start.style.display = 'none'; + this.axis_time_end.style.display = 'none'; + } + }; + + set_time_range(start, end) { + this.scale_x.domain([start, end]); + this.axis_time_start.textContent = start.toFixed(3); + this.axis_time_end.textContent = end.toFixed(3); + this.axis_x_g.call(this.axis_x); + }; + + on_resize(width, height) { + Axes2D.prototype.on_resize.call(this, width, height); + + var scale = parseFloat($('#main').css('font-size')); + var suppression_width = 6 * scale; + var text_offset = 1.2 * scale; + + if (width < suppression_width || this.display_time == false) { + this.axis_time_start.style.display = 'none'; + } else { + this.axis_time_start.style.display = 'block'; + } + + this.axis_time_start.setAttribute('y', this.ax_bottom + text_offset); + this.axis_time_start.setAttribute('x', this.ax_left - text_offset); + this.axis_time_end.setAttribute('y', this.ax_bottom + text_offset); + this.axis_time_end.setAttribute('x', this.ax_right - text_offset); + }; } diff --git a/nengo_gui/static/components/value.ts b/nengo_gui/static/components/value.ts index 7bed6a90..1d015d3c 100644 --- a/nengo_gui/static/components/value.ts +++ b/nengo_gui/static/components/value.ts @@ -24,438 +24,438 @@ import * as utils from "../utils"; export default class Value extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - var self = this; - this.n_lines = args.n_lines || 1; - this.sim = sim; - this.display_time = args.display_time; - this.synapse = args.synapse; - - // For storing the accumulated data - this.data_store = new DataStore(this.n_lines, this.sim, 0.0); - - this.axes2d = new TimeAxes(this.div, args); - - // Call schedule_update whenever the time is adjusted in the SimControl - this.sim.div.addEventListener('adjust_time', function(e) { - self.schedule_update(); - }, false); - - // Call reset whenever the simulation is reset - this.sim.div.addEventListener('sim_reset', function(e) { - self.reset(); - }, false); - - // Create the lines on the plots - this.line = d3.svg.line() - .x(function(d, i) { - return self.axes2d.scale_x( - self.data_store.times[i + self.data_store.first_shown_index]); - }).y(function(d) { - return self.axes2d.scale_y(d); - }); - this.path = this.axes2d.svg.append("g") - .selectAll('path') - .data(this.data_store.data); - - this.colors = utils.make_colors(this.n_lines); - this.path.enter() - .append('path') - .attr('class', 'line') - .style('stroke', function(d, i) { - return self.colors[i]; - }); + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + var self = this; + this.n_lines = args.n_lines || 1; + this.sim = sim; + this.display_time = args.display_time; + this.synapse = args.synapse; + + // For storing the accumulated data + this.data_store = new DataStore(this.n_lines, this.sim, 0.0); + + this.axes2d = new TimeAxes(this.div, args); + + // Call schedule_update whenever the time is adjusted in the SimControl + this.sim.div.addEventListener('adjust_time', function(e) { + self.schedule_update(); + }, false); + + // Call reset whenever the simulation is reset + this.sim.div.addEventListener('sim_reset', function(e) { + self.reset(); + }, false); + + // Create the lines on the plots + this.line = d3.svg.line() + .x(function(d, i) { + return self.axes2d.scale_x( + self.data_store.times[i + self.data_store.first_shown_index]); + }).y(function(d) { + return self.axes2d.scale_y(d); + }); + this.path = this.axes2d.svg.append("g") + .selectAll('path') + .data(this.data_store.data); + + this.colors = utils.make_colors(this.n_lines); + this.path.enter() + .append('path') + .attr('class', 'line') + .style('stroke', function(d, i) { + return self.colors[i]; + }); - // Flag for whether or not update code should be changing the crosshair. - // Both zooming and the simulator time changing cause an update, but the - // crosshair should only update when the time is changing. - this.crosshair_updates = false; - - // Keep track of mouse position TODO: fix this to be not required - this.crosshair_mouse = [0, 0]; - - this.crosshair_g = this.axes2d.svg.append('g') - .attr('class', 'crosshair'); - - // TODO: put the crosshair properties in CSS - this.crosshair_g.append('line') - .attr('id', 'crosshairX') - .attr('stroke', 'black') - .attr('stroke-width', '0.5px'); - - this.crosshair_g.append('line') - .attr('id', 'crosshairY') - .attr('stroke', 'black') - .attr('stroke-width', '0.5px'); - - // TODO: have the fonts and colour set appropriately - this.crosshair_g.append('text') - .attr('id', 'crosshairXtext') - .style('text-anchor', 'middle') - .attr('class', 'graph_text'); - - this.crosshair_g.append('text') - .attr('id', 'crosshairYtext') - .style('text-anchor', 'end') - .attr('class', 'graph_text'); - - this.axes2d.svg - .on('mouseover', function() { - var mouse = d3.mouse(this); - self.crosshair_updates = true; - self.crosshair_g.style('display', null); - self.cross_hair_mouse = [mouse[0], mouse[1]]; - }).on('mouseout', function() { - var mouse = d3.mouse(this); - self.crosshair_updates = false; - self.crosshair_g.style('display', 'none'); - self.cross_hair_mouse = [mouse[0], mouse[1]]; - }).on('mousemove', function() { - var mouse = d3.mouse(this); - self.crosshair_updates = true; - self.cross_hair_mouse = [mouse[0], mouse[1]]; - self.update_crosshair(mouse); - }).on('mousewheel', function() { - // Hide the crosshair when zooming, - // until a better option comes along - self.crosshair_updates = false; - self.crosshair_g.style('display', 'none'); - }); + // Flag for whether or not update code should be changing the crosshair. + // Both zooming and the simulator time changing cause an update, but the + // crosshair should only update when the time is changing. + this.crosshair_updates = false; + + // Keep track of mouse position TODO: fix this to be not required + this.crosshair_mouse = [0, 0]; + + this.crosshair_g = this.axes2d.svg.append('g') + .attr('class', 'crosshair'); + + // TODO: put the crosshair properties in CSS + this.crosshair_g.append('line') + .attr('id', 'crosshairX') + .attr('stroke', 'black') + .attr('stroke-width', '0.5px'); + + this.crosshair_g.append('line') + .attr('id', 'crosshairY') + .attr('stroke', 'black') + .attr('stroke-width', '0.5px'); + + // TODO: have the fonts and colour set appropriately + this.crosshair_g.append('text') + .attr('id', 'crosshairXtext') + .style('text-anchor', 'middle') + .attr('class', 'graph_text'); + + this.crosshair_g.append('text') + .attr('id', 'crosshairYtext') + .style('text-anchor', 'end') + .attr('class', 'graph_text'); + + this.axes2d.svg + .on('mouseover', function() { + var mouse = d3.mouse(this); + self.crosshair_updates = true; + self.crosshair_g.style('display', null); + self.cross_hair_mouse = [mouse[0], mouse[1]]; + }).on('mouseout', function() { + var mouse = d3.mouse(this); + self.crosshair_updates = false; + self.crosshair_g.style('display', 'none'); + self.cross_hair_mouse = [mouse[0], mouse[1]]; + }).on('mousemove', function() { + var mouse = d3.mouse(this); + self.crosshair_updates = true; + self.cross_hair_mouse = [mouse[0], mouse[1]]; + self.update_crosshair(mouse); + }).on('mousewheel', function() { + // Hide the crosshair when zooming, + // until a better option comes along + self.crosshair_updates = false; + self.crosshair_g.style('display', 'none'); + }); - this.update(); - this.on_resize(this.get_screen_width(), this.get_screen_height()); - this.axes2d.axis_y.tickValues([args.min_value, args.max_value]); - this.axes2d.fit_ticks(this); + this.update(); + this.on_resize(this.get_screen_width(), this.get_screen_height()); + this.axes2d.axis_y.tickValues([args.min_value, args.max_value]); + this.axes2d.fit_ticks(this); + + this.colors = utils.make_colors(6); + this.color_func = function(d, i) { + return self.colors[i % 6]; + }; + this.legend = document.createElement('div'); + this.legend.classList.add('legend'); + this.div.appendChild(this.legend); + + this.legend_labels = args.legend_labels || []; + if (this.legend_labels.length !== this.n_lines) { + // Fill up the array with temporary labels + for (var i = this.legend_labels.length; i < this.n_lines; i++) { + this.legend_labels[i] = "label_" + i; + } + } - this.colors = utils.make_colors(6); - this.color_func = function(d, i) { - return self.colors[i % 6]; - }; - this.legend = document.createElement('div'); - this.legend.classList.add('legend'); - this.div.appendChild(this.legend); - - this.legend_labels = args.legend_labels || []; - if (this.legend_labels.length !== this.n_lines) { - // Fill up the array with temporary labels - for (var i = this.legend_labels.length; i < this.n_lines; i++) { - this.legend_labels[i] = "label_" + i; + this.show_legend = args.show_legend || false; + if (this.show_legend === true) { + utils.draw_legend(this.legend, + this.legend_labels.slice(0, self.n_lines), + this.color_func); } - } - - this.show_legend = args.show_legend || false; - if (this.show_legend === true) { - utils.draw_legend(this.legend, - this.legend_labels.slice(0, self.n_lines), - this.color_func); - } -}; - -update_crosshair(mouse) { - var self = this; - var x = mouse[0]; - var y = mouse[1]; - - // TODO: I don't like having ifs here. - // Make a smaller rectangle for mouseovers - if (x > this.axes2d.ax_left && x < this.axes2d.ax_right && - y > this.axes2d.ax_top && y < this.axes2d.ax_bottom) { - this.crosshair_g.style('display', null); - - this.crosshair_g.select('#crosshairX') - .attr('x1', x) - .attr('y1', this.axes2d.ax_top) - .attr('x2', x) - .attr('y2', this.axes2d.ax_bottom); - - this.crosshair_g.select('#crosshairY') - .attr('x1', this.axes2d.ax_left) - .attr('y1', y) - .attr('x2', this.axes2d.ax_right) - .attr('y2', y); - - // TODO: don't use magic numbers - this.crosshair_g.select('#crosshairXtext') - .attr('x', x - 2) - .attr('y', this.axes2d.ax_bottom + 17) - .text(function() { - return Math.round(self.axes2d.scale_x.invert(x) * 100) / 100; - }); + }; - this.crosshair_g.select('#crosshairYtext') - .attr('x', this.axes2d.ax_left - 3) - .attr('y', y + 3) - .text(function() { - return Math.round(self.axes2d.scale_y.invert(y) * 100) / 100; - }); - } else { - this.crosshair_g.style('display', 'none'); - } -}; + update_crosshair(mouse) { + var self = this; + var x = mouse[0]; + var y = mouse[1]; -/** - * Receive new line data from the server. - */ -on_message(event) { - var data = new Float32Array(event.data); - data = Array.prototype.slice.call(data); - var size = this.n_lines + 1; - // Since multiple data packets can be sent with a single event, - // make sure to process all the packets. - while (data.length >= size) { - this.data_store.push(data.slice(0, size)); - data = data.slice(size); - } - if (data.length > 0) { - console.log('extra data: ' + data.length); - } - this.schedule_update(); -}; + // TODO: I don't like having ifs here. + // Make a smaller rectangle for mouseovers + if (x > this.axes2d.ax_left && x < this.axes2d.ax_right && + y > this.axes2d.ax_top && y < this.axes2d.ax_bottom) { + this.crosshair_g.style('display', null); + + this.crosshair_g.select('#crosshairX') + .attr('x1', x) + .attr('y1', this.axes2d.ax_top) + .attr('x2', x) + .attr('y2', this.axes2d.ax_bottom); + + this.crosshair_g.select('#crosshairY') + .attr('x1', this.axes2d.ax_left) + .attr('y1', y) + .attr('x2', this.axes2d.ax_right) + .attr('y2', y); + + // TODO: don't use magic numbers + this.crosshair_g.select('#crosshairXtext') + .attr('x', x - 2) + .attr('y', this.axes2d.ax_bottom + 17) + .text(function() { + return Math.round(self.axes2d.scale_x.invert(x) * 100) / 100; + }); + + this.crosshair_g.select('#crosshairYtext') + .attr('x', this.axes2d.ax_left - 3) + .attr('y', y + 3) + .text(function() { + return Math.round(self.axes2d.scale_y.invert(y) * 100) / 100; + }); + } else { + this.crosshair_g.style('display', 'none'); + } + }; -/** - * Redraw the lines and axis due to changed data. - */ -update() { - // Let the data store clear out old values - this.data_store.update(); + /** + * Receive new line data from the server. + */ + on_message(event) { + var data = new Float32Array(event.data); + data = Array.prototype.slice.call(data); + var size = this.n_lines + 1; + // Since multiple data packets can be sent with a single event, + // make sure to process all the packets. + while (data.length >= size) { + this.data_store.push(data.slice(0, size)); + data = data.slice(size); + } + if (data.length > 0) { + console.log('extra data: ' + data.length); + } + this.schedule_update(); + }; - // Determine visible range from the SimControl - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; + /** + * Redraw the lines and axis due to changed data. + */ + update() { + // Let the data store clear out old values + this.data_store.update(); - this.axes2d.set_time_range(t1, t2); + // Determine visible range from the SimControl + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; - // Update the lines - var self = this; - var shown_data = this.data_store.get_shown_data(); + this.axes2d.set_time_range(t1, t2); - this.path.data(shown_data) - .attr('d', self.line); + // Update the lines + var self = this; + var shown_data = this.data_store.get_shown_data(); - // Update the crosshair text if the mouse is on top - if (this.crosshair_updates) { - this.update_crosshair(this.cross_hair_mouse); - } -}; + this.path.data(shown_data) + .attr('d', self.line); -/** - * Adjust the graph layout due to changed size. - */ -on_resize(width, height) { - if (width < this.minWidth) { - width = this.minWidth; - } - if (height < this.minHeight) { - height = this.minHeight; + // Update the crosshair text if the mouse is on top + if (this.crosshair_updates) { + this.update_crosshair(this.cross_hair_mouse); + } }; - this.axes2d.on_resize(width, height); + /** + * Adjust the graph layout due to changed size. + */ + on_resize(width, height) { + if (width < this.minWidth) { + width = this.minWidth; + } + if (height < this.minHeight) { + height = this.minHeight; + }; - this.update(); + this.axes2d.on_resize(width, height); - this.label.style.width = width; + this.update(); - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height = height; -}; + this.label.style.width = width; -generate_menu() { - var self = this; - var items = []; - items.push(['Set range...', function() { - self.set_range(); - }]); - items.push(['Set synapse...', function() { - self.set_synapse_dialog(); - }]); + this.width = width; + this.height = height; + this.div.style.width = width; + this.div.style.height = height; + }; - if (this.show_legend) { - items.push(['Hide legend', function() { - self.set_show_legend(false); + generate_menu() { + var self = this; + var items = []; + items.push(['Set range...', function() { + self.set_range(); }]); - } else { - items.push(['Show legend', function() { - self.set_show_legend(true); + items.push(['Set synapse...', function() { + self.set_synapse_dialog(); }]); - } - // TODO: give the legend it's own context menu - items.push(['Set legend labels', function() { - self.set_legend_labels(); - }]); + if (this.show_legend) { + items.push(['Hide legend', function() { + self.set_show_legend(false); + }]); + } else { + items.push(['Show legend', function() { + self.set_show_legend(true); + }]); + } - // Add the parent's menu items to this - return $.merge(items, Component.prototype.generate_menu.call(this)); -}; + // TODO: give the legend it's own context menu + items.push(['Set legend labels', function() { + self.set_legend_labels(); + }]); -set_show_legend(value) { - if (this.show_legend !== value) { - this.show_legend = value; - this.save_layout(); + // Add the parent's menu items to this + return $.merge(items, Component.prototype.generate_menu.call(this)); + }; - if (this.show_legend === true) { - utils.draw_legend(this.legend, - this.legend_labels.slice(0, this.n_lines), - this.color_func); - } else { - // Delete the legend's children - while (this.legend.lastChild) { - this.legend.removeChild(this.legend.lastChild); + set_show_legend(value) { + if (this.show_legend !== value) { + this.show_legend = value; + this.save_layout(); + + if (this.show_legend === true) { + utils.draw_legend(this.legend, + this.legend_labels.slice(0, this.n_lines), + this.color_func); + } else { + // Delete the legend's children + while (this.legend.lastChild) { + this.legend.removeChild(this.legend.lastChild); + } } } - } -}; - -set_legend_labels() { - var self = this; - - self.sim.modal.title('Enter comma seperated legend label values'); - self.sim.modal.single_input_body('Legend label', 'New value'); - self.sim.modal.footer('ok_cancel', function(e) { - var label_csv = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - // No validation to do. - // Empty entries assumed to be indication to skip modification. - // Long strings okay. - // Excissive entries get ignored. - // TODO: Allow escaping of commas - if ((label_csv !== null) && (label_csv !== '')) { - var labels = label_csv.split(','); - - for (var i = 0; i < self.n_lines; i++) { - if (labels[i] !== "" && labels[i] !== undefined) { - self.legend_labels[i] = labels[i]; + }; + + set_legend_labels() { + var self = this; + + self.sim.modal.title('Enter comma seperated legend label values'); + self.sim.modal.single_input_body('Legend label', 'New value'); + self.sim.modal.footer('ok_cancel', function(e) { + var label_csv = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + // No validation to do. + // Empty entries assumed to be indication to skip modification. + // Long strings okay. + // Excissive entries get ignored. + // TODO: Allow escaping of commas + if ((label_csv !== null) && (label_csv !== '')) { + var labels = label_csv.split(','); + + for (var i = 0; i < self.n_lines; i++) { + if (labels[i] !== "" && labels[i] !== undefined) { + self.legend_labels[i] = labels[i]; + } } - } - // Redraw the legend with the updated label values - while (self.legend.lastChild) { - self.legend.removeChild(self.legend.lastChild); + // Redraw the legend with the updated label values + while (self.legend.lastChild) { + self.legend.removeChild(self.legend.lastChild); + } + + utils.draw_legend(self.legend, self.legend_labels, self.color_func); + self.save_layout(); } + $('#OK').attr('data-dismiss', 'modal'); + }); - utils.draw_legend(self.legend, self.legend_labels, self.color_func); - self.save_layout(); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - - self.sim.modal.show(); -}; - -layout_info() { - var info = Component.prototype.layout_info.call(this); - info.show_legend = this.show_legend; - info.legend_labels = this.legend_labels; - info.min_value = this.axes2d.scale_y.domain()[0]; - info.max_value = this.axes2d.scale_y.domain()[1]; - return info; -}; - -update_layout(config) { - this.update_range(config.min_value, config.max_value); - Component.prototype.update_layout.call(this, config); -}; - -set_range() { - var range = this.axes2d.scale_y.domain(); - var self = this; - self.sim.modal.title('Set graph range...'); - self.sim.modal.single_input_body(range, 'New range'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_range = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_range !== null) { - new_range = new_range.split(','); - var min = parseFloat(new_range[0]); - var max = parseFloat(new_range[1]); - self.update_range(min, max); - self.save_layout(); - self.axes2d.axis_y.tickValues([min, max]); - self.axes2d.fit_ticks(self); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - var valid = false; - if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { - if (Number(nums[0]) < Number(nums[1])) { - valid = true; // Two numbers, 1st less than 2nd + self.sim.modal.show(); + }; + + layout_info() { + var info = Component.prototype.layout_info.call(this); + info.show_legend = this.show_legend; + info.legend_labels = this.legend_labels; + info.min_value = this.axes2d.scale_y.domain()[0]; + info.max_value = this.axes2d.scale_y.domain()[1]; + return info; + }; + + update_layout(config) { + this.update_range(config.min_value, config.max_value); + Component.prototype.update_layout.call(this, config); + }; + + set_range() { + var range = this.axes2d.scale_y.domain(); + var self = this; + self.sim.modal.title('Set graph range...'); + self.sim.modal.single_input_body(range, 'New range'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_range = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; + } + if (new_range !== null) { + new_range = new_range.split(','); + var min = parseFloat(new_range[0]); + var max = parseFloat(new_range[1]); + self.update_range(min, max); + self.save_layout(); + self.axes2d.axis_y.tickValues([min, max]); + self.axes2d.fit_ticks(self); + } + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var nums = $item.val().split(','); + var valid = false; + if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { + if (Number(nums[0]) < Number(nums[1])) { + valid = true; // Two numbers, 1st less than 2nd + } } + return (nums.length == 2 && valid); } - return (nums.length == 2 && valid); - } - }, - }); - - $('#singleInput').attr('data-error', 'Input should be in the ' + - 'form ",".'); - self.sim.modal.show(); - $('#OK').on('click', function() { - var w = $(self.div).width(); - var h = $(self.div).height(); - self.on_resize(w, h); - }); -}; - -update_range(min, max) { - this.axes2d.scale_y.domain([min, max]); - this.axes2d.axis_y_g.call(this.axes2d.axis_y); -}; - -reset(event) { - this.data_store.reset(); - this.schedule_update(); -}; - -set_synapse_dialog() { - var self = this; - self.sim.modal.title('Set synaptic filter...'); - self.sim.modal.single_input_body(this.synapse, - 'Filter time constant (in seconds)'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_synapse = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_synapse !== null) { - new_synapse = parseFloat(new_synapse); - if (new_synapse === self.synapse) { + }, + }); + + $('#singleInput').attr('data-error', 'Input should be in the ' + + 'form ",".'); + self.sim.modal.show(); + $('#OK').on('click', function() { + var w = $(self.div).width(); + var h = $(self.div).height(); + self.on_resize(w, h); + }); + }; + + update_range(min, max) { + this.axes2d.scale_y.domain([min, max]); + this.axes2d.axis_y_g.call(this.axes2d.axis_y); + }; + + reset(event) { + this.data_store.reset(); + this.schedule_update(); + }; + + set_synapse_dialog() { + var self = this; + self.sim.modal.title('Set synaptic filter...'); + self.sim.modal.single_input_body(this.synapse, + 'Filter time constant (in seconds)'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_synapse = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { return; } - self.synapse = new_synapse; - self.ws.send('synapse:' + self.synapse); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var num = $item.val(); - if ($.isNumeric(num)) { - num = Number(num); - if (num >= 0) { - return true; - } + if (new_synapse !== null) { + new_synapse = parseFloat(new_synapse); + if (new_synapse === self.synapse) { + return; } - return false; + self.synapse = new_synapse; + self.ws.send('synapse:' + self.synapse); } - }, - }); - $('#singleInput').attr('data-error', 'should be a non-negative number'); - self.sim.modal.show(); -}; + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var num = $item.val(); + if ($.isNumeric(num)) { + num = Number(num); + if (num >= 0) { + return true; + } + } + return false; + } + }, + }); + $('#singleInput').attr('data-error', 'should be a non-negative number'); + self.sim.modal.show(); + }; } diff --git a/nengo_gui/static/components/xy_axes.ts b/nengo_gui/static/components/xy_axes.ts index f301557e..d97396d9 100644 --- a/nengo_gui/static/components/xy_axes.ts +++ b/nengo_gui/static/components/xy_axes.ts @@ -14,32 +14,32 @@ import Axes2D from "./2d_axes"; export default class XYAxes extends Axes2D { -constructor(parent, args) { - super(parent, args); - - this.scale_x.domain([args.min_value, args.max_value]); - this.axis_x.tickValues([args.min_value, args.max_value]); - this.axis_x.ticks(this.axis_y.ticks()[0]); - - this.min_val = args.min_value; - this.max_val = args.max_value; -}; - -/** - * Adjust the graph layout due to changed size. - */ -on_resize(width, height) { - Axes2D.prototype.on_resize.call(this, width, height); - - var x_offset = this.ax_bottom - this.min_val / - (this.max_val - this.min_val) * (this.ax_top - this.ax_bottom); - var y_offset = this.ax_left - this.min_val / - (this.max_val - this.min_val) * (this.ax_right - this.ax_left); - - this.axis_x_g.attr("transform", "translate(0," + x_offset + ")"); - this.axis_x_g.call(this.axis_x); - this.axis_y_g.attr("transform", "translate(" + y_offset + ", 0)"); - this.axis_y_g.call(this.axis_y); -}; + constructor(parent, args) { + super(parent, args); + + this.scale_x.domain([args.min_value, args.max_value]); + this.axis_x.tickValues([args.min_value, args.max_value]); + this.axis_x.ticks(this.axis_y.ticks()[0]); + + this.min_val = args.min_value; + this.max_val = args.max_value; + }; + + /** + * Adjust the graph layout due to changed size. + */ + on_resize(width, height) { + Axes2D.prototype.on_resize.call(this, width, height); + + var x_offset = this.ax_bottom - this.min_val / + (this.max_val - this.min_val) * (this.ax_top - this.ax_bottom); + var y_offset = this.ax_left - this.min_val / + (this.max_val - this.min_val) * (this.ax_right - this.ax_left); + + this.axis_x_g.attr("transform", "translate(0," + x_offset + ")"); + this.axis_x_g.call(this.axis_x); + this.axis_y_g.attr("transform", "translate(" + y_offset + ", 0)"); + this.axis_y_g.call(this.axis_y); + }; } diff --git a/nengo_gui/static/components/xyvalue.ts b/nengo_gui/static/components/xyvalue.ts index 82dd290c..d7a461b8 100644 --- a/nengo_gui/static/components/xyvalue.ts +++ b/nengo_gui/static/components/xyvalue.ts @@ -25,284 +25,284 @@ import XYAxes from "./xy_axes"; export default class XYValue extends Component { -constructor(parent, viewport, sim, args) { - super(parent, viewport, args); - var self = this; - - this.n_lines = args.n_lines || 1; - this.sim = sim; - - // For storing the accumulated data - this.data_store = new DataStore(this.n_lines, this.sim, 0); - - this.axes2d = new XYAxes(this.div, args); - - // The two indices of the multi-dimensional data to display - this.index_x = args.index_x; - this.index_y = args.index_y; - - // Call schedule_update whenever the time is adjusted in the SimControl - this.sim.div.addEventListener('adjust_time', function(e) { - self.schedule_update(); - }, false); - - // Call reset whenever the simulation is reset - this.sim.div.addEventListener('sim_reset', function(e) { - self.reset(); - }, false); - - // Create the lines on the plots - var line = d3.svg.line() - .x(function(d, i) { - return self.axes2d.scale_x(self.data_store.data[this.index_x][i]); - }).y(function(d) { - return self.axe2d.scale_y(d); - }); - this.path = this.axes2d.svg.append("g") - .selectAll('path') - .data([this.data_store.data[this.index_y]]); - this.path.enter().append('path') - .attr('class', 'line') - .style('stroke', utils.make_colors(1)); - - // Create a circle to track the most recent data - this.recent_circle = this.axes2d.svg.append("circle") - .attr("r", this.get_circle_radius()) - .attr('cx', this.axes2d.scale_x(0)) - .attr('cy', this.axes2d.scale_y(0)) - .style("fill", utils.make_colors(1)[0]) - .style('fill-opacity', 0); - - this.invalid_dims = false; - - this.axes2d.fit_ticks(this); - this.on_resize(this.get_screen_width(), this.get_screen_height()); -}; + constructor(parent, viewport, sim, args) { + super(parent, viewport, args); + var self = this; -/** - * Receive new line data from the server. - */ -on_message(event) { - var data = new Float32Array(event.data); - this.data_store.push(data); - this.schedule_update(); -}; + this.n_lines = args.n_lines || 1; + this.sim = sim; -/** - * Redraw the lines and axis due to changed data. - */ -update() { - var self = this; + // For storing the accumulated data + this.data_store = new DataStore(this.n_lines, this.sim, 0); - // Let the data store clear out old values - this.data_store.update(); + this.axes2d = new XYAxes(this.div, args); - // Update the lines if there is data with valid dimensions - var good_idx = self.index_x < self.n_lines && self.index_y < self.n_lines; - if (good_idx) { - var shown_data = this.data_store.get_shown_data(); + // The two indices of the multi-dimensional data to display + this.index_x = args.index_x; + this.index_y = args.index_y; - // Update the lines + // Call schedule_update whenever the time is adjusted in the SimControl + this.sim.div.addEventListener('adjust_time', function(e) { + self.schedule_update(); + }, false); + + // Call reset whenever the simulation is reset + this.sim.div.addEventListener('sim_reset', function(e) { + self.reset(); + }, false); + + // Create the lines on the plots var line = d3.svg.line() .x(function(d, i) { - return self.axes2d.scale_x(shown_data[self.index_x][i]); + return self.axes2d.scale_x(self.data_store.data[this.index_x][i]); }).y(function(d) { - return self.axes2d.scale_y(d); + return self.axe2d.scale_y(d); }); - this.path.data([shown_data[this.index_y]]) - .attr('d', line); - - var last_index = shown_data[self.index_x].length - 1; - - if (last_index >= 0) { - // Update the circle if there is valid data - this.recent_circle - .attr('cx', self.axes2d.scale_x( - shown_data[self.index_x][last_index])) - .attr('cy', self.axes2d.scale_y( - shown_data[self.index_y][last_index])) - .style('fill-opacity', 0.5); - } - - // If switching from invalids dimensions to valid dimensions, remove - // the label - if (this.invalid_dims === true) { - this.div.removeChild(this.warning_text); - this.invalid_dims = false; - } + this.path = this.axes2d.svg.append("g") + .selectAll('path') + .data([this.data_store.data[this.index_y]]); + this.path.enter().append('path') + .attr('class', 'line') + .style('stroke', utils.make_colors(1)); + + // Create a circle to track the most recent data + this.recent_circle = this.axes2d.svg.append("circle") + .attr("r", this.get_circle_radius()) + .attr('cx', this.axes2d.scale_x(0)) + .attr('cy', this.axes2d.scale_y(0)) + .style("fill", utils.make_colors(1)[0]) + .style('fill-opacity', 0); + + this.invalid_dims = false; + + this.axes2d.fit_ticks(this); + this.on_resize(this.get_screen_width(), this.get_screen_height()); + }; + + /** + * Receive new line data from the server. + */ + on_message(event) { + var data = new Float32Array(event.data); + this.data_store.push(data); + this.schedule_update(); + }; + + /** + * Redraw the lines and axis due to changed data. + */ + update() { + var self = this; + + // Let the data store clear out old values + this.data_store.update(); + + // Update the lines if there is data with valid dimensions + var good_idx = self.index_x < self.n_lines && self.index_y < self.n_lines; + if (good_idx) { + var shown_data = this.data_store.get_shown_data(); + + // Update the lines + var line = d3.svg.line() + .x(function(d, i) { + return self.axes2d.scale_x(shown_data[self.index_x][i]); + }).y(function(d) { + return self.axes2d.scale_y(d); + }); + this.path.data([shown_data[this.index_y]]) + .attr('d', line); + + var last_index = shown_data[self.index_x].length - 1; + + if (last_index >= 0) { + // Update the circle if there is valid data + this.recent_circle + .attr('cx', self.axes2d.scale_x( + shown_data[self.index_x][last_index])) + .attr('cy', self.axes2d.scale_y( + shown_data[self.index_y][last_index])) + .style('fill-opacity', 0.5); + } - } else if (this.invalid_dims == false) { - this.invalid_dims = true; + // If switching from invalids dimensions to valid dimensions, remove + // the label + if (this.invalid_dims === true) { + this.div.removeChild(this.warning_text); + this.invalid_dims = false; + } - // Create the HTML text element - this.warning_text = document.createElement('div'); - this.div.appendChild(this.warning_text); - this.warning_text.className = "warning-text"; - this.warning_text.innerHTML = "Change
Dimension
Indices"; - } -}; + } else if (this.invalid_dims == false) { + this.invalid_dims = true; -/** - * Adjust the graph layout due to changed size - */ -on_resize(width, height) { - this.axes2d.on_resize(width, height); - - this.update(); - - this.label.style.width = width; - this.width = width; - this.height = height; - this.div.style.width = width; - this.div.style.height = height; - this.recent_circle.attr("r", this.get_circle_radius()); -}; - -get_circle_radius() { - return Math.min(this.width, this.height) / 30; -}; - -generate_menu() { - var self = this; - var items = []; - items.push(['Set range...', function() { - self.set_range(); - }]); - items.push(['Set X, Y indices...', function() { - self.set_indices(); - }]); - - // Add the parent's menu items to this - return $.merge(items, Component.prototype.generate_menu.call(this)); -}; - -layout_info() { - var info = Component.prototype.layout_info.call(this); - info.min_value = this.axes2d.scale_y.domain()[0]; - info.max_value = this.axes2d.scale_y.domain()[1]; - info.index_x = this.index_x; - info.index_y = this.index_y; - return info; -}; - -update_layout(config) { - this.update_indices(config.index_x, config.index_y); - this.update_range(config.min_value, config.max_value); - Component.prototype.update_layout.call(this, config); -}; - -set_range() { - var range = this.axes2d.scale_y.domain(); - var self = this; - self.sim.modal.title('Set graph range...'); - self.sim.modal.single_input_body(range, 'New range'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_range = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_range !== null) { - new_range = new_range.split(','); - var min = parseFloat(new_range[0]); - var max = parseFloat(new_range[1]); - self.update_range(min, max); - self.update(); - self.save_layout(); + // Create the HTML text element + this.warning_text = document.createElement('div'); + this.div.appendChild(this.warning_text); + this.warning_text.className = "warning-text"; + this.warning_text.innerHTML = "Change
Dimension
Indices"; } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - var valid = false; - if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { - // Two numbers, 1st less than 2nd. - // The axes must intersect at 0. - var ordered = Number(nums[0]) < Number(nums[1]); - var zeroed = Number(nums[0]) * Number(nums[1]) <= 0; - if (ordered && zeroed) { - valid = true; + }; + + /** + * Adjust the graph layout due to changed size + */ + on_resize(width, height) { + this.axes2d.on_resize(width, height); + + this.update(); + + this.label.style.width = width; + this.width = width; + this.height = height; + this.div.style.width = width; + this.div.style.height = height; + this.recent_circle.attr("r", this.get_circle_radius()); + }; + + get_circle_radius() { + return Math.min(this.width, this.height) / 30; + }; + + generate_menu() { + var self = this; + var items = []; + items.push(['Set range...', function() { + self.set_range(); + }]); + items.push(['Set X, Y indices...', function() { + self.set_indices(); + }]); + + // Add the parent's menu items to this + return $.merge(items, Component.prototype.generate_menu.call(this)); + }; + + layout_info() { + var info = Component.prototype.layout_info.call(this); + info.min_value = this.axes2d.scale_y.domain()[0]; + info.max_value = this.axes2d.scale_y.domain()[1]; + info.index_x = this.index_x; + info.index_y = this.index_y; + return info; + }; + + update_layout(config) { + this.update_indices(config.index_x, config.index_y); + this.update_range(config.min_value, config.max_value); + Component.prototype.update_layout.call(this, config); + }; + + set_range() { + var range = this.axes2d.scale_y.domain(); + var self = this; + self.sim.modal.title('Set graph range...'); + self.sim.modal.single_input_body(range, 'New range'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_range = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; + } + if (new_range !== null) { + new_range = new_range.split(','); + var min = parseFloat(new_range[0]); + var max = parseFloat(new_range[1]); + self.update_range(min, max); + self.update(); + self.save_layout(); + } + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var nums = $item.val().split(','); + var valid = false; + if ($.isNumeric(nums[0]) && $.isNumeric(nums[1])) { + // Two numbers, 1st less than 2nd. + // The axes must intersect at 0. + var ordered = Number(nums[0]) < Number(nums[1]); + var zeroed = Number(nums[0]) * Number(nums[1]) <= 0; + if (ordered && zeroed) { + valid = true; + } } + return (nums.length == 2 && valid); } - return (nums.length == 2 && valid); } - } - }); - - $('#singleInput').attr('data-error', 'Input should be in the form ' + - '"," and the axes must cross at zero.'); - self.sim.modal.show(); -}; - -update_range(min, max) { - this.axes2d.min_val = min; - this.axes2d.max_val = max; - this.axes2d.scale_x.domain([min, max]); - this.axes2d.scale_y.domain([min, max]); - this.axes2d.axis_x.tickValues([min, max]); - this.axes2d.axis_y.tickValues([min, max]); - this.axes2d.axis_y_g.call(this.axes2d.axis_y); - this.axes2d.axis_x_g.call(this.axes2d.axis_x); - this.on_resize(this.get_screen_width(), this.get_screen_height()); -}; - -set_indices() { - var self = this; - self.sim.modal.title('Set X and Y indices...'); - self.sim.modal.single_input_body( - [this.index_x, this.index_y], 'New indices'); - self.sim.modal.footer('ok_cancel', function(e) { - var new_indices = $('#singleInput').val(); - var modal = $('#myModalForm').data('bs.validator'); - - modal.validate(); - if (modal.hasErrors() || modal.isIncomplete()) { - return; - } - if (new_indices !== null) { - new_indices = new_indices.split(','); - self.update_indices(parseInt(new_indices[0]), - parseInt(new_indices[1])); - self.save_layout(); - } - $('#OK').attr('data-dismiss', 'modal'); - }); - var $form = $('#myModalForm').validator({ - custom: { - my_validator: function($item) { - var nums = $item.val().split(','); - return ((parseInt(Number(nums[0])) == nums[0]) && - (parseInt(Number(nums[1])) == nums[1]) && - (nums.length == 2) && - (Number(nums[1]) < self.n_lines && - Number(nums[1]) >= 0) && - (Number(nums[0]) < self.n_lines && - Number(nums[0]) >= 0)); + }); + + $('#singleInput').attr('data-error', 'Input should be in the form ' + + '"," and the axes must cross at zero.'); + self.sim.modal.show(); + }; + + update_range(min, max) { + this.axes2d.min_val = min; + this.axes2d.max_val = max; + this.axes2d.scale_x.domain([min, max]); + this.axes2d.scale_y.domain([min, max]); + this.axes2d.axis_x.tickValues([min, max]); + this.axes2d.axis_y.tickValues([min, max]); + this.axes2d.axis_y_g.call(this.axes2d.axis_y); + this.axes2d.axis_x_g.call(this.axes2d.axis_x); + this.on_resize(this.get_screen_width(), this.get_screen_height()); + }; + + set_indices() { + var self = this; + self.sim.modal.title('Set X and Y indices...'); + self.sim.modal.single_input_body( + [this.index_x, this.index_y], 'New indices'); + self.sim.modal.footer('ok_cancel', function(e) { + var new_indices = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + modal.validate(); + if (modal.hasErrors() || modal.isIncomplete()) { + return; } - } - }); - - $('#singleInput').attr( - 'data-error', 'Input should be two positive ' + - 'integers in the form ",". ' + - 'Dimensions are zero indexed.'); - - self.sim.modal.show(); -}; - -update_indices(index_x, index_y) { - this.index_x = index_x; - this.index_y = index_y; - this.update(); -}; - -reset(event) { - this.data_store.reset(); - this.schedule_update(); -}; + if (new_indices !== null) { + new_indices = new_indices.split(','); + self.update_indices(parseInt(new_indices[0]), + parseInt(new_indices[1])); + self.save_layout(); + } + $('#OK').attr('data-dismiss', 'modal'); + }); + var $form = $('#myModalForm').validator({ + custom: { + my_validator: function($item) { + var nums = $item.val().split(','); + return ((parseInt(Number(nums[0])) == nums[0]) && + (parseInt(Number(nums[1])) == nums[1]) && + (nums.length == 2) && + (Number(nums[1]) < self.n_lines && + Number(nums[1]) >= 0) && + (Number(nums[0]) < self.n_lines && + Number(nums[0]) >= 0)); + } + } + }); + + $('#singleInput').attr( + 'data-error', 'Input should be two positive ' + + 'integers in the form ",". ' + + 'Dimensions are zero indexed.'); + + self.sim.modal.show(); + }; + + update_indices(index_x, index_y) { + this.index_x = index_x; + this.index_y = index_y; + this.update(); + }; + + reset(event) { + this.data_store.reset(); + this.schedule_update(); + }; } diff --git a/nengo_gui/static/config.ts b/nengo_gui/static/config.ts index 6b13c948..369f698a 100644 --- a/nengo_gui/static/config.ts +++ b/nengo_gui/static/config.ts @@ -1,49 +1,49 @@ export default class Config { -constructor() { - var self = this; + constructor() { + var self = this; - var define_option = function(key, default_val) { - var type = typeof(default_val); - Object.defineProperty(self, key, { - get: function() { - var val = localStorage.getItem("ng." + key) || default_val; - if (type === "boolean") { - return val === "true" || val === true; - } else if (type === "number") { - return Number(val); - } else { - return val; - } - }, - set: function(val) { - return localStorage.setItem("ng." + key, val); - }, - enumerable: true - }); - }; + var define_option = function(key, default_val) { + var type = typeof(default_val); + Object.defineProperty(self, key, { + get: function() { + var val = localStorage.getItem("ng." + key) || default_val; + if (type === "boolean") { + return val === "true" || val === true; + } else if (type === "number") { + return Number(val); + } else { + return val; + } + }, + set: function(val) { + return localStorage.setItem("ng." + key, val); + }, + enumerable: true + }); + }; - // General options accessible through Configuration Options - define_option("transparent_nets", false); - define_option("aspect_resize", false); - define_option("zoom_fonts", false); - define_option("font_size", 100); - define_option("scriptdir", "."); + // General options accessible through Configuration Options + define_option("transparent_nets", false); + define_option("aspect_resize", false); + define_option("zoom_fonts", false); + define_option("font_size", 100); + define_option("scriptdir", "."); - // Editor options - define_option("hide_editor", false); - define_option("editor_width", 580); - define_option("editor_font_size", 12); - define_option("auto_update", true); - define_option("console_height", 100); -}; + // Editor options + define_option("hide_editor", false); + define_option("editor_width", 580); + define_option("editor_font_size", 12); + define_option("auto_update", true); + define_option("console_height", 100); + }; -restore_defaults() { - for (var option in this) { - if (this.hasOwnProperty(option)) { - localStorage.removeItem("ng." + option); + restore_defaults() { + for (var option in this) { + if (this.hasOwnProperty(option)) { + localStorage.removeItem("ng." + option); + } } - } -}; + }; } diff --git a/nengo_gui/static/datastore.ts b/nengo_gui/static/datastore.ts index bc561ef0..09ee6565 100644 --- a/nengo_gui/static/datastore.ts +++ b/nengo_gui/static/datastore.ts @@ -10,147 +10,147 @@ export class DataStore { -constructor(dims, sim, synapse) { - this.synapse = synapse; // TODO: get from SimControl - this.sim = sim; - this.times = []; - this.data = []; - for (var i = 0; i < dims; i++) { - this.data.push([]); - } -}; + constructor(dims, sim, synapse) { + this.synapse = synapse; // TODO: get from SimControl + this.sim = sim; + this.times = []; + this.data = []; + for (var i = 0; i < dims; i++) { + this.data.push([]); + } + }; + + /** + * Add a set of data. + * + * @param {array} row - dims+1 data points, with time as the first one + */ + push(row) { + // If you get data out of order, wipe out the later data + if (row[0] < this.times[this.times.length - 1]) { + var index = 0; + while (this.times[index] < row[0]) { + index += 1; + } -/** - * Add a set of data. - * - * @param {array} row - dims+1 data points, with time as the first one - */ -push(row) { - // If you get data out of order, wipe out the later data - if (row[0] < this.times[this.times.length - 1]) { - var index = 0; - while (this.times[index] < row[0]) { - index += 1; + var dims = this.data.length; + this.times.splice(index, this.times.length); + for (var i = 0; i < this.data.length; i++) { + this.data[i].splice(index, this.data[i].length); + } + } + + // Compute lowpass filter (value = value*decay + new_value*(1-decay) + var decay = 0.0; + if ((this.times.length != 0) && (this.synapse > 0)) { + var dt = row[0] - this.times[this.times.length - 1]; + decay = Math.exp(-dt / this.synapse); } - var dims = this.data.length; + // Put filtered values into data array + for (var i = 0; i < this.data.length; i++) { + if (decay == 0.0) { + this.data[i].push(row[i + 1]); + } else { + this.data[i].push(row[i + 1] * (1 - decay) + + this.data[i][this.data[i].length - 1] * decay); + } + } + // Store the time as well + this.times.push(row[0]); + }; + + /** + * Reset the data storage. + * + * This will clear current data so there is + * nothing to display on a reset event. + */ + reset() { + var index = 0; this.times.splice(index, this.times.length); for (var i = 0; i < this.data.length; i++) { this.data[i].splice(index, this.data[i].length); } - } - - // Compute lowpass filter (value = value*decay + new_value*(1-decay) - var decay = 0.0; - if ((this.times.length != 0) && (this.synapse > 0)) { - var dt = row[0] - this.times[this.times.length - 1]; - decay = Math.exp(-dt / this.synapse); - } - - // Put filtered values into data array - for (var i = 0; i < this.data.length; i++) { - if (decay == 0.0) { - this.data[i].push(row[i + 1]); - } else { - this.data[i].push(row[i + 1] * (1 - decay) + - this.data[i][this.data[i].length - 1] * decay); + }; + + /** + * Update the data storage. + * + * This should be call periodically (before visual updates, but not necessarily + * after every push()). Removes old data outside the storage limit set by + * the SimControl. + */ + update() { + // Figure out how many extra values we have (values whose time stamp is + // outside the range to keep) + var extra = 0; + // How much has the most recent time exceeded how much is allowed to be kept + var limit = this.sim.time_slider.last_time - + this.sim.time_slider.kept_time; + while (this.times[extra] < limit) { + extra += 1; } - } - // Store the time as well - this.times.push(row[0]); -}; -/** - * Reset the data storage. - * - * This will clear current data so there is - * nothing to display on a reset event. - */ -reset() { - var index = 0; - this.times.splice(index, this.times.length); - for (var i = 0; i < this.data.length; i++) { - this.data[i].splice(index, this.data[i].length); - } -}; + // Remove the extra data + if (extra > 0) { + this.times = this.times.slice(extra); + for (var i = 0; i < this.data.length; i++) { + this.data[i] = this.data[i].slice(extra); + } + } + }; -/** - * Update the data storage. - * - * This should be call periodically (before visual updates, but not necessarily - * after every push()). Removes old data outside the storage limit set by - * the SimControl. - */ -update() { - // Figure out how many extra values we have (values whose time stamp is - // outside the range to keep) - var extra = 0; - // How much has the most recent time exceeded how much is allowed to be kept - var limit = this.sim.time_slider.last_time - - this.sim.time_slider.kept_time; - while (this.times[extra] < limit) { - extra += 1; - } - - // Remove the extra data - if (extra > 0) { - this.times = this.times.slice(extra); + /** + * Return just the data that is to be shown. + */ + get_shown_data() { + // Determine time range + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; + + // Find the corresponding index values + var index = 0; + while (this.times[index] < t1) { + index += 1; + } + var last_index = index; + while (this.times[last_index] < t2 && last_index < this.times.length) { + last_index += 1; + } + this.first_shown_index = index; + + // Return the visible slice of the data + var shown = []; for (var i = 0; i < this.data.length; i++) { - this.data[i] = this.data[i].slice(extra); + shown.push(this.data[i].slice(index, last_index)); + } + return shown; + }; + + is_at_end() { + var ts = this.sim.time_slider; + return (ts.last_time < ts.first_shown_time + ts.shown_time + 1e-9); + }; + + get_last_data() { + // Determine time range + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; + + // Find the corresponding index values + var last_index = 0; + while (this.times[last_index] < t2 && last_index < this.times.length - 1) { + last_index += 1; } - } -}; -/** - * Return just the data that is to be shown. - */ -get_shown_data() { - // Determine time range - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - // Find the corresponding index values - var index = 0; - while (this.times[index] < t1) { - index += 1; - } - var last_index = index; - while (this.times[last_index] < t2 && last_index < this.times.length) { - last_index += 1; - } - this.first_shown_index = index; - - // Return the visible slice of the data - var shown = []; - for (var i = 0; i < this.data.length; i++) { - shown.push(this.data[i].slice(index, last_index)); - } - return shown; -}; - -is_at_end() { - var ts = this.sim.time_slider; - return (ts.last_time < ts.first_shown_time + ts.shown_time + 1e-9); -}; - -get_last_data() { - // Determine time range - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - // Find the corresponding index values - var last_index = 0; - while (this.times[last_index] < t2 && last_index < this.times.length - 1) { - last_index += 1; - } - - // Return the visible slice of the data - var shown = []; - for (var i = 0; i < this.data.length; i++) { - shown.push(this.data[i][last_index]); - } - return shown; -}; + // Return the visible slice of the data + var shown = []; + for (var i = 0; i < this.data.length; i++) { + shown.push(this.data[i][last_index]); + } + return shown; + }; } @@ -165,201 +165,201 @@ get_last_data() { */ export class GrowableDataStore extends DataStore { -constructor (dims, sim, synapse) { - super(dims, sim, synapse); - - this._dims = dims; - - Object.defineProperty(this, "dims", { - get: function() { - return this._dims; - }, - set: function(dim_val) { - // Throw a bunch of errors if bad things happen. - // Assuming you can only grow dims and not shrink them... - if (this._dims < dim_val) { - for (var i = 0; i < dim_val - this._dims; i++) { - this.data.push([]); + constructor (dims, sim, synapse) { + super(dims, sim, synapse); + + this._dims = dims; + + Object.defineProperty(this, "dims", { + get: function() { + return this._dims; + }, + set: function(dim_val) { + // Throw a bunch of errors if bad things happen. + // Assuming you can only grow dims and not shrink them... + if (this._dims < dim_val) { + for (var i = 0; i < dim_val - this._dims; i++) { + this.data.push([]); + } + } else if (this._dims > dim_val) { + throw "can't decrease size of datastore"; } - } else if (this._dims > dim_val) { - throw "can't decrease size of datastore"; + this._dims = dim_val; + } + }); + }; + + get_offset() { + var offset = []; + offset.push(0); + + for (var i = 1; i < this._dims; i++) { + if (this.data[i] === undefined) { + offset.push(this.data[0].length); + } else { + offset.push(this.data[0].length - this.data[i].length); } - this._dims = dim_val; - } - }); -}; - -get_offset() { - var offset = []; - offset.push(0); - - for (var i = 1; i < this._dims; i++) { - if (this.data[i] === undefined) { - offset.push(this.data[0].length); - } else { - offset.push(this.data[0].length - this.data[i].length); } - } - return offset; -}; + return offset; + }; + + /** + * Add a set of data. + * + * @param {array} row - dims+1 data points, with time as the first one + */ + push(row) { + // Get the offsets + var offset = this.get_offset(); + + // If you get data out of order, wipe out the later data + if (row[0] < this.times[this.times.length - 1]) { + var index = 0; + while (this.times[index] < row[0]) { + index += 1; + } -/** - * Add a set of data. - * - * @param {array} row - dims+1 data points, with time as the first one - */ -push(row) { - // Get the offsets - var offset = this.get_offset(); + this.times.splice(index, this.times.length); + for (var i = 0; i < this._dims; i++) { + if (index - offset[i] >= 0) { + this.data[i].splice(index - offset[i], this.data[i].length); + } + } + } - // If you get data out of order, wipe out the later data - if (row[0] < this.times[this.times.length - 1]) { - var index = 0; - while (this.times[index] < row[0]) { - index += 1; + // Compute lowpass filter (value = value*decay + new_value*(1-decay) + var decay = 0.0; + if ((this.times.length != 0) && (this.synapse > 0)) { + var dt = row[0] - this.times[this.times.length - 1]; + decay = Math.exp(-dt / this.synapse); } - this.times.splice(index, this.times.length); + // Put filtered values into data array for (var i = 0; i < this._dims; i++) { - if (index - offset[i] >= 0) { - this.data[i].splice(index - offset[i], this.data[i].length); + if (decay == 0.0 || this.data[i].length === 0) { + this.data[i].push(row[i + 1]); + } else { + this.data[i].push(row[i + 1] * (1 - decay) + + this.data[i][this.data[i].length - 1] * decay); } } - } - - // Compute lowpass filter (value = value*decay + new_value*(1-decay) - var decay = 0.0; - if ((this.times.length != 0) && (this.synapse > 0)) { - var dt = row[0] - this.times[this.times.length - 1]; - decay = Math.exp(-dt / this.synapse); - } - - // Put filtered values into data array - for (var i = 0; i < this._dims; i++) { - if (decay == 0.0 || this.data[i].length === 0) { - this.data[i].push(row[i + 1]); - } else { - this.data[i].push(row[i + 1] * (1 - decay) + - this.data[i][this.data[i].length - 1] * decay); + // Store the time as well + this.times.push(row[0]); + }; + + /** + * Reset dimensions before resetting the datastore. + */ + reset() { + console.log("resetting growable"); + this._dims = 1; + DataStore.call(this, this._dims, this.sim, this.synapse); + }; + + /** + * Update the data storage. + * + * This should be call periodically (before visual updates, but not necessarily + * after every push()). Removes old data outside the storage limit set by + * the SimControl. + */ + update() { + // Figure out how many extra values we have (values whose time stamp is + // outside the range to keep) + var offset = this.get_offset(); + var extra = 0; + var limit = this.sim.time_slider.last_time - + this.sim.time_slider.kept_time; + while (this.times[extra] < limit) { + extra += 1; } - } - // Store the time as well - this.times.push(row[0]); -}; -/** - * Reset dimensions before resetting the datastore. - */ -reset() { - console.log("resetting growable"); - this._dims = 1; - DataStore.call(this, this._dims, this.sim, this.synapse); -}; - -/** - * Update the data storage. - * - * This should be call periodically (before visual updates, but not necessarily - * after every push()). Removes old data outside the storage limit set by - * the SimControl. - */ -update() { - // Figure out how many extra values we have (values whose time stamp is - // outside the range to keep) - var offset = this.get_offset(); - var extra = 0; - var limit = this.sim.time_slider.last_time - - this.sim.time_slider.kept_time; - while (this.times[extra] < limit) { - extra += 1; - } - - // Remove the extra data - if (extra > 0) { - this.times = this.times.slice(extra); - for (var i = 0; i < this.data.length; i++) { - if (extra - offset[i] >= 0) { - this.data[i] = this.data[i].slice(extra - offset[i]); + // Remove the extra data + if (extra > 0) { + this.times = this.times.slice(extra); + for (var i = 0; i < this.data.length; i++) { + if (extra - offset[i] >= 0) { + this.data[i] = this.data[i].slice(extra - offset[i]); + } } } - } -}; + }; + + /** + * Return just the data that is to be shown. + */ + get_shown_data() { + var offset = this.get_offset(); + // Determine time range + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; + + // Find the corresponding index values + var index = 0; + while (this.times[index] < t1) { + index += 1; + } + // Logically, you should start the search for the + var last_index = index; + while (this.times[last_index] < t2 && last_index < this.times.length) { + last_index += 1; + } + this.first_shown_index = index; + + // Return the visible slice of the data + var shown = []; + var nan_number = 0; + var slice_start = 0; + for (var i = 0; i < this._dims; i++) { + + if (last_index > offset[i] && offset[i] !== 0) { + + if (index < offset[i]) { + nan_number = offset[i] - index; + slice_start = 0; + } else { + nan_number = 0; + slice_start = index - offset[i]; + } + + shown.push( + Array.apply(null, Array(nan_number)).map(function() { + return "NaN"; + }).concat( + this.data[i].slice(slice_start, last_index - offset[i]) + )); -/** - * Return just the data that is to be shown. - */ -get_shown_data() { - var offset = this.get_offset(); - // Determine time range - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - // Find the corresponding index values - var index = 0; - while (this.times[index] < t1) { - index += 1; - } - // Logically, you should start the search for the - var last_index = index; - while (this.times[last_index] < t2 && last_index < this.times.length) { - last_index += 1; - } - this.first_shown_index = index; - - // Return the visible slice of the data - var shown = []; - var nan_number = 0; - var slice_start = 0; - for (var i = 0; i < this._dims; i++) { - - if (last_index > offset[i] && offset[i] !== 0) { - - if (index < offset[i]) { - nan_number = offset[i] - index; - slice_start = 0; } else { - nan_number = 0; - slice_start = index - offset[i]; - } - shown.push( - Array.apply(null, Array(nan_number)).map(function() { - return "NaN"; - }).concat( - this.data[i].slice(slice_start, last_index - offset[i]) - )); + shown.push(this.data[i].slice(index, last_index)); - } else { + } + } - shown.push(this.data[i].slice(index, last_index)); + return shown; + }; + get_last_data() { + var offset = this.get_offset(); + // Determine time range + var t1 = this.sim.time_slider.first_shown_time; + var t2 = t1 + this.sim.time_slider.shown_time; + + // Find the corresponding index values + var last_index = 0; + while (this.times[last_index] < t2 && last_index < this.times.length - 1) { + last_index += 1; } - } - - return shown; -}; - -get_last_data() { - var offset = this.get_offset(); - // Determine time range - var t1 = this.sim.time_slider.first_shown_time; - var t2 = t1 + this.sim.time_slider.shown_time; - - // Find the corresponding index values - var last_index = 0; - while (this.times[last_index] < t2 && last_index < this.times.length - 1) { - last_index += 1; - } - - // Return the visible slice of the data - var shown = []; - for (var i = 0; i < this._dims; i++) { - if (last_index - offset[i] >= 0) { - shown.push(this.data[i][last_index - offset[i]]); + + // Return the visible slice of the data + var shown = []; + for (var i = 0; i < this._dims; i++) { + if (last_index - offset[i] >= 0) { + shown.push(this.data[i][last_index - offset[i]]); + } } - } - return shown; -}; + return shown; + }; } diff --git a/nengo_gui/static/editor.ts b/nengo_gui/static/editor.ts index 0a976ae2..60f4da06 100644 --- a/nengo_gui/static/editor.ts +++ b/nengo_gui/static/editor.ts @@ -18,267 +18,267 @@ import * as utils from "./utils"; export default class Editor { -constructor(uid, netgraph) { - this.netgraph = netgraph; - this.config = this.netgraph.config; - this.viewport = this.netgraph.viewport; - - if (uid[0] === '<') { - console.log("invalid uid for Editor: " + uid); - } - var self = this; - this.min_width = 50; - this.max_width = $(window).width() - 100; - - this.ws = utils.create_websocket(uid); - this.ws.onmessage = function(event) { - self.on_message(event); - }; + constructor(uid, netgraph) { + this.netgraph = netgraph; + this.config = this.netgraph.config; + this.viewport = this.netgraph.viewport; - this.current_code = ''; - var code_div = document.createElement('div'); - code_div.id = 'editor'; - $('#rightpane').append(code_div); - this.editor = ace.edit('editor'); - this.editor.getSession().setMode("ace/mode/python"); - this.editor.gotoLine(1); - this.marker = null; - - this.console = document.createElement('div'); - this.console.id = 'console'; - $('#rightpane').append(this.console); - this.console_height = this.config.console_height; - this.console_stdout = document.createElement('pre'); - this.console_error = document.createElement('pre'); - this.console_stdout.id = 'console_stdout'; - this.console_error.id = 'console_error'; - this.console.appendChild(this.console_stdout); - this.console.appendChild(this.console_error); - $('#console').height(this.console_height); - - this.save_disabled = true; - // If an update of the model from the code editor is allowed - this.update_trigger = true; - // Automatically update the model based on the text - this.auto_update = true; - - // Setup the button to toggle the code editor - $('#Toggle_ace').on('click', function() { - self.toggle_shown(); - }); - $('#Save_file').on('click', function() { - self.save_file(); - }); - $('#Font_increase').on('click', function() { - self.font_size += 1; - }); - $('#Font_decrease').on('click', function() { - self.font_size -= 1; - }); - - this.schedule_updates(); - - var self = this; - - Object.defineProperty(this, 'width', { - get: function() { - return self.config.editor_width; - }, - set: function(val) { - val = Math.max(Math.min(val, this.max_width), this.min_width); - $('#rightpane').width(val); - self.config.editor_width = val; + if (uid[0] === '<') { + console.log("invalid uid for Editor: " + uid); } + var self = this; + this.min_width = 50; + this.max_width = $(window).width() - 100; + + this.ws = utils.create_websocket(uid); + this.ws.onmessage = function(event) { + self.on_message(event); + }; + + this.current_code = ''; + var code_div = document.createElement('div'); + code_div.id = 'editor'; + $('#rightpane').append(code_div); + this.editor = ace.edit('editor'); + this.editor.getSession().setMode("ace/mode/python"); + this.editor.gotoLine(1); + this.marker = null; + + this.console = document.createElement('div'); + this.console.id = 'console'; + $('#rightpane').append(this.console); + this.console_height = this.config.console_height; + this.console_stdout = document.createElement('pre'); + this.console_error = document.createElement('pre'); + this.console_stdout.id = 'console_stdout'; + this.console_error.id = 'console_error'; + this.console.appendChild(this.console_stdout); + this.console.appendChild(this.console_error); + $('#console').height(this.console_height); + + this.save_disabled = true; + // If an update of the model from the code editor is allowed + this.update_trigger = true; + // Automatically update the model based on the text + this.auto_update = true; + + // Setup the button to toggle the code editor + $('#Toggle_ace').on('click', function() { + self.toggle_shown(); + }); + $('#Save_file').on('click', function() { + self.save_file(); + }); + $('#Font_increase').on('click', function() { + self.font_size += 1; + }); + $('#Font_decrease').on('click', function() { + self.font_size -= 1; + }); - }); + this.schedule_updates(); - Object.defineProperty(this, 'hidden', { - get: function() { - return self.config.hide_editor; - }, - set: function(val) { - self.config.hide_editor = val; - if (val) { - this.hide_editor(); - } else { - this.show_editor(); + var self = this; + + Object.defineProperty(this, 'width', { + get: function() { + return self.config.editor_width; + }, + set: function(val) { + val = Math.max(Math.min(val, this.max_width), this.min_width); + $('#rightpane').width(val); + self.config.editor_width = val; } - } - }); - - Object.defineProperty(this, 'font_size', { - get: function() { - return self.config.editor_font_size; - }, - set: function(val) { - val = Math.max(val, 6); - this.editor.setFontSize(val); - self.config.editor_font_size = val; - } - }); - - // Automatically update the model based on the text - Object.defineProperty(this, 'auto_update', { - get: function() { - return self.config.auto_update; - }, - set: function(val) { - this.update_trigger = val; - self.config.auto_update = val; - } - }); - - this.width = this.config.editor_width; - this.hidden = this.config.hide_editor; - this.font_size = this.config.editor_font_size; - this.auto_update = this.config.auto_update; - this.redraw(); - - $(window).on('resize', function() {self.on_resize();}); - interact('#editor') - .resizable({ - edges: {left: true, right: false, bottom: false, top: false} - }).on('resizemove', function(event) { - self.width -= event.deltaRect.left; - self.redraw(); - }); - interact('#console') - .resizable({ - edges: {left: true, right: false, bottom: false, top: true} - }).on('resizemove', function(event) { - var max = $('#rightpane').height() - 40; - var min = 20; + }); - self.console_height -= event.deltaRect.top; + Object.defineProperty(this, 'hidden', { + get: function() { + return self.config.hide_editor; + }, + set: function(val) { + self.config.hide_editor = val; + if (val) { + this.hide_editor(); + } else { + this.show_editor(); + } + } + }); - self.console_height = utils.clip(self.console_height, min, max); - $('#console').height(self.console_height); + Object.defineProperty(this, 'font_size', { + get: function() { + return self.config.editor_font_size; + }, + set: function(val) { + val = Math.max(val, 6); + this.editor.setFontSize(val); + self.config.editor_font_size = val; + } + }); - self.width -= event.deltaRect.left; - self.redraw(); - }).on('resizeend', function(event) { - self.config.console_height = self.console_height; + // Automatically update the model based on the text + Object.defineProperty(this, 'auto_update', { + get: function() { + return self.config.auto_update; + }, + set: function(val) { + this.update_trigger = val; + self.config.auto_update = val; + } }); -}; -/** - * Send changes to the code to server every 100ms. - */ -schedule_updates() { - var self = this; - setInterval(function() { - var editor_code = self.editor.getValue(); - if (editor_code != self.current_code) { - if (self.update_trigger) { - self.update_trigger = self.auto_update; - self.ws.send(JSON.stringify({code: editor_code, save: false})); - self.current_code = editor_code; - self.enable_save(); - $('#Sync_editor_button').addClass('disabled'); + this.width = this.config.editor_width; + this.hidden = this.config.hide_editor; + this.font_size = this.config.editor_font_size; + this.auto_update = this.config.auto_update; + this.redraw(); + + $(window).on('resize', function() {self.on_resize();}); + interact('#editor') + .resizable({ + edges: {left: true, right: false, bottom: false, top: false} + }).on('resizemove', function(event) { + self.width -= event.deltaRect.left; + self.redraw(); + }); + + interact('#console') + .resizable({ + edges: {left: true, right: false, bottom: false, top: true} + }).on('resizemove', function(event) { + var max = $('#rightpane').height() - 40; + var min = 20; + + self.console_height -= event.deltaRect.top; + + self.console_height = utils.clip(self.console_height, min, max); + $('#console').height(self.console_height); + + self.width -= event.deltaRect.left; + self.redraw(); + }).on('resizeend', function(event) { + self.config.console_height = self.console_height; + }); + }; + + /** + * Send changes to the code to server every 100ms. + */ + schedule_updates() { + var self = this; + setInterval(function() { + var editor_code = self.editor.getValue(); + if (editor_code != self.current_code) { + if (self.update_trigger) { + self.update_trigger = self.auto_update; + self.ws.send(JSON.stringify({code: editor_code, save: false})); + self.current_code = editor_code; + self.enable_save(); + $('#Sync_editor_button').addClass('disabled'); + } else { + // Visual indication that the code is different + // than the model displayed + $('#Sync_editor_button').removeClass('disabled'); + } + } + }, 100); + }; + + save_file() { + if (!($('#Save_file').hasClass('disabled'))) { + var editor_code = this.editor.getValue(); + this.ws.send(JSON.stringify({code: editor_code, save: true})); + this.disable_save(); + } + }; + + enable_save() { + $('#Save_file').removeClass('disabled'); + }; + + disable_save() { + $('#Save_file').addClass('disabled'); + }; + + on_message(event) { + var msg = JSON.parse(event.data); + if (msg.code !== undefined) { + this.editor.setValue(msg.code); + this.current_code = msg.code; + this.editor.gotoLine(1); + this.redraw(); + this.disable_save(); + } else if (msg.error === null) { + if (this.marker !== null) { + this.editor.getSession().removeMarker(this.marker); + this.marker = null; + this.editor.getSession().clearAnnotations(); + } + $(this.console_stdout).text(msg.stdout); + $(this.console_error).text(''); + this.console.scrollTop = this.console.scrollHeight; + } else if (msg.filename !== undefined) { + if (msg.valid) { + $('#filename')[0].innerHTML = msg.filename; + // Update the URL so reload and bookmarks work as expected + history.pushState({}, msg.filename, '/?filename=' + msg.filename); } else { - // Visual indication that the code is different - // than the model displayed - $('#Sync_editor_button').removeClass('disabled'); + alert(msg.error); } + } else if (msg.error !== undefined) { + var line = msg.error.line; + this.marker = this.editor.getSession() + .addMarker(new Range(line - 1, 0, line - 1, 10), + 'highlight', 'fullLine', true); + this.editor.getSession().setAnnotations([{ + row: line - 1, + type: 'error', + text: msg.short_msg, + }]); + $(this.console_stdout).text(msg.stdout); + $(this.console_error).text(msg.error.trace); + this.console.scrollTop = this.console.scrollHeight; + } else { + console.log(msg); } - }, 100); -}; - -save_file() { - if (!($('#Save_file').hasClass('disabled'))) { - var editor_code = this.editor.getValue(); - this.ws.send(JSON.stringify({code: editor_code, save: true})); - this.disable_save(); - } -}; - -enable_save() { - $('#Save_file').removeClass('disabled'); -}; - -disable_save() { - $('#Save_file').addClass('disabled'); -}; - -on_message(event) { - var msg = JSON.parse(event.data); - if (msg.code !== undefined) { - this.editor.setValue(msg.code); - this.current_code = msg.code; - this.editor.gotoLine(1); - this.redraw(); - this.disable_save(); - } else if (msg.error === null) { - if (this.marker !== null) { - this.editor.getSession().removeMarker(this.marker); - this.marker = null; - this.editor.getSession().clearAnnotations(); + }; + + on_resize() { + this.max_width = $(window).width() - 100; + if (this.width > this.max_width) { + this.width = this.max_width; } - $(this.console_stdout).text(msg.stdout); - $(this.console_error).text(''); - this.console.scrollTop = this.console.scrollHeight; - } else if (msg.filename !== undefined) { - if (msg.valid) { - $('#filename')[0].innerHTML = msg.filename; - // Update the URL so reload and bookmarks work as expected - history.pushState({}, msg.filename, '/?filename=' + msg.filename); + this.redraw(); + }; + + show_editor() { + var editor = document.getElementById('rightpane'); + editor.style.display = 'flex'; + this.redraw(); + }; + + hide_editor() { + var editor = document.getElementById('rightpane'); + editor.style.display = 'none'; + this.redraw(); + }; + + toggle_shown() { + if (this.hidden) { + this.hidden = false; } else { - alert(msg.error); + this.hidden = true; + } + this.redraw(); + }; + + redraw() { + this.editor.resize(); + if (this.netgraph !== undefined) { + this.netgraph.on_resize(); } - } else if (msg.error !== undefined) { - var line = msg.error.line; - this.marker = this.editor.getSession() - .addMarker(new Range(line - 1, 0, line - 1, 10), - 'highlight', 'fullLine', true); - this.editor.getSession().setAnnotations([{ - row: line - 1, - type: 'error', - text: msg.short_msg, - }]); - $(this.console_stdout).text(msg.stdout); - $(this.console_error).text(msg.error.trace); - this.console.scrollTop = this.console.scrollHeight; - } else { - console.log(msg); - } -}; - -on_resize() { - this.max_width = $(window).width() - 100; - if (this.width > this.max_width) { - this.width = this.max_width; - } - this.redraw(); -}; - -show_editor() { - var editor = document.getElementById('rightpane'); - editor.style.display = 'flex'; - this.redraw(); -}; - -hide_editor() { - var editor = document.getElementById('rightpane'); - editor.style.display = 'none'; - this.redraw(); -}; - -toggle_shown() { - if (this.hidden) { - this.hidden = false; - } else { - this.hidden = true; - } - this.redraw(); -}; - -redraw() { - this.editor.resize(); - if (this.netgraph !== undefined) { - this.netgraph.on_resize(); - } - this.viewport.on_resize(); -}; + this.viewport.on_resize(); + }; } diff --git a/nengo_gui/static/hotkeys.ts b/nengo_gui/static/hotkeys.ts index d223bb52..2c803aca 100644 --- a/nengo_gui/static/hotkeys.ts +++ b/nengo_gui/static/hotkeys.ts @@ -5,24 +5,24 @@ */ export default class Hotkeys { -constructor(editor, modal) { - var self = this; + constructor(editor, modal) { + var self = this; - this.active = true; - this.editor = editor; - this.netgraph = this.editor.netgraph; - this.modal = modal; - this.sim = this.modal.sim; + this.active = true; + this.editor = editor; + this.netgraph = this.editor.netgraph; + this.modal = modal; + this.sim = this.modal.sim; - document.addEventListener('keydown', function(ev) { - if (self.active) { + document.addEventListener('keydown', function(ev) { + if (self.active) { - var on_editor = (ev.target.className === 'ace_text-input'); + var on_editor = (ev.target.className === 'ace_text-input'); - if (typeof ev.key != 'undefined') { - var key = ev.key; - } else { - switch (ev.keyCode) { + if (typeof ev.key != 'undefined') { + var key = ev.key; + } else { + switch (ev.keyCode) { case 191: var key = '?'; break; @@ -34,89 +34,89 @@ constructor(editor, modal) { break; default: var key = String.fromCharCode(ev.keyCode); + } } - } - var key = key.toLowerCase(); - var ctrl = ev.ctrlKey || ev.metaKey; + var key = key.toLowerCase(); + var ctrl = ev.ctrlKey || ev.metaKey; - // Toggle editor with ctrl-e - if (ctrl && key == 'e') { - self.editor.toggle_shown(); - ev.preventDefault(); - } - // Undo with ctrl-z - if (ctrl && key == 'z') { - self.netgraph.notify({undo: "1"}); - ev.preventDefault(); - } - // Redo with shift-ctrl-z - if (ctrl && ev.shiftKey && key == 'z') { - self.netgraph.notify({undo: "0"}); - ev.preventDefault(); - } - // Redo with ctrl-y - if (ctrl && key == 'y') { - self.netgraph.notify({undo: "0"}); - ev.preventDefault(); - } - // Save with save-s - if (ctrl && key == 's') { - self.editor.save_file(); - ev.preventDefault(); - } - // Run model with spacebar or with shift-enter - if ((key == ' ' && !on_editor) || + // Toggle editor with ctrl-e + if (ctrl && key == 'e') { + self.editor.toggle_shown(); + ev.preventDefault(); + } + // Undo with ctrl-z + if (ctrl && key == 'z') { + self.netgraph.notify({undo: "1"}); + ev.preventDefault(); + } + // Redo with shift-ctrl-z + if (ctrl && ev.shiftKey && key == 'z') { + self.netgraph.notify({undo: "0"}); + ev.preventDefault(); + } + // Redo with ctrl-y + if (ctrl && key == 'y') { + self.netgraph.notify({undo: "0"}); + ev.preventDefault(); + } + // Save with save-s + if (ctrl && key == 's') { + self.editor.save_file(); + ev.preventDefault(); + } + // Run model with spacebar or with shift-enter + if ((key == ' ' && !on_editor) || (ev.shiftKey && key == 'enter')) { - if (!ev.repeat) { - self.sim.on_pause_click(); + if (!ev.repeat) { + self.sim.on_pause_click(); + } + ev.preventDefault(); + } + // Bring up help menu with ? + if (key == '?' && !on_editor) { + self.callMenu(); + ev.preventDefault(); + } + // Bring up minimap with ctrl-m + if (ctrl && key == 'm') { + self.netgraph.toggleMiniMap(); + ev.preventDefault(); + } + // Disable backspace navigation + if (key == 'backspace' && !on_editor) { + ev.preventDefault(); + } + // Toggle auto-updating with TODO: pick a good shortcut + if (ctrl && ev.shiftKey && key == '1') { + self.editor.auto_update = !self.editor.auto_update; + self.editor.update_trigger = self.editor.auto_update; + ev.preventDefault(); + } + // Trigger a single update with TODO: pick a good shortcut + if (ctrl && !ev.shiftKey && key == '1') { + self.editor.update_trigger = true; + ev.preventDefault(); } - ev.preventDefault(); - } - // Bring up help menu with ? - if (key == '?' && !on_editor) { - self.callMenu(); - ev.preventDefault(); - } - // Bring up minimap with ctrl-m - if (ctrl && key == 'm') { - self.netgraph.toggleMiniMap(); - ev.preventDefault(); - } - // Disable backspace navigation - if (key == 'backspace' && !on_editor) { - ev.preventDefault(); - } - // Toggle auto-updating with TODO: pick a good shortcut - if (ctrl && ev.shiftKey && key == '1') { - self.editor.auto_update = !self.editor.auto_update; - self.editor.update_trigger = self.editor.auto_update; - ev.preventDefault(); - } - // Trigger a single update with TODO: pick a good shortcut - if (ctrl && !ev.shiftKey && key == '1') { - self.editor.update_trigger = true; - ev.preventDefault(); } - } - }); -}; + }); + }; -callMenu() { - this.modal.title("Hotkeys list"); - this.modal.footer('close'); - this.modal.help_body(); - this.modal.show(); -}; + callMenu() { + this.modal.title("Hotkeys list"); + this.modal.footer('close'); + this.modal.help_body(); + this.modal.show(); + }; -/** - * Turn hotkeys on or off. - * - * set_active is provided with a boolean argument, which will either - * turn the hotkeys on or off. - */ -set_active(bool) { - console.assert(typeof(bool) == 'boolean'); - this.active = bool; -}; + /** + * Turn hotkeys on or off. + * + * set_active is provided with a boolean argument, which will either + * turn the hotkeys on or off. + */ + set_active(bool) { + console.assert(typeof(bool) == 'boolean'); + this.active = bool; + }; } diff --git a/nengo_gui/static/menu.ts b/nengo_gui/static/menu.ts index f0e78429..3d70ee96 100644 --- a/nengo_gui/static/menu.ts +++ b/nengo_gui/static/menu.ts @@ -32,101 +32,101 @@ export function hide_any() { export class Menu { -constructor(div) { - this.visible = false; // Whether it's currently visible - this.div = div; // The parent div - this.menu_div = null; // The div for the menu itself - this.actions = null; // The current action list for the menu -}; - -/** - * Show this menu at the given (x,y) location. - * - * Automatically hides any menu that's in the same div - * Called by a listener from netgraph.js - */ -show(x, y, items) { - hide_menu_in(this.div); - - if (items.length == 0) { - return; - } - - // TODO: move this to the constructor - this.menu_div = document.createElement('div'); - this.menu_div.style.position = 'fixed'; - this.menu_div.style.left = x; - this.menu_div.style.top = y; - this.menu_div.style.zIndex = utils.next_zindex(); - - this.menu = document.createElement('ul'); - this.menu.className = 'dropdown-menu'; - this.menu.style.position = 'absolute'; - this.menu.style.display = 'block'; - this.menu.role = 'menu'; - - this.menu_div.appendChild(this.menu); - this.div.appendChild(this.menu_div); - - this.actions = {}; - - var self = this; - for (var i in items) { - var item = items[i]; - var b = document.createElement('li'); - var a = document.createElement('a'); - a.setAttribute('href', '#'); - a.className = 'menu-item'; - a.innerHTML = item[0]; - a.func = item[1]; - this.actions[a] = item[1]; - $(a).click(function(e) { - e.target.func(); - self.hide(); - }) - .on('contextmenu', function(e) { - e.preventDefault(); - e.target.func(); - self.hide(); - }); - b.appendChild(a); - this.menu.appendChild(b); - } - this.visible = true; - this.check_overflow(x, y); - visible_menus[this.div] = this; -}; - -/** - * Hide this menu. - */ -hide() { - this.div.removeChild(this.menu_div); - this.visible = false; - - this.menu_div = null; - delete visible_menus[this.div]; -}; - -visible_any() { - return visible_menus[this.div] !== undefined; -}; - -check_overflow(x, y) { - var corrected_y = y - $(toolbar.toolbar).height(); - var h = $(this.menu).outerHeight(); - var w = $(this.menu).outerWidth(); - - var main_h = $('#main').height(); - var main_w = $('#main').width(); - - if (corrected_y + h > main_h) { - this.menu_div.style.top = y - h; - } - - if (x + w > main_w) { - this.menu_div.style.left = main_w - w; - } -}; + constructor(div) { + this.visible = false; // Whether it's currently visible + this.div = div; // The parent div + this.menu_div = null; // The div for the menu itself + this.actions = null; // The current action list for the menu + }; + + /** + * Show this menu at the given (x,y) location. + * + * Automatically hides any menu that's in the same div + * Called by a listener from netgraph.js + */ + show(x, y, items) { + hide_menu_in(this.div); + + if (items.length == 0) { + return; + } + + // TODO: move this to the constructor + this.menu_div = document.createElement('div'); + this.menu_div.style.position = 'fixed'; + this.menu_div.style.left = x; + this.menu_div.style.top = y; + this.menu_div.style.zIndex = utils.next_zindex(); + + this.menu = document.createElement('ul'); + this.menu.className = 'dropdown-menu'; + this.menu.style.position = 'absolute'; + this.menu.style.display = 'block'; + this.menu.role = 'menu'; + + this.menu_div.appendChild(this.menu); + this.div.appendChild(this.menu_div); + + this.actions = {}; + + var self = this; + for (var i in items) { + var item = items[i]; + var b = document.createElement('li'); + var a = document.createElement('a'); + a.setAttribute('href', '#'); + a.className = 'menu-item'; + a.innerHTML = item[0]; + a.func = item[1]; + this.actions[a] = item[1]; + $(a).click(function(e) { + e.target.func(); + self.hide(); + }) + .on('contextmenu', function(e) { + e.preventDefault(); + e.target.func(); + self.hide(); + }); + b.appendChild(a); + this.menu.appendChild(b); + } + this.visible = true; + this.check_overflow(x, y); + visible_menus[this.div] = this; + }; + + /** + * Hide this menu. + */ + hide() { + this.div.removeChild(this.menu_div); + this.visible = false; + + this.menu_div = null; + delete visible_menus[this.div]; + }; + + visible_any() { + return visible_menus[this.div] !== undefined; + }; + + check_overflow(x, y) { + var corrected_y = y - $(toolbar.toolbar).height(); + var h = $(this.menu).outerHeight(); + var w = $(this.menu).outerWidth(); + + var main_h = $('#main').height(); + var main_w = $('#main').width(); + + if (corrected_y + h > main_h) { + this.menu_div.style.top = y - h; + } + + if (x + w > main_w) { + this.menu_div.style.left = main_w - w; + } + }; } diff --git a/nengo_gui/static/modal.ts b/nengo_gui/static/modal.ts index 3c9180cf..3fdd5a1e 100644 --- a/nengo_gui/static/modal.ts +++ b/nengo_gui/static/modal.ts @@ -9,814 +9,814 @@ import * as utils from "./utils"; export default class Modal { -constructor($div, editor, sim) { - var self = this; - this.$div = $div; - this.$title = this.$div.find('.modal-title').first(); - this.$footer = this.$div.find('.modal-footer').first(); - this.$body = this.$div.find('.modal-body').first(); - this.editor = editor; - this.sim = sim; - this.netgraph = this.editor.netgraph; - this.config = this.netgraph.config; - this.hotkeys = new Hotkeys(this.editor, this); - - this.sim_was_running = false; - - // This listener is triggered when the modal is closed - this.$div.on('hidden.bs.modal', function() { - if (self.sim_was_running) { - self.sim.play(); - } - self.hotkeys.set_active(true); - }); -}; - -show() { - this.hotkeys.set_active(false); - this.sim_was_running = !this.sim.paused; - this.$div.modal('show'); - this.sim.pause(); -}; - -title(title) { - this.$title.text(title); -}; - -footer(type, ok_function, cancel_function) { - var self = this; - this.$footer.empty(); - - if (type === "close") { - this.$footer.append(''); - } else if (type === "ok_cancel") { - var $footerBtn = $('
').appendTo(this.$footer); - $footerBtn.append(''); - $footerBtn.append(''); - $('#OK').on('click', ok_function); - if (typeof cancel_function !== 'undefined') { - $('#cancel-button').on('click', cancel_function); - } else { - $('#cancel-button').on('click', function() { - $('#cancel-button').attr('data-dismiss', 'modal'); + constructor($div, editor, sim) { + var self = this; + this.$div = $div; + this.$title = this.$div.find('.modal-title').first(); + this.$footer = this.$div.find('.modal-footer').first(); + this.$body = this.$div.find('.modal-body').first(); + this.editor = editor; + this.sim = sim; + this.netgraph = this.editor.netgraph; + this.config = this.netgraph.config; + this.hotkeys = new Hotkeys(this.editor, this); + + this.sim_was_running = false; + + // This listener is triggered when the modal is closed + this.$div.on('hidden.bs.modal', function() { + if (self.sim_was_running) { + self.sim.play(); + } + self.hotkeys.set_active(true); + }); + }; + + show() { + this.hotkeys.set_active(false); + this.sim_was_running = !this.sim.paused; + this.$div.modal('show'); + this.sim.pause(); + }; + + title(title) { + this.$title.text(title); + }; + + footer(type, ok_function, cancel_function) { + var self = this; + this.$footer.empty(); + + if (type === "close") { + this.$footer.append(''); + } else if (type === "ok_cancel") { + var $footerBtn = $('
').appendTo(this.$footer); + $footerBtn.append(''); + $footerBtn.append(''); + $('#OK').on('click', ok_function); + if (typeof cancel_function !== 'undefined') { + $('#cancel-button').on('click', cancel_function); + } else { + $('#cancel-button').on('click', function() { + $('#cancel-button').attr('data-dismiss', 'modal'); + }); + } + } else if (type === 'confirm_reset') { + this.$footer.append(''); + this.$footer.append(''); + $('#confirm_reset_button').on('click', function() { + self.toolbar.reset_model_layout(); + }); + } else if (type === 'confirm_savepdf') { + this.$footer.append( + ''); + this.$footer.append(''); + $('#confirm_savepdf_button').on('click', function() { + var svg = $("#main svg")[0]; + + // Serialize SVG as XML + var svg_xml = (new XMLSerializer).serializeToString(svg); + var source = '' + svg_xml; + source = source.replace("<", "<"); + source = source.replace(">", ">"); + + var svg_uri = 'data:image/svg+xml;base64,' + btoa(source); + + // Extract filename from the path + var path = $("#filename")[0].textContent; + var filename = path.split('/').pop(); + filename = filename.split('.')[0]; + + // Initiate download + var link = document.createElement("a"); + link.download = filename + ".svg"; + link.href = svg_uri; + + // Adding element to the DOM (needed for Firefox) + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); + } else if (type === 'confirm_savecsv') { + this.$footer.append( + ''); + this.$footer.append(''); + $('#confirm_savecsv_button').on('click', function() { + + var data_items = all_components; + var CSV = data_to_csv(data_items); + // Extract filename from the path + var path = $("#filename")[0].textContent; + var filename = path.split('/').pop(); + filename = filename.split('.')[0]; + + var uri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(CSV); + + var link = document.createElement("a"); + link.href = uri; + link.style = "visibility:hidden"; + // Adding element to the DOM (needed for Firefox) + link.download = filename + ".csv"; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }); + + } else if (type === 'refresh') { + this.$footer.append(''); + $('#refresh_button').on('click', function() { + location.reload(); }); + } else { + console.warn('Modal footer type ' + type + ' not recognized.'); } - } else if (type === 'confirm_reset') { - this.$footer.append(''); - this.$footer.append(''); - $('#confirm_reset_button').on('click', function() { - self.toolbar.reset_model_layout(); - }); - } else if (type === 'confirm_savepdf') { - this.$footer.append( - ''); - this.$footer.append(''); - $('#confirm_savepdf_button').on('click', function() { - var svg = $("#main svg")[0]; - - // Serialize SVG as XML - var svg_xml = (new XMLSerializer).serializeToString(svg); - var source = '' + svg_xml; - source = source.replace("<", "<"); - source = source.replace(">", ">"); - - var svg_uri = 'data:image/svg+xml;base64,' + btoa(source); - - // Extract filename from the path - var path = $("#filename")[0].textContent; - var filename = path.split('/').pop(); - filename = filename.split('.')[0]; - - // Initiate download - var link = document.createElement("a"); - link.download = filename + ".svg"; - link.href = svg_uri; - - // Adding element to the DOM (needed for Firefox) - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); - } else if (type === 'confirm_savecsv') { - this.$footer.append( - ''); - this.$footer.append(''); - $('#confirm_savecsv_button').on('click', function() { - - var data_items = all_components; - var CSV = data_to_csv(data_items); - // Extract filename from the path - var path = $("#filename")[0].textContent; - var filename = path.split('/').pop(); - filename = filename.split('.')[0]; - - var uri = 'data:text/csv;charset=utf-8,' + encodeURIComponent(CSV); - - var link = document.createElement("a"); - link.href = uri; - link.style = "visibility:hidden"; - // Adding element to the DOM (needed for Firefox) - link.download = filename + ".csv"; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - }); + }; - } else if (type === 'refresh') { - this.$footer.append(''); - $('#refresh_button').on('click', function() { - location.reload(); - }); - } else { - console.warn('Modal footer type ' + type + ' not recognized.'); - } -}; - -clear_body() { - this.$body.empty(); - this.$div.find('.modal-dialog').removeClass('modal-sm'); - this.$div.off('shown.bs.modal'); -}; - -text_body(text, type) { - if (typeof type === 'undefined') { type = "info"; } - - this.clear_body(); - var $alert = $('