Last active
April 20, 2016 11:32
-
-
Save nanyaks/5141ae12560e98862693542b1eb824e5 to your computer and use it in GitHub Desktop.
Stacked group chart graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: none |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
/*global $, d3, _ */ | |
var seriesNames = ["SO", "JP", "LO"], | |
numSeries = seriesNames.length, | |
projectTypes = [ | |
"Permit/Fenc", | |
"Prelift", | |
"Lift", | |
"Demo", | |
"Footings", | |
"Pilings", | |
"Foundation", | |
"Sill Plates", | |
"Set", | |
"Reconnect", | |
"Desk Steps", | |
"Landscaping", | |
"Punchlist" | |
], | |
numSamples = projectTypes.length, // number of samples is the number of project stages we have all in all, | |
mainBarValues = [], | |
data = seriesNames.map(function (name) { | |
var _value = bumpLayer(numSamples, 1); // return the values for each of seriesNames (should have come from the DB somehow) | |
var _obj = { | |
name: name, | |
values: _value | |
}; | |
mainBarValues.push(_obj); | |
return _obj; | |
}), | |
addressData = seriesNames.map(function (name, index) { | |
var _value = generateAddressBarLayer(mainBarValues[index].values); | |
return { | |
name: name, | |
values: _value | |
}; | |
}), | |
stack = d3.layout.stack().values(function (d) { return d.values; }), | |
addressStack = d3.layout.stack().values(function (d) { return d.values; }); | |
/* | |
* This was an attemp to sanitize the data and have values for y0 | |
* | |
*/ | |
var largestDetailArray = d3.max(addressData, function (d){ | |
return d.values; | |
}); | |
var maxAddressArrayLength = largestDetailArray.length; | |
// call sanitizeData | |
var sanitizedAddressData = sanitizeData(addressData, maxAddressArrayLength); | |
console.log(maxAddressArrayLength); | |
console.log(sanitizedAddressData); | |
stack(data); | |
addressStack(addressData); | |
//addressStack(data); | |
var chartMode = "stacked", | |
numEnabledSeries = numSeries, | |
lastHoveredBarIndex, | |
containerWidth = document.querySelector(".js-stacked-chart-container").clientWidth, | |
containerHeight = 500, | |
margin = {top: 150, right: 10, bottom: 20, left: 30}, | |
width = containerWidth - margin.left - margin.right, | |
height = containerHeight - margin.top - margin.bottom, | |
widthPerStack = width / numSamples, | |
animationDuration = 400, | |
delayBetweenBarAnimation = 10, | |
numYAxisTicks = 8, | |
maxStackY = d3.max(data, function (series) { return d3.max(series.values, function (d) { return d.y0 + d.y; }); }), | |
paddingBetweenLegendSeries = 5, | |
// legend series boxes | |
legendSeriesBoxX = 0, | |
legendSeriesBoxY = 0, | |
legendSeriesBoxWidth = 15, | |
legendSeriesBoxHeight = 15, | |
legendSeriesHeight = legendSeriesBoxHeight + paddingBetweenLegendSeries, | |
legendSeriesLabelX = -5, | |
legendSeriesLabelY = legendSeriesBoxHeight / 2, | |
legendMargin = 10, | |
legendX = containerWidth - legendSeriesBoxWidth - legendMargin, | |
legendY = legendMargin, | |
overlayTopPadding = 20, | |
tooltipBottomMargin = 12; | |
var binsScale = d3.scale.ordinal() | |
.domain(d3.range(numSamples)) | |
.rangeBands([0, width], 0.1, 0.05); | |
var xScale = d3.scale.linear() | |
.domain([0, numSamples]) | |
.range([0, width]); | |
var yScale = d3.scale.linear() | |
.domain([0, maxStackY]) | |
.range([height, 0]); | |
var heightScale = d3.scale.linear() | |
.domain([0, maxStackY]) | |
.range([0, height]); | |
// Dummy data for the x-axis | |
var xAxisData = ['A', 'B', 'C', 'D', 'E']; | |
var xAxis = d3.svg.axis() | |
.scale(xScale) //binsScale) | |
.tickFormat(function(d) { return projectTypes[d]; }) | |
.orient("bottom"); | |
var yAxis = d3.svg.axis() | |
.scale(yScale) | |
.orient("left"); | |
var enabledSeries = function () { return _.reject(data, function (series) { return series.disabled; }); }; | |
var seriesClass = function (seriesName) { return "series-" + seriesName.toLowerCase(); }; | |
var layerClass = function (d) { return "layer " + seriesClass(d.name); }; | |
var legendSeriesClass = function (d) { return "" + seriesClass(d); }; | |
var barDelay = function (d, i) { return i * delayBetweenBarAnimation; }; | |
var joinKey = function (d) { return d.name; }; | |
var stackedBarX = function (d) { return binsScale(d.x); }; | |
var stackedBarY = function (d) { return yScale(d.y0 + d.y); }; | |
var stackedBarBaseY = function (d) { return yScale(d.y0); }; | |
var stackedBarWidth = binsScale.rangeBand(); | |
var groupedBarX = function (d, i, j) { return binsScale(d.x) + j * groupedBarWidth(); }; | |
var groupedBarY = function (d) { return yScale(d.y); }; | |
var groupedBarBaseY = height; | |
var groupedBarWidth = function () { return binsScale.rangeBand() / numEnabledSeries; }; | |
var barHeight = function (d) { return heightScale(d.y); }; | |
/** | |
* Function to perform trasnsition on the chart bars | |
*/ | |
var transitionStackedBars = function (selection) { | |
selection.transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("y", stackedBarY) | |
.attr("height", barHeight); | |
}; | |
// Function to handle the address bars | |
var transitionStackedAddressBars = function (selection) { | |
selection.transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("y", stackedSecondBarY) | |
.attr("height", barHeight); | |
}; | |
var svg = d3.select(".js-stacked-chart") | |
.attr("width", containerWidth) | |
.attr("height", containerHeight); | |
var mainArea = svg.append("g") | |
.attr("class", "main-area") | |
.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | |
var layersArea = mainArea.append("g") | |
.attr("class", "layers"); | |
var addressLayer = mainArea.append("g") | |
.attr("class", "layers"); | |
var addressTextLayer = mainArea.append("g") | |
.attr("class", "layers"); | |
var layers = layersArea.selectAll(".layer").data(data) | |
.enter().append("g") | |
.attr("class", layerClass); // This line adds the different colors for the stacked chart | |
// The width of the second bar | |
var thinBarWidth = stackedBarWidth / 10.0; | |
// This is to displace the second bar to the right of the first | |
var stackedSecondBarX = function (d) { return binsScale(d.x) + thinBarWidth; }; // simple hack | |
var stackedSecondBarY = function (d) { | |
return yScale(d.y0 + d.y); | |
}; | |
var stackedTextX = function (d) { return binsScale(d.x) + thinBarWidth + 40; }; // simple hack | |
var stackedTextY = function (d) { | |
var _yScale = yScale(d.y0 + d.y) + 20; // Not entirely accurate but it gets the work done | |
return _yScale; | |
}; | |
var addressBarWidth = thinBarWidth * 9.0; // give the remainder 18th of the width to the addressBar | |
layers.selectAll("rect").data(function (d) { return d.values; }) | |
.enter().append("rect") | |
.attr("x", stackedBarX) | |
.attr("y", height) | |
.attr("width", thinBarWidth) | |
.attr("height", 0) | |
.call(transitionStackedBars); | |
/** | |
* Start second bar definition | |
*/ | |
var addressBar = addressLayer.selectAll(".layer").data(addressData) | |
.enter().append("g") | |
.attr("class", layerClass); | |
var addressText = addressTextLayer.selectAll(".layer").data(addressData) | |
.enter().append("g") | |
.attr("class", layerClass); | |
/** | |
* Address bar rectangles | |
*/ | |
addressBar.selectAll("rect") | |
.data(function (d) { | |
return d.values; | |
}) | |
.enter().append("rect") | |
.attr("x", stackedSecondBarX) | |
.attr("y", stackedSecondBarY) | |
.attr("width", addressBarWidth) | |
.attr("height", height) | |
.attr("class", "addressBar") | |
.call(transitionStackedAddressBars); | |
/** | |
* Address bar text | |
*/ | |
addressText.selectAll("text") | |
.data(function (d) { | |
return d.values; | |
}) | |
.enter().append("text") | |
.attr("x", stackedTextX) | |
.attr("y", stackedTextY) | |
.attr("fill", "#000") | |
.style("stroke-width", 1) | |
.style({"font-size":"10px","z-index":"999999999"}) | |
.style("text-anchor", "middle") | |
.text(function (d) { return d.y0 ; }); | |
// .text("123 Elm Street"); | |
/* | |
* End second bar | |
*/ | |
/* | |
* Add the onclick handler for individual rectangles | |
*/ | |
layers.selectAll("rect") | |
.on("click", function (d, i) { console.log(d, i); }); | |
mainArea.append("g") | |
.attr("class", "x axis") | |
.attr("transform", "translate(" + widthPerStack / 2 + ", -40)") // hack to provide a margin at the top - btw the x axis and the charts | |
.call(xAxis); | |
mainArea.append("g") | |
.attr("class", "y axis") | |
.call(yAxis); | |
var legendSeries = svg.append("g") | |
.attr("class", "legend") | |
.attr("transform", "translate(" + legendX + "," + legendY + ")") | |
.selectAll("g").data(seriesNames.reverse()) | |
.enter().append("g") | |
.attr("class", legendSeriesClass) | |
.attr("transform", function (d, i) { return "translate(0," + (i * legendSeriesHeight) + ")"; }) | |
.on("click", toggleSeries); | |
legendSeries.append("rect") | |
.attr("class", "series-box") | |
.attr("x", legendSeriesBoxX) | |
.attr("y", legendSeriesBoxY) | |
.attr("width", legendSeriesBoxWidth) | |
.attr("height", legendSeriesBoxHeight); | |
legendSeries.append("text") | |
.attr("class", "series-label") | |
.attr("x", legendSeriesLabelX) | |
.attr("y", legendSeriesLabelY) | |
.text(String); | |
d3.selectAll(".js-stacked-chart-container input").on("change", changeChartMode); | |
/** | |
* Generate data for the secondary stacked bar given the main stacked bar | |
*/ | |
function generateAddressBarLayer(mainStackedBar) { | |
var sub = []; | |
for (var i = 0, len = mainStackedBar.length; i < len; i++) { | |
var _yVal = mainStackedBar[i].y; | |
if (!_yVal > 0) { | |
continue; | |
} | |
var smallArr = []; | |
var count = 0; | |
for (var j = 0; j < _yVal; ++j) { | |
var _valueObj = {}; | |
_valueObj.x = +mainStackedBar[i].x; | |
_valueObj.y = 1; | |
sub.push(_valueObj); | |
} | |
} | |
return sub; | |
} | |
/** | |
* @function sanitizeData | |
* Sanitize the data for final rendering | |
* | |
*/ | |
function sanitizeData (series, maxLength) { | |
var _b = series.map(function (obj, index){ | |
var _arr = []; | |
var count = 0; | |
for (var i = 0; i < maxLength; i++) { | |
var o = {}; | |
if (!obj.values[i]) { | |
o.x = 0; | |
o.y = 1; | |
o.y0 = 1; | |
} else { | |
o.x = +obj.values[i].x; | |
o.y = +obj.values[i].y; | |
o.y0 = count++; | |
} | |
_arr.push(o); | |
} | |
return {name: obj.name, values: _arr}; | |
}); | |
return _b | |
} | |
/** | |
* Toggles a certain series. | |
* @param {String} seriesName The name of the series to be toggled | |
*/ | |
function toggleSeries (seriesName) { | |
var series, | |
isDisabling, | |
newData; | |
series = _.findWhere(data, { name: seriesName }); | |
isDisabling = !series.disabled; | |
if (isDisabling === true && numEnabledSeries === 1) { | |
return; | |
} | |
d3.select(this).classed("disabled", isDisabling); | |
series.disabled = isDisabling; | |
newData = stack(enabledSeries()); | |
numEnabledSeries = newData.length; | |
layers = layers.data(newData, joinKey); | |
if (isDisabling === true) { | |
removeSeries(); | |
} | |
else { | |
addSeries(); | |
} | |
} | |
/** | |
* Removes a certain series. | |
*/ | |
function removeSeries () { | |
var layerToBeRemoved; | |
layerToBeRemoved = layers.exit(); | |
if (chartMode === "stacked") { | |
removeStackedSeries(layerToBeRemoved); | |
} | |
else { | |
removeGroupedSeries(layerToBeRemoved); | |
} | |
} | |
/** | |
* Smoothly transitions and then removes a certain series when the chart is in `stacked` mode. | |
* @param {d3.selection} layerToBeRemoved The layer that contains the series' bars | |
*/ | |
function removeStackedSeries (layerToBeRemoved) { | |
layerToBeRemoved.selectAll("rect").transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("y", stackedBarBaseY) | |
.attr("height", 0) | |
.call(endAll, function () { | |
layerToBeRemoved.remove(); | |
}); | |
transitionStackedBars(layers.selectAll("rect")); | |
} | |
/** | |
* Smoothly transitions and then removes a certain series when the chart is in `grouped` mode. | |
* @param {d3.selection} layerToBeRemoved The layer that contains the series' bars | |
*/ | |
function removeGroupedSeries (layerToBeRemoved) { | |
layerToBeRemoved.selectAll("rect").transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("y", groupedBarBaseY) | |
.attr("height", 0) | |
.call(endAll, function () { | |
layerToBeRemoved.remove(); | |
layers.selectAll("rect").transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("x", groupedBarX) | |
.attr("width", groupedBarWidth); | |
}); | |
} | |
/** | |
* Adds a certain series. | |
*/ | |
function addSeries () { | |
var newLayer; | |
newLayer = layers.enter().append("g") | |
.attr("class", layerClass); | |
if (chartMode === "stacked") { | |
addStackedSeries(newLayer); | |
} | |
else { | |
addGroupedSeries(newLayer); | |
} | |
} | |
/** | |
* Smoothly transitions and adds a certain series when the chart is in `stacked` mode. | |
* @param {d3.selection} newLayer The new layer to be added | |
*/ | |
function addStackedSeries (newLayer) { | |
newLayer.selectAll("rect").data(function (d) { return d.values; }) | |
.enter().append("rect") | |
.attr("x", stackedBarX) | |
.attr("y", stackedBarBaseY) | |
// .attr("width", stackedBarWidth) | |
.attr("width", thinBarWidth) | |
.attr("height", 0); | |
transitionStackedBars(layers.selectAll("rect")); | |
} | |
/** | |
* Smoothly transitions and adds a certain series when the chart is in `grouped` mode. | |
* @param {d3.selection} newLayer The new layer to be added | |
*/ | |
function addGroupedSeries (newLayer) { | |
var newBars; | |
layers.selectAll("rect").transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("x", groupedBarX) | |
.attr("width", groupedBarWidth) | |
.call(endAll, function () { | |
newBars = newLayer.selectAll("rect").data(function (d) { return d.values; }) | |
.enter().append("rect") | |
.attr("y", groupedBarBaseY) | |
.attr("width", groupedBarWidth) | |
.attr("height", 0); | |
layers.selectAll("rect").attr("x", groupedBarX); | |
newBars.transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("y", groupedBarY) | |
.attr("height", barHeight); | |
}); | |
} | |
/** | |
* Changes the chart to the selected mode: `stacked` or `grouped`. | |
* In `stacked` mode, the bars of each bin are stacked together. | |
* In `grouped` mode, the bars of each bin are placed side by side. | |
*/ | |
function changeChartMode() { | |
chartMode = this.value; | |
if (chartMode === "stacked") { | |
stackBars(); | |
} | |
else { | |
groupBars(); | |
} | |
} | |
/** | |
* Smoothly transitions the chart to `stacked` mode. | |
* In this mode, the bars of each bin are stacked together. | |
*/ | |
function stackBars() { | |
layers.selectAll("rect").transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("y", stackedBarY) | |
.transition() | |
.duration(animationDuration) | |
.attr("x", stackedBarX) | |
.attr("width", stackedBarWidth); | |
} | |
/** | |
* Smoothly transitions the chart to `grouped` mode. | |
* In this mode, the bars of each bin are placed side by side. | |
*/ | |
function groupBars() { | |
layers.selectAll("rect").transition() | |
.duration(animationDuration) | |
.delay(barDelay) | |
.attr("x", groupedBarX) | |
.attr("width", groupedBarWidth) | |
.transition() | |
.duration(animationDuration) | |
.attr("y", groupedBarY); | |
} | |
/** | |
* Shows the tooltip. | |
*/ | |
function showTooltip() { | |
var hoveredBarIndex, | |
tooltip; | |
hoveredBarIndex = (d3.mouse(this)[0] / widthPerStack) | 0; | |
if (hoveredBarIndex === lastHoveredBarIndex) { | |
return; | |
} | |
lastHoveredBarIndex = hoveredBarIndex; | |
layers.selectAll("rect").classed("highlighted", function (d, i) { return (i === hoveredBarIndex); }); | |
tooltip = $(".js-tooltip"); | |
tooltip.find(".js-tooltip-table").html(tooltipContent()); | |
tooltip.css({ | |
top: margin.top + highestBinBarHeight() - tooltip.outerHeight() - tooltipBottomMargin, | |
left: margin.left + (hoveredBarIndex * widthPerStack) + (widthPerStack / 2) - (tooltip.outerWidth() / 2), | |
}).fadeIn(); | |
} | |
function tooltipContent () { | |
var bars, template; | |
bars = []; | |
layers.each(function (d) { | |
bars.unshift({ name: d.name, value: d.values[lastHoveredBarIndex].y.toFixed(4) }); | |
}); | |
template = $(".js-tooltip-table-content").html(); | |
return _.template(template, { bars: bars }); | |
} | |
/** | |
* Hides the tooltip. | |
*/ | |
function hideTooltip () { | |
$(".js-tooltip").stop().hide(); | |
layers.selectAll("rect") | |
.filter(function (d, i) { return (i === lastHoveredBarIndex); }) | |
.classed("highlighted", false); | |
lastHoveredBarIndex = undefined; | |
} | |
/** | |
* Calculates the height of the highest (not tallest) bar within a certain bin. | |
* @return {Number} The height, in pixels, of the highest bar within a certain bin | |
*/ | |
function highestBinBarHeight() { | |
var bars, | |
highestGroupBar; | |
if (chartMode === "stacked") { | |
highestGroupBar = _.last(layers.data()).values[lastHoveredBarIndex]; | |
return yScale(highestGroupBar.y0 + highestGroupBar.y); | |
} | |
else { | |
bars = _.map(layers.data(), function (series) { return series.values[lastHoveredBarIndex]; }); | |
highestGroupBar = _.max(bars, function (bar) { return bar.y; }); | |
return yScale(highestGroupBar.y); | |
} | |
} | |
/** | |
* Calls a function at the end of **all** transitions. | |
* @param {d3.transition} transition A D3 transition | |
* @param {Function} callback The function to be called at the end of **all** transitions | |
*/ | |
function endAll (transition, callback) { | |
var n; | |
if (transition.empty()) { | |
callback(); | |
} | |
else { | |
n = transition.size(); | |
transition.each("end", function () { | |
n--; | |
if (n === 0) { | |
callback(); | |
} | |
}); | |
} | |
} | |
// Inspired by Lee Byron's test data generator. | |
function bumpLayer(n, o) { | |
function bump(a) { | |
var x = 1 / (.1 + Math.random()), | |
y = 2 * (Math.random() - .5), | |
z = 10 / (.1 + Math.random()); | |
for (var i = 0; i < n; i++) { | |
var w = (i / n - y) * z; | |
a[i] += x * Math.exp(-w * w); | |
} | |
} | |
var a = [], i; | |
for (i = 0; i < n; ++i) a[i] = o + o * Math.random(); | |
for (i = 0; i < 5; ++i) bump(a); | |
return a.map(function (d, i) { return {x: i, y: Math.max(0, Math.floor(d))}; }); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
body { | |
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; | |
} | |
.stacked-chart-container { | |
position: relative; | |
} | |
.stacked-chart-container .controls { | |
position: absolute; | |
top: 24px; | |
left: 18px; | |
} | |
.stacked-chart .clickable { | |
cursor: pointer; | |
} | |
.stacked-chart-container .tooltip { | |
position: absolute; | |
font-size: 13px; | |
white-space: nowrap; | |
border: 1px solid black; | |
background-color: white; | |
pointer-events: none; | |
border-radius: 5px; | |
display: none; | |
} | |
.stacked-chart-container .tooltip-wrapper { | |
position: relative; | |
padding: 6px; | |
} | |
.stacked-chart-container .tooltip-wrapper:before { | |
content: ""; | |
position: absolute; | |
width: 0; | |
height: 0; | |
bottom: -20px; | |
left: 50%; | |
transform: translateX(-50%); | |
border: 10px solid; | |
border-color: black transparent transparent transparent; | |
} | |
.stacked-chart-container .tooltip-wrapper:after { | |
content: ""; | |
position: absolute; | |
width: 0; | |
height: 0; | |
bottom: -19px; | |
left: 50%; | |
transform: translateX(-50%); | |
border: 10px solid; | |
border-color: white transparent transparent transparent; | |
} | |
.stacked-chart-container .tooltip-table { | |
text-align: right; | |
} | |
.stacked-chart path, | |
.stacked-chart line, | |
.stacked-chart rect { | |
shape-rendering: crispEdges; | |
} | |
.stacked-chart text { | |
font: 10px sans-serif; | |
font-weight: 700; | |
} | |
.stacked-chart .axis path, | |
.stacked-chart .axis line { | |
fill: none; | |
/* stroke: #000; */ | |
} | |
.stacked-chart .series-so { | |
fill: steelblue; | |
} | |
.stacked-chart .series-jp { | |
fill: #CCC; | |
} | |
.stacked-chart .series-lo { | |
fill: #CD4638; | |
} | |
.stacked-chart .grid-lines { | |
fill: none; | |
stroke: lightgrey; | |
} | |
.stacked-chart .layer rect { | |
opacity: 0.8; | |
transition: opacity 0.5s ease; | |
} | |
.stacked-chart .layer rect.highlighted { | |
opacity: 1; | |
} | |
.stacked-chart .overlay { | |
opacity: 0; | |
} | |
.stacked-chart .series-box { | |
stroke-width: 2px; | |
} | |
/* Series classes */ | |
.stacked-chart .series-so .series-box { | |
stroke: steelblue; | |
} | |
.stacked-chart .series-jp .series-box { | |
stroke: #CCC; | |
} | |
.stacked-chart .series-lo .series-box { | |
stroke: #CD4638; | |
} | |
/* Stroke for the addressBar */ | |
.stacked-chart .addressBar { | |
fill: #fff; | |
stroke: #ececec; | |
} | |
.stacked-chart .disabled .series-box { | |
fill-opacity: 0; | |
} | |
.stacked-chart .series-label { | |
fill: black; | |
text-anchor: end; | |
alignment-baseline: central; | |
} | |
.tick line {display: none;} | |
</style> | |
<body> | |
<div class="stacked-chart-container js-stacked-chart-container"> | |
<!-- <form class="controls"> --> | |
<!-- <label><input type="radio" name="mode" value="stacked" checked>Stacked</label> --> | |
<!-- <label><input type="radio" name="mode" value="grouped">Grouped</label> --> | |
<!-- </form> --> | |
<svg class="stacked-chart js-stacked-chart"></svg> | |
<div class="tooltip js-tooltip"> | |
<div class="tooltip-wrapper"> | |
<table class="tooltip-table js-tooltip-table"></table> | |
</div> | |
</div> | |
</div> | |
<div class="js-legend-chart-container"> | |
<svg class="legend-chart js-legend-chart"></svg> | |
</div> | |
<script type="text/x-underscore" class="js-tooltip-table-content"> | |
<table> | |
<% _.each(bars, function (bar) { %> | |
<tr> | |
<td><%= bar.name %></td> | |
<td><%= bar.value %></td> | |
</tr> | |
<% }); %> | |
</table> | |
</script> | |
<script src="http://d3js.org/d3.v3.min.js"></script> | |
<script src="http://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.5.2/underscore-min.js"></script> | |
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script> | |
<script src="dynamic-stack-chart.js"></script> | |
</body> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment