doevisualization
Version:
Data Visualization Library based on RequireJS and D3.js (v4+)
688 lines (583 loc) • 29.3 kB
JavaScript
define(['jquery',
'underscore',
'jqueryuiwidget',
'handlebars',
'd3'
],
function($, _, ui, Handlebars, d3) {
$.widget("doe.doestackedbar", {
// Options to be used as defaults
options: {},
_globalData: {},
_template: function() {
var arrHTML = [];
arrHTML.push('<div class="doestackedbar">');
arrHTML.push('</div>');
return arrHTML.join('');
},
_create: function() {
this._fetchAndRender();
},
_fetchAndRender: function() {
this._compileTemplate();
},
_compileTemplate: function() {
var compiled = Handlebars.compile(this._template());
this.element.html(compiled({}));
this.element.addClass('doestackedbarcontainer');
this._bindEvents();
},
_generateRawTable: function() {
var arrHTML = [];
var data = this.options["Data"];
if (data.length > 0) {
arrHTML.push('<div class ="doehide">');
arrHTML.push('<table>');
arrHTML.push('<thead>');
arrHTML.push('<tr>');
for (p in data[1]) {
arrHTML.push('<th>');
arrHTML.push(p);
arrHTML.push('</th>');
}
arrHTML.push('</tr>');
for (var i = 0; i < data.length; i++) {
arrHTML.push('<tr>');
for (prop in data[i]) {
arrHTML.push('<td>');
arrHTML.push(data[i][prop]);
arrHTML.push('</td>');
}
arrHTML.push('</tr>');
}
arrHTML.push('</thead>');
arrHTML.push('</table>');
arrHTML.push('</div>');
}
this.element.append(arrHTML.join(''));
},
_bindEvents: function() {
var minWidth = 700,
minHeight = 620,
maxStackedBarSize = 80,
widthSVG = this.element.width() < minWidth ? minWidth : this.element.width(),
heightSVG = this.element.height() < minHeight ? minHeight : this.element.height(),
width = widthSVG * 0.8,
height = heightSVG * 0.7 - 40;
this._globalData.heightSVG = heightSVG;
this._globalData.widthSVG = widthSVG;
this._globalData.width = width;
this._globalData.height = height;
var color = d3.scaleOrdinal()
.range(this.options.ColorPalette);
// .range(["#98abc5", "#8a89a6", "#7b6888", "#6b486b", "#a05d56", "#d0743c", "#ff8c00"]);
var formatValue = d3.format(".2s");
var xField = this.options["XFields"];
var yField = this.options["YFields"];
var xTitle = this.options["XTitle"];
var yTitle = this.options["YTitle"];
var title = this.options["ChartTitle"]
var subTitle = this.options["ChartSubTitle"];
var toolTipMasterTemplate = [];
var toolTipSuppressedTemplate = [];
toolTipMasterTemplate.push('<div class="tooltipprogress">');
toolTipMasterTemplate.push('<i class="fa fa-spinner fa-spin fa-3x fa-fw"></i>');
toolTipMasterTemplate.push('</div>');
toolTipMasterTemplate.push('<div class="tooltipcontent doehide">');
toolTipMasterTemplate.push(this.options["TooltipTemplate"]);
toolTipMasterTemplate.push('</div>');
toolTipSuppressedTemplate.push('<div class="tooltipprogress">');
toolTipSuppressedTemplate.push('<i class="fa fa-spinner fa-spin fa-3x fa-fw"></i>');
toolTipSuppressedTemplate.push('</div>');
toolTipSuppressedTemplate.push('<div class="tooltipcontent doehide">');
toolTipSuppressedTemplate.push("This data has been suppressed for student privacy.");
toolTipSuppressedTemplate.push('</div>');
var tooltipTemplate = this.options["TooltipTemplate"];
var tooltipCompiled = Handlebars.compile(toolTipMasterTemplate.join(''));
var tooltipSuppressedCompiled = Handlebars.compile(toolTipSuppressedTemplate.join(''));
var formatComma = d3.format(","); // format large number with commas
var tooltipFields = this.options["TooltipFields"];
var showDataLabel = this.options["ShowDataLabel"];
var showLegend = this.options["Legend"];
var SuppressionFlag = false;
var that = this;
var maxValue = d3.max(this.options.Data, function(d) {
return parseFloat(d[yField[0]]);
})
var groupeddata = _.groupBy(this.options.Data, $.proxy(function(item) {
return item[this.options["Groupings"][0]];
}, this));
// console.log("grouped data", groupeddata);
var keys = [];
for (var key in groupeddata) {
keys.push(key);
}
keys.reverse();
// var format = d3.format(",d");
var finalobject = [];
for (var i = 0; i < keys.length; i++) {
var k = keys[i]
var uniquescope = _.uniq(_.map(groupeddata[k], $.proxy(function(d) {
return d[this.options["XFields"][0]];
}, this)));
var uniquelabel = _.uniq(_.map(groupeddata[k], $.proxy(function(d) {
return d[this.options["YFields"][1]];
}, this)));
// console.log("X Field:", uniquescope);
// console.log("Y labels:", uniquelabel);
_.each(uniquescope, $.proxy(function(item) {
var index = uniquelabel.length;
var filtereddata = _.filter(groupeddata[k], $.proxy(function(sitem) {
return sitem[this.options["XFields"][0]] === item;
}, this));
if (filtereddata.length > uniquelabel.length) {
filtereddata = filtereddata.slice(index); // just in case we have redundant data
}
var obj = {};
obj["rows"] = item;
obj["origdata"] = [];
_.each(filtereddata, $.proxy(function(tem) {
var defaultvalue = maxValue > 0 ? maxValue : 100;
if (tem[this.options["YFields"][0]] == "*") {
tem[this.options["YFields"][0]] = -0.15 * defaultvalue;
SuppressionFlag = true;
}
obj[tem[this.options["YFields"][1]]] = tem[this.options["YFields"][0]];
// if (tem[this.options["YFields"][0]] !== 0) {
// tem[this.options["YFields"][0]] = format(tem[this.options["YFields"][0]]);
// }
obj["origdata"].push(tem);
}, this));
finalobject.push(obj);
}, this));
// console.log("final obj:", finalobject);
}
var data = finalobject;
var rateNames = d3.keys(data[0]).filter(function(key) {
return (key !== "rows" && key !== "origdata");
});
var rowsNames = data.map(function(d) {
return d.rows;
});
var neutralIndex = Math.floor(rateNames.length / 2);
color.domain(rateNames);
data.forEach(function(row) {
row.total = d3.sum(rateNames.map(function(name) {
return +row[name];
}));
rateNames.forEach(function(name) {
row['relative' + name] = (row.total !== 0 ? +row[name] : 0);
});
var y0 = 0;
row.boxes = rateNames.map(function(name) {
return {
name: name,
y0: y0,
y1: y0 += row['relative' + name],
total: row.total,
absolute: row[name],
origdata: row.origdata
};
});
});
var top = -heightSVG * 0.05 + 10;
var svg = d3.select(this.element.find('.doestackedbar').get(0))
.append("svg")
.attr("width", widthSVG)
.attr("height", heightSVG)
.append("g")
.attr("transform", "translate(" + widthSVG * 0.2 + "," + (heightSVG * 0.15 + top) + ")");
this._globalData.svg = svg;
var tooltip = d3.select(this.element.find('.doestackedbar').get(0))
.append('div')
.attr("class", "tooltip");
if (data.length < 4) {
var x = d3.scaleBand()
.rangeRound([0, Math.min(maxStackedBarSize * data.length * 2, width)])
.padding(0.5);
var left = (width - widthSVG * 0.15 - Math.min(maxStackedBarSize * data.length * 2, width)) / 2;
} else {
var x = d3.scaleBand()
// .rangeRound([0, width])
.rangeRound([0, Math.min(maxStackedBarSize * data.length * 1.5, width)])
.padding(0.3);
var left = (width - widthSVG * 0.15 - Math.min(maxStackedBarSize * data.length * 1.5, width)) / 2;
}
var y = d3.scaleLinear()
.rangeRound([height, 0]);
// var keys = this.options["Groupings"];
// console.log("#stack#", keys);
// data.forEach(function(d) {
// d.total = 0;
// keys.forEach(function(k) {
// d.total += d[k];
// })
// });
// data.sort(function(a, b) { return b.total - a.total; });
// x.domain(data.map(function(d) {
// return d.key; // d.SchoolYear
// }));
x.domain(uniquescope);
if (maxValue > 0) {
y.domain([0, d3.max(data, function(d) {
return d.total;
})]).nice();
} else {
y.domain([0, 100]).nice();
}
// var stack = d3.stack()
// .keys(rateNames)
// .order(d3.stackOrderNone)
// .offset(d3.stackOffsetNone);
//var tempX = left + x(d.rows);
var cat = svg.selectAll("g")
.data(data)
.enter().append("g")
.attr("transform", function(d) { return "translate(" + x(d.rows) + ",0)"; });
var rect = cat.selectAll("rect")
.data(function(d) { return d.boxes; })
.enter()
for (var j = 0; j < data.length; j++) {
var dataset = data[j].origdata;
dataset.forEach(function(d) { //format data for tooltip
for (var i = 0; i < tooltipFields.length; i++) {
if (isNaN(parseFloat(d[tooltipFields[i]]))) {
d[tooltipFields[i]] = d[tooltipFields[i]];
} else {
d[tooltipFields[i]] = parseFloat(d[tooltipFields[i]]);
}
if (typeof(d[tooltipFields[i]]) === "number") {
d[tooltipFields[i]] = formatComma(d[tooltipFields[i]]);
}
}
});
}
rect.append("rect")
.attr("class", "bar")
// .attr("x", function(d) {
// return x(d.data.rows); //d.data.SchoolYear
// })
.attr("transform", function(d) { return "translate(" + left + ",0)"; })
.attr("y", function(d) {
return y(Math.abs(d.y1));
})
.attr("height", function(d) {
return y(Math.abs(d.y0)) - y(Math.abs(d.y1));
})
.attr("width", x.bandwidth())
.style("fill", function(d) { return color(d.name); })
.on('mouseover', function(d, i) {
var compileTmpl = tooltipCompiled(d.origdata[i]);
if (d.absolute < 0) {
tooltip
.style("left", (d3.event.layerX + 10) + "px")
.style("top", (d3.event.layerY + 10) + "px")
.style("display", "block")
.html(tooltipSuppressedCompiled);
// .html("This data has been suppressed for student privacy.");
} else {
tooltip
.style("left", (d3.event.layerX + 10) + "px")
.style("top", (d3.event.layerY + 10) + "px")
.style("display", "block")
.html(compileTmpl);
// .html(d3.format(".3s")(d[1] - d[0]) + '%');
}
that._trigger("tooltiprefreshed", null, {});
svg.selectAll(".bar")
.transition()
.attr('opacity', 0.5);
d3.select(this)
.transition()
// .duration(300)
.attr('opacity', 1)
.style("stroke", function(d) {
return color(d.name);
});
var highlight = d.origdata[0][xField[0]];
svg.selectAll(".axis text")
.filter(function(d) {
return d == highlight
})
.classed("highlight", true);
// svg.selectAll(".bartext")
// .transition()
// .attr('opacity', 0);
})
.on('mouseout', function(d) {
tooltip.style("display", "none");
svg.selectAll(".bar")
.transition()
// .duration(300)
.attr('opacity', 1)
.style("stroke", "none");
var highlight = d.origdata[0][xField[0]];
svg.selectAll(".axis text")
.filter(function(d) {
return d == highlight
})
.classed("highlight", false);
// svg.selectAll(".bartext")
// .transition()
// .attr('opacity', 1);
});
if (SuppressionFlag) {
rect.append("text")
.style('font-family', 'FontAwesome')
.style('fill', "black")
.style('font-size', function(d) {
var barSize = 0.9 * Math.min(x.bandwidth(), y(Math.abs(d.y0)) - y(Math.abs(d.y1)));
var maxSize = 25;
if (barSize > maxSize) {
return maxSize;
} else {
return barSize;
}
})
.text(function(d) {
if (d.absolute < 0) {
return '\uf023'; // \uf023
} else {
if (showDataLabel == true) {
if (d.absolute != 0) {
return d.absolute;
} else {
return "";
}
} else {
return "";
}
}
})
.attr("class", "bartext notranslate")
.attr("transform", function(d) { return "translate(" + left + ",0)"; })
// .attr("y", function(d) { return y(Math.abs(d.y1)); })
// .attr("x", function(d) {
// return x(d.data.rows);
// }) //y(Math.abs(d.y0)) - y(Math.abs(d.y1))
.attr("text-anchor", "middle")
.attr("dx", x.bandwidth() / 2)
.attr("dy", ".35em")
.attr("y", function(d) {
return y(Math.abs(d.y1)) + (y(Math.abs(d.y0)) - y(Math.abs(d.y1))) / 2;
});
// .attr("dy", function(d) {
// return (y(Math.abs(d.y0)) - y(Math.abs(d.y1))) / 2;
// });
} else {
rect.append("text")
// .text(function(d) { return d.absolute; })
.text(function(d) {
if (showDataLabel == true) {
if (d.absolute != 0) {
return d.absolute;
} else {
return "";
}
} else {
return "";
}
})
.attr("class", "bartext notranslate")
.attr("transform", function(d) { return "translate(" + left + ",0)"; })
.attr("y", function(d) { return y(d.y1); })
// .attr("x", function(d) {
// return x(d.data.rows);
// })
.attr("dx", x.bandwidth() / 4)
.attr("dy", function(d) {
return (y(d.y0) - y(d.y1)) / 2;
});
}
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(" + left + "," + height + ")")
.call(d3.axisBottom(x))
.selectAll("text")
.attr("transform", "rotate(-25)")
.style("text-anchor", "end");
//var yAxis = d3.axisLeft(y).ticks(null, "s");
var yAxis = d3.axisLeft(y).tickFormat(function(d) { return formatValue(d) });
svg.append("g")
.attr("class", "y axis")
.attr("transform", "translate(" + left + ", 0)")
.call(yAxis)
.append("text")
.attr("x", 2)
.attr("y", y(y.ticks().pop()) + 0.5)
.attr("dy", "0.32em")
.attr("fill", "#000")
.attr("font-weight", "bold")
.attr("text-anchor", "start");
// .text("Population");
svg.append("text")
.attr("class", "xLabel")
.attr("y", height * 1.1 + 10)
.attr("x", (width - widthSVG * 0.15) / 2)
// .attr("dy", "1em")
.style("text-anchor", "middle")
.text(xTitle);
svg.append("text")
.attr("class", "yLabel")
.attr("transform", "rotate(-90)")
// .attr("y", 0 - 40)
// .attr("x", 0 - (height / 2))
.attr("y", -45 + left)
.attr("x", -height / 2)
// .attr("dy", "1em")
.style("text-anchor", "middle")
.text(yTitle);
svg.append("text")
.attr('class', 'title')
.attr("x", (width - widthSVG * 0.15) / 2)
.attr("y", -height * 0.1)
.style("text-anchor", "middle")
.text(title);
svg.append("text")
.attr('class', 'subTitle')
.attr("x", (width - widthSVG * 0.15) / 2)
.attr("y", -height * 0.03)
.style("text-anchor", "middle")
.text(subTitle);
if (SuppressionFlag) {
svg.append("text")
.attr('class', 'note')
.attr("x", 0)
.attr("y", heightSVG * 0.8 + 45)
.style('font-family', 'FontAwesome')
.style("text-anchor", "start")
.text("(\uf023) Some information may be protected for student privacy.");
}
var legendArr = rateNames.slice();
this._globalData.legendArr = legendArr;
if (showLegend == true) {
this._drawLegend(legendArr);
}
// this._trigger("complete", null, {});
this._trigger("complete", null, { "legendArr": legendArr });
// var test = ["150000000000", "160000000000", "170000000000"]
// this.setLegend(test);
this._generateRawTable();
},
_drawLegend: function(data) {
svg = this._globalData.svg;
var color = d3.scaleOrdinal()
.range(this.options.ColorPalette);
heightSVG = this._globalData.heightSVG;
widthSVG = this._globalData.widthSVG;
width = this._globalData.width;
height = this._globalData.height;
var dataOrigin = this._globalData.legendArr;
d3.select('.stackLegend').remove();
var stackLegend = svg.append('g')
.attr('class', 'stackLegend');
var legend = svg.select('.stackLegend')
.selectAll("g")
// .data(rateNames.slice().reverse())
.data(data)
.enter().append("g");
// .attr("transform", function(d, i) {
// return "translate(" + width / rateNames.slice().reverse().length * i + "," + heightSVG * 0.8 + ")";
// });
legend.append("rect")
.attr("x", 0)
.attr("width", 20)
.attr("height", 20)
.attr("fill", color)
.on('mouseover', function(d, i) {
d3.select(this)
.style("stroke", function(d) {
return color(d);
});
var tem = i;
svg.selectAll(".bar")
.filter(function(d, i) {
return dataOrigin.indexOf(d.name) !== tem;
})
.attr('opacity', 0);
svg.selectAll(".bartext")
.filter(function(d) {
return dataOrigin.indexOf(d.name) !== tem;
})
.attr('opacity', 0);
})
.on('mouseout', function(d, i) {
d3.select(this)
.style("stroke", "none");
svg.selectAll(".bar")
.attr('opacity', 1);
svg.selectAll(".bartext")
.attr('opacity', 1);
});
legend.append("text")
.attr("x", 22)
.attr("y", 11)
.attr("dy", "0.32em")
.attr("class", "legend")
.style("text-anchor", "begin")
.text(function(d) {
return d;
})
.on('mouseover', function(d, i) {
d3.select(this)
.classed("highlight", true);
var tem = i;
svg.selectAll(".bar")
.filter(function(d) {
return dataOrigin.indexOf(d.name) !== tem;
})
.attr('opacity', 0);
svg.selectAll(".bartext")
.filter(function(d) {
return dataOrigin.indexOf(d.name) !== tem;
})
.attr('opacity', 0);
})
.on('mouseout', function(d, i) {
d3.select(this)
.classed("highlight", false);
svg.selectAll(".bar")
.attr('opacity', 1);
svg.selectAll(".bartext")
.attr('opacity', 1);
});
var sum = 0;
var xPos = 0;
var yPos = heightSVG * 0.8;
legend
.attr(
"transform",
function(d, i) {
var ret = "translate(" + (xPos) + "," + yPos + ")";
xPos += this.getBBox().width + 10;
sum += this.getBBox().width + 10;
// xPos += d.length;
if (xPos > width - widthSVG * 0.15) {
xPos = 0;
yPos += 30;
}
return ret;
}
);
//this.element.find('.stackLegend').get(0)
var legendWidth = d3.select(this.element.find('.stackLegend').get(0)).node().getBBox().width;
stackLegend
.attr("transform", function(d, i) {
return "translate(" + (width - widthSVG * 0.2 - legendWidth) / 2 + "," + 0 + ")";
});
},
destroy: function() {
},
setLegend: function(data) {
this._drawLegend(data);
},
getLegend: function() {
},
_setOption: function(key, value) {
this._super(key, value);
},
_setOptions: function(options) {
this._super(options);
}
});
});