Skip to content

Instantly share code, notes, and snippets.

@lwhorton
Created February 11, 2014 16:34
Show Gist options
  • Select an option

  • Save lwhorton/8938460 to your computer and use it in GitHub Desktop.

Select an option

Save lwhorton/8938460 to your computer and use it in GitHub Desktop.
Basic flag-configurable line chart.
// These are the most basic properties required by a line chart.
function Chart() {}
Chart.prototype.data = null;
Chart.prototype.svgWidth = 600;
Chart.prototype.svgHeight = 500;
Chart.prototype.margin = {top: 20.0, right: 20.0, bottom: 20.0, left: 20.0};
Chart.prototype.width = 500;
Chart.prototype.height = 400;
Chart.prototype.xValue = function (d) { return d[0]; };
Chart.prototype.yValue = function (d) { return d[1]; };
Chart.prototype.xScale = d3.time.scale();
Chart.prototype.yScale = d3.scale.linear();
Chart.prototype.line = {
render: function (lineSelection, lineFn) {
// The 'this' context is the chart object instance.
lineSelection
.transition()
.duration(this.transitionDuration)
.attr("d", lineFn)
}
};
// Add-ons: functionality beyond this point is extra fluff not required to render a line chart.
// Animation
Chart.prototype.transitionDuration = 500;
// Axes with default render methods.
Chart.prototype.xAxis = {
flag: false,
render: function (axisSelection, axisFn) {
axisSelection
.transition()
.duration(this.transitionDuration)
.attr("transform", "translate(0," + this.yScale.range()[0] + ")")
.call(axisFn);
}
};
Chart.prototype.yAxis = {
flag: false,
render: function (axisSelection, axisFn) {
axisSelection
.transition()
.duration(this.transitionDuration)
.call(axisFn);
}
};
return {
lineChart: function (config) {
// Instantiate a new chart and override its default prototype settings with properties
// set on the chart that are provided by the given config.
var c = new Chart();
Object.keys(config).forEach(function(key) {
if (config[key] === true) {
// If the config is a truthy boolean, enable the feature and use
// the default render method provided by the prototype.
c[key].flag = true;
} else if (config[key] === false) {
// If the config is a falsy boolean, disable the render method provided
// by the prototype, but allow function continuity without actually rendering.
c[key].flag = false;
c[key].render = function () { return false; };
} else if (Object.prototype.toString.call(config[key]) === '[object Function]') {
// Use inflection on the default prototype to determine if the provided
// function is an accessor that simply needs overriding, or a render function
// that also requires setting a flag on the property object.
if (Object.prototype.toString.call(Chart.prototype[key]) === '[object Object]') {
c[key].flag = true;
c[key].render = config[key];
} else {
c[key] = config[key];
}
} else {
// If the config is anything else (number, string, etc.), just override the prototype default.
c[key] = config[key];
}
});
// Rendering or re-rendering the chart follows the same procedure - given a d3 selection,
// define that selection's dimensions, scales, axes, or other components, and either
// (add to) or (update existing) elements on the DOM.
c.render = function (selection) {
// Note: 'this' refers to the HTML element, as per d3js API documentation on each().
selection.each(function (chartData) {
// If we have no data to render, return.
if (!chartData) return c;
// Convert chartData to a copy of standard representations
// (greedily, which is needed for nondeterministic accessors).
// This allows us to reference all future data
c.data = chartData.map(function(d, i) {
return [c.xValue.call(null, d, i), c.yValue.call(null, d, i)];
});
// Update our chart's width / height.
c.width = c.svgWidth - c.margin.left - c.margin.right;
c.height = c.svgHeight - c.margin.top - c.margin.bottom;
// Update the x-scale.
c.xScale
.domain(d3.extent(c.data, Chart.prototype.xValue))
.range([0, c.width]);
// Update the y-scale.
c.yScale
.domain([0, d3.max(c.data, Chart.prototype.yValue)])
.range([c.height, 0]);
// Select the svg element, if it exists. Because we have been
// handed a d3 selection, "this" provides a "selectAll". Because
// we attached data to the chart with a .datum() previously,
// we have access to a .enter() method within this selection.
var svg = d3.select(this).selectAll("svg").data([c.data]);
// Create the chart's components.
var gEnter = svg.enter().append("svg").append("g");
gEnter.append("path")
.datum(c.data)
.attr("class", "line line-color-1")
.attr('stroke', '#569bbe')
.attr('stroke-width', 1)
.attr('fill', 'none');
gEnter.append("g").attr("class", "x-axis");
gEnter.append("g").attr("class", "y-axis");
// Update the svg dimensions.
svg.attr("width", c.svgWidth)
.attr("height", c.svgHeight);
// Update the chart dimensions (centered inside the svg).
var g = svg.select("g")
.attr("transform", "translate(" + c.margin.left + "," + c.margin.top + ")");
// Render the line by calling the line render function, which can be the default
// or the user-provided function. In the instance of a user's render function, we
// pass them handles to the lineSelection, as well as the function describing the line.
c.line.render.call(c,
g.select(".line"),
d3.svg.line()
.x(function (d, i) { return c.xScale(Chart.prototype.xValue.call(null, d, i)); })
.y(function (d, i) { return c.yScale(Chart.prototype.yValue.call(null, d, i)); }));
// -------------------------------------------------------------------------------------
// Add-ons: functionality beyond this point is optional. In most instances, we are
// providing user's custom functions a handle to the selection, and the 'this' context
// of the chart instance (see line-render notes above).
// -------------------------------------------------------------------------------------
// Update the x-axis.
if (c.xAxis.flag)
c.xAxis.render.call(c,
g.select(".x-axis"),
d3.svg.axis().scale(c.xScale).orient('top'));
// Update the y-axis.
if (c.yAxis.flag)
c.yAxis.render.call(c,
g.select(".y-axis"),
d3.svg.axis().scale(c.yScale).orient('left'));
});
}
return c;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment