diff --git a/lcopt/analysis.py b/lcopt/analysis.py index 2cfce8e..1502e79 100644 --- a/lcopt/analysis.py +++ b/lcopt/analysis.py @@ -1,5 +1,6 @@ from lcopt.bw2_export import Bw2Exporter from lcopt.utils import DEFAULT_DB_NAME, FORWAST_DB_NAME +from lcopt.mass_balance import recurse_mass import brightway2 as bw2 from bw2analyzer.tagged import recurse_tagged_database, aggregate_tagged_graph from copy import deepcopy @@ -248,7 +249,8 @@ def run_analyses(self, demand_item, demand_item_code, amount=1, methods=[('IPCC 'foreground_results': foreground_result, 'graph': recursed_graph, 'dropped_graph': dropped_graph, - 'original_graph': str(type_graph[0]) + 'original_graph': str(type_graph[0]), + 'mass_flow': recurse_mass(type_graph[0]) } ps_results.append(result_set) diff --git a/lcopt/interact.py b/lcopt/interact.py index 139255d..1e1ad40 100644 --- a/lcopt/interact.py +++ b/lcopt/interact.py @@ -1003,6 +1003,10 @@ def locations_as_json(): return json.dumps(filtered_locations) + @app.route('/mass_flow') + def mass_flow(): + return render_template('mass_flow.html') + return app def run(self): # pragma: no cover diff --git a/lcopt/mass_balance.py b/lcopt/mass_balance.py new file mode 100644 index 0000000..e12a765 --- /dev/null +++ b/lcopt/mass_balance.py @@ -0,0 +1,48 @@ +def recurse_mass(d): + + to_return = {} + #cum_impact = 0 + + for k, v in d.items(): + if k == 'amount' and d['tag'] == 'biosphere': + #print (k, -v) + to_return[k] = -v + + elif k == 'technosphere': + #print('technosphere') + for e in v: + #print (e['activity']) + #cum_impact += e['impact'] + #if 'cum_impact' in e.keys(): + # cum_impact += e['cum_impact'] + + if k in to_return.keys(): + to_return[k].append(recurse_mass(e)) + else: + to_return[k] = [recurse_mass(e)] + + elif k in['biosphere', 'impact']: + pass + + elif k == 'activity': + #print (k,v) + #activity_list = v.split('(') + activity = v['name'] # activity_list[0].strip() + unit = v['unit'] # activity_list[1].split(',')[0] + #print(activity, unit) + to_return['activity'] = str(activity) + to_return['unit'] = unit + if unit in ['kg', 'g']: + to_return['is_mass'] = True + else: + to_return['is_mass'] = False + + #elif k == 'impact': + # print('impact of {} = {}'.format(d['activity'], v)) + + else: + to_return[k] = v + #print('cum_impact of {} = {}'.format(d['activity'], cum_impact)) + #to_return['cum_impact'] = cum_impact + + return to_return diff --git a/lcopt/static/css/mass_flow.css b/lcopt/static/css/mass_flow.css new file mode 100644 index 0000000..bd02fff --- /dev/null +++ b/lcopt/static/css/mass_flow.css @@ -0,0 +1,38 @@ +.mf_node rect { + cursor: move; + fill-opacity: .9; + /*shape-rendering: crispEdges;*/ +} + +.mf_node text { + pointer-events: none; + font-size: 8px; +} + +.mf_link { + fill: none; + stroke: rgba(0,0,0,0.2); + /*stroke-opacity: .2;*/ +} + +.mf_link:hover { + stroke-opacity: .5; +} + +.tag_other > rect{ + fill: rgba(0,0,0,0.2) !important; + stroke: none !important; + /*stroke-opacity:.2; + opacity: .2;*/ +} + +.tag_other > rect:hover{ + fill: rgba(255,255,255,0.5) !important; + stroke: #000 !important; + +} + +.tag_other > text{ + display: none; + fill: rgba(0,0,0,0); +} \ No newline at end of file diff --git a/lcopt/static/js/cdn_cache/sankey.js b/lcopt/static/js/cdn_cache/sankey.js new file mode 100644 index 0000000..7ec93d6 --- /dev/null +++ b/lcopt/static/js/cdn_cache/sankey.js @@ -0,0 +1,294 @@ +d3.sankey = function() { + var sankey = {}, + nodeWidth = 24, + nodePadding = 8, + size = [1, 1], + nodes = [], + links = []; + + sankey.nodeWidth = function(_) { + if (!arguments.length) return nodeWidth; + nodeWidth = +_; + return sankey; + }; + + sankey.nodePadding = function(_) { + if (!arguments.length) return nodePadding; + nodePadding = +_; + return sankey; + }; + + sankey.nodes = function(_) { + if (!arguments.length) return nodes; + nodes = _; + return sankey; + }; + + sankey.links = function(_) { + if (!arguments.length) return links; + links = _; + return sankey; + }; + + sankey.size = function(_) { + if (!arguments.length) return size; + size = _; + return sankey; + }; + + sankey.layout = function(iterations) { + computeNodeLinks(); + computeNodeValues(); + computeNodeBreadths(); + computeNodeDepths(iterations); + computeLinkDepths(); + return sankey; + }; + + sankey.relayout = function() { + computeLinkDepths(); + return sankey; + }; + + sankey.link = function() { + var curvature = .5; + + function link(d) { + var x0 = d.source.x + d.source.dx, + x1 = d.target.x, + xi = d3.interpolateNumber(x0, x1), + x2 = xi(curvature), + x3 = xi(1 - curvature), + y0 = d.source.y + d.sy + d.dy / 2, + y1 = d.target.y + d.ty + d.dy / 2; + return "M" + x0 + "," + y0 + + "C" + x2 + "," + y0 + + " " + x3 + "," + y1 + + " " + x1 + "," + y1; + } + + link.curvature = function(_) { + if (!arguments.length) return curvature; + curvature = +_; + return link; + }; + + return link; + }; + + // Populate the sourceLinks and targetLinks for each node. + // Also, if the source and target are not objects, assume they are indices. + function computeNodeLinks() { + nodes.forEach(function(node) { + node.sourceLinks = []; + node.targetLinks = []; + }); + links.forEach(function(link) { + var source = link.source, + target = link.target; + if (typeof source === "number") source = link.source = nodes[link.source]; + if (typeof target === "number") target = link.target = nodes[link.target]; + source.sourceLinks.push(link); + target.targetLinks.push(link); + }); + } + + // Compute the value (size) of each node by summing the associated links. + function computeNodeValues() { + nodes.forEach(function(node) { + node.value = Math.max( + d3.sum(node.sourceLinks, value), + d3.sum(node.targetLinks, value) + ); + }); + } + + // Iteratively assign the breadth (x-position) for each node. + // Nodes are assigned the maximum breadth of incoming neighbors plus one; + // nodes with no incoming links are assigned breadth zero, while + // nodes with no outgoing links are assigned the maximum breadth. + function computeNodeBreadths() { + var remainingNodes = nodes, + nextNodes, + x = 0; + + while (remainingNodes.length) { + nextNodes = []; + remainingNodes.forEach(function(node) { + node.x = x; + node.dx = nodeWidth; + node.sourceLinks.forEach(function(link) { + if (nextNodes.indexOf(link.target) < 0) { + nextNodes.push(link.target); + } + }); + }); + remainingNodes = nextNodes; + ++x; + } + + // + moveSinksRight(x); + scaleNodeBreadths((size[0] - nodeWidth) / (x - 1)); + } + + function moveSourcesRight() { + nodes.forEach(function(node) { + if (!node.targetLinks.length) { + node.x = d3.min(node.sourceLinks, function(d) { return d.target.x; }) - 1; + } + }); + } + + function moveSinksRight(x) { + nodes.forEach(function(node) { + if (!node.sourceLinks.length) { + node.x = x - 1; + } + }); + } + + function scaleNodeBreadths(kx) { + nodes.forEach(function(node) { + node.x *= kx; + }); + } + + function computeNodeDepths(iterations) { + var nodesByBreadth = d3.nest() + .key(function(d) { return d.x; }) + .sortKeys(d3.ascending) + .entries(nodes) + .map(function(d) { return d.values; }); + + // + initializeNodeDepth(); + resolveCollisions(); + for (var alpha = 1; iterations > 0; --iterations) { + relaxRightToLeft(alpha *= .99); + resolveCollisions(); + relaxLeftToRight(alpha); + resolveCollisions(); + } + + function initializeNodeDepth() { + var ky = d3.min(nodesByBreadth, function(nodes) { + return (size[1] - (nodes.length - 1) * nodePadding) / d3.sum(nodes, value); + }); + + nodesByBreadth.forEach(function(nodes) { + nodes.forEach(function(node, i) { + node.y = i; + node.dy = node.value * ky; + }); + }); + + links.forEach(function(link) { + link.dy = link.value * ky; + }); + } + + function relaxLeftToRight(alpha) { + nodesByBreadth.forEach(function(nodes, breadth) { + nodes.forEach(function(node) { + if (node.targetLinks.length) { + var y = d3.sum(node.targetLinks, weightedSource) / d3.sum(node.targetLinks, value); + node.y += (y - center(node)) * alpha; + } + }); + }); + + function weightedSource(link) { + return center(link.source) * link.value; + } + } + + function relaxRightToLeft(alpha) { + nodesByBreadth.slice().reverse().forEach(function(nodes) { + nodes.forEach(function(node) { + if (node.sourceLinks.length) { + var y = d3.sum(node.sourceLinks, weightedTarget) / d3.sum(node.sourceLinks, value); + node.y += (y - center(node)) * alpha; + } + }); + }); + + function weightedTarget(link) { + return center(link.target) * link.value; + } + } + + function resolveCollisions() { + nodesByBreadth.forEach(function(nodes) { + var node, + dy, + y0 = 0, + n = nodes.length, + i; + + // Push any overlapping nodes down. + nodes.sort(ascendingDepth); + for (i = 0; i < n; ++i) { + node = nodes[i]; + dy = y0 - node.y; + if (dy > 0) node.y += dy; + y0 = node.y + node.dy + nodePadding; + } + + // If the bottommost node goes outside the bounds, push it back up. + dy = y0 - nodePadding - size[1]; + if (dy > 0) { + y0 = node.y -= dy; + + // Push any overlapping nodes back up. + for (i = n - 2; i >= 0; --i) { + node = nodes[i]; + dy = node.y + node.dy + nodePadding - y0; + if (dy > 0) node.y -= dy; + y0 = node.y; + } + } + }); + } + + function ascendingDepth(a, b) { + return a.y - b.y; + } + } + + function computeLinkDepths() { + nodes.forEach(function(node) { + node.sourceLinks.sort(ascendingTargetDepth); + node.targetLinks.sort(ascendingSourceDepth); + }); + nodes.forEach(function(node) { + var sy = 0, ty = 0; + node.sourceLinks.forEach(function(link) { + link.sy = sy; + sy += link.dy; + }); + node.targetLinks.forEach(function(link) { + link.ty = ty; + ty += link.dy; + }); + }); + + function ascendingSourceDepth(a, b) { + return a.source.y - b.source.y; + } + + function ascendingTargetDepth(a, b) { + return a.target.y - b.target.y; + } + } + + function center(node) { + return node.y + node.dy / 2; + } + + function value(link) { + return link.value; + } + + return sankey; +}; \ No newline at end of file diff --git a/lcopt/static/js/export_chart.js b/lcopt/static/js/export_chart.js index b562237..e7ed527 100644 --- a/lcopt/static/js/export_chart.js +++ b/lcopt/static/js/export_chart.js @@ -59,7 +59,7 @@ function export_StyledSVG(svg_id, filename, height, width){ var data = new XMLSerializer().serializeToString(oDOM); console.log(data); - var imgData = 'data:image/svg+xml;base64,' + btoa(data); + var imgData = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(data))); var canvas = document.getElementById('export_canvas'); diff --git a/lcopt/static/js/mass_flow.js b/lcopt/static/js/mass_flow.js new file mode 100644 index 0000000..ac7f225 --- /dev/null +++ b/lcopt/static/js/mass_flow.js @@ -0,0 +1,208 @@ + +function mass_flow(ps){ + + console.log(bound_data); + + var units = "kg"; + + // set the dimensions and margins of the graph + var margin = {top: 10, right: 10, bottom: 200, left: 10}, + full_width = 1200, + full_height = 500, + width = full_width - margin.left - margin.right, + height = full_height - margin.top - margin.bottom; + + // format variables + var formatNumber = d3.format(",.2f"), // zero decimal places + format = function(d) { return formatNumber(d) + " " + units; }; + color = d3.scaleOrdinal(d3.schemeCategory20); + + + var svg = d3.select("#mass_flow_svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom); + + svg.selectAll("*").remove(); + + svg.append("g") + .attr("transform", + "translate(" + margin.left + "," + margin.top + ")"); + + // Set the sankey diagram properties + var sankey = d3.sankey() + .nodeWidth(6) + .nodePadding(4) + .size([width, height]); + + var path = sankey.link(); + + // load the data + var data = bound_data.results[ps][0].mass_flow; + console.log(data); + + var hierarchy = d3.hierarchy(data, function(d) { + return d.technosphere; + }); + + console.log(hierarchy); + + d3.tree(hierarchy); + + var raw_nodes = hierarchy.descendants(); + + var raw_links = hierarchy.links(); + + var nodes = [], + i = 0, + node_dict = {}; + + raw_nodes.forEach(function(item){ + + + + if(item.data.is_mass && item.data.amount !== 0){ + id = item.data.activity + "_" + item.depth + "_" + item.height; + tag = item.data.tag; + activity = item.data.activity; + if (!item.parent){ + tag = "final"; + activity = "Functional unit"; + } + nodes.push({"node": i, "name": activity, "tag": tag}); + node_dict[id] = i; + i++; + } + + }); + + var links = []; + + raw_links.forEach(function(item){ + + if (item.source.data.is_mass && item.target.data.is_mass && item.target.data.amount !== 0){ + + + value = item.target.data.amount; + + var flip = false; + + if(value < 0){ + value = Math.abs(value); + flip = true; + } + + source_id = item.source.data.activity + "_" + item.source.depth + "_" + item.source.height; + target_id = item.target.data.activity + "_" + item.target.depth + "_" + item.target.height; + if(flip){ + source = node_dict[source_id]; + target = node_dict[target_id]; + }else{ + source = node_dict[target_id]; + target = node_dict[source_id]; + } + + links.push({ + "source": source, + "target": target, + "value": value + }); + } + }); + + //console.log(nodes); + //console.log(raw_nodes); + //console.log(nodes); + //console.log(node_dict); + //console.log(raw_links); + //console.log(links); + + graph = {nodes:nodes, links:links}; + +sankey + .nodes(graph.nodes) + .links(graph.links) + .layout(2); + +// add in the links +var link = svg.append("g").selectAll(".mf_link") + .data(graph.links) + .enter().append("path") + .attr("class", "mf_link") + .attr("d", path) + .style("stroke-width", function(d) { return Math.max(1, d.dy); }) + .sort(function(a, b) { return b.dy - a.dy; }); + +// add the link titles +link.append("title") + .text(function(d) { + return d.source.name + " → " + + d.target.name + "\n" + format(d.value); }); + +// add in the nodes +var node = svg.append("g").selectAll(".mf_node") + .data(graph.nodes) + .enter().append("g") + .attr("class", function(d){return "mf_node tag_" + d.tag;}) + .attr("transform", function(d) { + return "translate(" + d.x + "," + d.y + ")"; }) + .call(d3.drag() + .subject(function(d) { + return d; + }) + .on("start", function() { + this.parentNode.appendChild(this); + }) + .on("drag", dragmove)); + +// add the rectangles for the nodes +node.append("rect") + .attr("height", function(d) { return d.dy; }) + .attr("width", sankey.nodeWidth()) + .style("fill", function(d) { + return d.color = color(d.name.replace(/ .*/, "")); }) + .style("stroke", function(d) { + return d3.rgb(d.color).darker(2); }) + .append("title") + .text(function(d) { + return d.name + "\n" + format(d.value); }); + +// add in the title for the nodes +node.append("text") + .attr("x", -6) + .attr("y", function(d) { return d.dy / 2; }) + .attr("dy", ".35em") + .attr("text-anchor", "end") + .attr("transform", null) + .text(function(d) { return d.name + " (" + format(d.value) + ")"; }) + .filter(function(d) { return d.x < width / 2; }) + .attr("x", 6 + sankey.nodeWidth()) + .attr("text-anchor", "start"); + +// the function for moving the nodes +function dragmove(d) { + d3.select(this) + .attr("transform", + "translate(" + + (d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))) + "," + + (d.y = Math.max( + 0, Math.min(height + margin.bottom - d.dy, d3.event.y)) + ) + ")"); + sankey.relayout(); + link.attr("d", path); +} +} +$(document).ready(function(){ + $('#mass_flow_export_button').click(function(){ + + var margin = {top: 10, right: 10, bottom: 200, left: 10}, + full_width = 1200, + full_height = 500, + width = full_width - margin.left - margin.right, + height = full_height - margin.top - margin.bottom; + + var ps = $('#parameterSetChoice option:selected').text(); + + export_StyledSVG('mass_flow_svg', 'mass_flow_' + ps + '.png', full_height, full_width); + }); + +}); diff --git a/lcopt/static/js/pie2.js b/lcopt/static/js/pie2.js index dbcf60c..6c189e7 100644 --- a/lcopt/static/js/pie2.js +++ b/lcopt/static/js/pie2.js @@ -54,6 +54,7 @@ d3.json("results.json", function(data) { draw_tree(); setup_stack_bar(); update_summary_table(); + mass_flow(0); //draw_sunburst() @@ -461,6 +462,8 @@ function polylineTweenPoints(a){ $('#parameterSetChoice').change(function(){ change2() draw_tree() + console.log($('#parameterSetChoice').val()-1) + mass_flow($('#parameterSetChoice').val()-1) //create_force_layout() }) $('#methodChoice').change(function(){ diff --git a/lcopt/templates/analysis.html b/lcopt/templates/analysis.html index 90506e3..d627a28 100644 --- a/lcopt/templates/analysis.html +++ b/lcopt/templates/analysis.html @@ -8,6 +8,7 @@ + @@ -19,12 +20,15 @@ + + + {% endblock%} {% block content %} @@ -77,6 +81,7 @@

{{args.result_sets.settings.item}} ({{args.result_s
  • Tree
  • Pie Chart
  • Bullseye Hotspot Chart
  • +
  • Mass Flow
  • @@ -208,6 +213,24 @@

    {{args.result_sets.settings.item}} ({{args.result_s +
    +
    +
    + +
    +
    + + +
    + +
    + +
    + +
    + +
    + diff --git a/lcopt/templates/base.html b/lcopt/templates/base.html index a2ce432..ea9e1d4 100644 --- a/lcopt/templates/base.html +++ b/lcopt/templates/base.html @@ -78,6 +78,7 @@
  • Create Functions
  • Settings
  • Results
  • + diff --git a/lcopt/templates/mass_flow.html b/lcopt/templates/mass_flow.html new file mode 100644 index 0000000..e5b7ec7 --- /dev/null +++ b/lcopt/templates/mass_flow.html @@ -0,0 +1,29 @@ +{% extends 'base.html' %} +{% block head_postload %} + + + + + + + + +{% endblock %} + +{% block content %} + + +
    +
    +
    + +

    Mass Flow

    + +
    + +
    +
    +
    + + +{% endblock %}