UNPKG

dc.graph

Version:

Graph visualizations integrated with crossfilter and dc.js

881 lines (844 loc) 31.4 kB
var data_stats; var cb_colors = colorbrewer.Paired[12]; cb_colors[5] = cb_colors[11]; // arbitrary assigning of shapes as POC var shapes = ['invtrapezium', 'ellipse', 'diamond', 'trapezium', 'pentagon', 'hexagon', 'egg', 'parallelogram', 'septagon', 'square', 'triangle', 'invtriangle'], curr_shape = 0, shape_map = {}; function show_stats(data_stats, layout_stats) { $('#shown-nodes').html('' + (layout_stats.nnodes || 0) + '/' + (data_stats.totnodes || 0)); $('#shown-edges').html('' + (layout_stats.nedges || 0) + '/' + (data_stats.totedges || 0)); $('#time-last').html('' + ((runner.lastTime() || 0)/1000).toFixed(3)); $('#time-avg').html('' + ((runner.avgTime() || 0)/1000).toFixed(3)); } function show_start() { $('#run-indicator').show(); } function show_stop() { $('#run-indicator').hide(); } function toggle_stats() { var val = !tracker.vals.stats; toggle_stats.callback(val); } function toggle_options() { var val = !tracker.vals.options; toggle_options.callback(val); } function apply_heading(link, section) { return function(val) { $(link).text(val ? 'hide' : 'show'); $(section).toggle(val); }; } function choose_view(view) { var types; switch(view) { case 'all': types = null; break; case 'user': types = [['FS', 'HYP', 'NET', 'OTHER', 'PRT', 'RTR', 'U', 'VM']]; break; case 'image': types = [['FS', 'HYP', 'IMG', 'NET', 'OTHER', 'PRT', 'RTR', 'VM']]; break; } osTypeSelect.replaceFilter(types); osTypeSelect.redrawGroup(); } var options = { server: { default: '' }, statserv: { default: '' }, histserv: { default: '' }, tenant: { default: '', selector: '#tenant-select' }, interval: { default: 5000 }, transition: { default: 2000 }, delete_delay: { default: 0, query: 'ddelay', exert: function(val, diagram) { diagram.deleteDelay(val); } }, date: { default: '' }, play: { default: false }, slow_transition: { default: 15000, query: 'slow' }, staged_transitions: { default: 'none', query: 'stage', selector: '#stage-transitions', exert: function(val, diagram) { diagram.stageTransitions(val); } }, stats: { default: false, subscribe: function(k) { toggle_stats.callback = k; }, exert: apply_heading('#show-stats', '#graph-stats') }, options: { default: false, subscribe: function(k) { toggle_options.callback = k; }, exert: apply_heading('#show-options', '#options') }, timeLimit: { default: 750, query: 'limit', exert: function(val, diagram, filters) { diagram.timeLimit(val); } }, ostype_select: { default: [], query: 'ostype', set: function(val) { osTypeSelect.filter(val); }, subscribe: function(k) { osTypeSelect.on('filtered', function() { var filters = osTypeSelect.filters(); vizgemsDiagram.legend() && vizgemsDiagram.legend() .replaceFilter([filters]) .redraw(); k(filters); }); }, dont_exert_after_subscribe: true, exert: function(val, diagram, filters) { if(filters.filterOSTypes) { osTypeSelect .dimension(filters.filterOSTypes) .group(filters.filterOSTypes.group()) .replaceFilter([val]); } } }, show_steps: { default: false, query: 'steps', selector: '#show-steps', needs_redraw: false, exert: function(val, diagram, filters) { diagram.showLayoutSteps(val); } }, show_arrows: { default: false, query: 'arrows', selector: '#show-arrows', needs_redraw: true, exert: function(val, diagram, filters) { diagram.edgeArrowhead(val ? 'vee' : null); } }, highlight_neighbors: { default: false, query: 'neighbors', selector: '#highlight-neighbors', needs_redraw: true, exert: function() { var highlighter = dc_graph.highlight_neighbors({edgeStroke: 'orangered', edgeStrokeWidth: 3}); return function(val, diagram) { diagram.child('highlight-neighbors', val ? highlighter : null); }; }() }, disconnected: { default: true }, use_colors: { default: true, query: 'usecolor', selector: '#use-colors', needs_redraw: true, exert: function(val, diagram, filters) { if(val) { diagram .nodeStrokeWidth(function(kv) { return kv.key === selected_node ? 5 : 0; }) .nodeFillScale(d3.scale.ordinal().domain(_.keys(ostypes)).range(cb_colors)) .nodeFill(function(kv) { return kv.value.ostype; }) .nodeLabelFill(function(n) { var rgb = d3.rgb(diagram.nodeFillScale()(diagram.nodeFill()(n))), // https://www.w3.org/TR/AERT#color-contrast brightness = (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000; return brightness > 127 ? 'black' : 'ghostwhite'; }); } else { diagram .nodeStrokeWidth(1) .nodeFillScale(null) .nodeFill('white') .nodeLabelFill('black'); } } }, use_shapes: { default: true, query: 'useshape', selector: '#use-shapes', needs_relayout: true, needs_redraw: true, exert: function(val, diagram, filters) { if(val) { diagram .nodeShape(function(kv) { var t = kv.value.ostype; if(!shape_map[t]) { shape_map[t] = shapes[curr_shape]; curr_shape = (curr_shape+1)%shapes.length; } return {shape: shape_map[t]}; }); } else { diagram .nodeShape({shape: 'ellipse'}); } } }, layout_unchanged: { default: false, query: 'unchanged', selector: '#layout-unchanged', needs_redraw: false, exert: function(val, diagram, filters) { diagram.layoutUnchanged(val); } }, direct_vm: { default: false, query: 'vmlayout', selector: '#layout-vms', needs_relayout: true, exert: function(val, diagram) { diagram.constrain(val ? vm_constraints : function() { return []; }); } }, flow_direction: { default: "x", query: 'flow', selector: '#flow-direction', needs_relayout: true, exert: function(val, diagram, filters) { var modf; switch(val) { case 'x': diagram.layoutEngine().flowLayout({axis: 'x', minSeparation: 200}); break; case 'y': diagram.layoutEngine().flowLayout({axis: 'y', minSeparation: 200}); break; case 'none': diagram.layoutEngine().flowLayout(null); break; default: throw new Error('unknown flow direction ' + val); } } }, node_limit: { default: 0, query: 'nlimit' }, fit_labels: { default: true, selector: '#fit-labels', needs_relayout: true, query: 'fit' } }; var osTypeSelect = dc.selectMenu('#ostype-select', 'network'); var filters = {}; var vizgemsDiagram = dc_graph.diagram('#graph', 'network'); var timeline = timeline('#timeline'); var node_inv = null, edge_inv = null; var tracker = sync_url_options(options, dcgraph_domain(vizgemsDiagram, 'network'), vizgemsDiagram, filters); var is_running = tracker.vals.play; function display_running() { $('#play-button i').attr('class', is_running ? 'fas fa-pause' : 'fas fa-play'); } display_running(); $('#play-button').click(function(e) { if(e.shiftKey) vizgemsDiagram.transitionDuration(tracker.vals.slow_transition); else vizgemsDiagram.transitionDuration(tracker.vals.transition); if(is_running) { runner.pause(); is_running = false; } else { runner.unpause(); is_running = true; } display_running(); }); $('#last-button').click(function(e) { curr_hist = (curr_hist+hist_files.length-2)%hist_files.length; timeline.events(hist_events).current(hist_events[curr_hist].key).redraw(); if(e.shiftKey) vizgemsDiagram.transitionDuration(tracker.vals.slow_transition); else vizgemsDiagram.transitionDuration(tracker.vals.transition); runner.step(); }); $('#next-button').click(function(e) { //curr_hist = (curr_hist+1)%hist_files.length; timeline.events(hist_events).current(hist_events[curr_hist].key).redraw(); if(e.shiftKey) vizgemsDiagram.transitionDuration(tracker.vals.slow_transition); else vizgemsDiagram.transitionDuration(tracker.vals.transition); runner.step(); }); // demo of constraints applied by pattern var vm_rules = { nodes: [ {id: 'ostype', partition: 'ostype', typename: function(id, value) { return value; }} ], edges: [ {source: 'VM', target: 'PRT', produce: dc_graph.gap_x(200, false)}, {source: 'VM', target: 'FS', produce: dc_graph.gap_x(200, false)}, {source: 'PRT', target: 'HYP', produce: dc_graph.gap_x(200, false)}, {source: 'FS', target: 'HYP', produce: dc_graph.gap_x(200, false)}, {source: 'VM', target: 'HYP', produce: dc_graph.gap_x(200, false)} ] }; var vm_constraints = dc_graph.constraint_pattern(vm_rules); function nocache_query() { return '?nocache=' + Date.now(); } function read_data(vertices, edges, inv_vertices, inv_edges, is_hist, callback) { var n, id; if(inv_vertices) { node_inv = {}; inv_vertices.forEach(function(n) { var nn = node_inv[n.id] = node_inv[n.id] || {}; nn[n.key] = n.val; }); for(id in node_inv) { n = node_inv[id]; // if it has a host field, alias that key if('host' in n) node_inv[n.host] = n; // rename attribute that will collide with cache n.itype = n.type; delete n.type; // remove ostype prefix from name n.name = n.name.replace(/^[^:]*:/,''); }; } if(inv_edges) { edge_inv = {}; inv_edges.forEach(function(e) { var id = e.id1 + '|' + e.id2; var ee = edge_inv[e.id] = node_inv[e.id] || {}; ee[e.key] = e.val; }); } // we pass in edges as vertices for live case, seems more complete vertices = vertices.filter(function(e) { return !e.id2; }); edges = edges.filter(function(e) { return !!e.id2; }); var warnings = {}; var sttype = {}; var vert_map = {}; // historical nodes come in multiple key/value rows if(is_hist) { vertices.forEach(function(n) { var v = vert_map[n.id1] = vert_map[n.id1] || {id1: n.id1}; v[n.key] = n.value; }); for(id in vert_map) { n = vert_map[id]; // remove ostype prefix from name if(n.name) n.name = n.name.replace(/^[^:]*:/,''); } edges.forEach(function(e) { e.id2 = e.id2.replace(/lvm$/, ''); }); } else { vertices.forEach(function(n) { vert_map[n.id1] = n; }); } var added_nodes = []; function add_v(id) { if(!vert_map[id]) { added_nodes.push(id); vert_map[id] = {id1: id}; } } var conflicting_source_warnings = [], conflicting_target_warnings = []; edges.forEach(function(e) { add_v(e.id1); add_v(e.id2); if(e.metatype==='ostype' && e.type) { var types = e.type.toUpperCase().split('2'); if(sttype[e.id1] && sttype[e.id1] != types[0]) conflicting_source_warnings.push({curr_type: types[0], node: e.id1, prior_type: sttype[e.id1]}); if(sttype[e.id2] && sttype[e.id2] != types[1]) conflicting_target_warnings.push({curr_type: types[1], node: e.id2, prior_type: sttype[e.id2]}); sttype[e.id1] = types[0]; sttype[e.id2] = types[1]; } }); if(conflicting_source_warnings.length) warnings['conflicting source types'] = conflicting_source_warnings; if(conflicting_target_warnings.length) warnings['conflicting target types'] = conflicting_target_warnings; if(added_nodes.length) warnings['added unknown nodes from edges'] = added_nodes; vertices = _.values(vert_map); if(node_inv) { var node_not_found_warnings = [], node_attr_mismatch_warnings = []; // populate vertex/edge properties from inventory, but warn about any problems vertices.forEach(function(n) { var invn = node_inv[n.id1]; if(!invn) { node_not_found_warnings.push(n.id1); return; } for(var a in n) if(a in invn && n[a] !== invn[a]) node_attr_mismatch_warnings.push({attr: a, node: n.id1, prior_value: n[a], inv_value: invn[a]}); _.extend(n, invn); if(!n.ostype) n.ostype = sttype[n.id1]; }); if(node_not_found_warnings.length) warnings['nodes not found in inventory'] = node_not_found_warnings; if(node_attr_mismatch_warnings.length) warnings['node attributes mismatched'] = node_attr_mismatch_warnings; } if(edge_inv) { var edge_not_found_warnings = [], edge_attr_mismatch_warnings = []; edges.forEach(function(e) { var id = e.id1 + '|' + e.id2; var inve = edge_inv[id]; if(!inve) { edge_not_found_warnings.push(id); return; } for(var a in e) if(a in inve && e[a] !== inve[a]) edge_attr_mismatch_warnings.push({attr: a, edge: id, prior_value: e[a], inv_value: inve[a]}); _.extend(e, inve); }); if(edge_not_found_warnings.length) warnings['edges not found in inventory'] = edge_not_found_warnings; if(edge_attr_mismatch_warnings.length) warnings['edge attributes mismatched'] = edge_attr_mismatch_warnings; } var mismatched_sttype_warnings = []; // infer node ostype from edge, if consistent vertices.forEach(function(n) { if(n.ostype) n.ostype = n.ostype.toUpperCase(); if(n.ostype && sttype[n.id1] && n.ostype !== sttype[n.id1]) mismatched_sttype_warnings.push({node: n.id1, ostype: n.ostype, sttype: sttype[n.id1]}); if(sttype[n.id1]) n.ostype = sttype[n.id1]; if(n.ostype === 'HOST') n.ostype = 'HYP'; else // regardless, make sure all vertices have some ostype n.ostype = n.ostype || 'OTHER'; }); if(mismatched_sttype_warnings.length) warnings['nodes ostype did not match edges'] = mismatched_sttype_warnings; if(Object.keys(warnings).length) console.log('graph read warnings', warnings); callback(vertices, edges); } var psv = d3.dsv("|", "text/plain"); function queue_inv(Q) { var inv_nodes_url = tracker.vals.server + '/inv-nodes.psv', inv_edges_url = tracker.vals.server + '/inv-edges.psv'; Q.defer(psv, inv_nodes_url + nocache_query()) .defer(psv, inv_edges_url + nocache_query()); } function load_live(get_inv, callback) { var vertices_url = tracker.vals.server + '/nodes.psv', edges_url = tracker.vals.server + '/edges.psv'; var Q = queue() .defer(psv, vertices_url + nocache_query()) .defer(psv, edges_url + nocache_query()); if(get_inv) queue_inv(Q); Q.await(function(error, vertices, edges, inv_vertices, inv_edges) { if(error) throw new Error(error); // in cache, edges seems to be a superset of vertices, use that instead read_data(edges, edges, inv_vertices, inv_edges, false, callback); }); } // from http://stackoverflow.com/questions/4833651/javascript-array-sort-and-unique function sort_unique(arr) { arr = arr.sort(function (a, b) { return a*1 - b*1; }); var ret = [arr[0]]; for (var i = 1; i < arr.length; i++) { // start loop at 1 as element 0 can never be a duplicate if (arr[i-1] !== arr[i]) { ret.push(arr[i]); } } return ret; } var edge_header = "object|level1|id1|level2|id2|metatype|type|extra", node_header = "object|level1|id1|key|value|extra"; var ndicts = [], edicts = []; function load_hist(file, get_inv, callback) { var Q = queue() .defer(d3.text, tracker.vals.histserv + '/' + file); if(get_inv && tracker.vals.server) queue_inv(Q); Q.await(function(error, hist, inv_vertices, inv_edges) { if(error) throw new Error(error); var pdata = hist.split('\n').slice(3,-2); var ntext = pdata.filter(function(r) { return /^node/.test(r); }), etext = pdata.filter(function(r) { return /^edge/.test(r); }); ntext.unshift(node_header); etext.unshift(edge_header); var nodes = psv.parse(ntext.join('\n')), edges = psv.parse(etext.join('\n')); if(!ndicts[curr_hist]) { ndicts[curr_hist] = sort_unique(nodes.map(function(n) { return n.id1; })); edicts[curr_hist] = sort_unique(edges.map(function(e) { return e.id1 + '-' + e.id2; })); if(curr_hist>0) { var adds = _.difference(ndicts[curr_hist], ndicts[curr_hist-1]).length + _.difference(edicts[curr_hist], edicts[curr_hist-1]).length; var dels = _.difference(ndicts[curr_hist-1], ndicts[curr_hist]).length + _.difference(edicts[curr_hist-1], edicts[curr_hist]).length; hist_events[curr_hist].value = {adds: adds, dels: dels}; } } timeline.events(hist_events).current(hist_events[curr_hist].key).redraw(); read_data(nodes, edges, inv_vertices, inv_edges, true, callback); curr_hist = (curr_hist+1)%hist_files.length; }); } function load(get_inv, callback) { if(hist_files) { var file = hist_files[curr_hist]; load_hist(file, get_inv, callback); } else load_live(get_inv, callback); } function crossfilters(nodes, edges) { if(tracker.vals.node_limit) nodes = nodes.slice(0, tracker.vals.node_limit); var node_stuff = dc_graph.flat_group.make(nodes, function(d) { return d.id1; }), edge_stuff = dc_graph.flat_group.make(edges, function(d) { return d.id1 + '-' + d.id2; }), filterIds = node_stuff.crossfilter.dimension(function(d) { return d.id1; }), filterOSTypes = node_stuff.crossfilter.dimension(function(d) { return d.ostype; }); return {nodeDimension: node_stuff.Dimension, nodeGroup: node_stuff.group, edgeDimension: edge_stuff.Dimension, edgeGroup: edge_stuff.group, filterIds: filterIds, filterOSTypes: filterOSTypes}; } var selected_node = null; function clickiness() { vizgemsDiagram.selectAll('.draw g.node') .on('click.vizgems', function(d) { selected_node = d.orig.key; dc.redrawAll('network'); if(tracker.vals.statserv) { var req = tracker.vals.statserv + "/rest/dataquery/stat/json/level_o=" + d.orig.key; //not valid jsond3.json(req, function(error, data) { d3.xhr(req, function(error, response) { if(error) { console.log(error); return; } try { var data = JSON.parse('{' + response.responseText + '}'); } catch(xep) { console.log(xep); return; } var charts_area = $('#charts-area'), charts_container = $('#charts-container'); charts_container.empty(); if(data.response.stats.length) { var vars = {}; data.response.stats.forEach(function(stat) { vars[stat.vars[0].k] = stat.vars[0]; }); vars = _.values(vars); var N = Math.min(vars.length, 4); vars = vars.slice(0,4); var ndx = crossfilter(data.response.stats); // index on time-of-day data.response.stats.forEach(function(d) { d.time = new Date(d.ti); }); var dim = ndx.dimension(function(r) { return new Date(r.time); }); var w = charts_area.width(), h = Math.min(charts_area.height()/N - 15, 150); var charts = []; var tickFormat = formatValue = d3.format(".2s"); vars.forEach(function(v,i) { charts_container.append($('<p></p>').append($('<b></b>').append(v.l))); var group = dim.group().reduceSum(function(r) { return r.vars[0].k === v.k ? r.vars[0].n : 0; }); var id = 'chart' + (i+1); charts_container.append($('<div></div>', {id: id})); var chart = dc.lineChart('#' + id, d.orig.key) .width(w).height(h) .margins({left: 40, top:0, right: 0, bottom:20}) .dimension(dim) .group(group) .elasticX(true) .elasticY(true) .xUnits(d3.time.minutes) .x(d3.time.scale()) .yAxisLabel(v.k); chart.yAxis().tickFormat(tickFormat); }); dc.renderAll(d.orig.key); } }); } $('#chart1').show(); }); } var ostypes = { RTR: "Router", PRT: "Port", U: "User", CUS: "Tenant", // Customer OTHER: 'Other', // perhaps grey VM: "Virtual Machine", FS: "Volume", // File System IMG: "Image", HYP: "Host", SUB: "Subnet", NET: "Network" }; function init() { load(true, function(vertices, edges) { data_stats = {totnodes: vertices.length, totedges: edges.length}; Object.assign(filters, crossfilters(vertices, edges)); // basic diagram setup vizgemsDiagram .width($(window).width()) .height($(window).height()) .transitionDuration(tracker.vals.transition) .showLayoutSteps(false) .handleDisconnected(tracker.vals.disconnected) .lengthStrategy('jaccard') .baseLength(150) .nodeTitle(function(kv) { return kv.value.ostype==='PRT' ? kv.value.name : kv.key; }) .nodeDimension(filters.nodeDimension).nodeGroup(filters.nodeGroup) .edgeDimension(filters.edgeDimension).edgeGroup(filters.edgeGroup) .edgeSource(function(e) { return e.value.id1; }) .edgeTarget(function(e) { return e.value.id2; }) .on('start', show_start) .on('end', function(happens) { show_stop(); if(happens) runner.endStep(); else runner.skip(); show_stats(data_stats, vizgemsDiagram.getStats()); }); osTypeSelect .promptText('Show all types') .title(function(d) { return ostypes[d.key] + ': ' + d.value; }) .order(function(a,b) { return d3.ascending(ostypes[a.key], ostypes[b.key]); }) .multiple(true) .numberVisible(12); tracker.exert(); // respond to browser resize (not necessary if width/height is static) $(window).resize(function() { vizgemsDiagram .width($(window).width()) .height($(window).height()); }); function isUUID(s) { return /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/.test(s); } function bad_name(s) { return !s || isUUID(s); } // aesthetics: look at kv.value for node/edge attributes and return appropriate values vizgemsDiagram .initLayoutOnRedraw(true) .nodeFitLabel(tracker.vals.fit_labels) .nodeRadius(function(n) { switch(n.value.ostype) { case 'PRT': return 15; default: return 30; } }) .induceNodes(true) // drop zero-degree nodes for now .nodeLabel(function(kv) { switch(kv.value.ostype) { case 'PRT': return ''; case 'FS': if(!bad_name(kv.value.name)) { var parts = kv.value.name.split('-'); if(parts.length===10 && parts[0]==='ceph') return [parts[0], parts[3], parts[4], parts[9]].join('-'); } // fall thru default: return bad_name(kv.value.name) ? bad_name(kv.key) ? '' : kv.key : kv.value.name; } }); var exs = []; for(var ost in ostypes) exs.push({key: ost, name: ostypes[ost], value: {ostype: ost}}); var legend = dc_graph.legend() .nodeWidth(70).nodeHeight(60) .exemplars(exs) .dimension(filters.filterOSTypes) .replaceFilter([osTypeSelect.filters()]); vizgemsDiagram.legend(legend); legend.on('filtered', function() { osTypeSelect.replaceFilter([legend.filters()]).redraw(); }); osTypeSelect.render(); vizgemsDiagram.render(); clickiness(); data_stats = {totnodes: vertices.length, totedges: edges.length}; }); } function step() { load(false, function(vertices, edges) { if(!vertices.length || !edges.length) { runner.endStep(); return; // cola sometimes dies on empty input; hope that next iteration will succeed } Object.assign(filters, crossfilters(vertices, edges)); tracker.exert(); vizgemsDiagram .nodeDimension(filters.nodeDimension).nodeGroup(filters.nodeGroup) .edgeDimension(filters.edgeDimension).edgeGroup(filters.edgeGroup); dc.redrawAll('network'); clickiness(); }); } var preload, snapshots, hist_files, hist_events, curr_hist, runner, tenant_name; function history_index(t) { // last date that is less than argument var i = hist_events.findIndex(function(e) { return e.key > t; }); return i > 0 ? i-1 : i; } function load_history(tenant, k) { hist_files = snapshots.filter(function(r) { return new RegExp("auto-shagrat-" + tenant).test(r); }); console.log('tenant ' + tenant_name[tenant] + ': ' + hist_files.length + ' snapshots'); ndicts = []; edicts = []; var dtreg = /^cm\.([0-9]{8}-[0-9]{6})\./; var datef = d3.time.format('%Y%m%d-%H%M%S'); var hist_times = hist_files.map(function(f) { var match = dtreg.exec(f); if(!match) { console.log('filename ' + f + " didn't match datetime regex"); return null; } return datef.parse(match[1]); }).filter(function(dt) { return !!dt; }); hist_events = hist_times.map(function(dt) { return {key: dt, value: {}}; }); timeline.width($('#timeline').innerWidth()).height(20).events(hist_events).render(); timeline.on('jump', function(t) { var i = history_index(t); if(i === 0) curr_hist = 0; else if(i === -1) curr_hist = hist_events.length-1; else curr_hist = i; if(!is_running) runner.step(); }); curr_hist = -1; if(tracker.vals.date) { var date = datef.parse(tracker.vals.date); if(!date) date = d3.time.format('%Y%m%d').parse(tracker.vals.date); if(date) curr_hist = history_index(date); } if(curr_hist === -1) curr_hist = 0; k(); } function populate_tenant_select(tenants, curr) { tenant_name = tenants.reduce(function(m, v) { m[v[0]] = v[1]; return m; }, {}); $('#tenant-option').show(); var sel = d3.select('#tenant-select'); sel.selectAll('option') .data(tenants) .enter().append('option') .attr({ value: function(d) { return d[0]; }, selected: function(_,i) { return i===0; } }) .text(function(d) { return d[1]; }); $('#tenant-select').val(curr); sel.on('change', function() { runner.stop(); load_history(this.selectedOptions[0].value, function() {}); runner.start(!is_running); }); } if(tracker.vals.histserv) { preload = function(k) { var Q = queue() .defer(d3.text, tracker.vals.histserv + '/list.txt' + nocache_query()) .defer(d3.text, tracker.vals.histserv + '/customer.txt' + nocache_query()); Q.await(function(error, list, tenants) { snapshots = list.split('\n'); tenants = tenants.split('\n'); tenants = tenants.filter(function(t) { return !!t; }) .map(function(c) { return c.split('|'); }); var tenant = tracker.vals.tenant || tenants[0][0]; populate_tenant_select(tenants, tenant); load_history(tenant, k); }); }; } else preload = function(k) { k(); }; preload(function() { runner = make_runner(init, step, tracker.vals.interval); runner.start(!is_running); });