|
// A reusable bar chart module. |
|
// Draws from D3 bar chart example http://bl.ocks.org/mbostock/3885304 |
|
// Curran Kelleher March 2015 |
|
define(["d3", "model", "lodash"], function (d3, Model, _) { |
|
|
|
// A representation for an optional Model property that is not specified. |
|
// This allows the "when" approach to support optional properties. |
|
// Inspired by Scala's Option type. |
|
// See http://alvinalexander.com/scala/using-scala-option-some-none-idiom-function-java-null |
|
var None = "__none__"; |
|
|
|
// The constructor function, accepting default values. |
|
return function BarChart(defaults) { |
|
|
|
// Create a Model instance for the bar chart. |
|
// This will serve as the public API for the visualization. |
|
var model = Model(); |
|
|
|
// Create the SVG element from the container DOM element. |
|
model.when("container", function (container) { |
|
model.svg = d3.select(container).append('svg'); |
|
}); |
|
|
|
// Adjust the size of the SVG based on the `box` property. |
|
model.when(["svg", "box"], function (svg, box) { |
|
svg.attr("width", box.width).attr("height", box.height); |
|
}); |
|
|
|
// Create the SVG group that will contain the visualization. |
|
model.when("svg", function (svg) { |
|
model.g = svg.append("g"); |
|
}); |
|
|
|
// Adjust the translation of the group based on the margin. |
|
model.when(["g", "margin"], function (g, margin) { |
|
g.attr("transform", "translate(" + margin.left + "," + margin.top + ")"); |
|
}); |
|
|
|
// Create the title text element. |
|
model.when("g", function (g){ |
|
model.titleText = g.append("text").attr("class", "title-text"); |
|
}); |
|
|
|
// Center the title text when width changes. |
|
model.when(["titleText", "width"], function (titleText, width) { |
|
titleText.attr("x", width / 2); |
|
}); |
|
|
|
// Update the title text based on the `title` property. |
|
model.when(["titleText", "title"], function (titleText, title){ |
|
titleText.text(title); |
|
}); |
|
|
|
// Update the title text offset. |
|
model.when(["titleText", "titleOffset"], function (titleText, titleOffset){ |
|
titleText.attr("dy", titleOffset + "em"); |
|
}); |
|
|
|
// Compute the inner box from the outer box and margin. |
|
// See Margin Convention http://bl.ocks.org/mbostock/3019563 |
|
model.when(["box", "margin"], function (box, margin) { |
|
model.width = box.width - margin.left - margin.right; |
|
model.height = box.height - margin.top - margin.bottom; |
|
}); |
|
|
|
// Generate a function for getting the X value. |
|
model.when(["data", "xAttribute"], function (data, xAttribute) { |
|
model.getX = function (d) { return d[xAttribute]; }; |
|
}); |
|
|
|
// Handle sorting. |
|
model.when(["sortField", "sortOrder", "data"], function (sortField, sortOrder, data){ |
|
var sortedData = _.sortBy(data, sortField); |
|
if(sortOrder === "descending"){ |
|
sortedData.reverse(); |
|
} |
|
model.sortedData = sortedData; |
|
}); |
|
|
|
// Compute the domain of the X attribute. |
|
model.when(["sortedData", "getX"], function (sortedData, getX) { |
|
model.xDomain = sortedData.map(getX); |
|
}); |
|
|
|
// Compute the X scale. |
|
model.when(["xDomain", "width", "barPadding"], function (xDomain, width, padding) { |
|
model.xScale = d3.scale.ordinal().domain(xDomain).rangeRoundBands([0, width], padding); |
|
}); |
|
|
|
// Generate a function for getting the scaled X value. |
|
model.when(["data", "xScale", "getX"], function (data, xScale, getX) { |
|
model.getXScaled = function (d) { return xScale(getX(d)); }; |
|
}); |
|
|
|
// Set up the X axis. |
|
model.when("g", function (g) { |
|
model.xAxisG = g.append("g").attr("class", "x axis"); |
|
model.xAxisText = model.xAxisG.append("text").style("text-anchor", "middle"); |
|
}); |
|
|
|
// Move the X axis label based on its specified offset. |
|
model.when(["xAxisText", "xAxisLabelOffset"], function (xAxisText, xAxisLabelOffset){ |
|
xAxisText.attr("dy", xAxisLabelOffset + "em"); |
|
}); |
|
|
|
// Update the X axis transform when height changes. |
|
model.when(["xAxisG", "height"], function (xAxisG, height) { |
|
xAxisG.attr("transform", "translate(0," + height + ")"); |
|
}); |
|
|
|
// Center the X axis label when width changes. |
|
model.when(["xAxisText", "width"], function (xAxisText, width) { |
|
xAxisText.attr("x", width / 2); |
|
}); |
|
|
|
// Update the X axis based on the X scale. |
|
model.when(["xAxisG", "xScale"], function (xAxisG, xScale) { |
|
xAxisG.call(d3.svg.axis().orient("bottom").scale(xScale)); |
|
}); |
|
|
|
// Update X axis label. |
|
model.when(["xAxisText", "xAxisLabel"], function (xAxisText, xAxisLabel) { |
|
xAxisText.text(xAxisLabel); |
|
}); |
|
|
|
// Generate a function for getting the Y value. |
|
model.when(["data", "yAttribute"], function (data, yAttribute) { |
|
model.getY = function (d) { return d[yAttribute]; }; |
|
}); |
|
|
|
// Compute the domain of the Y attribute. |
|
|
|
// Allow the API client to optionally specify fixed min and max values. |
|
model.yDomainMin = None; |
|
model.yDomainMax = None; |
|
model.when(["data", "getY", "yDomainMin", "yDomainMax"], |
|
function (data, getY, yDomainMin, yDomainMax) { |
|
|
|
if(yDomainMin === None && yDomainMax === None){ |
|
model.yDomain = d3.extent(data, getY); |
|
} else { |
|
if(yDomainMin === None){ |
|
yDomainMin = d3.min(data, getY); |
|
} |
|
if(yDomainMax === None){ |
|
yDomainMax = d3.max(data, getY); |
|
} |
|
model.yDomain = [yDomainMin, yDomainMax] |
|
} |
|
}); |
|
|
|
// Compute the Y scale. |
|
model.when(["data", "yDomain", "height"], function (data, yDomain, height) { |
|
model.yScale = d3.scale.linear().domain(yDomain).range([height, 0]); |
|
}); |
|
|
|
// Generate a function for getting the scaled Y value. |
|
model.when(["data", "yScale", "getY"], function (data, yScale, getY) { |
|
model.getYScaled = function (d) { return yScale(getY(d)); }; |
|
}); |
|
|
|
// Set up the Y axis. |
|
model.when("g", function (g) { |
|
model.yAxisG = g.append("g").attr("class", "y axis"); |
|
model.yAxisText = model.yAxisG.append("text") |
|
.style("text-anchor", "middle") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", 0); |
|
}); |
|
|
|
// Move the Y axis label based on its specified offset. |
|
model.when(["yAxisText", "yAxisLabelOffset"], function (yAxisText, yAxisLabelOffset){ |
|
yAxisText.attr("dy", "-" + yAxisLabelOffset + "em") |
|
}); |
|
|
|
// Center the Y axis label when height changes. |
|
model.when(["yAxisText", "height"], function (yAxisText, height) { |
|
yAxisText.attr("x", -height / 2); |
|
}); |
|
|
|
// Update Y axis label. |
|
model.when(["yAxisText", "yAxisLabel"], function (yAxisText, yAxisLabel) { |
|
yAxisText.text(yAxisLabel); |
|
}); |
|
|
|
// Update the Y axis based on the Y scale. |
|
model.when(["yAxisG", "yScale"], function (yAxisG, yScale) { |
|
yAxisG.call(d3.svg.axis().orient("left").scale(yScale)); |
|
}); |
|
|
|
// Adjust Y axis tick mark parameters. |
|
// See https://github.com/mbostock/d3/wiki/Quantitative-Scales#linear_tickFormat |
|
model.when(['yAxisNumTicks', 'yAxisTickFormat'], function (count, format) { |
|
yAxis.ticks(count, format); |
|
}); |
|
|
|
// Add an SVG group to contain the line. |
|
model.when("g", function (g) { |
|
model.barsG = g.append("g"); |
|
}); |
|
|
|
// Draw the bars. |
|
model.when(["barsG", "sortedData", "getXScaled", "getYScaled", "xScale", "height"], |
|
function (barsG, sortedData, getXScaled, getYScaled, xScale, height){ |
|
var bars = barsG.selectAll("rect").data(sortedData); |
|
bars.enter().append("rect"); |
|
bars.attr("x", getXScaled).attr("y", getYScaled) |
|
.attr("width", xScale.rangeBand()) |
|
.attr("height", function(d) { return height - getYScaled(d); }); |
|
bars.exit().remove(); |
|
}); |
|
|
|
// Set defaults at the end so they override optional properties set to None. |
|
model.set(defaults); |
|
|
|
return model; |
|
}; |
|
}); |