diff --git a/nengo_gui/components/__init__.py b/nengo_gui/components/__init__.py index 0c4defb3..4823b0d2 100644 --- a/nengo_gui/components/__init__.py +++ b/nengo_gui/components/__init__.py @@ -12,6 +12,7 @@ from .spa_similarity import SpaSimilarity from .htmlview import HTMLView from .spike_grid import SpikeGrid +from .bg_plot import BGPlot # Old versions of the .cfg files used Templates which had slightly different # names than the Components currently use. This code allows us to diff --git a/nengo_gui/components/bg_plot.py b/nengo_gui/components/bg_plot.py new file mode 100644 index 00000000..89ad03fb --- /dev/null +++ b/nengo_gui/components/bg_plot.py @@ -0,0 +1,59 @@ +import struct + +import nengo +import numpy as np + +from nengo_gui.components.value import Value + + +class BGPlot(Value): + """The server-side system for the SPA Basal Ganglia plot.""" + + # the parameters to be stored in the .cfg file + config_defaults = Value.config_defaults + config_defaults["palette_index"] = 1 + config_defaults["show_legend"] = True + + def __init__(self, obj, **kwargs): + args = kwargs["args"] + super(BGPlot, self).__init__(obj, args["n_lines"]) + + # default legends to show + self.def_legend_labels = args["legend_labels"] + + # the item to connect to + self.probe_target = args["probe_target"] + + self.label = "bg " + self.probe_target + + def attach(self, page, config, uid): + super(Value, self).attach(page, config, uid) + + def add_nengo_objects(self, page): + # create a Node and a Connection so the Node will be given the + # data we want to show while the model is running. + with page.model: + self.node = nengo.Node(self.gather_data, + size_in=self.n_lines) + if self.probe_target == "input": + self.conn = nengo.Connection(self.obj.input, self.node, synapse=0.01) + else: + self.conn = nengo.Connection(self.obj.output, self.node, synapse=0.01) + + def javascript(self): + # generate the javascript that will create the client-side object + info = dict(uid=id(self), label=self.label, + n_lines=self.n_lines, synapse=0) + + if getattr(self.config, "legend_labels") == []: + self.config.legend_labels = self.def_legend_labels + + json = self.javascript_config(info) + return 'new Nengo.Value(main, sim, %s);' % json + + def code_python_args(self, uids): + return [ + uids[self.obj], + ' args=dict(n_lines=%s, legend_labels=%s, probe_target="%s")' + % (self.n_lines, self.config.legend_labels, self.probe_target,) + ] diff --git a/nengo_gui/components/netgraph.py b/nengo_gui/components/netgraph.py index 02e57672..db7c3be8 100644 --- a/nengo_gui/components/netgraph.py +++ b/nengo_gui/components/netgraph.py @@ -531,6 +531,15 @@ def get_extra_info(self, obj): elif isinstance(obj, nengo.Ensemble): info['dimensions'] = int(obj.size_out) info['n_neurons'] = int(obj.n_neurons) + # TODO: Add the same functionality for the BasalGanglia non-spa net + elif isinstance(obj, spa.BasalGanglia): + info['bg_inputs'] = obj.input.size_in + info['input_labels'] = [] + for ac in obj.actions.actions: + if ac.name == None: + info['input_labels'].append(ac.condition.expression.__str__()) + else: + info['input_labels'].append(ac.name) elif Value.default_output(obj) is not None: info['default_output'] = True diff --git a/nengo_gui/components/spa_similarity.py b/nengo_gui/components/spa_similarity.py index eb8831dd..a2d2cbed 100644 --- a/nengo_gui/components/spa_similarity.py +++ b/nengo_gui/components/spa_similarity.py @@ -8,9 +8,8 @@ class SpaSimilarity(SpaPlot): """Line graph showing semantic pointer decoded values over time""" - config_defaults = dict(max_value=1, - min_value=-1, - show_pairs=False, + config_defaults = dict(max_value=1.5, min_value=-1.5, + palette_index=1, show_pairs=False, **Component.config_defaults) def __init__(self, obj, **kwargs): @@ -80,8 +79,7 @@ def update_legend(self, vocab): def javascript(self): """Generate the javascript that will create the client-side object""" info = dict(uid=id(self), label=self.label, n_lines=len(self.labels), - synapse=0, min_value=-1.5, max_value=1.5, - pointer_labels=self.labels) + synapse=0, pointer_labels=self.labels) json = self.javascript_config(info) return 'new Nengo.SpaSimilarity(main, sim, %s);' % json diff --git a/nengo_gui/components/value.py b/nengo_gui/components/value.py index b74363b1..2016ed28 100644 --- a/nengo_gui/components/value.py +++ b/nengo_gui/components/value.py @@ -11,6 +11,7 @@ class Value(Component): # the parameters to be stored in the .cfg file config_defaults = dict(max_value=1, min_value=-1, + show_legend=False, legend_labels=[], palette_index=0, **Component.config_defaults) def __init__(self, obj): diff --git a/nengo_gui/static/color.js b/nengo_gui/static/color.js new file mode 100644 index 00000000..026638d5 --- /dev/null +++ b/nengo_gui/static/color.js @@ -0,0 +1,45 @@ +/** + * Generate a color sequence of a given length. + */ + +/** + * Generate a color sequence of a given length. + * + * Colors are defined using a color blind-friendly palette. + */ +Nengo.make_colors = function(N) { + // Color blind palette with blue, green, red, magenta, yellow, cyan + var palette = ["#1c73b3", "#039f74", "#d65e00", "#cd79a7", "#f0e542", "#56b4ea"]; + var c = []; + + for (var i = 0; i < N; i++) { + c.push(palette[i % palette.length]); + } + return c; +} + +/** + * Color blind-friendly palette. + */ +Nengo.default_colors = function() { + // Color blind palette with blue, green, red, magenta, yellow, cyan + var palette = ["#1c73b3", "#039f74", "#d65e00", "#cd79a7", "#f0e542", "#56b4ea"]; + return function(i){ return palette[i%palette.length] }; +} + +/** + * Color palette use by Google for graphics, trends, etc... + */ +Nengo.google_colors = function() { + var palette = ["#3366cc", "#dc3912", "#ff9900", "#109618", "#990099", "#0099c6", "#dd4477", "#66aa00", "#b82e2e", "#316395", "#994499", "#22aa99", "#aaaa11", "#6633cc", "#e67300", "#8b0707", "#651067", "#329262", "#5574a6", "#3b3eac"]; + return function(i){ return palette[i%palette.length] }; +} + +/** list of valid color choices */ +Nengo.color_choices = [ + ["Nengo Color-Blind Friendly (6 colors)", {"func":Nengo.default_colors(), "mod":6}], + ["Google (20 colors)", {"func":Nengo.google_colors(), "mod":20}], + ["D3.js A (20 colors)", {"func":d3.scale.category20(), "mod":20}], + ["D3.js B (20 colors)", {"func":d3.scale.category20b(), "mod":20}], + ["D3.js C (20 colors)", {"func":d3.scale.category20c(), "mod":20}] +]; diff --git a/nengo_gui/static/components/netgraph_item.js b/nengo_gui/static/components/netgraph_item.js index 8cdf84e2..3120e8fa 100644 --- a/nengo_gui/static/components/netgraph_item.js +++ b/nengo_gui/static/components/netgraph_item.js @@ -35,6 +35,12 @@ Nengo.NetGraphItem = function(ng, info, minimap, mini_item) { this.g_items = ng.g_items_mini; } + // SPA network specific parameter + this.sp_targets = info.sp_targets; + // Basal Ganglia network specific parameters + this.bg_inputs = info.bg_inputs; + this.input_labels = info.input_labels; + /** if this is a network, the children list is the set of NetGraphItems * and NetGraphConnections that are inside this network */ this.children = []; @@ -367,6 +373,22 @@ Nengo.NetGraphItem.prototype.generate_menu = function () { items.push(['Semantic pointer plot', function() {self.create_graph('SpaSimilarity', self.sp_targets[0]);}]); } + if (this.bg_inputs) { + items.push(['Input Plot', + function () { + self.create_graph('BGPlot', + {"n_lines":self.bg_inputs, "legend_labels":self.input_labels, "probe_target":"input"} + ); + } + ]); + items.push(['Output Plot', + function () { + self.create_graph('BGPlot', + {"n_lines":self.bg_inputs, "legend_labels":self.input_labels, "probe_target":"output"} + ); + } + ]); + } items.push(['Details ...', function() {self.create_modal();}]); return items; }; diff --git a/nengo_gui/static/components/spa_similarity.js b/nengo_gui/static/components/spa_similarity.js index 36633d5f..ecdc5c1e 100644 --- a/nengo_gui/static/components/spa_similarity.js +++ b/nengo_gui/static/components/spa_similarity.js @@ -17,17 +17,14 @@ Nengo.SpaSimilarity = function(parent, sim, args) { var self = this; - this.colors = Nengo.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.pointer_labels = args.pointer_labels; + 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 = Nengo.draw_legend(this.legend, args.pointer_labels, this.color_func); + this.legend_svg = Nengo.draw_legend(this.legend, this.legend_labels, this.color_func, this.uid); }; Nengo.SpaSimilarity.prototype = Object.create(Nengo.Value.prototype); @@ -42,10 +39,10 @@ Nengo.SpaSimilarity.prototype.reset_legend_and_data = function(new_labels){ while(this.legend.lastChild){ this.legend.removeChild(this.legend.lastChild); } - this.legend_svg = d3.select(this.legend).append("svg"); + this.legend_svg = d3.select(this.legend).append("svg").attr("id", "id"+this.uid); // redraw all the legends if they exist - this.pointer_labels = []; + this.legend_labels = []; if(new_labels[0] != ""){ this.update_legend(new_labels); } @@ -71,16 +68,16 @@ Nengo.SpaSimilarity.prototype.data_msg = function(push_data){ Nengo.SpaSimilarity.prototype.update_legend = function(new_labels){ var self = this; - this.pointer_labels = this.pointer_labels.concat(new_labels); + 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.pointer_labels.length); + this.legend_svg.attr("height", 20 * this.legend_labels.length); // Data join - var recs = this.legend_svg.selectAll("rect").data(this.pointer_labels); - var legend_labels = this.legend_svg.selectAll(".legend-label").data(this.pointer_labels); - var val_texts = this.legend_svg.selectAll(".val").data(this.pointer_labels); + 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") @@ -88,18 +85,18 @@ Nengo.SpaSimilarity.prototype.update_legend = function(new_labels){ .attr("y", function(d, i){ return i * 20;}) .attr("width", 10) .attr("height", 10) - .style("fill", this.color_func); + .style("fill", function(d, i) {return self.color_func(i)}); 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.pointer_labels[i]; + return self.legend_labels[i]; }); // expand the width of the svg of the longest string - var label_list = $(".legend-label").toArray(); + var label_list = $("#id"+this.uid+" .legend-label").toArray(); var longest_label = label_list.sort( function (a, b) { return b.getBBox().width - a.getBBox().width; } )[0]; @@ -152,7 +149,7 @@ Nengo.SpaSimilarity.prototype.update = function() { this.path.enter() .append('path') .attr('class', 'line') - .style('stroke', this.color_func) + .style('stroke', function(d, i) {return self.color_func(i)}) .attr('d', self.line); // remove any lines that aren't needed anymore this.path.exit().remove(); @@ -166,7 +163,7 @@ Nengo.SpaSimilarity.prototype.update = function() { } // update the text in the legend - var texts = this.legend_svg.selectAll(".val").data(this.pointer_labels); + var texts = this.legend_svg.selectAll(".val").data(this.legend_labels); texts.html(function(d, i) { var sign = ''; @@ -190,10 +187,60 @@ Nengo.SpaSimilarity.prototype.generate_menu = function() { items.push(['Show pairs', function() {self.set_show_pairs(true);}]); } + items.push(['Change color palette', function() {self.set_color_func();}]) + // add the parent's menu items to this return $.merge(items, Nengo.Component.prototype.generate_menu.call(this)); }; +Nengo.SpaSimilarity.prototype.set_color_func = function() { + var self = this; + + Nengo.modal.clear_body(); + // TODO: Let the user define their own palette + Nengo.modal.title('Choose a palette'); + + // Create a radio button form with the available palettes + var body = Nengo.modal.$body; + var radio_html = "
"; + radio_html += ""+Nengo.color_choices[0][0]+"
" + for (i = 1; i < Nengo.color_choices.length; i++) { + radio_html += ""+Nengo.color_choices[i][0]+"
" + } + radio_html += "
"; + body.append(radio_html); + + // TODO: Make this thing easier to select for the user + Nengo.modal.footer('ok_cancel', function(e) { + self.palette_index = Number($("#palette input:radio[name='pal']:checked").val()); + self.color_func = Nengo.color_choices[self.palette_index][1]["func"]; + + self.path.style('stroke', function(d, i){return self.color_func(i)}); + self.legend_svg.selectAll("rect").remove(); + var recs = self.legend_svg.selectAll("rect").data(self.legend_labels); + recs.enter() + .append("rect") + .attr("x", 0) + .attr("y", function(d, i){ return i * 20;}) + .attr("width", 10) + .attr("height", 10) + .style("fill", function(d, i) {return self.color_func(i)}); + self.save_layout(); + $('#OK').attr('data-dismiss', 'modal'); + }); + + // allow "Enter" keypress + $("#palette").keypress(function(event) { + if (event.which == 13) { + event.preventDefault(); + $('#OK').click(); + } + }); + + Nengo.modal.show(); +} + + Nengo.SpaSimilarity.prototype.set_show_pairs = function(value) { if (this.show_pairs !== value) { this.show_pairs = value; diff --git a/nengo_gui/static/components/value.js b/nengo_gui/static/components/value.js index 97073b0a..1414af22 100644 --- a/nengo_gui/static/components/value.js +++ b/nengo_gui/static/components/value.js @@ -46,11 +46,14 @@ Nengo.Value = function(parent, sim, args) { this.path = this.axes2d.svg.append("g").selectAll('path') .data(this.data_store.data); - this.colors = Nengo.make_colors(this.n_lines); + // create the color function + this.palette_index = args.palette_index || 0; + this.color_func = Nengo.color_choices[this.palette_index][1]["func"]; + this.path.enter() .append('path') .attr('class', 'line') - .style('stroke', function(d, i) {return self.colors[i];}); + .style('stroke', function(d, i){return self.color_func(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 @@ -114,6 +117,25 @@ Nengo.Value = function(parent, sim, args) { 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.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 an array with temporary labels + for (var i=0; i"+Nengo.color_choices[0][0]+"
" + for (i = 1; i < Nengo.color_choices.length; i++) { + radio_html += ""+Nengo.color_choices[i][0]+"
" + } + radio_html += ""; + body.append(radio_html); + + // TODO: Make this thing easier to select for the user + Nengo.modal.footer('ok_cancel', function(e) { + self.palette_index = Number($("#palette input:radio[name='pal']:checked").val()); + self.color_func = Nengo.color_choices[self.palette_index][1]["func"]; + + self.path.style('stroke', function(d, i){return self.color_func(i)}); + if(self.show_legend === true){ + self.clear_legend(); + Nengo.draw_legend(self.legend, self.legend_labels, self.color_func, self.uid); + } + self.save_layout(); + $('#OK').attr('data-dismiss', 'modal'); + }); + + // allow "Enter" keypress + $("#palette").keypress(function(event) { + if (event.which == 13) { + event.preventDefault(); + $('#OK').click(); + } + }); + + Nengo.modal.show(); +} + +Nengo.Value.prototype.set_show_legend = function(value){ + if (this.show_legend !== value) { + this.show_legend = value; + this.save_layout(); + + if (this.show_legend == true) { + Nengo.draw_legend(this.legend, this.legend_labels, this.color_func, this.uid); + } else { + // delete the legend's children + this.clear_legend(); + } + } +} + +Nengo.Value.prototype.clear_legend = function() { + while(this.legend.lastChild){ + this.legend.removeChild(this.legend.lastChild); + } +} + +Nengo.Value.prototype.set_legend_labels = function() { + var self = this; + + Nengo.modal.title('Enter comma seperated legend label values'); + Nengo.modal.single_input_body('Legend label', 'New value'); + Nengo.modal.footer('ok_cancel', function(e) { + var label_csv = $('#singleInput').val(); + var modal = $('#myModalForm').data('bs.validator'); + + // No validation to do. + // Blank string mean do nothing + // Long strings okay + // Excissive entries get ignored + // Missing entries get replaced by default value + // Empty entries assumed to be indication to skip modification + // TODO: Allow escaping of commas + if ((label_csv !== null) && (label_csv !== '')) { + labels = label_csv.split(','); + + for (var i=0; i +