diff --git a/panoramix/assets/html/base.html b/panoramix/assets/html/base.html index b6b3b9c53fb39..4baa4648e1678 100644 --- a/panoramix/assets/html/base.html +++ b/panoramix/assets/html/base.html @@ -4,7 +4,7 @@ {% block head_css %} {{super()}} - + @@ -13,9 +13,8 @@ {% endblock %} {% block body %} - - {% include 'appbuilder/general/confirm.html' %} - {% include 'appbuilder/general/alert.html' %} + {% include 'appbuilder/general/confirm.html' %} + {% include 'appbuilder/general/alert.html' %} {% block navbar %}
@@ -42,6 +41,6 @@ {% block tail_js %} {{ super() }} - + {% endblock %} diff --git a/panoramix/assets/html/explore.html b/panoramix/assets/html/explore.html index f3b8282f655af..27f49b0a0129d 100644 --- a/panoramix/assets/html/explore.html +++ b/panoramix/assets/html/explore.html @@ -1,11 +1,5 @@ {% extends "refactor/base.html" %} -{% block head_css %} - {{super()}} - - -{% endblock %} - {% block content_fluid %} {% set datasource = viz.datasource %} {% set form = viz.form %} diff --git a/panoramix/assets/html/viz.html b/panoramix/assets/html/viz.html index 7a8cb1e7424af..4b35018beb9e6 100644 --- a/panoramix/assets/html/viz.html +++ b/panoramix/assets/html/viz.html @@ -8,20 +8,4 @@ {% extends 'refactor/explore.html' %} {% endif %} - - {% block head_css %} - {{super()}} - {% for css in viz.css_files %} - - {% endfor %} - {% endblock %} - - - {% block tail %} - {{super()}} - {% for js in viz.js_files %} - - {% endfor %} - {% endblock %} - {% endif %} diff --git a/panoramix/assets/javascripts/explore.js b/panoramix/assets/javascripts/explore.js index 50096be3c5264..aa0b30fff943b 100644 --- a/panoramix/assets/javascripts/explore.js +++ b/panoramix/assets/javascripts/explore.js @@ -1,17 +1,70 @@ +// Javascript for the explorer page +// Init explorer view -> load vis dependencies -> read data (from dynamic html) -> render slice +// nb: to add a new vis, you must also add a Python fn in viz.py + +// css +require('../vendor/pygments.css'); +require('../vendor/bootstrap-toggle/bootstrap-toggle.min.css'); + +// js +var $ = window.$ || require('jquery'); +var px = window.px || require('./modules/panoramix.js'); require('select2'); -require('./vendor/bootstrap-toggle.min.js'); -require('./vendor/select2.sortable.js'); +require('../vendor/bootstrap-toggle/bootstrap-toggle.min.js'); +require('../vendor/select2.sortable.js'); + +// vis sources +var sourceMap = { + area: 'nvd3_vis.js', + bar: 'nvd3_vis.js', + bubble: 'nvd3_vis.js', + big_number: 'big_number.js', + compare: 'nvd3_vis.js', + dist_bar: 'nvd3_vis.js', + directed_force: 'directed_force.js', + filter_box: 'filter_box.js', + heatmap: 'heatmap.js', + iframe: 'iframe.js', + line: 'nvd3_vis.js', + markup: 'markup.js', + para: 'parallel_coordinates.js', + pie: 'nvd3_vis.js', + // pivot_table: undefined, + sankey: 'sankey.js', + sunburst: 'sunburst.js', + table: 'table.js', + word_cloud: 'word_cloud.js', + world_map: 'world_map.js', +}; $(document).ready(function() { px.initExploreView(); + // Dynamically register this visualization + var visType = window.viz_type.value; + var visSource = sourceMap[visType]; + + if (visSource) { + var visFactory = require('../visualizations/' + visSource); + if (typeof visFactory === 'function') { + // @TODO handle px.registerViz here instead of in each file? + px.registerViz(visType, visFactory); + } + } + else { + console.error("require(", visType, ") failed."); + } + var data = $('.slice').data('slice'); var slice = px.Slice(data); + // $('.slice').data('slice', slice); + // call vis render method, which issues ajax px.renderSlice(); + // make checkbox inputs display as toggles $(':checkbox') .addClass('pull-right') .attr("data-onstyle", "default") diff --git a/panoramix/assets/javascripts/modules/panoramix.js b/panoramix/assets/javascripts/modules/panoramix.js index 26512be12f91a..e6924cbe5ecf8 100644 --- a/panoramix/assets/javascripts/modules/panoramix.js +++ b/panoramix/assets/javascripts/modules/panoramix.js @@ -1,3 +1,6 @@ +var $ = window.$ || require('jquery'); +var d3 = window.d3 || require('d3'); + var color = function(){ // Color related utility functions go in this object var bnbColors = [ @@ -7,8 +10,8 @@ var color = function(){ '#ff3339', '#ff1ab1', '#005c66', '#00b3a5', '#55d12e', '#b37e00', '#988b4e', ]; var spectrums = { - 'fire': ['white', 'yellow', 'red', 'black'], 'blue_white_yellow': ['#00d1c1', 'white', '#ffb400'], + 'fire': ['white', 'yellow', 'red', 'black'], 'white_black': ['white', 'black'], 'black_white': ['black', 'white'], } @@ -346,6 +349,7 @@ var px = (function() { }); }); $("#viz_type").change(function() {$("#query").submit();}); + var collapsed_fieldsets = get_collapsed_fieldsets(); for(var i=0; i < collapsed_fieldsets.length; i++){ toggle_fieldset($('legend:contains("' + collapsed_fieldsets[i] + '")'), false); @@ -382,6 +386,7 @@ var px = (function() { $(this).parent().parent().remove(); }); } + $(window).bind("popstate", function(event) { // Browser back button var returnLocation = history.location || document.location; @@ -409,7 +414,7 @@ var px = (function() { $("#query").submit(); } }); - add_filter(); + $(".druidify").click(druidify); function create_choices(term, data) { diff --git a/panoramix/assets/package.json b/panoramix/assets/package.json index af295987e50f0..10a743d6bc7d3 100644 --- a/panoramix/assets/package.json +++ b/panoramix/assets/package.json @@ -38,13 +38,17 @@ "babel-preset-es2015": "^6.3.13", "babel-preset-react": "^6.3.13", "bootstrap": "^3.3.6", + "bootstrap-datepicker": "^1.6.0", "brace": "^0.7.0", "d3": "^3.5.14", "d3-cloud": "^1.2.1", "d3-sankey": "^0.2.1", "d3-tip": "^0.6.7", + "d3.layout.cloud": "^1.2.0", + "exports-loader": "^0.6.3", "gridster": "^0.5.6", - "jquery": "^2.2.0", + "imports-loader": "^0.6.5", + "jquery": "^2.2.1", "jquery-ui": "^1.10.5", "react": "^0.14.7", "react-bootstrap": "^0.28.3", diff --git a/panoramix/static/lib/bootstrap-toggle.min.css b/panoramix/assets/vendor/bootstrap-toggle/bootstrap-toggle.min.css similarity index 100% rename from panoramix/static/lib/bootstrap-toggle.min.css rename to panoramix/assets/vendor/bootstrap-toggle/bootstrap-toggle.min.css diff --git a/panoramix/assets/javascripts/vendor/bootstrap-toggle.min.js b/panoramix/assets/vendor/bootstrap-toggle/bootstrap-toggle.min.js similarity index 100% rename from panoramix/assets/javascripts/vendor/bootstrap-toggle.min.js rename to panoramix/assets/vendor/bootstrap-toggle/bootstrap-toggle.min.js diff --git a/panoramix/static/lib/dataTables/dataTables.bootstrap.css b/panoramix/assets/vendor/dataTables/dataTables.bootstrap.css similarity index 100% rename from panoramix/static/lib/dataTables/dataTables.bootstrap.css rename to panoramix/assets/vendor/dataTables/dataTables.bootstrap.css diff --git a/panoramix/static/lib/dataTables/dataTables.bootstrap.js b/panoramix/assets/vendor/dataTables/dataTables.bootstrap.js similarity index 99% rename from panoramix/static/lib/dataTables/dataTables.bootstrap.js rename to panoramix/assets/vendor/dataTables/dataTables.bootstrap.js index dcc561fcfd90e..122f960761539 100644 --- a/panoramix/static/lib/dataTables/dataTables.bootstrap.js +++ b/panoramix/assets/vendor/dataTables/dataTables.bootstrap.js @@ -125,7 +125,7 @@ DataTable.ext.renderer.pageButton.bootstrap = function ( settings, host, idx, bu }; // IE9 throws an 'unknown error' if document.activeElement is used - // inside an iframe or frame. + // inside an iframe or frame. var activeEl; try { diff --git a/panoramix/static/lib/dataTables/jquery.dataTables.min.css b/panoramix/assets/vendor/dataTables/jquery.dataTables.min.css similarity index 100% rename from panoramix/static/lib/dataTables/jquery.dataTables.min.css rename to panoramix/assets/vendor/dataTables/jquery.dataTables.min.css diff --git a/panoramix/static/lib/dataTables/jquery.dataTables.min.js b/panoramix/assets/vendor/dataTables/jquery.dataTables.min.js similarity index 100% rename from panoramix/static/lib/dataTables/jquery.dataTables.min.js rename to panoramix/assets/vendor/dataTables/jquery.dataTables.min.js diff --git a/panoramix/static/lib/datamaps.all.js b/panoramix/assets/vendor/datamaps/datamaps.all.js similarity index 99% rename from panoramix/static/lib/datamaps.all.js rename to panoramix/assets/vendor/datamaps/datamaps.all.js index e3a9b36d1b5cd..6b27f7781f613 100644 --- a/panoramix/static/lib/datamaps.all.js +++ b/panoramix/assets/vendor/datamaps/datamaps.all.js @@ -12131,8 +12131,8 @@ "scale": [0.036003600360036005, 0.016927109510951093], "translate": [-180, -85.609038] } -} -; + }; + Datamap.prototype.abwTopo = '__ABW__'; Datamap.prototype.afgTopo = '__AFG__'; Datamap.prototype.agoTopo = '__AGO__'; diff --git a/panoramix/static/lib/datamaps.world.min.js b/panoramix/assets/vendor/datamaps/datamaps.world.min.js similarity index 100% rename from panoramix/static/lib/datamaps.world.min.js rename to panoramix/assets/vendor/datamaps/datamaps.world.min.js diff --git a/panoramix/assets/stylesheets/vendor/jquery-ui.min.css b/panoramix/assets/vendor/jquery-ui/jquery-ui.min.css similarity index 100% rename from panoramix/assets/stylesheets/vendor/jquery-ui.min.css rename to panoramix/assets/vendor/jquery-ui/jquery-ui.min.css diff --git a/panoramix/assets/javascripts/vendor/jquery-ui.min.js b/panoramix/assets/vendor/jquery-ui/jquery-ui.min.js similarity index 100% rename from panoramix/assets/javascripts/vendor/jquery-ui.min.js rename to panoramix/assets/vendor/jquery-ui/jquery-ui.min.js diff --git a/panoramix/static/lib/nvd3/nv.d3.css b/panoramix/assets/vendor/nvd3/nv.d3.css similarity index 100% rename from panoramix/static/lib/nvd3/nv.d3.css rename to panoramix/assets/vendor/nvd3/nv.d3.css diff --git a/panoramix/static/lib/nvd3/nv.d3.min.js b/panoramix/assets/vendor/nvd3/nv.d3.min.js similarity index 100% rename from panoramix/static/lib/nvd3/nv.d3.min.js rename to panoramix/assets/vendor/nvd3/nv.d3.min.js diff --git a/panoramix/static/lib/para/d3.parcoords.css b/panoramix/assets/vendor/parallel_coordinates/d3.parcoords.css similarity index 81% rename from panoramix/static/lib/para/d3.parcoords.css rename to panoramix/assets/vendor/parallel_coordinates/d3.parcoords.css index cccc1072d26e0..b53849c3673bf 100644 --- a/panoramix/static/lib/para/d3.parcoords.css +++ b/panoramix/assets/vendor/parallel_coordinates/d3.parcoords.css @@ -54,7 +54,18 @@ clear: left; font-size: 12px; line-height: 18px; height: 18px; margin: 0px; } -.parcoords .row:nth-child(odd) { background: rgba(0,0,0,0.05); } -.parcoords .header { font-weight: bold; } -.parcoords .cell { float: left; overflow: hidden; white-space: nowrap; width: 100px; height: 18px; } -.parcoords .col-0 { width: 180px; } +.parcoords .row:nth-child(odd) { + background: rgba(0,0,0,0.05); +} +.parcoords .header { + font-weight: bold; +} +.parcoords .cell { + float: left; + overflow: hidden; + white-space: nowrap; + width: 100px; height: 18px; +} +.parcoords .col-0 { + width: 180px; +} diff --git a/panoramix/assets/vendor/parallel_coordinates/d3.parcoords.js b/panoramix/assets/vendor/parallel_coordinates/d3.parcoords.js new file mode 100644 index 0000000000000..04095f106e4ab --- /dev/null +++ b/panoramix/assets/vendor/parallel_coordinates/d3.parcoords.js @@ -0,0 +1,2224 @@ +module.exports = function(config) { + var __ = { + data: [], + highlighted: [], + dimensions: [], + dimensionTitles: {}, + dimensionTitleRotation: 0, + types: {}, + brushed: false, + brushedColor: null, + alphaOnBrushed: 0.0, + mode: "default", + rate: 20, + width: 600, + height: 300, + margin: { top: 24, right: 0, bottom: 12, left: 0 }, + nullValueSeparator: "undefined", // set to "top" or "bottom" + nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 }, + color: "#069", + composite: "source-over", + alpha: 0.7, + bundlingStrength: 0.5, + bundleDimension: null, + smoothness: 0.0, + showControlPoints: false, + hideAxis : [] + }; + + extend(__, config); + + var pc = function(selection) { + selection = pc.selection = d3.select(selection); + + __.width = selection[0][0].clientWidth; + __.height = selection[0][0].clientHeight; + + // canvas data layers + ["marks", "foreground", "brushed", "highlight"].forEach(function(layer) { + canvas[layer] = selection + .append("canvas") + .attr("class", layer)[0][0]; + ctx[layer] = canvas[layer].getContext("2d"); + }); + + // svg tick and brush layers + pc.svg = selection + .append("svg") + .attr("width", __.width) + .attr("height", __.height) + .append("svg:g") + .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")"); + + return pc; + }; + var events = d3.dispatch.apply(this,["render", "resize", "highlight", "brush", "brushend", "axesreorder"].concat(d3.keys(__))), + w = function() { return __.width - __.margin.right - __.margin.left; }, + h = function() { return __.height - __.margin.top - __.margin.bottom; }, + flags = { + brushable: false, + reorderable: false, + axes: false, + interactive: false, + debug: false + }, + xscale = d3.scale.ordinal(), + yscale = {}, + dragging = {}, + line = d3.svg.line(), + axis = d3.svg.axis().orient("left").ticks(5), + g, // groups for axes, brushes + ctx = {}, + canvas = {}, + clusterCentroids = []; + + // side effects for setters + var side_effects = d3.dispatch.apply(this,d3.keys(__)) + .on("composite", function(d) { + ctx.foreground.globalCompositeOperation = d.value; + ctx.brushed.globalCompositeOperation = d.value; + }) + .on("alpha", function(d) { + ctx.foreground.globalAlpha = d.value; + ctx.brushed.globalAlpha = d.value; + }) + .on("brushedColor", function (d) { + ctx.brushed.strokeStyle = d.value; + }) + .on("width", function(d) { pc.resize(); }) + .on("height", function(d) { pc.resize(); }) + .on("margin", function(d) { pc.resize(); }) + .on("rate", function(d) { + brushedQueue.rate(d.value); + foregroundQueue.rate(d.value); + }) + .on("dimensions", function(d) { + xscale.domain(__.dimensions); + if (flags.interactive){pc.render().updateAxes();} + }) + .on("bundleDimension", function(d) { + if (!__.dimensions.length) pc.detectDimensions(); + if (!(__.dimensions[0] in yscale)) pc.autoscale(); + if (typeof d.value === "number") { + if (d.value < __.dimensions.length) { + __.bundleDimension = __.dimensions[d.value]; + } else if (d.value < __.hideAxis.length) { + __.bundleDimension = __.hideAxis[d.value]; + } + } else { + __.bundleDimension = d.value; + } + + __.clusterCentroids = compute_cluster_centroids(__.bundleDimension); + }) + .on("hideAxis", function(d) { + if (!__.dimensions.length) pc.detectDimensions(); + pc.dimensions(without(__.dimensions, d.value)); + }); + + // expose the state of the chart + pc.state = __; + pc.flags = flags; + + // create getter/setters + getset(pc, __, events); + + // expose events + d3.rebind(pc, events, "on"); + + // getter/setter with event firing + function getset(obj,state,events) { + d3.keys(state).forEach(function(key) { + obj[key] = function(x) { + if (!arguments.length) { + return state[key]; + } + var old = state[key]; + state[key] = x; + side_effects[key].call(pc,{"value": x, "previous": old}); + events[key].call(pc,{"value": x, "previous": old}); + return obj; + }; + }); + }; + + function extend(target, source) { + for (var key in source) { + target[key] = source[key]; + } + return target; + }; + + function without(arr, item) { + return arr.filter(function(elem) { return item.indexOf(elem) === -1; }) + }; + /** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */ + function getRange() { + if (__.nullValueSeparator=="bottom") { + return [h()+1-__.nullValueSeparatorPadding.bottom-__.nullValueSeparatorPadding.top, 1]; + } else if (__.nullValueSeparator=="top") { + return [h()+1, 1+__.nullValueSeparatorPadding.bottom+__.nullValueSeparatorPadding.top]; + } + return [h()+1, 1]; + }; + + pc.autoscale = function() { + // yscale + var defaultScales = { + "date": function(k) { + var extent = d3.extent(__.data, function(d) { + return d[k] ? d[k].getTime() : null; + }); + + // special case if single value + if (extent[0] === extent[1]) { + return d3.scale.ordinal() + .domain([extent[0]]) + .rangePoints(getRange()); + } + + return d3.time.scale() + .domain(extent) + .range(getRange()); + }, + "number": function(k) { + var extent = d3.extent(__.data, function(d) { return +d[k]; }); + + // special case if single value + if (extent[0] === extent[1]) { + return d3.scale.ordinal() + .domain([extent[0]]) + .rangePoints(getRange()); + } + + return d3.scale.linear() + .domain(extent) + .range(getRange()); + }, + "string": function(k) { + var counts = {}, + domain = []; + + // Let's get the count for each value so that we can sort the domain based + // on the number of items for each value. + __.data.map(function(p) { + if (p[k] === undefined && __.nullValueSeparator!== "undefined"){ + return; // null values will be drawn beyond the horizontal null value separator! + } + if (counts[p[k]] === undefined) { + counts[p[k]] = 1; + } else { + counts[p[k]] = counts[p[k]] + 1; + } + }); + + domain = Object.getOwnPropertyNames(counts).sort(function(a, b) { + return counts[a] - counts[b]; + }); + + return d3.scale.ordinal() + .domain(domain) + .rangePoints(getRange()); + } + }; + + __.dimensions.forEach(function(k) { + yscale[k] = defaultScales[__.types[k]](k); + }); + + __.hideAxis.forEach(function(k) { + yscale[k] = defaultScales[__.types[k]](k); + }); + + // xscale + xscale.rangePoints([0, w()], 1); + + // canvas sizes + pc.selection.selectAll("canvas") + .style("margin-top", __.margin.top + "px") + .style("margin-left", __.margin.left + "px") + .attr("width", w()+2) + .attr("height", h()+2); + + // default styles, needs to be set when canvas width changes + ctx.foreground.strokeStyle = __.color; + ctx.foreground.lineWidth = 1.4; + ctx.foreground.globalCompositeOperation = __.composite; + ctx.foreground.globalAlpha = __.alpha; + ctx.brushed.strokeStyle = __.brushedColor; + ctx.brushed.lineWidth = 1.4; + ctx.brushed.globalCompositeOperation = __.composite; + ctx.brushed.globalAlpha = __.alpha; + ctx.highlight.lineWidth = 3; + + return this; + }; + + pc.scale = function(d, domain) { + yscale[d].domain(domain); + + return this; + }; + + pc.flip = function(d) { + //yscale[d].domain().reverse(); // does not work + yscale[d].domain(yscale[d].domain().reverse()); // works + + return this; + }; + + pc.commonScale = function(global, type) { + var t = type || "number"; + if (typeof global === 'undefined') { + global = true; + } + + // scales of the same type + var scales = __.dimensions.concat(__.hideAxis).filter(function(p) { + return __.types[p] == t; + }); + + if (global) { + var extent = d3.extent(scales.map(function(p,i) { + return yscale[p].domain(); + }).reduce(function(a,b) { + return a.concat(b); + })); + + scales.forEach(function(d) { + yscale[d].domain(extent); + }); + + } else { + scales.forEach(function(k) { + yscale[k].domain(d3.extent(__.data, function(d) { return +d[k]; })); + }); + } + + // update centroids + if (__.bundleDimension !== null) { + pc.bundleDimension(__.bundleDimension); + } + + return this; + }; + pc.detectDimensions = function() { + pc.types(pc.detectDimensionTypes(__.data)); + pc.dimensions(d3.keys(pc.types())); + return this; + }; + + // a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable + pc.toType = function(v) { + return ({}).toString.call(v).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); + }; + + // try to coerce to number before returning type + pc.toTypeCoerceNumbers = function(v) { + if ((parseFloat(v) == v) && (v != null)) { + return "number"; + } + return pc.toType(v); + }; + + // attempt to determine types of each dimension based on first row of data + pc.detectDimensionTypes = function(data) { + var types = {}; + d3.keys(data[0]) + .forEach(function(col) { + types[col] = pc.toTypeCoerceNumbers(data[0][col]); + }); + return types; + }; + pc.render = function() { + // try to autodetect dimensions and create scales + if (!__.dimensions.length) pc.detectDimensions(); + if (!(__.dimensions[0] in yscale)) pc.autoscale(); + + pc.render[__.mode](); + + events.render.call(this); + return this; + }; + + pc.renderBrushed = function() { + if (!__.dimensions.length) pc.detectDimensions(); + if (!(__.dimensions[0] in yscale)) pc.autoscale(); + + pc.renderBrushed[__.mode](); + + events.render.call(this); + return this; + }; + + function isBrushed() { + if (__.brushed && __.brushed.length !== __.data.length) + return true; + + var object = brush.currentMode().brushState(); + + for (var key in object) { + if (object.hasOwnProperty(key)) { + return true; + } + } + return false; + }; + + pc.render.default = function() { + pc.clear('foreground'); + pc.clear('highlight'); + + pc.renderBrushed.default(); + + __.data.forEach(path_foreground); + }; + + var foregroundQueue = d3.renderQueue(path_foreground) + .rate(50) + .clear(function() { + pc.clear('foreground'); + pc.clear('highlight'); + }); + + pc.render.queue = function() { + pc.renderBrushed.queue(); + + foregroundQueue(__.data); + }; + + pc.renderBrushed.default = function() { + pc.clear('brushed'); + + if (isBrushed()) { + __.brushed.forEach(path_brushed); + } + }; + + var brushedQueue = d3.renderQueue(path_brushed) + .rate(50) + .clear(function() { + pc.clear('brushed'); + }); + + pc.renderBrushed.queue = function() { + if (isBrushed()) { + brushedQueue(__.brushed); + } else { + brushedQueue([]); // This is needed to clear the currently brushed items + } + }; + function compute_cluster_centroids(d) { + + var clusterCentroids = d3.map(); + var clusterCounts = d3.map(); + // determine clusterCounts + __.data.forEach(function(row) { + var scaled = yscale[d](row[d]); + if (!clusterCounts.has(scaled)) { + clusterCounts.set(scaled, 0); + } + var count = clusterCounts.get(scaled); + clusterCounts.set(scaled, count + 1); + }); + + __.data.forEach(function(row) { + __.dimensions.map(function(p, i) { + var scaled = yscale[d](row[d]); + if (!clusterCentroids.has(scaled)) { + var map = d3.map(); + clusterCentroids.set(scaled, map); + } + if (!clusterCentroids.get(scaled).has(p)) { + clusterCentroids.get(scaled).set(p, 0); + } + var value = clusterCentroids.get(scaled).get(p); + value += yscale[p](row[p]) / clusterCounts.get(scaled); + clusterCentroids.get(scaled).set(p, value); + }); + }); + + return clusterCentroids; + + } + + function compute_centroids(row) { + var centroids = []; + + var p = __.dimensions; + var cols = p.length; + var a = 0.5; // center between axes + for (var i = 0; i < cols; ++i) { + // centroids on 'real' axes + var x = position(p[i]); + var y = yscale[p[i]](row[p[i]]); + centroids.push($V([x, y])); + + // centroids on 'virtual' axes + if (i < cols - 1) { + var cx = x + a * (position(p[i+1]) - x); + var cy = y + a * (yscale[p[i+1]](row[p[i+1]]) - y); + if (__.bundleDimension !== null) { + var leftCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i]); + var rightCentroid = __.clusterCentroids.get(yscale[__.bundleDimension](row[__.bundleDimension])).get(p[i+1]); + var centroid = 0.5 * (leftCentroid + rightCentroid); + cy = centroid + (1 - __.bundlingStrength) * (cy - centroid); + } + centroids.push($V([cx, cy])); + } + } + + return centroids; + } + + function compute_control_points(centroids) { + + var cols = centroids.length; + var a = __.smoothness; + var cps = []; + + cps.push(centroids[0]); + cps.push($V([centroids[0].e(1) + a*2*(centroids[1].e(1)-centroids[0].e(1)), centroids[0].e(2)])); + for (var col = 1; col < cols - 1; ++col) { + var mid = centroids[col]; + var left = centroids[col - 1]; + var right = centroids[col + 1]; + + var diff = left.subtract(right); + cps.push(mid.add(diff.x(a))); + cps.push(mid); + cps.push(mid.subtract(diff.x(a))); + } + cps.push($V([centroids[cols-1].e(1) + a*2*(centroids[cols-2].e(1)-centroids[cols-1].e(1)), centroids[cols-1].e(2)])); + cps.push(centroids[cols - 1]); + + return cps; + + }; + + pc.shadows = function() { + flags.shadows = true; + pc.alphaOnBrushed(0.1); + pc.render(); + return this; + }; + + // draw dots with radius r on the axis line where data intersects + pc.axisDots = function(r) { + var r = r || 0.1; + var ctx = pc.ctx.marks; + var startAngle = 0; + var endAngle = 2 * Math.PI; + ctx.globalAlpha = d3.min([ 1 / Math.pow(__.data.length, 1 / 2), 1 ]); + __.data.forEach(function(d) { + __.dimensions.map(function(p, i) { + ctx.beginPath(); + ctx.arc(position(p), yscale[p](d[p]), r, startAngle, endAngle); + ctx.stroke(); + ctx.fill(); + }); + }); + return this; + }; + + // draw single cubic bezier curve + function single_curve(d, ctx) { + + var centroids = compute_centroids(d); + var cps = compute_control_points(centroids); + + ctx.moveTo(cps[0].e(1), cps[0].e(2)); + for (var i = 1; i < cps.length; i += 3) { + if (__.showControlPoints) { + for (var j = 0; j < 3; j++) { + ctx.fillRect(cps[i+j].e(1), cps[i+j].e(2), 2, 2); + } + } + ctx.bezierCurveTo(cps[i].e(1), cps[i].e(2), cps[i+1].e(1), cps[i+1].e(2), cps[i+2].e(1), cps[i+2].e(2)); + } + }; + + // draw single polyline + function color_path(d, ctx) { + ctx.beginPath(); + if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) { + single_curve(d, ctx); + } else { + single_path(d, ctx); + } + ctx.stroke(); + }; + + // draw many polylines of the same color + function paths(data, ctx) { + ctx.clearRect(-1, -1, w() + 2, h() + 2); + ctx.beginPath(); + data.forEach(function(d) { + if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) { + single_curve(d, ctx); + } else { + single_path(d, ctx); + } + }); + ctx.stroke(); + }; + + // returns the y-position just beyond the separating null value line + function getNullPosition() { + if (__.nullValueSeparator=="bottom") { + return h()+1; + } else if (__.nullValueSeparator=="top") { + return 1; + } else { + console.log("A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'."); + } + return h()+1; + }; + + function single_path(d, ctx) { + __.dimensions.map(function(p, i) { + if (i == 0) { + ctx.moveTo(position(p), typeof d[p] =='undefined' ? getNullPosition() : yscale[p](d[p])); + } else { + ctx.lineTo(position(p), typeof d[p] =='undefined' ? getNullPosition() : yscale[p](d[p])); + } + }); + }; + + function path_brushed(d, i) { + if (__.brushedColor !== null) { + ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i); + } else { + ctx.brushed.strokeStyle = d3.functor(__.color)(d, i); + } + return color_path(d, ctx.brushed) + }; + + function path_foreground(d, i) { + ctx.foreground.strokeStyle = d3.functor(__.color)(d, i); + return color_path(d, ctx.foreground); + }; + + function path_highlight(d, i) { + ctx.highlight.strokeStyle = d3.functor(__.color)(d, i); + return color_path(d, ctx.highlight); + }; + pc.clear = function(layer) { + ctx[layer].clearRect(0, 0, w() + 2, h() + 2); + + // This will make sure that the foreground items are transparent + // without the need for changing the opacity style of the foreground canvas + // as this would stop the css styling from working + if(layer === "brushed" && isBrushed()) { + ctx.brushed.fillStyle = pc.selection.style("background-color"); + ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed; + ctx.brushed.fillRect(0, 0, w() + 2, h() + 2); + ctx.brushed.globalAlpha = __.alpha; + } + return this; + }; + + d3.rebind(pc, axis, "ticks", "orient", "tickValues", "tickSubdivide", "tickSize", "tickPadding", "tickFormat"); + + function flipAxisAndUpdatePCP(dimension) { + var g = pc.svg.selectAll(".dimension"); + + pc.flip(dimension); + + d3.select(this.parentElement) + .transition() + .duration(1100) + .call(axis.scale(yscale[dimension])); + + pc.render(); + } + + function rotateLabels() { + var delta = d3.event.deltaY; + delta = delta < 0 ? -5 : delta; + delta = delta > 0 ? 5 : delta; + + __.dimensionTitleRotation += delta; + pc.svg.selectAll("text.label") + .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")"); + d3.event.preventDefault(); + } + + function dimensionLabels(d) { + return d in __.dimensionTitles ? __.dimensionTitles[d] : d; // dimension display names + } + + pc.createAxes = function() { + if (g) pc.removeAxes(); + + // Add a group element for each dimension. + g = pc.svg.selectAll(".dimension") + .data(__.dimensions, function(d) { return d; }) + .enter().append("svg:g") + .attr("class", "dimension") + .attr("transform", function(d) { return "translate(" + xscale(d) + ")"; }); + + // Add an axis and title. + g.append("svg:g") + .attr("class", "axis") + .attr("transform", "translate(0,0)") + .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); }) + .append("svg:text") + .attr({ + "text-anchor": "middle", + "y": 0, + "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")", + "x": 0, + "class": "label" + }) + .text(dimensionLabels) + .on("dblclick", flipAxisAndUpdatePCP) + .on("wheel", rotateLabels); + + if (__.nullValueSeparator=="top") { + pc.svg.append("line") + .attr("x1", 0) + .attr("y1", 1+__.nullValueSeparatorPadding.top) + .attr("x2", w()) + .attr("y2", 1+__.nullValueSeparatorPadding.top) + .attr("stroke-width", 1) + .attr("stroke", "#777") + .attr("fill", "none") + .attr("shape-rendering", "crispEdges"); + } else if (__.nullValueSeparator=="bottom") { + pc.svg.append("line") + .attr("x1", 0) + .attr("y1", h()+1-__.nullValueSeparatorPadding.bottom) + .attr("x2", w()) + .attr("y2", h()+1-__.nullValueSeparatorPadding.bottom) + .attr("stroke-width", 1) + .attr("stroke", "#777") + .attr("fill", "none") + .attr("shape-rendering", "crispEdges"); + } + + flags.axes= true; + return this; + }; + + pc.removeAxes = function() { + g.remove(); + return this; + }; + + pc.updateAxes = function() { + var g_data = pc.svg.selectAll(".dimension").data(__.dimensions); + + // Enter + g_data.enter().append("svg:g") + .attr("class", "dimension") + .attr("transform", function(p) { return "translate(" + position(p) + ")"; }) + .style("opacity", 0) + .append("svg:g") + .attr("class", "axis") + .attr("transform", "translate(0,0)") + .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); }) + .append("svg:text") + .attr({ + "text-anchor": "middle", + "y": 0, + "transform": "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")", + "x": 0, + "class": "label" + }) + .text(dimensionLabels) + .on("dblclick", flipAxisAndUpdatePCP) + .on("wheel", rotateLabels); + + // Update + g_data.attr("opacity", 0); + g_data.select(".axis") + .transition() + .duration(1100) + .each(function(d) { + d3.select(this).call(axis.scale(yscale[d])); + }); + g_data.select(".label") + .transition() + .duration(1100) + .text(dimensionLabels) + .attr("transform", "translate(0,-5) rotate(" + __.dimensionTitleRotation + ")"); + + // Exit + g_data.exit().remove(); + + g = pc.svg.selectAll(".dimension"); + g.transition().duration(1100) + .attr("transform", function(p) { return "translate(" + position(p) + ")"; }) + .style("opacity", 1); + + pc.svg.selectAll(".axis") + .transition() + .duration(1100) + .each(function(d) { d3.select(this).call(axis.scale(yscale[d])); }); + + if (flags.brushable) pc.brushable(); + if (flags.reorderable) pc.reorderable(); + if (pc.brushMode() !== "None") { + var mode = pc.brushMode(); + pc.brushMode("None"); + pc.brushMode(mode); + } + return this; + }; + + // Jason Davies, http://bl.ocks.org/1341281 + pc.reorderable = function() { + if (!g) pc.createAxes(); + + g.style("cursor", "move") + .call(d3.behavior.drag() + .on("dragstart", function(d) { + dragging[d] = this.__origin__ = xscale(d); + }) + .on("drag", function(d) { + dragging[d] = Math.min(w(), Math.max(0, this.__origin__ += d3.event.dx)); + __.dimensions.sort(function(a, b) { return position(a) - position(b); }); + xscale.domain(__.dimensions); + pc.render(); + g.attr("transform", function(d) { return "translate(" + position(d) + ")"; }); + }) + .on("dragend", function(d) { + // Let's see if the order has changed and send out an event if so. + var i = 0, + j = __.dimensions.indexOf(d), + elem = this, + parent = this.parentElement; + + while((elem = elem.previousElementSibling) != null) ++i; + if (i !== j) { + events.axesreorder.call(pc, __.dimensions); + // We now also want to reorder the actual dom elements that represent + // the axes. That is, the g.dimension elements. If we don't do this, + // we get a weird and confusing transition when updateAxes is called. + // This is due to the fact that, initially the nth g.dimension element + // represents the nth axis. However, after a manual reordering, + // without reordering the dom elements, the nth dom elements no longer + // necessarily represents the nth axis. + // + // i is the original index of the dom element + // j is the new index of the dom element + if (i > j) { // Element moved left + parent.insertBefore(this, parent.children[j - 1]); + } else { // Element moved right + if ((j + 1) < parent.children.length) { + parent.insertBefore(this, parent.children[j + 1]); + } else { + parent.appendChild(this); + } + } + } + + delete this.__origin__; + delete dragging[d]; + d3.select(this).transition().attr("transform", "translate(" + xscale(d) + ")"); + pc.render(); + })); + flags.reorderable = true; + return this; + }; + + // Reorder dimensions, such that the highest value (visually) is on the left and + // the lowest on the right. Visual values are determined by the data values in + // the given row. + pc.reorder = function(rowdata) { + var dims = __.dimensions.slice(0); + __.dimensions.sort(function(a, b) { + var pixelDifference = yscale[a](rowdata[a]) - yscale[b](rowdata[b]); + + // Array.sort is not necessarily stable, this means that if pixelDifference is zero + // the ordering of dimensions might change unexpectedly. This is solved by sorting on + // variable name in that case. + if (pixelDifference === 0) { + return a.localeCompare(b); + } // else + return pixelDifference; + }); + + // NOTE: this is relatively cheap given that: + // number of dimensions < number of data items + // Thus we check equality of order to prevent rerendering when this is the case. + var reordered = false; + dims.some(function(val, index) { + reordered = val !== __.dimensions[index]; + return reordered; + }); + + if (reordered) { + xscale.domain(__.dimensions); + var highlighted = __.highlighted.slice(0); + pc.unhighlight(); + + g.transition() + .duration(1500) + .attr("transform", function(d) { + return "translate(" + xscale(d) + ")"; + }); + pc.render(); + + // pc.highlight() does not check whether highlighted is length zero, so we do that here. + if (highlighted.length !== 0) { + pc.highlight(highlighted); + } + } + } + + // pairs of adjacent dimensions + pc.adjacent_pairs = function(arr) { + var ret = []; + for (var i = 0; i < arr.length-1; i++) { + ret.push([arr[i],arr[i+1]]); + }; + return ret; + }; + + var brush = { + modes: { + "None": { + install: function(pc) {}, // Nothing to be done. + uninstall: function(pc) {}, // Nothing to be done. + selected: function() { return []; }, // Nothing to return + brushState: function() { return {}; } + } + }, + mode: "None", + predicate: "AND", + currentMode: function() { + return this.modes[this.mode]; + } + }; + + // This function can be used for 'live' updates of brushes. That is, during the + // specification of a brush, this method can be called to update the view. + // + // @param newSelection - The new set of data items that is currently contained + // by the brushes + function brushUpdated(newSelection) { + __.brushed = newSelection; + events.brush.call(pc,__.brushed); + pc.renderBrushed(); + } + + function brushPredicate(predicate) { + if (!arguments.length) { return brush.predicate; } + + predicate = String(predicate).toUpperCase(); + if (predicate !== "AND" && predicate !== "OR") { + throw "Invalid predicate " + predicate; + } + + brush.predicate = predicate; + __.brushed = brush.currentMode().selected(); + pc.renderBrushed(); + return pc; + } + + pc.brushModes = function() { + return Object.getOwnPropertyNames(brush.modes); + }; + + pc.brushMode = function(mode) { + if (arguments.length === 0) { + return brush.mode; + } + + if (pc.brushModes().indexOf(mode) === -1) { + throw "pc.brushmode: Unsupported brush mode: " + mode; + } + + // Make sure that we don't trigger unnecessary events by checking if the mode + // actually changes. + if (mode !== brush.mode) { + // When changing brush modes, the first thing we need to do is clearing any + // brushes from the current mode, if any. + if (brush.mode !== "None") { + pc.brushReset(); + } + + // Next, we need to 'uninstall' the current brushMode. + brush.modes[brush.mode].uninstall(pc); + // Finally, we can install the requested one. + brush.mode = mode; + brush.modes[brush.mode].install(); + if (mode === "None") { + delete pc.brushPredicate; + } else { + pc.brushPredicate = brushPredicate; + } + } + + return pc; + }; + + // brush mode: 1D-Axes + + (function() { + var brushes = {}; + + function is_brushed(p) { + return !brushes[p].empty(); + } + + // data within extents + function selected() { + var actives = __.dimensions.filter(is_brushed), + extents = actives.map(function(p) { return brushes[p].extent(); }); + + // We don't want to return the full data set when there are no axes brushed. + // Actually, when there are no axes brushed, by definition, no items are + // selected. So, let's avoid the filtering and just return false. + //if (actives.length === 0) return false; + + // Resolves broken examples for now. They expect to get the full dataset back from empty brushes + if (actives.length === 0) return __.data; + + // test if within range + var within = { + "date": function(d,p,dimension) { + if (typeof yscale[p].rangePoints === "function") { // if it is ordinal + return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1] + } else { + return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1] + } + }, + "number": function(d,p,dimension) { + if (typeof yscale[p].rangePoints === "function") { // if it is ordinal + return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1] + } else { + return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1] + } + }, + "string": function(d,p,dimension) { + return extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1] + } + }; + + return __.data + .filter(function(d) { + switch(brush.predicate) { + case "AND": + return actives.every(function(p, dimension) { + return within[__.types[p]](d,p,dimension); + }); + case "OR": + return actives.some(function(p, dimension) { + return within[__.types[p]](d,p,dimension); + }); + default: + throw "Unknown brush predicate " + __.brushPredicate; + } + }); + }; + + function brushExtents(extents) { + if(typeof(extents) === 'undefined') + { + var extents = {}; + __.dimensions.forEach(function(d) { + var brush = brushes[d]; + if (brush !== undefined && !brush.empty()) { + var extent = brush.extent(); + extent.sort(d3.ascending); + extents[d] = extent; + } + }); + return extents; + } + else + { + //first get all the brush selections + var brushSelections = {}; + g.selectAll('.brush') + .each(function(d) { + brushSelections[d] = d3.select(this); + + }); + + // loop over each dimension and update appropriately (if it was passed in through extents) + __.dimensions.forEach(function(d) { + if (extents[d] === undefined){ + return; + } + + var brush = brushes[d]; + if (brush !== undefined) { + //update the extent + brush.extent(extents[d]); + + //redraw the brush + brush(brushSelections[d]); + + //fire some events + brush.event(brushSelections[d]); + } + }); + + //redraw the chart + pc.renderBrushed(); + } + } + function brushFor(axis) { + var brush = d3.svg.brush(); + + brush + .y(yscale[axis]) + .on("brushstart", function() { + if(d3.event.sourceEvent !== null) { + d3.event.sourceEvent.stopPropagation(); + } + }) + .on("brush", function() { + brushUpdated(selected()); + }) + .on("brushend", function() { + events.brushend.call(pc, __.brushed); + }); + + brushes[axis] = brush; + return brush; + }; + function brushReset(dimension) { + __.brushed = false; + if (g) { + g.selectAll('.brush') + .each(function(d) { + d3.select(this).call( + brushes[d].clear() + ); + }); + pc.renderBrushed(); + } + return this; + }; + + function install() { + if (!g) pc.createAxes(); + + // Add and store a brush for each axis. + g.append("svg:g") + .attr("class", "brush") + .each(function(d) { + d3.select(this).call(brushFor(d)); + }) + .selectAll("rect") + .style("visibility", null) + .attr("x", -15) + .attr("width", 30); + + pc.brushExtents = brushExtents; + pc.brushReset = brushReset; + return pc; + }; + + brush.modes["1D-axes"] = { + install: install, + uninstall: function() { + g.selectAll(".brush").remove(); + brushes = {}; + delete pc.brushExtents; + delete pc.brushReset; + }, + selected: selected, + brushState: brushExtents + } + })(); + // brush mode: 2D-strums + // bl.ocks.org/syntagmatic/5441022 + + (function() { + var strums = {}, + strumRect; + + function drawStrum(strum, activePoint) { + var svg = pc.selection.select("svg").select("g#strums"), + id = strum.dims.i, + points = [strum.p1, strum.p2], + line = svg.selectAll("line#strum-" + id).data([strum]), + circles = svg.selectAll("circle#strum-" + id).data(points), + drag = d3.behavior.drag(); + + line.enter() + .append("line") + .attr("id", "strum-" + id) + .attr("class", "strum"); + + line + .attr("x1", function(d) { return d.p1[0]; }) + .attr("y1", function(d) { return d.p1[1]; }) + .attr("x2", function(d) { return d.p2[0]; }) + .attr("y2", function(d) { return d.p2[1]; }) + .attr("stroke", "black") + .attr("stroke-width", 2); + + drag + .on("drag", function(d, i) { + var ev = d3.event; + i = i + 1; + strum["p" + i][0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX); + strum["p" + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY); + drawStrum(strum, i - 1); + }) + .on("dragend", onDragEnd()); + + circles.enter() + .append("circle") + .attr("id", "strum-" + id) + .attr("class", "strum"); + + circles + .attr("cx", function(d) { return d[0]; }) + .attr("cy", function(d) { return d[1]; }) + .attr("r", 5) + .style("opacity", function(d, i) { + return (activePoint !== undefined && i === activePoint) ? 0.8 : 0; + }) + .on("mouseover", function() { + d3.select(this).style("opacity", 0.8); + }) + .on("mouseout", function() { + d3.select(this).style("opacity", 0); + }) + .call(drag); + } + + function dimensionsForPoint(p) { + var dims = { i: -1, left: undefined, right: undefined }; + __.dimensions.some(function(dim, i) { + if (xscale(dim) < p[0]) { + var next = __.dimensions[i + 1]; + dims.i = i; + dims.left = dim; + dims.right = next; + return false; + } + return true; + }); + + if (dims.left === undefined) { + // Event on the left side of the first axis. + dims.i = 0; + dims.left = __.dimensions[0]; + dims.right = __.dimensions[1]; + } else if (dims.right === undefined) { + // Event on the right side of the last axis + dims.i = __.dimensions.length - 1; + dims.right = dims.left; + dims.left = __.dimensions[__.dimensions.length - 2]; + } + + return dims; + } + + function onDragStart() { + // First we need to determine between which two axes the sturm was started. + // This will determine the freedom of movement, because a strum can + // logically only happen between two axes, so no movement outside these axes + // should be allowed. + return function() { + var p = d3.mouse(strumRect[0][0]), + dims, + strum; + + p[0] = p[0] - __.margin.left; + p[1] = p[1] - __.margin.top; + + dims = dimensionsForPoint(p), + strum = { + p1: p, + dims: dims, + minX: xscale(dims.left), + maxX: xscale(dims.right), + minY: 0, + maxY: h() + }; + + strums[dims.i] = strum; + strums.active = dims.i; + + // Make sure that the point is within the bounds + strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX); + strum.p2 = strum.p1.slice(); + }; + } + + function onDrag() { + return function() { + var ev = d3.event, + strum = strums[strums.active]; + + // Make sure that the point is within the bounds + strum.p2[0] = Math.min(Math.max(strum.minX + 1, ev.x - __.margin.left), strum.maxX); + strum.p2[1] = Math.min(Math.max(strum.minY, ev.y - __.margin.top), strum.maxY); + drawStrum(strum, 1); + }; + } + + function containmentTest(strum, width) { + var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX], + p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX], + m1 = 1 - width / p1[0], + b1 = p1[1] * (1 - m1), + m2 = 1 - width / p2[0], + b2 = p2[1] * (1 - m2); + + // test if point falls between lines + return function(p) { + var x = p[0], + y = p[1], + y1 = m1 * x + b1, + y2 = m2 * x + b2; + + if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) { + return true; + } + + return false; + }; + } + + function selected() { + var ids = Object.getOwnPropertyNames(strums), + brushed = __.data; + + // Get the ids of the currently active strums. + ids = ids.filter(function(d) { + return !isNaN(d); + }); + + function crossesStrum(d, id) { + var strum = strums[id], + test = containmentTest(strum, strums.width(id)), + d1 = strum.dims.left, + d2 = strum.dims.right, + y1 = yscale[d1], + y2 = yscale[d2], + point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX]; + return test(point); + } + + if (ids.length === 0) { return brushed; } + + return brushed.filter(function(d) { + switch(brush.predicate) { + case "AND": + return ids.every(function(id) { return crossesStrum(d, id); }); + case "OR": + return ids.some(function(id) { return crossesStrum(d, id); }); + default: + throw "Unknown brush predicate " + __.brushPredicate; + } + }); + } + + function removeStrum() { + var strum = strums[strums.active], + svg = pc.selection.select("svg").select("g#strums"); + + delete strums[strums.active]; + strums.active = undefined; + svg.selectAll("line#strum-" + strum.dims.i).remove(); + svg.selectAll("circle#strum-" + strum.dims.i).remove(); + } + + function onDragEnd() { + return function() { + var brushed = __.data, + strum = strums[strums.active]; + + // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is + // considered a drag without move. So we have to deal with that case + if (strum && strum.p1[0] === strum.p2[0] && strum.p1[1] === strum.p2[1]) { + removeStrum(strums); + } + + brushed = selected(strums); + strums.active = undefined; + __.brushed = brushed; + pc.renderBrushed(); + events.brushend.call(pc, __.brushed); + }; + } + + function brushReset(strums) { + return function() { + var ids = Object.getOwnPropertyNames(strums).filter(function(d) { + return !isNaN(d); + }); + + ids.forEach(function(d) { + strums.active = d; + removeStrum(strums); + }); + onDragEnd(strums)(); + }; + } + + function install() { + var drag = d3.behavior.drag(); + + // Map of current strums. Strums are stored per segment of the PC. A segment, + // being the area between two axes. The left most area is indexed at 0. + strums.active = undefined; + // Returns the width of the PC segment where currently a strum is being + // placed. NOTE: even though they are evenly spaced in our current + // implementation, we keep for when non-even spaced segments are supported as + // well. + strums.width = function(id) { + var strum = strums[id]; + + if (strum === undefined) { + return undefined; + } + + return strum.maxX - strum.minX; + }; + + pc.on("axesreorder.strums", function() { + var ids = Object.getOwnPropertyNames(strums).filter(function(d) { + return !isNaN(d); + }); + + // Checks if the first dimension is directly left of the second dimension. + function consecutive(first, second) { + var length = __.dimensions.length; + return __.dimensions.some(function(d, i) { + return (d === first) + ? i + i < length && __.dimensions[i + 1] === second + : false; + }); + } + + if (ids.length > 0) { // We have some strums, which might need to be removed. + ids.forEach(function(d) { + var dims = strums[d].dims; + strums.active = d; + // If the two dimensions of the current strum are not next to each other + // any more, than we'll need to remove the strum. Otherwise we keep it. + if (!consecutive(dims.left, dims.right)) { + removeStrum(strums); + } + }); + onDragEnd(strums)(); + } + }); + + // Add a new svg group in which we draw the strums. + pc.selection.select("svg").append("g") + .attr("id", "strums") + .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")"); + + // Install the required brushReset function + pc.brushReset = brushReset(strums); + + drag + .on("dragstart", onDragStart(strums)) + .on("drag", onDrag(strums)) + .on("dragend", onDragEnd(strums)); + + // NOTE: The styling needs to be done here and not in the css. This is because + // for 1D brushing, the canvas layers should not listen to + // pointer-events. + strumRect = pc.selection.select("svg").insert("rect", "g#strums") + .attr("id", "strum-events") + .attr("x", __.margin.left) + .attr("y", __.margin.top) + .attr("width", w()) + .attr("height", h() + 2) + .style("opacity", 0) + .call(drag); + } + + brush.modes["2D-strums"] = { + install: install, + uninstall: function() { + pc.selection.select("svg").select("g#strums").remove(); + pc.selection.select("svg").select("rect#strum-events").remove(); + pc.on("axesreorder.strums", undefined); + delete pc.brushReset; + + strumRect = undefined; + }, + selected: selected, + brushState: function () { return strums; } + }; + + }()); + + // brush mode: 1D-Axes with multiple extents + // requires d3.svg.multibrush + + (function() { + if (typeof d3.svg.multibrush !== 'function') { + return; + } + var brushes = {}; + + function is_brushed(p) { + return !brushes[p].empty(); + } + + // data within extents + function selected() { + var actives = __.dimensions.filter(is_brushed), + extents = actives.map(function(p) { return brushes[p].extent(); }); + + // We don't want to return the full data set when there are no axes brushed. + // Actually, when there are no axes brushed, by definition, no items are + // selected. So, let's avoid the filtering and just return false. + //if (actives.length === 0) return false; + + // Resolves broken examples for now. They expect to get the full dataset back from empty brushes + if (actives.length === 0) return __.data; + + // test if within range + var within = { + "date": function(d,p,dimension,b) { + if (typeof yscale[p].rangePoints === "function") { // if it is ordinal + return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1] + } else { + return b[0] <= d[p] && d[p] <= b[1] + } + }, + "number": function(d,p,dimension,b) { + if (typeof yscale[p].rangePoints === "function") { // if it is ordinal + return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1] + } else { + return b[0] <= d[p] && d[p] <= b[1] + } + }, + "string": function(d,p,dimension,b) { + return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1] + } + }; + + return __.data + .filter(function(d) { + switch(brush.predicate) { + case "AND": + return actives.every(function(p, dimension) { + return extents[dimension].some(function(b) { + return within[__.types[p]](d,p,dimension,b); + }); + }); + case "OR": + return actives.some(function(p, dimension) { + return extents[dimension].some(function(b) { + return within[__.types[p]](d,p,dimension,b); + }); + }); + default: + throw "Unknown brush predicate " + __.brushPredicate; + } + }); + }; + + function brushExtents() { + var extents = {}; + __.dimensions.forEach(function(d) { + var brush = brushes[d]; + if (brush !== undefined && !brush.empty()) { + var extent = brush.extent(); + extents[d] = extent; + } + }); + return extents; + } + + function brushFor(axis) { + var brush = d3.svg.multibrush(); + + brush + .y(yscale[axis]) + .on("brushstart", function() { + if(d3.event.sourceEvent !== null) { + d3.event.sourceEvent.stopPropagation(); + } + }) + .on("brush", function() { + brushUpdated(selected()); + }) + .on("brushend", function() { + // d3.svg.multibrush clears extents just before calling 'brushend' + // so we have to update here again. + // This fixes issue #103 for now, but should be changed in d3.svg.multibrush + // to avoid unnecessary computation. + brushUpdated(selected()); + events.brushend.call(pc, __.brushed); + }) + .extentAdaption(function(selection) { + selection + .style("visibility", null) + .attr("x", -15) + .attr("width", 30); + }) + .resizeAdaption(function(selection) { + selection + .selectAll("rect") + .attr("x", -15) + .attr("width", 30); + }); + + brushes[axis] = brush; + return brush; + } + + function brushReset(dimension) { + __.brushed = false; + if (g) { + g.selectAll('.brush') + .each(function(d) { + d3.select(this).call( + brushes[d].clear() + ); + }); + pc.renderBrushed(); + } + return this; + }; + + function install() { + if (!g) pc.createAxes(); + + // Add and store a brush for each axis. + g.append("svg:g") + .attr("class", "brush") + .each(function(d) { + d3.select(this).call(brushFor(d)); + }) + .selectAll("rect") + .style("visibility", null) + .attr("x", -15) + .attr("width", 30); + + pc.brushExtents = brushExtents; + pc.brushReset = brushReset; + return pc; + } + + brush.modes["1D-axes-multi"] = { + install: install, + uninstall: function() { + g.selectAll(".brush").remove(); + brushes = {}; + delete pc.brushExtents; + delete pc.brushReset; + }, + selected: selected, + brushState: brushExtents + } + })(); + // brush mode: angular + // code based on 2D.strums.js + + (function() { + var arcs = {}, + strumRect; + + function drawStrum(arc, activePoint) { + var svg = pc.selection.select("svg").select("g#arcs"), + id = arc.dims.i, + points = [arc.p2, arc.p3], + line = svg.selectAll("line#arc-" + id).data([{p1:arc.p1,p2:arc.p2},{p1:arc.p1,p2:arc.p3}]), + circles = svg.selectAll("circle#arc-" + id).data(points), + drag = d3.behavior.drag(), + path = svg.selectAll("path#arc-" + id).data([arc]); + + path.enter() + .append("path") + .attr("id", "arc-" + id) + .attr("class", "arc") + .style("fill", "orange") + .style("opacity", 0.5); + + path + .attr("d", arc.arc) + .attr("transform", "translate(" + arc.p1[0] + "," + arc.p1[1] + ")"); + + line.enter() + .append("line") + .attr("id", "arc-" + id) + .attr("class", "arc"); + + line + .attr("x1", function(d) { return d.p1[0]; }) + .attr("y1", function(d) { return d.p1[1]; }) + .attr("x2", function(d) { return d.p2[0]; }) + .attr("y2", function(d) { return d.p2[1]; }) + .attr("stroke", "black") + .attr("stroke-width", 2); + + drag + .on("drag", function(d, i) { + var ev = d3.event, + angle = 0; + + i = i + 2; + + arc["p" + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX); + arc["p" + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY); + + angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id); + + if ((arc.startAngle < Math.PI && arc.endAngle < Math.PI && angle < Math.PI) || + (arc.startAngle >= Math.PI && arc.endAngle >= Math.PI && angle >= Math.PI)) { + + if (i === 2) { + arc.endAngle = angle; + arc.arc.endAngle(angle); + } else if (i === 3) { + arc.startAngle = angle; + arc.arc.startAngle(angle); + } + + } + + drawStrum(arc, i - 2); + }) + .on("dragend", onDragEnd()); + + circles.enter() + .append("circle") + .attr("id", "arc-" + id) + .attr("class", "arc"); + + circles + .attr("cx", function(d) { return d[0]; }) + .attr("cy", function(d) { return d[1]; }) + .attr("r", 5) + .style("opacity", function(d, i) { + return (activePoint !== undefined && i === activePoint) ? 0.8 : 0; + }) + .on("mouseover", function() { + d3.select(this).style("opacity", 0.8); + }) + .on("mouseout", function() { + d3.select(this).style("opacity", 0); + }) + .call(drag); + } + + function dimensionsForPoint(p) { + var dims = { i: -1, left: undefined, right: undefined }; + __.dimensions.some(function(dim, i) { + if (xscale(dim) < p[0]) { + var next = __.dimensions[i + 1]; + dims.i = i; + dims.left = dim; + dims.right = next; + return false; + } + return true; + }); + + if (dims.left === undefined) { + // Event on the left side of the first axis. + dims.i = 0; + dims.left = __.dimensions[0]; + dims.right = __.dimensions[1]; + } else if (dims.right === undefined) { + // Event on the right side of the last axis + dims.i = __.dimensions.length - 1; + dims.right = dims.left; + dims.left = __.dimensions[__.dimensions.length - 2]; + } + + return dims; + } + + function onDragStart() { + // First we need to determine between which two axes the arc was started. + // This will determine the freedom of movement, because a arc can + // logically only happen between two axes, so no movement outside these axes + // should be allowed. + return function() { + var p = d3.mouse(strumRect[0][0]), + dims, + arc; + + p[0] = p[0] - __.margin.left; + p[1] = p[1] - __.margin.top; + + dims = dimensionsForPoint(p), + arc = { + p1: p, + dims: dims, + minX: xscale(dims.left), + maxX: xscale(dims.right), + minY: 0, + maxY: h(), + startAngle: undefined, + endAngle: undefined, + arc: d3.svg.arc().innerRadius(0) + }; + + arcs[dims.i] = arc; + arcs.active = dims.i; + + // Make sure that the point is within the bounds + arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX); + arc.p2 = arc.p1.slice(); + arc.p3 = arc.p1.slice(); + }; + } + + function onDrag() { + return function() { + var ev = d3.event, + arc = arcs[arcs.active]; + + // Make sure that the point is within the bounds + arc.p2[0] = Math.min(Math.max(arc.minX + 1, ev.x - __.margin.left), arc.maxX); + arc.p2[1] = Math.min(Math.max(arc.minY, ev.y - __.margin.top), arc.maxY); + arc.p3 = arc.p2.slice(); + // console.log(arcs.angle(arcs.active)); + // console.log(signedAngle(arcs.unsignedAngle(arcs.active))); + drawStrum(arc, 1); + }; + } + + // some helper functions + function hypothenuse(a, b) { + return Math.sqrt(a*a + b*b); + } + + var rad = (function() { + var c = Math.PI / 180; + return function(angle) { + return angle * c; + }; + })(); + + var deg = (function() { + var c = 180 / Math.PI; + return function(angle) { + return angle * c; + }; + })(); + + // [0, 2*PI] -> [-PI/2, PI/2] + var signedAngle = function(angle) { + var ret = angle; + if (angle > Math.PI) { + ret = angle - 1.5 * Math.PI; + ret = angle - 1.5 * Math.PI; + } else { + ret = angle - 0.5 * Math.PI; + ret = angle - 0.5 * Math.PI; + } + return -ret; + } + + /** + * angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock. + * However, one can only select lines from 0 to PI, so we compute the + * 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2 + * are 12 and 6 o'clock respectively. + */ + function containmentTest(arc) { + var startAngle = signedAngle(arc.startAngle); + var endAngle = signedAngle(arc.endAngle); + + if (startAngle > endAngle) { + var tmp = startAngle; + startAngle = endAngle; + endAngle = tmp; + } + + // test if segment angle is contained in angle interval + return function(a) { + + if (a >= startAngle && a <= endAngle) { + return true; + } + + return false; + }; + } + + function selected() { + var ids = Object.getOwnPropertyNames(arcs), + brushed = __.data; + + // Get the ids of the currently active arcs. + ids = ids.filter(function(d) { + return !isNaN(d); + }); + + function crossesStrum(d, id) { + var arc = arcs[id], + test = containmentTest(arc), + d1 = arc.dims.left, + d2 = arc.dims.right, + y1 = yscale[d1], + y2 = yscale[d2], + a = arcs.width(id), + b = y1(d[d1]) - y2(d[d2]), + c = hypothenuse(a, b), + angle = Math.asin(b/c); // rad in [-PI/2, PI/2] + return test(angle); + } + + if (ids.length === 0) { return brushed; } + + return brushed.filter(function(d) { + switch(brush.predicate) { + case "AND": + return ids.every(function(id) { return crossesStrum(d, id); }); + case "OR": + return ids.some(function(id) { return crossesStrum(d, id); }); + default: + throw "Unknown brush predicate " + __.brushPredicate; + } + }); + } + + function removeStrum() { + var arc = arcs[arcs.active], + svg = pc.selection.select("svg").select("g#arcs"); + + delete arcs[arcs.active]; + arcs.active = undefined; + svg.selectAll("line#arc-" + arc.dims.i).remove(); + svg.selectAll("circle#arc-" + arc.dims.i).remove(); + svg.selectAll("path#arc-" + arc.dims.i).remove(); + } + + function onDragEnd() { + return function() { + var brushed = __.data, + arc = arcs[arcs.active]; + + // Okay, somewhat unexpected, but not totally unsurprising, a mousclick is + // considered a drag without move. So we have to deal with that case + if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) { + removeStrum(arcs); + } + + if (arc) { + var angle = arcs.startAngle(arcs.active); + + arc.startAngle = angle; + arc.endAngle = angle; + arc.arc + .outerRadius(arcs.length(arcs.active)) + .startAngle(angle) + .endAngle(angle); + } + + + brushed = selected(arcs); + arcs.active = undefined; + __.brushed = brushed; + pc.renderBrushed(); + events.brushend.call(pc, __.brushed); + }; + } + + function brushReset(arcs) { + return function() { + var ids = Object.getOwnPropertyNames(arcs).filter(function(d) { + return !isNaN(d); + }); + + ids.forEach(function(d) { + arcs.active = d; + removeStrum(arcs); + }); + onDragEnd(arcs)(); + }; + } + + function install() { + var drag = d3.behavior.drag(); + + // Map of current arcs. arcs are stored per segment of the PC. A segment, + // being the area between two axes. The left most area is indexed at 0. + arcs.active = undefined; + // Returns the width of the PC segment where currently a arc is being + // placed. NOTE: even though they are evenly spaced in our current + // implementation, we keep for when non-even spaced segments are supported as + // well. + arcs.width = function(id) { + var arc = arcs[id]; + + if (arc === undefined) { + return undefined; + } + + return arc.maxX - arc.minX; + }; + + // returns angles in [-PI/2, PI/2] + angle = function(p1, p2) { + var a = p1[0] - p2[0], + b = p1[1] - p2[1], + c = hypothenuse(a, b); + + return Math.asin(b/c); + } + + // returns angles in [0, 2 * PI] + arcs.endAngle = function(id) { + var arc = arcs[id]; + if (arc === undefined) { + return undefined; + } + var sAngle = angle(arc.p1, arc.p2), + uAngle = -sAngle + Math.PI / 2; + + if (arc.p1[0] > arc.p2[0]) { + uAngle = 2 * Math.PI - uAngle; + } + + return uAngle; + } + + arcs.startAngle = function(id) { + var arc = arcs[id]; + if (arc === undefined) { + return undefined; + } + + var sAngle = angle(arc.p1, arc.p3), + uAngle = -sAngle + Math.PI / 2; + + if (arc.p1[0] > arc.p3[0]) { + uAngle = 2 * Math.PI - uAngle; + } + + return uAngle; + } + + arcs.length = function(id) { + var arc = arcs[id]; + + if (arc === undefined) { + return undefined; + } + + var a = arc.p1[0] - arc.p2[0], + b = arc.p1[1] - arc.p2[1], + c = hypothenuse(a, b); + + return(c); + } + + pc.on("axesreorder.arcs", function() { + var ids = Object.getOwnPropertyNames(arcs).filter(function(d) { + return !isNaN(d); + }); + + // Checks if the first dimension is directly left of the second dimension. + function consecutive(first, second) { + var length = __.dimensions.length; + return __.dimensions.some(function(d, i) { + return (d === first) + ? i + i < length && __.dimensions[i + 1] === second + : false; + }); + } + + if (ids.length > 0) { // We have some arcs, which might need to be removed. + ids.forEach(function(d) { + var dims = arcs[d].dims; + arcs.active = d; + // If the two dimensions of the current arc are not next to each other + // any more, than we'll need to remove the arc. Otherwise we keep it. + if (!consecutive(dims.left, dims.right)) { + removeStrum(arcs); + } + }); + onDragEnd(arcs)(); + } + }); + + // Add a new svg group in which we draw the arcs. + pc.selection.select("svg").append("g") + .attr("id", "arcs") + .attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")"); + + // Install the required brushReset function + pc.brushReset = brushReset(arcs); + + drag + .on("dragstart", onDragStart(arcs)) + .on("drag", onDrag(arcs)) + .on("dragend", onDragEnd(arcs)); + + // NOTE: The styling needs to be done here and not in the css. This is because + // for 1D brushing, the canvas layers should not listen to + // pointer-events. + strumRect = pc.selection.select("svg").insert("rect", "g#arcs") + .attr("id", "arc-events") + .attr("x", __.margin.left) + .attr("y", __.margin.top) + .attr("width", w()) + .attr("height", h() + 2) + .style("opacity", 0) + .call(drag); + } + + brush.modes["angular"] = { + install: install, + uninstall: function() { + pc.selection.select("svg").select("g#arcs").remove(); + pc.selection.select("svg").select("rect#arc-events").remove(); + pc.on("axesreorder.arcs", undefined); + delete pc.brushReset; + + strumRect = undefined; + }, + selected: selected, + brushState: function () { return arcs; } + }; + + }()); + + pc.interactive = function() { + flags.interactive = true; + return this; + }; + + // expose a few objects + pc.xscale = xscale; + pc.yscale = yscale; + pc.ctx = ctx; + pc.canvas = canvas; + pc.g = function() { return g; }; + + // rescale for height, width and margins + // TODO currently assumes chart is brushable, and destroys old brushes + pc.resize = function() { + // selection size + pc.selection.select("svg") + .attr("width", __.width) + .attr("height", __.height) + pc.svg.attr("transform", "translate(" + __.margin.left + "," + __.margin.top + ")"); + + // FIXME: the current brush state should pass through + if (flags.brushable) pc.brushReset(); + + // scales + pc.autoscale(); + + // axes, destroys old brushes. + if (g) pc.createAxes(); + if (flags.brushable) pc.brushable(); + if (flags.reorderable) pc.reorderable(); + + events.resize.call(this, {width: __.width, height: __.height, margin: __.margin}); + return this; + }; + + // highlight an array of data + pc.highlight = function(data) { + if (arguments.length === 0) { + return __.highlighted; + } + + __.highlighted = data; + pc.clear("highlight"); + d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", true); + data.forEach(path_highlight); + events.highlight.call(this, data); + return this; + }; + + // clear highlighting + pc.unhighlight = function() { + __.highlighted = []; + pc.clear("highlight"); + d3.selectAll([canvas.foreground, canvas.brushed]).classed("faded", false); + return this; + }; + + // calculate 2d intersection of line a->b with line c->d + // points are objects with x and y properties + pc.intersection = function(a, b, c, d) { + return { + x: ((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)), + y: ((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) / ((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)) + }; + }; + + function position(d) { + var v = dragging[d]; + return v == null ? xscale(d) : v; + } + pc.version = "0.7.0"; + // this descriptive text should live with other introspective methods + pc.toString = function() { return "Parallel Coordinates: " + __.dimensions.length + " dimensions (" + d3.keys(__.data[0]).length + " total) , " + __.data.length + " rows"; }; + + return pc; + }; + + d3.renderQueue = (function(func) { + var _queue = [], // data to be rendered + _rate = 10, // number of calls per frame + _clear = function() {}, // clearing function + _i = 0; // current iteration + + var rq = function(data) { + if (data) rq.data(data); + rq.invalidate(); + _clear(); + rq.render(); + }; + + rq.render = function() { + _i = 0; + var valid = true; + rq.invalidate = function() { valid = false; }; + + function doFrame() { + if (!valid) return true; + if (_i > _queue.length) return true; + + // Typical d3 behavior is to pass a data item *and* its index. As the + // render queue splits the original data set, we'll have to be slightly + // more carefull about passing the correct index with the data item. + var end = Math.min(_i + _rate, _queue.length); + for (var i = _i; i < end; i++) { + func(_queue[i], i); + } + _i += _rate; + } + + d3.timer(doFrame); + }; + + rq.data = function(data) { + rq.invalidate(); + _queue = data.slice(0); + return rq; + }; + + rq.rate = function(value) { + if (!arguments.length) return _rate; + _rate = value; + return rq; + }; + + rq.remaining = function() { + return _queue.length - _i; + }; + + // clear the canvas + rq.clear = function(func) { + if (!arguments.length) { + _clear(); + return rq; + } + _clear = func; + return rq; + }; + + rq.invalidate = function() {}; + + return rq; + }); diff --git a/panoramix/static/lib/para/divgrid.js b/panoramix/assets/vendor/parallel_coordinates/divgrid.js similarity index 94% rename from panoramix/static/lib/para/divgrid.js rename to panoramix/assets/vendor/parallel_coordinates/divgrid.js index 532a63d9c1750..e4086e8bae5bf 100644 --- a/panoramix/static/lib/para/divgrid.js +++ b/panoramix/assets/vendor/parallel_coordinates/divgrid.js @@ -1,5 +1,5 @@ -// http://bl.ocks.org/3687826 -d3.divgrid = function(config) { +// from http://bl.ocks.org/3687826 +module.exports = function(config) { var columns = []; var dg = function(selection) { diff --git a/panoramix/static/lib/pygments.css b/panoramix/assets/vendor/pygments.css similarity index 100% rename from panoramix/static/lib/pygments.css rename to panoramix/assets/vendor/pygments.css diff --git a/panoramix/assets/javascripts/vendor/select2.sortable.js b/panoramix/assets/vendor/select2.sortable.js similarity index 100% rename from panoramix/assets/javascripts/vendor/select2.sortable.js rename to panoramix/assets/vendor/select2.sortable.js diff --git a/panoramix/static/widgets/viz_bignumber.css b/panoramix/assets/visualizations/big_number.css similarity index 100% rename from panoramix/static/widgets/viz_bignumber.css rename to panoramix/assets/visualizations/big_number.css diff --git a/panoramix/static/widgets/viz_bignumber.js b/panoramix/assets/visualizations/big_number.js similarity index 95% rename from panoramix/static/widgets/viz_bignumber.js rename to panoramix/assets/visualizations/big_number.js index 4d76a2f9fc227..78850c9608216 100644 --- a/panoramix/static/widgets/viz_bignumber.js +++ b/panoramix/assets/visualizations/big_number.js @@ -1,4 +1,10 @@ -px.registerViz('big_number', function(slice) { +// JS +var d3 = window.d3 || require('d3'); + +// CSS +require('./big_number.css'); + +function bigNumberVis(slice) { var data_attribute = slice.data; var div = d3.select(slice.selector); @@ -22,12 +28,12 @@ px.registerViz('big_number', function(slice) { var svg = div.append('svg'); svg.attr("width", width); svg.attr("height", height); - data = json.data; + var data = json.data; var compare_suffix = ' ' + json.compare_suffix; var v_compare = null; var v = data[data.length - 1][1]; if (json.compare_lag > 0){ - pos = data.length - (json.compare_lag + 1); + var pos = data.length - (json.compare_lag + 1); if (pos >= 0){ v_compare = (v / data[pos][1]) - 1; } @@ -141,5 +147,6 @@ px.registerViz('big_number', function(slice) { render: render, resize: render, } +}; -}); +module.exports = bigNumberVis; diff --git a/panoramix/static/widgets/viz_directed_force.css b/panoramix/assets/visualizations/directed_force.css similarity index 100% rename from panoramix/static/widgets/viz_directed_force.css rename to panoramix/assets/visualizations/directed_force.css diff --git a/panoramix/assets/visualizations/directed_force.js b/panoramix/assets/visualizations/directed_force.js new file mode 100644 index 0000000000000..812cc769eded9 --- /dev/null +++ b/panoramix/assets/visualizations/directed_force.js @@ -0,0 +1,165 @@ +// JS +var d3 = window.d3 || require('d3'); + +// CSS +require('./directed_force.css'); + +/* Modified from http://bl.ocks.org/d3noob/5141278 */ +function directedForceVis(slice) { + var div = d3.select(slice.selector); + var link_length = slice.data.form_data['link_length'] || 200; + var charge = slice.data.form_data['charge'] || -500; + + var render = function() { + var width = slice.width(); + var height = slice.height() - 25; + d3.json(slice.jsonEndpoint(), function(error, json) { + + if (error != null){ + slice.error(error.responseText); + return ''; + } + var links = json.data; + var nodes = {}; + // Compute the distinct nodes from the links. + links.forEach(function(link) { + link.source = nodes[link.source] || (nodes[link.source] = {name: link.source}); + link.target = nodes[link.target] || (nodes[link.target] = {name: link.target}); + link.value = +link.value; + + var target_name = link.target.name; + var source_name = link.source.name; + + if (nodes[target_name]['total'] === undefined) { + nodes[target_name]['total'] = link.value; + } + if (nodes[source_name]['total'] === undefined) { + nodes[source_name]['total'] = 0; + } + if (nodes[target_name]['max'] === undefined) { + nodes[target_name]['max'] = 0; + } + if (link.value > nodes[target_name]['max']) { + nodes[target_name]['max'] = link.value; + } + if (nodes[target_name]['min'] === undefined) { + nodes[target_name]['min'] = 0; + } + if (link.value > nodes[target_name]['min']) { + nodes[target_name]['min'] = link.value; + } + + nodes[target_name]['total'] += link.value; + }); + + var force = d3.layout.force() + .nodes(d3.values(nodes)) + .links(links) + .size([width, height]) + .linkDistance(link_length) + .charge(charge) + .on("tick", tick) + .start(); + + var svg = div.append("svg") + .attr("width", width) + .attr("height", height); + + // build the arrow. + svg.append("svg:defs").selectAll("marker") + .data(["end"]) // Different link/path types can be defined here + .enter().append("svg:marker") // This section adds in the arrows + .attr("id", String) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 15) + .attr("refY", -1.5) + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("svg:path") + .attr("d", "M0,-5L10,0L0,5"); + + var edgeScale = d3.scale.linear() + .range([0.1, 0.5]); + // add the links and the arrows + var path = svg.append("svg:g").selectAll("path") + .data(force.links()) + .enter().append("svg:path") + .attr("class", "link") + .style("opacity", function(d){ + return edgeScale(d.value/d.target.max); + }) + .attr("marker-end", "url(#end)"); + + // define the nodes + var node = svg.selectAll(".node") + .data(force.nodes()) + .enter().append("g") + .attr("class", "node") + .on("mouseenter", function(d){ + d3.select(this) + .select("circle") + .transition() + .style('stroke-width', 5); + + d3.select(this) + .select("text") + .transition() + .style('font-size', 25); + }) + .on("mouseleave", function(d){ + d3.select(this) + .select("circle") + .transition() + .style('stroke-width', 1.5); + d3.select(this) + .select("text") + .transition() + .style('font-size', 12); + }) + .call(force.drag); + + // add the nodes + var ext = d3.extent(d3.values(nodes), function(d) { return Math.sqrt(d.total); }); + var circleScale = d3.scale.linear() + .domain(ext) + .range([3, 30]); + + node.append("circle") + .attr("r", function(d){return circleScale(Math.sqrt(d.total));}); + + // add the text + node.append("text") + .attr("x", 6) + .attr("dy", ".35em") + .text(function(d) { return d.name; }); + + // add the curvy lines + function tick() { + path.attr("d", function(d) { + var dx = d.target.x - d.source.x, + dy = d.target.y - d.source.y, + dr = Math.sqrt(dx * dx + dy * dy); + return "M" + + d.source.x + "," + + d.source.y + "A" + + dr + "," + dr + " 0 0,1 " + + d.target.x + "," + + d.target.y; + }); + + node.attr("transform", function(d) { + return "translate(" + d.x + "," + d.y + ")"; + }); + } + + slice.done(json); + }); + }; + return { + render: render, + resize: render, + }; +} + +module.exports = directedForceVis; diff --git a/panoramix/static/widgets/viz_filter_box.css b/panoramix/assets/visualizations/filter_box.css similarity index 100% rename from panoramix/static/widgets/viz_filter_box.css rename to panoramix/assets/visualizations/filter_box.css diff --git a/panoramix/static/widgets/viz_filter_box.js b/panoramix/assets/visualizations/filter_box.js similarity index 75% rename from panoramix/static/widgets/viz_filter_box.js rename to panoramix/assets/visualizations/filter_box.js index 2e7e4dfc00645..5ced2eacd347a 100644 --- a/panoramix/static/widgets/viz_filter_box.js +++ b/panoramix/assets/visualizations/filter_box.js @@ -1,15 +1,21 @@ -px.registerViz('filter_box', function(slice) { +// JS +var d3 = window.d3 || require('d3'); + +// CSS +require('./filter_box.css'); + +function filterBox(slice) { var slice = slice; var filtersObj = {}; - d3token = d3.select(slice.selector); + var d3token = d3.select(slice.selector); var fltChanged = function() { - filters = [] - for(flt in filtersObj) { - obj = filtersObj[flt]; - val = obj.val() - if(val !== ''){ - filters.push([flt, val.split(',')]); + var filters = [] + for (var filter in filtersObj) { + var obj = filtersObj[filter]; + var val = obj.val(); + if (val !== '') { + filters.push([filter, val.split(',')]); } } slice.addFilter(filters); @@ -20,9 +26,10 @@ px.registerViz('filter_box', function(slice) { var container = d3token .append('div') .classed('padded', true); + $.getJSON(slice.jsonEndpoint(), function(payload) { var maxes = {}; - for (filter in payload.data){ + for (var filter in payload.data) { var data = payload.data[filter]; maxes[filter] = d3.max(data, function(d){return d.metric}); var id = 'fltbox__' + filter; @@ -54,14 +61,6 @@ px.registerViz('filter_box', function(slice) { }, }) .on('change', fltChanged); - /* - .style('background-image', function(d){ - if (d.isMetric){ - var perc = Math.round((d.val / maxes[d.col]) * 100); - return "linear-gradient(to right, lightgrey, lightgrey " + perc + "%, rgba(0,0,0,0) " + perc + "%"; - } - }) - */ } slice.done(); }) @@ -73,4 +72,6 @@ px.registerViz('filter_box', function(slice) { render: refresh, resize: refresh, }; -}); +} + +module.exports = filterBox; diff --git a/panoramix/static/lib/d3.tip.css b/panoramix/assets/visualizations/heatmap.css similarity index 54% rename from panoramix/static/lib/d3.tip.css rename to panoramix/assets/visualizations/heatmap.css index bb9a5451a99be..bce1248212303 100644 --- a/panoramix/static/lib/d3.tip.css +++ b/panoramix/assets/visualizations/heatmap.css @@ -1,6 +1,30 @@ +.heatmap .axis text { + font: 10px sans-serif; +} + +.heatmap .axis path, +.heatmap .axis line { + fill: none; + stroke: #000; + shape-rendering: crispEdges; +} + +.heatmap svg { +} + +.heatmap canvas, .heatmap img { + image-rendering: optimizeSpeed; /* Older versions of FF */ + image-rendering: -moz-crisp-edges; /* FF 6.0+ */ + image-rendering: -webkit-optimize-contrast; /* Safari */ + image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */ + image-rendering: pixelated; /* Awesome future-browsers */ + -ms-interpolation-mode: nearest-neighbor; /* IE */ +} + +/* from d3-tip */ .d3-tip { line-height: 1; - font-size: 12px; + font-weight: bold; padding: 12px; background: rgba(0, 0, 0, 0.8); color: #fff; diff --git a/panoramix/static/widgets/viz_heatmap.js b/panoramix/assets/visualizations/heatmap.js similarity index 72% rename from panoramix/static/widgets/viz_heatmap.js rename to panoramix/assets/visualizations/heatmap.js index 8a04ab0581ee4..039a2c507c6cf 100644 --- a/panoramix/static/widgets/viz_heatmap.js +++ b/panoramix/assets/visualizations/heatmap.js @@ -1,7 +1,17 @@ +// JS +var $ = window.$ || require('jquery'); +var px = window.px || require('../javascripts/modules/panoramix.js'); +var d3 = require('d3'); + +d3.tip = require('d3-tip'); //using window.d3 doesn't capture events properly bc of multiple instances + +// CSS +require('./heatmap.css'); + // Inspired from http://bl.ocks.org/mbostock/3074470 // https://jsfiddle.net/cyril123/h0reyumq/ -px.registerViz('heatmap', function(slice) { - var margins = {t:0, r:0, b:50, l:50}; +function heatmapVis(slice) { + var margins = {t:10, r:10, b:50, l:60}; function refresh() { var width = slice.width(); var height = slice.height(); @@ -17,15 +27,19 @@ px.registerViz('heatmap', function(slice) { var fd = payload.form_data; var data = payload.data; function ordScale(k, rangeBands, reverse) { - if (reverse === undefined) + if (reverse === undefined) { reverse = false; - domain = {}; + } + var domain = {}; $.each(data, function(i, d){ domain[d[k]] = true; }); - domain = Object.keys(domain).sort(); - if (reverse) + domain = Object.keys(domain).sort(function(a, b) { + return b - a; + }); + if (reverse) { domain.reverse(); + } if (rangeBands === undefined) { return d3.scale.ordinal().domain(domain).range(d3.range(domain.length)); } @@ -52,34 +66,34 @@ px.registerViz('heatmap', function(slice) { ]; var container = d3.select(slice.selector) - .style("left", "0px") - .style("position", "relative") - .style("top", "0px"); + .style("left", "0px") + .style("position", "relative") + .style("top", "0px"); var canvas = container.append("canvas") - .attr("width", heatmapDim[X]) - .attr("height", heatmapDim[Y]) - .style("width", hmWidth + "px") - .style("height", hmHeight + "px") - .style("image-rendering", fd.canvas_image_rendering) - .style("left", margins.l + "px") - .style("top", margins.t + "px") - .style("position", "absolute"); + .attr("width", heatmapDim[X]) + .attr("height", heatmapDim[Y]) + .style("width", hmWidth + "px") + .style("height", hmHeight + "px") + .style("image-rendering", fd.canvas_image_rendering) + .style("left", margins.l + "px") + .style("top", margins.t + "px") + .style("position", "absolute"); var svg = container.append("svg") - .attr("width", width) - .attr("height", height) - .style("left", "0px") - .style("top", "0px") - .style("position", "absolute"); + .attr("width", width) + .attr("height", height) + .style("left", "0px") + .style("top", "0px") + .style("position", "absolute"); var rect = svg.append('g') - .attr("transform", "translate(" + margins.l + "," + margins.t + ")") + .attr("transform", "translate(" + margins.l + "," + margins.t + ")") .append('rect') - .style('fill-opacity', 0) - .attr('stroke', 'black') - .attr("width", hmWidth) - .attr("height", hmHeight); + .style('fill-opacity', 0) + .attr('stroke', 'black') + .attr("width", hmWidth) + .attr("height", hmHeight); var tip = d3.tip() .attr('class', 'd3-tip') @@ -101,23 +115,25 @@ px.registerViz('heatmap', function(slice) { s += "
%: " + fp(obj.perc) + "
" return s; } - }) + }); + rect.call(tip); + var xscale_skip = 2; var yscale_skip = 2; - xAxis = d3.svg.axis() + var xAxis = d3.svg.axis() .scale(xRbScale) .tickValues(xRbScale.domain().filter( function(d, i) { return !(i % (parseInt(fd.xscale_interval))); })) .orient("bottom"); - yAxis = d3.svg.axis() + var yAxis = d3.svg.axis() .scale(yRbScale) .tickValues(yRbScale.domain().filter( function(d, i) { return !(i % (parseInt(fd.yscale_interval))); })) .orient("left"); - svg.append("g") + svg.append("g") .attr("class", "x axis") .attr("transform", "translate(" + margins.l + "," + (margins.t + hmHeight) + ")") .call(xAxis) @@ -125,7 +141,8 @@ px.registerViz('heatmap', function(slice) { .style("text-anchor", "end") .attr("transform", "rotate(-45)") .style("font-weight", "bold"); - svg.append("g") + + svg.append("g") .attr("class", "y axis") .attr("transform", "translate(" + margins.l + ", 0)") .call(yAxis); @@ -142,8 +159,8 @@ px.registerViz('heatmap', function(slice) { // Compute the pixel colors; scaled by CSS. function createImageObj() { - imageObj = new Image(); - image = context.createImageData(heatmapDim[0], heatmapDim[1]); + var imageObj = new Image(); + var image = context.createImageData(heatmapDim[0], heatmapDim[1]); var pixs = {}; $.each(data, function(i, d) { var c = d3.rgb(color(d.perc)); @@ -156,9 +173,9 @@ px.registerViz('heatmap', function(slice) { matrix[x][y] = d; }); - p = -1; - for(var i=0; i< heatmapDim[0] * heatmapDim[1]; i++){ - c = pixs[i]; + var p = -1; + for(var i = 0; i < heatmapDim[0] * heatmapDim[1]; i++){ + var c = pixs[i]; var alpha = 255; if (c === undefined){ c = d3.rgb('#F00'); @@ -180,5 +197,6 @@ px.registerViz('heatmap', function(slice) { render: refresh, resize: refresh, }; -}); +} +module.exports = heatmapVis; diff --git a/panoramix/assets/visualizations/iframe.js b/panoramix/assets/visualizations/iframe.js new file mode 100644 index 0000000000000..546c28230f4a5 --- /dev/null +++ b/panoramix/assets/visualizations/iframe.js @@ -0,0 +1,25 @@ +var $ = window.$ || require('jquery'); + +function iframeWidget(slice) { + + function refresh() { + $('#code').attr('rows', '15') + $.getJSON(slice.jsonEndpoint(), function(payload) { + slice.container.html(''); + var iframe = slice.container.find('iframe'); + iframe.css('height', slice.height()); + iframe.attr('src', payload.form_data.url); + slice.done(); + }) + .fail(function(xhr) { + slice.error(xhr.responseText); + }); + }; + + return { + render: refresh, + resize: refresh, + }; +} + +module.exports = iframeWidget diff --git a/panoramix/assets/visualizations/markup.js b/panoramix/assets/visualizations/markup.js new file mode 100644 index 0000000000000..4fe808b4e4d3e --- /dev/null +++ b/panoramix/assets/visualizations/markup.js @@ -0,0 +1,23 @@ +var $ = window.$ || require('jquery'); + +function markupWidget(slice) { + + function refresh() { + $('#code').attr('rows', '15'); + + $.getJSON(slice.jsonEndpoint(), function(payload) { + slice.container.html(payload.data.html); + slice.done(); + }) + .fail(function(xhr) { + slice.error(xhr.responseText); + }); + }; + + return { + render: refresh, + resize: refresh, + }; +} + +module.exports = markupWidget; diff --git a/panoramix/assets/visualizations/nvd3_vis.js b/panoramix/assets/visualizations/nvd3_vis.js new file mode 100644 index 0000000000000..665a47b62b38e --- /dev/null +++ b/panoramix/assets/visualizations/nvd3_vis.js @@ -0,0 +1,209 @@ +// JS +var $ = window.$ || require('jquery'); +var px = window.px || require('../javascripts/modules/panoramix.js'); +require('../vendor/nvd3/nv.d3.min.js'); + +// CSS +require('../vendor/nvd3/nv.d3.css'); + +function nvd3Vis(slice) { + var chart = undefined; + var data = {}; + + var render = function() { + $.getJSON(slice.jsonEndpoint(), function(payload) { + var fd = payload.form_data; + var viz_type = fd.viz_type; + + var f = d3.format('.3s'); + var colorKey = 'key'; + + nv.addGraph(function() { + switch (viz_type) { + + case 'line': + if (fd.show_brush) { + chart = nv.models.lineWithFocusChart(); + chart.lines2.xScale(d3.time.scale.utc()); + chart.x2Axis + .showMaxMin(fd.x_axis_showminmax) + .staggerLabels(true); + } else { + chart = nv.models.lineChart() + } + // To alter the tooltip header + // chart.interactiveLayer.tooltip.headerFormatter(function(){return '';}); + chart.xScale(d3.time.scale.utc()); + chart.interpolate(fd.line_interpolation); + chart.xAxis + .showMaxMin(fd.x_axis_showminmax) + .staggerLabels(true); + break; + + case 'bar': + chart = nv.models.multiBarChart() + .showControls(true) + .groupSpacing(0.1); + + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + + chart.stacked(fd.bar_stacked); + break; + + case 'dist_bar': + chart = nv.models.multiBarChart() + .showControls(true) //Allow user to switch between 'Grouped' and 'Stacked' mode. + .reduceXTicks(false) + .rotateLabels(45) + .groupSpacing(0.1); //Distance between each group of bars. + + chart.xAxis + .showMaxMin(false); + + chart.stacked(fd.bar_stacked); + break; + + case 'pie': + chart = nv.models.pieChart() + colorKey = 'x'; + chart.valueFormat(f); + if (fd.donut) { + chart.donut(true); + chart.labelsOutside(true); + } + chart.labelsOutside(true); + chart.cornerRadius(true); + break; + + case 'column': + chart = nv.models.multiBarChart() + .reduceXTicks(false) + .rotateLabels(45); + break; + + case 'compare': + chart = nv.models.cumulativeLineChart(); + chart.xScale(d3.time.scale.utc()); + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + break; + + case 'bubble': + var row = function(col1, col2) { + return "" + col1 + "" + col2 + ""; + }; + chart = nv.models.scatterChart(); + chart.showDistX(true); + chart.showDistY(true); + chart.tooltip.contentGenerator(function (obj) { + var p = obj.point; + var s = ""; + s += ''; + s += row(fd.x, f(p.x)); + s += row(fd.y, f(p.y)); + s += row(fd.size, f(p.size)); + s += "
' + p[fd.entity] + ' (' + p.group + ')
"; + return s; + }); + chart.pointRange([5, fd.max_bubble_size * fd.max_bubble_size]); + break; + + case 'area': + chart = nv.models.stackedAreaChart(); + chart.style(fd.stacked_style); + chart.xScale(d3.time.scale.utc()); + chart.xAxis + .showMaxMin(false) + .staggerLabels(true); + break; + + default: + console.error("unrecognized visualization for nvd3", viz_type); + } + + if ("showLegend" in chart && typeof fd.show_legend !== 'undefined') { + chart.showLegend(fd.show_legend); + } + + var height = slice.height(); + + if(chart.hasOwnProperty("x2Axis")) { + height += 30; + } + chart.height(height); + slice.container.css('height', height + 'px'); + + if ((viz_type === "line" || viz_type === "area") && fd.rich_tooltip) { + chart.useInteractiveGuideline(true); + } + if (fd.y_axis_zero) { + chart.forceY([0, 1]); + } + else if (fd.y_log_scale) { + chart.yScale(d3.scale.log()); + } + if (fd.x_log_scale) { + chart.xScale(d3.scale.log()); + } + if (viz_type === 'bubble') { + chart.xAxis.tickFormat(d3.format('.3s')); + } + else if (fd.x_axis_format == 'smart_date') { + chart.xAxis.tickFormat(px.formatDate); + } + else if (fd.x_axis_format !== undefined) { + chart.xAxis.tickFormat(px.timeFormatFactory(fd.x_axis_format)); + } + if (chart.yAxis !== undefined) { + chart.yAxis.tickFormat(d3.format('.3s')); + } + + if (fd.contribution || fd.num_period_compare || viz_type == 'compare') { + chart.yAxis.tickFormat(d3.format('.3p')); + if (chart.y2Axis != undefined) { + chart.y2Axis.tickFormat(d3.format('.3p')); + } + } else if (fd.y_axis_format) { + chart.yAxis.tickFormat(d3.format(fd.y_axis_format)); + + if (chart.y2Axis != undefined) { + chart.y2Axis.tickFormat(d3.format(fd.y_axis_format)); + } + } + + chart.color(function(d, i){ + return px.color.category21(d[colorKey]); + }); + + d3.select(slice.selector).append("svg") + .datum(payload.data) + .transition().duration(500) + .attr('height', height) + .call(chart); + + return chart; + }); + + slice.done(payload); + }) + .fail(function(xhr) { + slice.error(xhr.responseText); + }); + }; + + var update = function() { + if (chart && chart.update) { + chart.update(); + } + }; + + return { + render: render, + resize: update, + }; +}; + +module.exports = nvd3Vis; diff --git a/panoramix/static/widgets/viz_para.js b/panoramix/assets/visualizations/parallel_coordinates.js similarity index 82% rename from panoramix/static/widgets/viz_para.js rename to panoramix/assets/visualizations/parallel_coordinates.js index 9dd22e985fa22..fa80224bd1c8e 100644 --- a/panoramix/static/widgets/viz_para.js +++ b/panoramix/assets/visualizations/parallel_coordinates.js @@ -1,11 +1,19 @@ -px.registerViz('para', function(slice) { +// JS +var d3 = window.d3 || require('d3'); +d3.parcoords = require('../vendor/parallel_coordinates/d3.parcoords.js'); +d3.divgrid = require('../vendor/parallel_coordinates/divgrid.js'); + +// CSS +require('../vendor/parallel_coordinates/d3.parcoords.css'); + +function parallelCoordVis(slice) { function refresh() { $('#code').attr('rows', '15') $.getJSON(slice.jsonEndpoint(), function(payload) { var data = payload.data; var fd = payload.form_data; - ext = d3.extent(data, function(d){ + var ext = d3.extent(data, function(d){ return d[fd.secondary_metric]; }); ext = [ext[0], (ext[1]-ext[0])/2,ext[1]]; @@ -13,17 +21,16 @@ px.registerViz('para', function(slice) { .domain(ext) .range(['red', 'grey', 'blue']) .interpolate(d3.interpolateLab); + var color = function(d){return cScale(d[fd.secondary_metric])}; var container = d3.select(slice.selector); - if (fd.show_datatable) - var eff_height = slice.height() / 2; - else - var eff_height = slice.height(); + var eff_height = fd.show_datatable ? (slice.height() / 2) : slice.height(); var div = container.append('div') .attr('id', 'parcoords_' + slice.container_id) .style('height', eff_height + 'px') .classed("parcoords", true); + var parcoords = d3.parcoords()('#parcoords_' + slice.container_id) .width(slice.width()) .color(color) @@ -68,8 +75,11 @@ px.registerViz('para', function(slice) { slice.error(xhr.responseText); }); }; + return { render: refresh, resize: refresh, }; -}); +}; + +module.exports = parallelCoordVis; diff --git a/panoramix/assets/visualizations/pivot_table.css b/panoramix/assets/visualizations/pivot_table.css new file mode 100644 index 0000000000000..6a35fdab24b77 --- /dev/null +++ b/panoramix/assets/visualizations/pivot_table.css @@ -0,0 +1,4 @@ +.gridster li.widget.pivot_table, +div.widget.pivot_table{ + overflow: auto !important; +} diff --git a/panoramix/assets/visualizations/pivot_table.js b/panoramix/assets/visualizations/pivot_table.js new file mode 100644 index 0000000000000..8fff0ff78760b --- /dev/null +++ b/panoramix/assets/visualizations/pivot_table.js @@ -0,0 +1,40 @@ +// // This is a hack because shimming for $ extensions is not working. +// $('body').append([ +// '', +// '', +// ]); + +// // require('datatables'); +// // console.log(jQuery.fn.dataTable); +// // require('../vendor/dataTables/jquery.dataTables.min.js'); +// // require('../vendor/dataTables/dataTables.bootstrap.js'); + +// // CSS +// require('./pivot_table.css'); +// require('../vendor/dataTables/dataTables.bootstrap.css'); + +// module.exports = function(slice) { +// var container = slice.container; +// var form_data = slice.data.form_data; + +// function refresh() { +// $.getJSON(slice.jsonEndpoint(), function(json){ +// container.html(json.data); +// if (form_data.groupby.length == 1){ +// var table = container.find('table').DataTable({ +// paging: false, +// searching: false, +// }); +// table.column('-1').order( 'desc' ).draw(); +// } +// slice.done(json); +// }).fail(function(xhr){ +// slice.error(xhr.responseText); +// }); +// } +// return { +// render: refresh, +// resize: refresh, +// }; + +// }; diff --git a/panoramix/static/widgets/viz_sankey.css b/panoramix/assets/visualizations/sankey.css similarity index 99% rename from panoramix/static/widgets/viz_sankey.css rename to panoramix/assets/visualizations/sankey.css index 866a4de428653..9a2a0c88ae2e6 100644 --- a/panoramix/static/widgets/viz_sankey.css +++ b/panoramix/assets/visualizations/sankey.css @@ -18,5 +18,3 @@ .sankey .link:hover { stroke-opacity: .5; } - - diff --git a/panoramix/assets/visualizations/sankey.js b/panoramix/assets/visualizations/sankey.js new file mode 100644 index 0000000000000..dc6cd76b972e1 --- /dev/null +++ b/panoramix/assets/visualizations/sankey.js @@ -0,0 +1,108 @@ +// CSS +require('./sankey.css'); +// JS +var px = window.px || require('../javascripts/modules/panoramix.js'); +var d3 = window.d3 || require('d3'); +d3.sankey = require('d3-sankey').sankey; + +function sankeyVis(slice) { + var div = d3.select(slice.selector); + + var render = function() { + var margin = {top: 5, right: 5, bottom: 5, left: 5}; + var width = slice.width() - margin.left - margin.right; + var height = slice.height() - margin.top - margin.bottom; + + var formatNumber = d3.format(",.0f"), + format = function(d) { return formatNumber(d) + " TWh"; }; + + var svg = div.append("svg") + .attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); + + var sankey = d3.sankey() + .nodeWidth(15) + .nodePadding(10) + .size([width, height]); + + var path = sankey.link(); + + d3.json(slice.jsonEndpoint(), function(error, json) { + if (error != null){ + slice.error(error.responseText); + return ''; + } + var links = json.data; + var nodes = {}; + // Compute the distinct nodes from the links. + links.forEach(function(link) { + link.source = nodes[link.source] || + (nodes[link.source] = {name: link.source}); + link.target = nodes[link.target] || + (nodes[link.target] = {name: link.target}); + link.value = +link.value; + }); + nodes = d3.values(nodes); + + sankey + .nodes(nodes) + .links(links) + .layout(32); + + var link = svg.append("g").selectAll(".link") + .data(links) + .enter().append("path") + .attr("class", "link") + .attr("d", path) + .style("stroke-width", function(d) { return Math.max(1, d.dy); }) + .sort(function(a, b) { return b.dy - a.dy; }); + + link.append("title") + .text(function(d) { return d.source.name + " → " + d.target.name + "\n" + format(d.value); }); + + var node = svg.append("g").selectAll(".node") + .data(nodes) + .enter().append("g") + .attr("class", "node") + .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; }) + .call(d3.behavior.drag() + .origin(function(d) { return d; }) + .on("dragstart", function() { this.parentNode.appendChild(this); }) + .on("drag", dragmove)); + + node.append("rect") + .attr("height", function(d) { return d.dy; }) + .attr("width", sankey.nodeWidth()) + .style("fill", function(d) { return d.color = px.color.category21(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); }); + + 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; }) + .filter(function(d) { return d.x < width / 2; }) + .attr("x", 6 + sankey.nodeWidth()) + .attr("text-anchor", "start"); + + function dragmove(d) { + d3.select(this).attr("transform", "translate(" + d.x + "," + (d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))) + ")"); + sankey.relayout(); + link.attr("d", path); + } + slice.done(json); + }); + } + return { + render: render, + resize: render, + }; +} + +module.exports = sankeyVis; diff --git a/panoramix/static/widgets/viz_sunburst.css b/panoramix/assets/visualizations/sunburst.css similarity index 100% rename from panoramix/static/widgets/viz_sunburst.css rename to panoramix/assets/visualizations/sunburst.css diff --git a/panoramix/static/widgets/viz_sunburst.js b/panoramix/assets/visualizations/sunburst.js similarity index 69% rename from panoramix/static/widgets/viz_sunburst.js rename to panoramix/assets/visualizations/sunburst.js index 0546a6ea2d7b0..e9435478e050a 100644 --- a/panoramix/static/widgets/viz_sunburst.js +++ b/panoramix/assets/visualizations/sunburst.js @@ -1,8 +1,9 @@ -/* - Modified from http://bl.ocks.org/kerryrodden/7090426 - */ +require('./sunburst.css'); -function viz_sunburst(slice) { +/* + Modified from http://bl.ocks.org/kerryrodden/7090426 + */ +function sunburstVis(slice) { var container = d3.select(slice.selector); var render = function() { var width = slice.width(); @@ -13,25 +14,26 @@ function viz_sunburst(slice) { var radius = Math.min(width, height) / 2; container.select("svg").remove(); + var vis = container.append("svg:svg") - .attr("width", width) - .attr("height", height) - .append("svg:g") - .attr("id", "container") - .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); + .attr("width", width) + .attr("height", height) + .append("svg:g") + .attr("id", "container") + .attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"); var arcs = vis.append("svg:g").attr("id", "arcs"); var gMiddleText = vis.append("svg:g").attr("id", "gMiddleText"); var partition = d3.layout.partition() - .size([2 * Math.PI, radius * radius]) - .value(function(d) { return d.m1; }); + .size([2 * Math.PI, radius * radius]) + .value(function(d) { return d.m1; }); var arc = d3.svg.arc() - .startAngle(function(d) { return d.x; }) - .endAngle(function(d) { return d.x + d.dx; }) - .innerRadius(function(d) { return Math.sqrt(d.y); }) - .outerRadius(function(d) { return Math.sqrt(d.y + d.dy); }); + .startAngle(function(d) { return d.x; }) + .endAngle(function(d) { return d.x + d.dx; }) + .innerRadius(function(d) { return Math.sqrt(d.y); }) + .outerRadius(function(d) { return Math.sqrt(d.y + d.dy); }); var ext; d3.json(slice.jsonEndpoint(), function(error, json){ @@ -41,6 +43,7 @@ function viz_sunburst(slice) { return ''; } var tree = buildHierarchy(json.data); + createVisualization(tree); slice.done(json); }); @@ -51,31 +54,31 @@ function viz_sunburst(slice) { // Bounding circle underneath the sunburst, to make it easier to detect // when the mouse leaves the parent g. arcs.append("svg:circle") - .attr("r", radius) - .style("opacity", 0); + .attr("r", radius) + .style("opacity", 0); // For efficiency, filter nodes to keep only those large enough to see. var nodes = partition.nodes(json) - .filter(function(d) { - return (d.dx > 0.005); // 0.005 radians = 0.29 degrees - }); + .filter(function(d) { + return (d.dx > 0.005); // 0.005 radians = 0.29 degrees + }); ext = d3.extent(nodes, function(d){return d.m2 / d.m1;}); var colorScale = d3.scale.linear() - .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]]) - .range(["#00D1C1", "white","#FFB400"]); + .domain([ext[0], ext[0] + ((ext[1] - ext[0]) / 2), ext[1]]) + .range(["#00D1C1", "white","#FFB400"]); var path = arcs.data([json]).selectAll("path") - .data(nodes) - .enter().append("svg:path") - .attr("display", function(d) { return d.depth ? null : "none"; }) - .attr("d", arc) - .attr("fill-rule", "evenodd") - .style("stroke", "grey") - .style("stroke-width", "1px") - .style("fill", function(d) { return colorScale(d.m2/d.m1); }) - .style("opacity", 1) - .on("mouseenter", mouseenter); + .data(nodes) + .enter().append("svg:path") + .attr("display", function(d) { return d.depth ? null : "none"; }) + .attr("d", arc) + .attr("fill-rule", "evenodd") + .style("stroke", "grey") + .style("stroke-width", "1px") + .style("fill", function(d) { return colorScale(d.m2/d.m1); }) + .style("opacity", 1) + .on("mouseenter", mouseenter); // Add the mouseleave handler to the bounding circle. @@ -84,8 +87,9 @@ function viz_sunburst(slice) { // Get total size of the tree = value of root node from partition. totalSize = path.node().__data__.value; }; - f = d3.format(".3s"); - fp = d3.format(".3p"); + var f = d3.format(".3s"); + var fp = d3.format(".3p"); + // Fade all but the current sequence, and show it in the breadcrumb trail. function mouseenter(d) { @@ -94,19 +98,19 @@ function viz_sunburst(slice) { gMiddleText.selectAll("*").remove(); gMiddleText.append("text") - .classed("middle", true) - .style("font-size", "50px") - .text(percentageString); + .classed("middle", true) + .style("font-size", "50px") + .text(percentageString); gMiddleText.append("text") - .classed("middle", true) - .style("font-size", "20px") - .attr("y", "25") - .text("m1: " + f(d.m1) + " | m2: " + f(d.m2)); + .classed("middle", true) + .style("font-size", "20px") + .attr("y", "25") + .text("m1: " + f(d.m1) + " | m2: " + f(d.m2)); gMiddleText.append("text") - .classed("middle", true) - .style("font-size", "15px") - .attr("y", "50") - .text("m2/m1: " + fp(d.m2/d.m1)); + .classed("middle", true) + .style("font-size", "15px") + .attr("y", "50") + .text("m2/m1: " + fp(d.m2/d.m1)); var sequenceArray = getAncestors(d); function breadcrumbPoints(d, i) { @@ -124,13 +128,15 @@ function viz_sunburst(slice) { // Update the breadcrumb trail to show the current sequence and percentage. function updateBreadcrumbs(nodeArray, percentageString) { - l = []; + var l = []; for(var i=0; i ') - gMiddleText.append("text").text(s).classed("middle", true) - .attr("y", -75); + var s = l.join(' > ') + gMiddleText.append("text") + .text(s) + .classed("middle", true) + .attr("y", -75); } updateBreadcrumbs(sequenceArray, percentageString); @@ -141,12 +147,12 @@ function viz_sunburst(slice) { // Then highlight only those that are an ancestor of the current segment. arcs.selectAll("path") - .filter(function(node) { - return (sequenceArray.indexOf(node) >= 0); - }) - .style("opacity", 1) - .style("stroke", "#888") - .style("stroke-width", "2px"); + .filter(function(node) { + return (sequenceArray.indexOf(node) >= 0); + }) + .style("opacity", 1) + .style("stroke", "#888") + .style("stroke-width", "2px"); } // Restore everything to full opacity when moving off the visualization. @@ -154,7 +160,8 @@ function viz_sunburst(slice) { // Hide the breadcrumb trail arcs.select("#trail") - .style("visibility", "hidden"); + .style("visibility", "hidden"); + gMiddleText.selectAll("*").remove(); // Deactivate all segments during transition. @@ -163,14 +170,14 @@ function viz_sunburst(slice) { // Transition each segment to full opacity and then reactivate it. arcs.selectAll("path") - .transition() - .duration(200) - .style("opacity", 1) - .style("stroke", "grey") - .style("stroke-width", "1px") - .each("end", function() { - d3.select(this).on("mouseenter", mouseenter); - }); + .transition() + .duration(200) + .style("opacity", 1) + .style("stroke", "grey") + .style("stroke-width", "1px") + .each("end", function() { + d3.select(this).on("mouseenter", mouseenter); + }); } // Given a node in a partition layout, return an array of all of its ancestor @@ -225,6 +232,7 @@ function viz_sunburst(slice) { } function recurse(node){ if (node.children){ + var sums; var m1 = 0; var m2 = 0; for (var i=0; i', + '', +]); + +// require('datatables'); +// console.log(jQuery.fn.dataTable); +// require('../vendor/dataTables/jquery.dataTables.min.js'); +// require('../vendor/dataTables/dataTables.bootstrap.js'); + +// CSS +require('./table.css'); +require('../vendor/dataTables/dataTables.bootstrap.css'); + +function tableVis(slice) { var data = slice.data; var form_data = data.form_data; var f = d3.format('.3s'); @@ -9,23 +24,26 @@ px.registerViz('table', function(slice) { var data = json.data; var metrics = json.form_data.metrics; function col(c){ - arr = []; + var arr = []; for (var i=0; i').html(this.options.on).addClass(this._onstyle+" "+b),d=a('