var dimensions = []; // definition of dimensions var dimensionsValues = []; // current choice for each dimension var previousDimValues = []; // previous choice of dimensions values (for rollback in case of error) var MAX_DIM = 6; // maximum number of dimensions // titles, labels etc. - strings used in the config controls var EMPTY_DIM_LABEL_TEXT = "EMPTY"; var DIM_BUTTON_TEXT = "LEVEL"; var DEFAULT_TITLE = "Analytics"; var MODAL_TITLE = ""; var dataSourceURLFn; //helper property definition for getting the d3 selection size (useful for debugging) d3.selection.prototype.size = function() { var n = 0; this.each(function() { ++n; }); return n; }; // visualization dimensions var w = 800, h = 500; // scaling functions for x and y coordinates that maps the visualized data domain (values) to visualization viewport size var x = d3.scale.linear().range([0, w]), y = d3.scale.linear().range([0, h]); var kx, ky; //visualization viewport var vis = null; var g = null; // global variable containing the group of visualized 'svg:g' elements var treeRoot = null; // root for visualized data var isInChangeDimension = false; // flag indicating that the visualization is in fading out stage //----- visualization config controls functions comes below ----------------------- //inits drop-downlists with the content based on the passed dimensions function initConfigDropdowns(){ for (i=1, len=dimensions.length; i<=len; i++) for (j=0; j<len; j++) $("div.input-group[dimensionId='" + i + "'] ul") .append("<li dimensionName='" + dimensions[j] + "'>" + "<a href='#'>" + dimensions[j] + "</a></li>"); } // on-click handlers for the config drop-down lists function registerDimensionsOnClick(){ $("li[dimensionName] > a").click(function(){ var dimensionId = parseInt($(this).parents(".input-group").attr("dimensionId")); var dimensionValue = $(this).text(); // disable this dimension value in this dropdown $(this).parent().addClass("disabled"); // disable this dimension value in this dropdown // remove link block on old value if any if (dimensionsValues[dimensionId-1] != ""){ $("div.input-group[dimensionId='" + dimensionId + "'] li[dimensionName='" + dimensionsValues[dimensionId-1] + "']") .removeClass("disabled"); } // update dimension label $("div.input-group[dimensionId='" + dimensionId + "'] > span") .removeClass("label-danger") .addClass("label-success") .text(dimensionValue); var index = dimensionsValues.indexOf(dimensionValue); if (index != -1){ var updatingDimensionId = index + 1; $("div.input-group[dimensionId='" + updatingDimensionId + "'] li[dimensionName='" + dimensionValue + "']") .removeClass("disabled"); $("div.input-group[dimensionId='" + updatingDimensionId + "'] > span") .removeClass("label-success") .addClass("label-danger") .text(EMPTY_DIM_LABEL_TEXT); dimensionsValues[index] = ""; } dimensionsValues[dimensionId-1] = dimensionValue; if (dimensionsValues.indexOf("") == -1){ $("#show-vis-button").prop('disabled', false); } else $("#show-vis-button").prop('disabled', true); }); } //creates the modal title based on the chosen dimensions function buildTitleString(){ var dimensionsOrder = " - "; for (i=0, len=dimensionsValues.length; i<len; i++){ dimensionsOrder = dimensionsOrder.concat(dimensionsValues[i]); if (i != len-1) dimensionsOrder = dimensionsOrder.concat(", "); } dimensionsOrder = dimensionsOrder.concat("."); $("#analytics-modal-label").text(MODAL_TITLE + dimensionsOrder); } // fires visualization function registerShowOnClick(){ $("#show-vis-button").click(function(){ d3.json(dataSourceURLFn(dimensionsValues)) .on("progress", function(){ $("#analytics-modal-label").text("Loading data ..."); $("#show-vis-button").text("Loading data ..."); $("#show-vis-button").prop('disabled', true); }) .get(function(error, root){ if (typeof root === "undefined"){ rollbackDimensions(previousDimValues); $("#analytics-modal-no-data-alert").show(); return; } previousDimValues = dimensionsValues.slice(0); var data = d3.nest(); function addKey(index) { data.key(function(d) { return d[dimensionsValues[index]]; }) } for(var i=0; i<dimensionsValues.length-1; i++) { addKey(i); } var visObj = {"key": root.name, "values" : data.entries(root.data)}; var statisticsName = root.statistics; if ($("div.chart g").length == 0){ visualize(visObj, statisticsName); } else { fadeOut(); d3.timer(function(){ if ($("div.chart g").length == 0){ visualize(visObj, statisticsName); return true; } }); } }); // end of xhr.get }); // end of on-click } // restores previously chosen dimensions in case of error (values, dropdown links' states as well as labels are rollbacked) function rollbackDimensions(values){ $("#analytics-modal-label").text(MODAL_TITLE); $("#show-vis-button").text("Show"); dimensionsValues = values.slice(0); $("li[dimensionName]").removeClass("disabled"); if (values.indexOf("") == -1) { $("#show-vis-button").prop('disabled', false); for (i=0, len=values.length; i<len; i++){ $("div.input-group[dimensionId='" + (i+1) + "'] li[dimensionName='" + values[i] + "']") .addClass("disabled"); $("div.input-group[dimensionId='" + (i+1) + "'] > span") .addClass("label-success") .text(values[i]); } } else{ $("div.input-group[dimensionId] > span") .addClass("label-danger") .text(EMPTY_DIM_LABEL_TEXT); $("#show-vis-button").prop('disabled', true); } } // init all the text fields in the modal (label, dimensions buttons' texts and modal title) function initModalTexts(){ $("div.input-group[dimensionId] > span") .addClass("label-danger") .text(EMPTY_DIM_LABEL_TEXT); $("div.input-group-btn button:first-child", $("#analyticsModal")) .each(function(i){ $(this) .html(DIM_BUTTON_TEXT + " " + $(this).parents(".input-group").attr("dimensionId") + " <span class='caret'></span>"); }); $("#analytics-modal-label").html(MODAL_TITLE); } // this function creates dynamically the config row based on the passed dimensions function initConfigRow(){ var sideWidth = MAX_DIM - dimensions.length; $(".analytics-text-pattern") .clone() .removeClass("hide analytics-text-pattern") .addClass("col-sm-" + sideWidth) .appendTo($("#config-row", $("#analyticsModal"))); for (i=0, len=dimensions.length; i<len; i++) $(".analytics-dropdown-pattern") .clone() .removeClass("hide analytics-dropdown-pattern") .addClass("col-sm-2") .children(".input-group") .attr("dimensionId",i+1) .parent() .appendTo($("#config-row", $("#analyticsModal"))); $(".analytics-show-btn-pattern") .clone() .removeClass("hide analytics-show-btn-pattern") .addClass("col-sm-" + sideWidth) .appendTo($("#config-row", $("#analyticsModal"))); } // this function shall be called from the external code to open and init the analytics modal // it expects the table of dimension names as strings e.g. ["dim1", "dim2", "dim3"] // URL of data source and title of the analytics function openVisualizationModal(dims, getDataSourceURL, title){ if(typeof dims === "undefined" || !$.isArray(dims)){ console.log("Wrong arguments passed to openning analytics modal: dimensions table incorrect"); return false; } if(!getDataSourceURL){ console.log("Wrong arguments passed to openning analytics modal: please provide function returning your data URL"); return false; } dataSourceURLFn = getDataSourceURL; dimensions = dims; if (typeof title === "undefined") MODAL_TITLE = DEFAULT_TITLE else MODAL_TITLE = title; $("#analyticsModal").one("show.bs.modal", function() { var height = $(window).height() - 100; $(this).find(".modal-body").css("max-height", height); for(i=0,len=dimensions.length; i<len; i++){ dimensionsValues[i] = ""; previousDimValues[i] = ""; } $("#show-vis-button") .prop('disabled', true) .text("Show"); $("button[data-hide='alert']", $("#analyticsModal")).click(function(){ $("#analytics-modal-no-data-alert").hide(); }); $("#analytics-modal-no-data-alert").hide(); // creates the container and SVG placeholder for visualization vis = d3.select(".modal-body").append("div") .attr("class", "chart") .style("width", w + "px") .style("height", h + "px") .append("svg:svg") .attr("width", w) .attr("height", h); initConfigRow(); initModalTexts(); initConfigDropdowns(); registerDimensionsOnClick(); registerShowOnClick(); }); // register handler for clearing things up after modal is closed $("#analyticsModal").one("hidden.bs.modal", function() { // removes list items for dimensions choice (and all the on-click handlers) $("div.input-group li", $("#analyticsModal")).remove(); // remove chart container where visualization is displayed and all linked event handlers $("div.chart", $("#analyticsModal")).remove(); // remove on-click handler for Show button $("#show-vis-button").off("click"); //remove alert close on-click $("button[data-hide='alert']", $("#analyticsModal")).off("click"); // empty config row $("#config-row > div", $("#analyticsModal")).remove(); dimensions = []; dimensionsValues = []; previousDimValues = []; }); // show the modal $("#analyticsModal").modal('show'); return true; } // ----------------- Visualization code comes below ----------------------------- // helper function to apply callback at the end of all independent transitions scheduled on a group of elements function endall(transition, callback) { var n = 0; transition .each(function() { ++n; }) .each("end", function() { if (!--n) callback.apply(this, arguments); }); } // definition of the function visualizing data attached to the root of the JSON tree object function visualize(root, statisticsName){ isInChangeDimension = false; //d3.parition initialization, setting value and children accessors var partition = d3.layout.partition() .value(function(d) { return d[statisticsName]; }) .children(function(d) { return d.values; }); x = d3.scale.linear().range([0, w]); y = d3.scale.linear().range([0, h]); $("#analytics-modal-label").text("Rendering ..."); $("#show-vis-button").text("Rendering ..."); $("#show-vis-button").prop('disabled', true); treeRoot = root; g = vis.selectAll("g") .data(partition.nodes(root)) .enter().append("svg:g") .attr("transform", function(d) { return "translate(" + x(d.y) + ", 0)"; }) .on("click", click); kx = w / root.dx; ky = h / 1; g.append("svg:rect") .attr("width", root.dy * kx) .attr("height", 0) .attr("class", function(d){ return d.children ? "parent" : "child"; }); g.append("svg:text") .attr("transform", "translate(8,0)") .attr("dy", ".35em") .style("opacity", 0) .text(function(d) { return (d.children ? d.key : d[dimensionsValues[dimensionsValues.length-1]]) + ": " + d.value; }); var t = g.transition() .duration(750) .attr("transform", function(d) { return "translate(" + x(d.y) + "," + y(d.x) + ")"; }) .call(endall, function(){ buildTitleString(); $("#show-vis-button").text("Show"); $("#show-vis-button").prop('disabled', false); }); t.select("rect") .attr("width", root.dy * kx) .attr("height", function(d) { return d.dx * ky; }); t.select("text") .attr("transform", function(d){ return "translate(8," + d.dx * ky / 2 + ")"; }) .attr("dy", ".35em") .style("opacity", function(d) { return d.dx * ky > 12 ? 1 : 0; }); } // end of visualize function // data element on-click handler to zoom in/out the visualization function click(d) { $("#show-vis-button").prop('disabled', true); if (!d.children) d = treeRoot; kx = (d.y ? w - 40 : w) / (1 - d.y); ky = h / d.dx; x.domain([d.y, 1]).range([d.y ? 40 : 0, w]); y.domain([d.x, d.x + d.dx]); var gTransition = g.transition() .duration(1500) .attr("transform", function(d) { return "translate(" + x(d.y) + "," + y(d.x) + ")"; }) .call(endall, function(){ if (!isInChangeDimension && dimensionsValues.indexOf("") == -1) $("#show-vis-button").prop('disabled', false); }); var rectTransition = gTransition.select("rect") .attr("width", d.dy * kx) .attr("height", function(d) { return d.dx * ky; }); var textTransition = gTransition.select("text") .attr("transform", function(d){ return "translate(8," + d.dx * ky / 2 + ")"; }) .style("opacity", function(d) { return d.dx * ky > 12 ? 1 : 0; }); if (isInChangeDimension){ gTransition.transition() .duration(1500) .attr("transform", function(d) { return "translate(" + x(d.y) + ", 0)"; }) .remove() .call(endall, function(){ if (dimensionsValues.indexOf("") == -1) $("#show-vis-button").prop('disabled', false); }); rectTransition.transition() .duration(1500) .attr("height", 0); textTransition.transition() .duration(1500) .attr("transform", "translate(8,0)") .style("opacity",0); } } // fading out the current visualization function fadeOut() { $("#analytics-modal-label").text("Rendering ..."); $("#show-vis-button").text("Rendering ..."); $("#show-vis-button").prop('disabled', true); isInChangeDimension = true; if (!(kx == w && ky == h)) click(treeRoot); else { var t = g.transition() .duration(2000) .attr("transform", function(d) { return "translate(" + x(d.y) + ", 0)"; }) .remove() .call(endall, function(){ $("#show-vis-button").prop('disabled', false); }); t.select("rect") .attr("height", 0); t.select("text") .attr("transform", "translate(8,0)") .style("opacity",0); } }