UNPKG

dc.graph

Version:

Graph visualizations integrated with crossfilter and dc.js

975 lines (911 loc) 34.7 kB
var qs = querystring.parse(); var options = Object.assign({ catalog: 'catalog/get.json', catformat: 'demo', solution: '' }, qs); // abstract away the data formats function demo_catalog_reader(catalog) { return { models: function() { return catalog.components; }, composites: function() { if(arguments.length) { catalog.solutions = arguments[0]; return this; } return catalog.solutions; }, fModelId: function(model) { return model.name; }, fModelName: function(model) { return model.name; }, fModelCategory: function(model) { return model.category; }, fModelUrl: function(model) { return model.url; }, fCompositeId: function(comp) { return comp.name; }, fTypeName: function(type) { return type.name; }, ports: function ports(nid, def) { return def.requirements.map(function (r) { return { nodeId: nid, portname: 'req-' + r, wild: r === 'wild', type: r === 'wild' ? null : r, bounds: inbounds }; }).concat(def.capabilities.map(function (r) { return { nodeId: nid, portname: 'cap-' + r, wild: r === 'wild', type: r === 'wild' ? null : r, bounds: outbounds }; })).concat((def.extras || []).map(function (x) { return { nodeId: nid, portname: 'xtra-' + x, wild: x === 'wild', type: x === 'wild' ? null : x, bounds: xtrabounds }; })); } }; } var catalog_readers = { 'demo': demo_catalog_reader }; function show_while_promise(selector, promise) { d3.select(selector).style('visibility', 'visible'); promise.then(function() { // let it run a little longer so that it's guaranteed to show window.setTimeout(function() { d3.select(selector).style('visibility', 'hidden'); }, 100); }); return promise; } function json_promise(url) { var request = d3.json(url).get; return new Promise(function(resolve, reject) { request(function(error, data) { if(error) reject(error); else { resolve(data); } }); }); } function get_catalog() { return json_promise(options.catalog); } // canvas var _compositionDiagram, _rendered = false, _drawGraphs, _solution, _ports = []; // save-area (needs version too) var _currentSoln = null, _solutionName, _description, _dirty = false; // palette var _catalog, _components, _palette; function set_dirty(whether) { _dirty = whether; if(whether) { $('#save-button') .removeClass('button-disabled') .attr('title', 'solution has changes'); } else { $('#save-button') .addClass('button-disabled') .attr('title', 'solution is saved'); } } // // CANVAS // function redraw_promise(diagram) { return new Promise(function(resolve, reject) { diagram.on('end', function() { resolve(); }); diagram.redraw(); }); } var lbounds = [Math.PI*5/6, -Math.PI*5/6], rbounds = [-Math.PI/6, Math.PI/6], dbounds = [Math.PI/6, Math.PI*5/6], ubounds = [-Math.PI*5/6, -Math.PI/6]; var inbounds, outbounds, xtrabounds; if(options.rankdir === 'TB') { inbounds = ubounds; outbounds = dbounds; xtrabounds = [Math.PI, Math.PI]; } else { inbounds = lbounds; outbounds = rbounds; xtrabounds = [-Math.PI/2, -Math.PI/2]; } function update_ports() { var port_flat = dc_graph.flat_group.make(_ports, function (d) { return d.nodeId + '/' + d.portname; }); _compositionDiagram .portDimension(port_flat.dimension).portGroup(port_flat.group); } var _fakeDB = {}; function display_solution(catalog, solution) { _compositionDiagram.child('fix-nodes') .clearFixes(); _description.editable('setValue', solution.description || null); var types = d3.set(solution.nodes.map(function (n) { return n.type; })).values(); Promise.all(types.map(function (t) { return _components.get(t).url; }).map(json_promise)).then(function (defns) { var defn = {}; types.forEach(function (t, i) { return defn[t] = defns[i]; }); _ports = []; solution.nodes.forEach(function(n) { _ports = _ports.concat(catalog.ports(n.id, defn[n.type])); }); var node_flat = dc_graph.flat_group.make(solution.nodes, function (d) { return d.id; }), edge_flat = dc_graph.flat_group.make(solution.edges, function (e) { return e.id; }); _compositionDiagram .nodeDimension(node_flat.dimension).nodeGroup(node_flat.group) .edgeDimension(edge_flat.dimension).edgeGroup(edge_flat.group); update_ports(); _drawGraphs .nodeCrossfilter(node_flat.crossfilter) .edgeCrossfilter(edge_flat.crossfilter); if(!_rendered) { _compositionDiagram.render(); _rendered = true; } else _compositionDiagram.redraw(); }); } // // SAVE AREA // & loading composite solutions // function load_solution(name, url) { if(_fakeDB[name]) return Promise.resolve(_fakeDB[name]); else return json_promise(url); } function load_sol(name, url) { load_solution(name, url).then(function(solution) { _solution = solution; return display_solution(_catalog, _solution); }); } function save_solution(catalog, name) { _compositionDiagram.child('fix-nodes').fixAllNodes(); _solution.nodes = _drawGraphs.nodeCrossfilter().all(); _solution.edges = _drawGraphs.edgeCrossfilter().all(); _fakeDB[name] = _solution; set_dirty(false); if (!_.find(catalog.composites(), function (comp) { return catalog.fCompositeId(comp) === name; })) catalog.composites().push({ name: name, url: null }); return Promise.resolve(catalog); } function maybe_save_solution(catalog) { if(!_dirty || !confirm('Current solution is unsaved - save it now?')) return Promise.resolve(catalog); var name = _currentSoln; if(!name) name = prompt('Enter a solution name', 'Solution'); return save_solution(catalog, name); } function rename_solution(catalog, oldname, newname) { if (_.find(catalog.composites(), function (comp) { return catalog.fCompositeId(comp) === newname; })) return Promise.reject('name already used'); catalog.composites(catalog.composites().map(function(soln) { soln = Object.assign({}, soln); if(catalog.fCompositeId(soln) === oldname) soln.name = newname; return soln; })); if(_fakeDB[oldname]) { _fakeDB[newname] = _fakeDB[oldname]; delete _fakeDB[oldname]; } return Promise.resolve(catalog); } function delete_solution(catalog, name) { if (!_.find(catalog.composites(), function (comp) { return catalog.fCompositeId(comp) === name; })) return Promise.reject('solution not in catalog'); catalog.composites(catalog.composites().filter(function (soln) { return catalog.fCompositeId(soln) !== name; })); if (_fakeDB[name]) delete _fakeDB[name]; return Promise.resolve(catalog); } // // PROPERTIES PANE // function print_value(v) { if(!v || ['string','number','boolean'].indexOf(typeof v) !== -1) return v.toString(); else return JSON.stringify(v); } function display_properties(catalog, content) { var dest = d3.select('#properties-content'); if(typeof content === 'string') { // url json_promise(content).then(function(content) { display_properties(catalog, content); }); } else if(typeof content === 'object') { // json content = Object.assign({}, content); var name = catalog.fTypeName(content); delete content.name; dest.style('visibility', 'visible'); d3.select('#selected-name') .text(name); var table = d3.select('#properties-table'); var keys = Object.keys(content).sort(); var rows = table.selectAll('tr.property').data(keys); rows.exit().remove(); rows.enter().append('tr').attr('class', 'property'); var cols = rows.selectAll('td').data(function(x) { return [x, print_value(content[x])]; }); cols.enter().append('td'); cols.text(function(x) { return x; }); } else dest.style('visibility', 'hidden'); } // // PALETTE // function make_palette(selector) { var _dispatch = d3.dispatch('selected'); var _categories, _keyFunction, _nameFunction; function sanitize_id(key) { return key.toLowerCase().replace(' ', '-').replace(/[.]/g, ''); } function heading_id(kvs) { return 'heading-' + sanitize_id(kvs.key); } function collapse_id(kvs) { return 'collapse-' + sanitize_id(kvs.key); } function select_id(comp) { return 'select-' + sanitize_id(_keyFunction(comp)); } function pass_through(x) { return [x]; } function show_selection(parent, key) { parent.selectAll('li') .classed('ui-selected', function(comp2) { return _keyFunction(comp2) === key; }); } function data(categories) { var nulls = [], i = 0; categories.forEach(function(c) { if(!c) nulls.push(i); else ++i; }); categories = categories.filter(function(c) { return !!c; }); var card = d3.select('#palette') .selectAll('div.card').data(categories, function(kvs) { return kvs.key; } ); var cardEnter = card.enter().append('div') .attr('class', 'card'); cardEnter.each(function() { var card = d3.select(this); if(!card.datum().noheader) { card = card.append('div') .attr({ class: 'card-header', role: 'tab' }) .append('h5') .attr('class', 'mb-0'); } card.append('a') .attr({ class: 'collapser', 'data-toggle': 'collapse', 'data-parent': '#palette', 'aria-expanded': 'true' }) .text(function(kvs) { return kvs.key; } ); }); card.selectAll('div.card-header') .data(pass_through) .attr('id', heading_id); card.selectAll('a.collapser') .data(pass_through) .attr({ 'href': function(kvs) { return '#' + collapse_id(kvs); }, 'aria-controls': collapse_id }); var contentEnter = cardEnter.insert('div') .attr({ class: 'collapse', role: 'tabpanel' }) .append('div') .attr('class', 'card-block'); card.selectAll('div.collapse') .data(pass_through) .attr({ id: collapse_id, 'aria-labelledby': heading_id }); card.exit().remove(); d3.select('#palette hr.separator').remove(); nulls.forEach(function(j) { d3.select('#palette') .insert('hr', 'div.card:nth-child(' + (j+1) + ')') .attr('class', 'separator'); }); $('#palette').on('hide.bs.collapse', function () { _palette.select(null); }); contentEnter.append('ul') .attr('class', 'component-selection'); var selection = card.selectAll('ul.component-selection') .data(pass_through); var components = selection.selectAll('li') .data(function(kvs) { return kvs.values; }); components.enter().append('li') .on('mousedown', function(comp) { show_selection(d3.select(this.parentElement), _keyFunction(comp)); _dispatch.selected(comp); }); components .attr('id', select_id) .text(function(comp) { return _nameFunction(comp); }); $('ul.component-selection').each(function() { if(d3.select(this).datum().draggable) $('li', this).draggable({ helper: "clone", opacity: 0.5 }); }); components.exit().remove(); } var _palette = { data: function(categories) { if(!arguments.length) return _categories; _categories = categories; data(categories); return this; }, keyFunction: function(keyFunction) { if(!arguments.length) return _keyFunction; _keyFunction = keyFunction; return this; }, nameFunction: function(nameFunction) { if(!arguments.length) return _keyFunction; _nameFunction = nameFunction; return this; }, select: function(id) { if(!id) { _dispatch.selected(null); d3.selectAll('ul.component-selection li.ui-selected') .classed('ui-selected', false); } else { throw new Error('not implemented'); // will need to open its drawer, apply class as in click handler // and then dispatch event } }, on: function(event, callback) { switch(arguments.length) { case 0: throw new Error('event is not optional'); case 1: return _dispatch.on(event); default: _dispatch.on(event, callback); return this; } } }; return _palette; } function update_palette(catalog) { // throw out any models which don't have a category, to avoid "null drawer" var models = catalog.models().filter(catalog.fModelCategory); var categories = d3.nest().key(catalog.fModelCategory) .sortKeys(d3.ascending) .entries(models); categories.forEach(function(kvs) { kvs.draggable = true; }); categories.unshift(null); categories.unshift({ key: "Saved Solutions", draggable: false, noheader: true, values: catalog.composites().map(function(v) { v.category = 'solution'; return v; }).sort(function(a,b) { return d3.ascending(catalog.fModelName(a), catalog.fModelName(b)); }) }); _palette.data(categories); } // https://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery function hashCode(s) { var hash = 0, i, chr; if (s.length === 0) return hash; for (i = 0; i < s.length; i++) { chr = s.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; // Convert to 32bit integer } return hash >>> 0; // convert to unsigned }; var _icons; d3.text('iconlist.txt', function(error, list) { _icons = list.split(/\n/); }); function hashIcon(icons, type) { var h = hashCode(type); return _icons[h%icons.length]; } var _ionicons = { AlarmGenerator: 'ion-arrow-graph-up-right.png', Classifier: 'ion-levels.png', Aggregator: 'ion-pie-graph.png', Predictor: 'ion-stats-bars.png' }; // // INITIALIZATION // get_catalog().then(function(catalog) { _catalog = catalog = catalog_readers[options.catformat](catalog); _components = d3.map(catalog.models(), catalog.fModelName); // PALETTE _palette = make_palette('#palette') .keyFunction(catalog.fModelId) .nameFunction(catalog.fModelName) .on('selected', function(comp) { if(!comp) display_properties(catalog, null); else if(comp.category === 'solution') { _solutionName.editable('setValue', _currentSoln = comp.name); $('#delete-button').removeClass('button-disabled'); load_sol(comp.name, comp.url); } else display_properties(catalog, catalog.fModelUrl(comp)); }); update_palette(catalog); // CANVAS _compositionDiagram = dc_graph.diagram('#canvas'); var layout = dc_graph.cola_layout() .baseLength(5) .groupConnected(true) .handleDisconnected(false) .unconstrainedIterations(5) .userConstraintIterations(5) .allConstraintsIterations(5) .flowLayout({ axis: options.rankdir === 'TB' ? 'y' : 'x', minSeparation: options.rankdir === 'TB' ? function(e) { return (e.source.height + e.target.height) / 2 + layout.ranksep(); } : function(e) { return (e.source.width + e.target.width) / 2 + layout.ranksep(); } }); _compositionDiagram .width('auto') .height('auto') .layoutEngine(layout) .timeLimit(500) .margins({left: 5, top: 5, right: 5, bottom: 5}) .modKeyZoom(options.mkzoom || null) .transitionDuration(qs.duration !== undefined ? +qs.duration : 1000) .fitStrategy('zoom') .autoZoom('always') .restrictPan(true) .stageTransitions(qs.stage || 'insmod') .enforceEdgeDirection(options.rankdir || 'LR') .edgeSource(function(e) { return e.value.sourcename; }) .edgeTarget(function(e) { return e.value.targetname; }) .edgeArrowhead(null) .enforceEdgeDirection('LR') .edgeLabel(function(e) { return e.value.name || ''; }) .nodeLabel(function(n) { return n.value.name || n.key; }) .nodeLabelPadding({x: 10, y: 0}) .nodeTitle(null) .nodeStrokeWidth(1) .nodeStroke('#777') .edgeStroke('#777') .nodeShape({shape: 'rounded-rect'}) .nodePadding(20) .nodeContent('text-with-icon') .nodeIcon(function(d) { return hashIcon(_icons, d.value.type); }) .nodeFixed(function(n) { return n.value.fixedPos; }) .portNodeKey(function (p) { return p.value.nodeId; }).portName(function (p) { return p.value.portname; }).portBounds(function (p) { return p.value.bounds; }).edgeSourcePortName(function (e) { return e.value.sourceport; }).edgeTargetPortName(function (e) { return e.value.targetport; }); if (qs.showFixed) _compositionDiagram.nodeStrokeDashArray(function (n) { return n.value.fixedPos ? null : '5,5'; }); _compositionDiagram.content('text-with-icon', dc_graph.with_icon_contents(dc_graph.text_contents(), 35, 35)); _compositionDiagram.child('place-ports', dc_graph.place_ports()); var symbolPorts = dc_graph.symbol_port_style() //.outline(dc_graph.symbol_port_style.outline.square()) .outlineStrokeWidth(1) // .portLabel(p => p.value.portname) .symbol(function (p) { return p.orig.value.type; }).color(function (p) { return p.orig.value.type; }).colorScale(d3.scale.ordinal().range( // colorbrewer qualitative scale d3.shuffle( ['#e41a1c','#377eb8','#4daf4a','#984ea3','#ff7f00','#eebb22','#a65628','#f781bf'] // 8-class set1 //['#1b9e77','#d95f02','#7570b3','#e7298a','#66a61e','#e6ab02','#a6761d','#666666'] // 8-class dark2 ))); if(qs.direcports) symbolPorts.outline(dc_graph.symbol_port_style.outline.arrow() .outie(function (p) { return p.value.bounds === outbounds; })); if(qs.lettports) symbolPorts .content(dc_graph.symbol_port_style.content.letter()); var letterPorts = dc_graph.symbol_port_style() .content(dc_graph.symbol_port_style.content.letter()) .outlineStrokeWidth(1) .symbol('S') .symbolScale(function (x) { return x; }) .color('black') .colorScale(null); _compositionDiagram .portStyle('symbols', symbolPorts) .portStyle('letters', letterPorts) .portStyleName(function(p) { return /^xtra-/.test(p.value.portname) ? 'letters' : 'symbols'; }); var portMatcher = dc_graph.match_ports(_compositionDiagram, symbolPorts) .allowParallel(qs.parallel || false); var wildcard = dc_graph.wildcard_ports({ get_type: function get_type(p) { return p.orig.value.type; }, set_type: function set_type(p, src) { return p.orig.value.type = src && src.orig.value.type; }, get_wild: function get_wild(p) { return p.orig.value.wild; }, update_ports: update_ports }); portMatcher.isValid(function(sourcePort, targetPort) { return wildcard.isValid(sourcePort, targetPort) && sourcePort.orig.value.bounds !== xtrabounds && targetPort.orig.value.bounds !== xtrabounds && sourcePort.orig.value.bounds !== targetPort.orig.value.bounds; }); portMatcher.whyInvalid(function(sourcePort, targetPort) { return sourcePort.orig.value.bounds === xtrabounds && "can't connect to that type of source port" || targetPort.orig.value.bounds === xtrabounds && "can't connect to that type of target port" || sourcePort.orig.value.bounds === targetPort.orig.value.bounds && "can't connect ports facing the same direction" || wildcard.whyInvalid(sourcePort, targetPort); }); var gropts; _drawGraphs = dc_graph.draw_graphs(gropts = { idTag: 'id', sourceTag: 'sourcename', targetTag: 'targetname' }) .clickCreatesNodes(false) .usePorts(symbolPorts) .conduct(portMatcher) .addEdge(function(e, sport, tport) { set_dirty(true); // reverse edge if it's going from requirement to capability // again, the bounds object comparison is not good. // maybe it would be clearer to return a new edge object. if(sport.orig.value.bounds === inbounds) { console.assert(tport.orig.value.bounds === outbounds); var t; t = sport; sport = tport; tport = t; t = e.sourcename; e.sourcename = e.targetname; e.targetname = t; } e.sourceport = sport.name; e.targetport = tport.name; return wildcard.copyType(e, sport, tport); }); _compositionDiagram.mode('draw-graphs', _drawGraphs); var select_nodes = dc_graph.select_nodes({ nodeStroke: 'orange', nodeStrokeWidth: 3, nodeLabelFill: 'orange' }).multipleSelect(false); _compositionDiagram.child('select-nodes', select_nodes); var select_nodes_group = dc_graph.select_things_group('select-nodes-group', 'select-nodes'); select_nodes_group.on('set_changed.show-info', function(nodes, refresh) { _palette.select(null); if(nodes.length>1) throw new Error('not expecting multiple select'); else if(nodes.length === 1) { select_edges_group.set_changed([], refresh); select_ports_group.set_changed([], refresh); var type = _compositionDiagram.getNode(nodes[0]).value.type; var comps = catalog.models().filter(function(comp) { return catalog.fModelName(comp) === type; }); if(comps.length === 1) display_properties(catalog, catalog.fModelUrl(comps[0])); } else display_properties(catalog, null); }); var select_edges = dc_graph.select_edges({ edgeStroke: 'lightblue', edgeStrokeWidth: 3 }).multipleSelect(false); _compositionDiagram.child('select-edges', select_edges); var select_edges_group = dc_graph.select_things_group('select-edges-group', 'select-edges'); select_edges_group.on('set_changed.show-info', function(edges, refresh) { _palette.select(null); if(edges.length>0) { select_nodes_group.set_changed([], refresh); select_ports_group.set_changed([], refresh); var edge = _compositionDiagram.getEdge(edges[0]); display_properties(catalog, edge); } else display_properties(catalog, null); }); var select_ports = dc_graph.select_ports({ portBackgroundFill: 'orange' // portBackgroundStroke: 'lightblue', // portBackgroundStrokeWidth: 2 }).multipleSelect(false); _compositionDiagram.child('select-ports', select_ports); var select_ports_group = dc_graph.select_things_group('select-ports-group', 'select-ports'); select_ports_group.on('set_changed.show-info', function(ports, refresh) { _palette.select(null); if(ports.length>0) { select_nodes_group.set_changed([], refresh); select_edges_group.set_changed([], refresh); display_properties(catalog, ports[0]); } else display_properties(catalog, null); }); var move_nodes = dc_graph.move_nodes(); _compositionDiagram.child('move-nodes', move_nodes); var fix_nodes = dc_graph.fix_nodes() .strategy(dc_graph.fix_nodes.strategy.last_N_per_component(Infinity)); _compositionDiagram.child('fix-nodes', fix_nodes); var label_nodes = dc_graph.label_nodes({ labelTag: 'name', align: 'left', class: 'node-label' }).changeNodeLabel(function(nodeId, text) { var node = _compositionDiagram.getNode(nodeId); // execute on server first, which could reject or change text return Promise.resolve(text); }); _compositionDiagram.child('label-nodes', label_nodes); var label_edges = dc_graph.label_edges({ labelTag: 'name', align: 'center', class: 'edge-label' }).changeEdgeLabel(function(edgeId, text) { // execute on server first, which could reject or change text return Promise.resolve(text); }); _compositionDiagram.child('label-edges', label_edges); var delete_nodes = dc_graph.delete_nodes() .crossfilterAccessor(function(diagram) { return _drawGraphs.nodeCrossfilter(); }) .dimensionAccessor(function(diagram) { return _compositionDiagram.nodeDimension(); }) .onDelete(function(nodes) { // confirm with server here return Promise.resolve(nodes) .then(function(nodes) { // after the back-end has accepted the deletion, we can remove unneeded ports _ports = _ports.filter(function (p) { return p.nodeId !== nodes[0]; }); update_ports(); return nodes; }); }); _compositionDiagram.child('delete-nodes', delete_nodes); var delete_edges = dc_graph.delete_things(select_edges_group, 'delete-edges', 'id') .crossfilterAccessor(function(diagram) { return _drawGraphs.edgeCrossfilter(); }) .dimensionAccessor(function(diagram) { return _compositionDiagram.edgeDimension(); }) .onDelete(function(edges) { // confirm with server here, promise-then pass to wildcard return wildcard.resetTypes(_compositionDiagram, edges); }); _compositionDiagram.child('delete-edges', delete_edges); var operations = ['run', 'jump', 'talk', 'sleep']; var messages = ['hill', 'storm', 'furiously', 'stile', 'mile']; function generate_operation(id) { var op = operations[Math.floor(id%operations.length)], msgs = d3.range(Math.floor(id%3)).map(function() { return messages[Math.floor(id%messages.length)]; }); return op + '(' + msgs.map(function(msg) { return '<a href="#" class="tip-link" id="' + op + '_' + msg + '">' + msg + '</a>'; }).join(', ') + ')'; } var port_tips = dc_graph.tip() .delay(200) .clickable(true) .selection(dc_graph.tip.select_port()) .content(function(d, k) { k(generate_operation(hashCode(d.node.orig.key + '-' + d.name))); }) .offset(function() { // I don't entirely understand how d3-tip is calculating position // this attempts to keep position fixed even though size of g.port is changing return [this.getBBox().height / 2 - 20, 0]; }) .linkCallback(function(id) { alert(id); }); _compositionDiagram.child('port-tips', port_tips); var node_tips = dc_graph.tip({namespace: 'node-tips'}) .selection(dc_graph.tip.select_node()) .content(function(d, k) { k(d.orig.value && d.orig.value.type); }); _compositionDiagram.child('node-tips', node_tips); var negative_tips = dc_graph.tip({namespace: 'hint-negative-tips', class: 'd3-tip hint-negative'}) .selection(dc_graph.tip.select_port()) .programmatic(true) .hideDelay(1000); _compositionDiagram.child('hint-negative-tips', negative_tips); var positive_tips = dc_graph.tip({namespace: 'hint-positive-tips', class: 'd3-tip hint-positive'}) .selection(dc_graph.tip.select_port()) .direction('s') .programmatic(true) .hideDelay(1000); _compositionDiagram.child('hint-positive-tips', positive_tips); gropts.tipsDisable = [port_tips, node_tips]; gropts.negativeTip = negative_tips; gropts.positiveTip = positive_tips; if(qs.debug) { var troubleshoot = dc_graph.troubleshoot(); _compositionDiagram.child('troubleshoot', troubleshoot); } if(qs.validate) { var validate = dc_graph.validate(); _compositionDiagram.child('validate', validate); } $('#canvas').droppable({ drop: function(event, ui) { set_dirty(true); var component = d3.select(ui.draggable[0]).datum(); var type = catalog.fModelName(component); var max = 0; _drawGraphs.nodeCrossfilter().all().forEach(function(n) { var number = n.id.match(/[0-9]+$/); if(!number) return; // currently all ids will be type + number number = number[0]; var type2 = n.id.slice(0, -number.length); if(type2 === type && +number > max) max = +number; }); var data = { id: type + (max+1), type: type }; var bound = _compositionDiagram.root().node().getBoundingClientRect(); var pos = _compositionDiagram.invertCoord([event.clientX - bound.left, event.clientY - bound.top]); json_promise(catalog.fModelUrl(_components.get(type))).then(function(def) { _ports = _ports.concat(catalog.ports(data.id, def)); update_ports(); _drawGraphs.createNode(pos, data); }); } }); // SAVE AREA $.fn.editable.defaults.mode = 'inline'; _solutionName = $('#solution-name').editable({ emptytext: '(untitled)', success: function(response, value) { var promise = Promise.resolve(catalog); if(_currentSoln) { promise = save_solution(catalog, _currentSoln).then(function(cat2) { return rename_solution(cat2, _currentSoln, value); }); } else { promise = save_solution(catalog, value); } promise.then(function(cat3) { catalog = cat3; update_palette(catalog); _currentSoln = value; $('#delete-button').removeClass('button-disabled'); }); } }); _description = $('#description').editable({ emptytext: '(no description)', success: function(response, value) { set_dirty(true); _solution.description = value; } }); $('#new-button').click(function() { maybe_save_solution(catalog).then(function(cat2) { catalog = cat2; update_palette(catalog); _currentSoln = null; $('#delete-button').addClass('button-disabled'); _solution = {nodes: [], edges: []}; display_solution(catalog, _solution); }); }); $('#save-button').click(function() { if(_dirty) { if(!_currentSoln) { _currentSoln = prompt('Enter a solution name', 'Solution'); $('#delete-button').removeClass('button-disabled'); } save_solution(catalog, _currentSoln) .then(function(cat2) { catalog = cat2; update_palette(catalog); }); } }); $('#delete-button').click(function() { if(_currentSoln && confirm('Really delete solution "' + _currentSoln + '"?')) delete_solution(catalog, _currentSoln).then(function(cat2) { catalog = cat2; update_palette(catalog); }); }); // load initial composite solution var catsol; if(options.solution) catsol = catalog.composites().find(function (sol) { return sol.name === options.solution; }); if(catsol) load_sol(catsol.name, catsol.url); else { _solution = {nodes: [], edges: []}; display_solution(catalog, _solution); } });