dimple-js
Version:
Dimple is an object-oriented API allowing you to create flexible axis-based charts using [d3.js](http://d3js.org "d3.js").
445 lines (431 loc) • 23.9 kB
JavaScript
// Copyright: 2015 AlignAlytics
// License: "https://github.com/PMSI-AlignAlytics/dimple/blob/master/MIT-LICENSE.txt"
// Source: /src/objects/chart/methods/draw.js
// Help: http://github.com/PMSI-AlignAlytics/dimple/wiki/dimple.chart#wiki-draw
this.draw = function (duration, noDataChange) {
// Deal with optional parameter
duration = duration || 0;
// Catch the first x and y
var firstX = null,
firstY = null,
distinctCats,
xGridSet = false,
yGridSet = false,
chartX = this._xPixels(),
chartY = this._yPixels(),
chartWidth = this._widthPixels(),
chartHeight = this._heightPixels(),
linkedDimensions;
// Many of the draw methods use positioning data in each series. Therefore we should
// decorate the series with it now
if (noDataChange === undefined || noDataChange === null || noDataChange === false) {
this._getSeriesData();
}
// Clear all scales, this is required to fix Issue #67
this.axes.forEach(function (axis) {
axis._scale = null;
}, this);
// Iterate the axes and calculate bounds, this is done within the chart because an
// axis' bounds are determined by other axes and the way that series tie them together
this.axes.forEach(function (axis) {
axis._min = 0;
axis._max = 0;
linkedDimensions = [];
// Check that the axis has a measure
if (axis._hasMeasure()) {
// Is this axis linked to a series
var linked = false;
// Find any linked series
this.series.forEach(function (series) {
// if this axis is linked
if (series._deepMatch(axis)) {
// Get the bounds
var bounds = series._axisBounds(axis.position);
if (axis._min > bounds.min) { axis._min = bounds.min; }
if (axis._max < bounds.max) { axis._max = bounds.max; }
linked = true;
}
}, this);
// If the axis is not linked, use the data bounds, this is unlikely to be used
// in a real context, but when developing it is nice to see axes before any series have
// been added.
if (!linked) {
this._getAllData().forEach(function (d) {
if (axis._min > d[axis.measure]) { axis._min = d[axis.measure]; }
if (axis._max < d[axis.measure]) { axis._max = d[axis.measure]; }
}, this);
}
} else if (axis._hasTimeField()) {
// Parse the dates and assign the min and max
axis._min = null;
axis._max = null;
// Create an array of dimensions for this axis
this.series.forEach(function (series) {
// if this axis is linked
if (series._deepMatch(axis)
&& series[axis.position].timeField !== null
&& series[axis.position].timeField !== undefined
&& linkedDimensions.indexOf(series[axis.position].timeField) === -1) {
linkedDimensions.push(series[axis.position].timeField);
}
}, this);
// Iterate the data
axis._getAxisData().forEach(function (d) {
// Find any linked series
linkedDimensions.forEach(function (dimension) {
// Check it's timeField
var dt = axis._parseDate(d[dimension]);
if (axis._min === null || dt < axis._min) {
axis._min = dt;
}
if (axis._max === null || dt > axis._max) {
axis._max = dt;
}
}, this);
}, this);
} else if (axis._hasCategories()) {
// A category axis is just set to show the number of categories
axis._min = 0;
distinctCats = [];
// Create an array of dimensions for this axis
this.series.forEach(function (series) {
// if this axis is linked
if (series._deepMatch(axis)
&& series[axis.position].categoryFields[0] !== null
&& series[axis.position].categoryFields[0] !== undefined
&& linkedDimensions.indexOf(series[axis.position].categoryFields[0]) === -1) {
linkedDimensions.push(series[axis.position].categoryFields[0]);
}
}, this);
axis._getAxisData().forEach(function (d) {
linkedDimensions.forEach(function (dimension) {
if (distinctCats.indexOf(d[dimension]) === -1) {
distinctCats.push(d[dimension]);
}
}, this);
}, this);
axis._max = distinctCats.length;
}
// Set the bounds on all slaves
if (axis._slaves !== null && axis._slaves !== undefined && axis._slaves.length > 0) {
axis._slaves.forEach(function (slave) {
slave._min = axis._min;
slave._max = axis._max;
}, this);
}
// Update the axis now we have all information set
axis._update();
// Record the index of the first x and first y axes
if (firstX === null && axis.position === "x") {
firstX = axis;
} else if (firstY === null && axis.position === "y") {
firstY = axis;
}
}, this);
// Iterate the axes again
this.axes.forEach(function (axis) {
// Don't animate axes on first draw
var firstDraw = false,
transform = null,
gridSize = 0,
gridTransform = null,
rotated = false,
widest = 0,
box = { l: null, t: null, r: null, b: null },
titleX = 0,
titleY = 0,
rotate = "",
chart = this,
handleTrans = function (ob) {
// Draw the axis
// This code might seem unnecessary but even applying a duration of 0 to a transition will cause the code to execute after the
// code below and precedence is important here.
var returnObj;
if (transform === null || duration === 0 || firstDraw) {
returnObj = ob;
} else {
returnObj = chart._handleTransition(ob, duration, chart);
}
return returnObj;
},
transformLabels = function () {
var t = d3.select(this).selectAll("text");
if (!axis.measure && axis._max > 0) {
if (axis.position === "x") {
t.attr("x", (chartWidth / axis._max) / 2);
} else if (axis.position === "y") {
t.attr("y", -1 * (chartHeight / axis._max) / 2);
}
}
if (axis.categoryFields && axis.categoryFields.length > 0) {
// Off set the labels to counter the transform. This will put the labels along the outside of the chart so they
// don't interfere with the chart contents
if (axis === firstX && (firstY.categoryFields === null || firstY.categoryFields.length === 0)) {
t.attr("y", chartY + chartHeight - firstY._scale(0) + 9);
}
if (axis === firstY && (firstX.categoryFields === null || firstX.categoryFields.length === 0)) {
t.attr("x", -1 * (firstX._scale(0) - chartX) - 9);
}
}
return this;
},
appendClass = function (css) {
return function () {
var currentCss = d3.select(this).attr("class") || "";
if (currentCss.indexOf(css) === -1) {
currentCss += " " + css;
}
return currentCss.trim();
};
};
if (axis.gridlineShapes === null) {
if (axis.showGridlines || (axis.showGridlines === null && !axis._hasCategories() && ((!xGridSet && axis.position === "x") || (!yGridSet && axis.position === "y")))) {
// Add a group for the gridlines to allow css formatting
axis.gridlineShapes = this._group.append("g").attr("class", "dimple-gridline");
if (axis.position === "x") {
xGridSet = true;
} else {
yGridSet = true;
}
}
} else {
if (axis.position === "x") {
xGridSet = true;
} else {
yGridSet = true;
}
}
if (axis.shapes === null) {
// Add a group for the axes to allow css formatting
axis.shapes = this._group.append("g")
.attr("class", "dimple-axis dimple-axis-" + axis.position)
.each(function () {
if (!chart.noFormats) {
d3.select(this)
.style("font-family", axis.fontFamily)
.style("font-size", axis._getFontSize());
}
});
firstDraw = true;
}
// If this is the first x and there is a y axis cross them at zero
if (axis === firstX && firstY !== null) {
transform = "translate(0, " + (firstY.categoryFields === null || firstY.categoryFields.length === 0 ? firstY._scale(0) : chartY + chartHeight) + ")";
gridTransform = "translate(0, " + (axis === firstX ? chartY + chartHeight : chartY) + ")";
gridSize = -chartHeight;
} else if (axis === firstY && firstX !== null) {
transform = "translate(" + (firstX.categoryFields === null || firstX.categoryFields.length === 0 ? firstX._scale(0) : chartX) + ", 0)";
gridTransform = "translate(" + (axis === firstY ? chartX : chartX + chartWidth) + ", 0)";
gridSize = -chartWidth;
} else if (axis.position === "x") {
gridTransform = transform = "translate(0, " + (axis === firstX ? chartY + chartHeight : chartY) + ")";
gridSize = -chartHeight;
} else if (axis.position === "y") {
gridTransform = transform = "translate(" + (axis === firstY ? chartX : chartX + chartWidth) + ", 0)";
gridSize = -chartWidth;
}
if (transform !== null && axis._draw !== null) {
// Add a tick format
if (axis._hasTimeField()) {
handleTrans(axis.shapes)
.call(axis._draw.ticks(axis._getTimePeriod(), axis.timeInterval).tickFormat(axis._getFormat()))
.attr("transform", transform)
.each(transformLabels);
} else if (axis.useLog) {
handleTrans(axis.shapes)
.call(axis._draw.ticks(4, axis._getFormat()))
.attr("transform", transform)
.each(transformLabels);
} else {
handleTrans(axis.shapes)
.call(axis._draw.tickFormat(axis._getFormat()))
.attr("transform", transform)
.each(transformLabels);
}
if (axis.gridlineShapes !== null) {
handleTrans(axis.gridlineShapes)
.call(axis._draw.tickSize(gridSize, 0, 0).tickFormat(""))
.attr("transform", gridTransform);
}
}
// Set some initial css values
handleTrans(axis.shapes.selectAll("text"))
.attr("class", appendClass(chart.customClassList.axisLabel))
.call(function() {
if (!chart.noFormats) {
this.style("font-family", axis.fontFamily)
.style("font-size", axis._getFontSize());
}
});
handleTrans(axis.shapes.selectAll("path, line"))
.attr("class", appendClass(chart.customClassList.axisLine))
.call(function() {
if (!chart.noFormats) {
this.style("fill", "none")
.style("stroke", "black")
.style("shape-rendering", "crispEdges");
}
});
if (axis.gridlineShapes !== null) {
handleTrans(axis.gridlineShapes.selectAll("line"))
.attr("class", appendClass(chart.customClassList.gridline))
.call(function() {
if (!chart.noFormats) {
this.style("fill", "none")
.style("stroke", "lightgray")
.style("opacity", 0.8);
}
});
}
// Rotate labels, this can only be done once the formats are set
if (axis.autoRotateLabel && (axis.measure === null || axis.measure === undefined)) {
if (axis === firstX) {
// If the gaps are narrower than the widest label display all labels horizontally
widest = 0;
axis.shapes.selectAll("text").each(function () {
var w = this.getComputedTextLength();
widest = (w > widest ? w : widest);
});
if (widest > chartWidth / axis.shapes.selectAll("text")[0].length) {
rotated = true;
axis.shapes.selectAll("text")
.style("text-anchor", "start")
.each(function () {
var rec = this.getBBox();
d3.select(this)
.attr("transform", "rotate(90," + rec.x + "," + (rec.y + (rec.height / 2)) + ") translate(-5, 0)");
});
} else {
// For redraw operations we need to clear the transform
rotated = false;
axis.shapes.selectAll("text")
.style("text-anchor", "middle")
.attr("transform", "");
}
} else if (axis.position === "x") {
// If the gaps are narrower than the widest label display all labels horizontally
widest = 0;
axis.shapes.selectAll("text")
.each(function () {
var w = this.getComputedTextLength();
widest = (w > widest ? w : widest);
});
if (widest > chartWidth / axis.shapes.selectAll("text")[0].length) {
rotated = true;
axis.shapes.selectAll("text")
.style("text-anchor", "end")
.each(function () {
var rec = this.getBBox();
d3.select(this)
.attr("transform", "rotate(90," + (rec.x + rec.width) + "," + (rec.y + (rec.height / 2)) + ") translate(5, 0)");
});
} else {
// For redraw operations we need to clear the transform
rotated = false;
axis.shapes.selectAll("text")
.style("text-anchor", "middle")
.attr("transform", "");
}
}
} else {
rotated = false;
axis.shapes.selectAll("text")
.style("text-anchor", "middle")
.attr("transform", "");
}
if (axis.titleShape !== null && axis.titleShape !== undefined) {
axis.titleShape.remove();
}
// Get the bounds of the axis objects
axis.shapes.selectAll("text")
.each(function () {
var rec = this.getBBox();
if (box.l === null || -9 - rec.width < box.l) {
box.l = -9 - rec.width;
}
if (box.r === null || rec.x + rec.width > box.r) {
box.r = rec.x + rec.width;
}
if (rotated) {
if (box.t === null || rec.y + rec.height - rec.width < box.t) {
box.t = rec.y + rec.height - rec.width;
}
if (box.b === null || rec.height + rec.width > box.b) {
box.b = rec.height + rec.width;
}
} else {
if (box.t === null || rec.y < box.t) {
box.t = rec.y;
}
if (box.b === null || 9 + rec.height > box.b) {
box.b = 9 + rec.height;
}
}
});
if (axis.position === "x") {
if (axis === firstX) {
titleY = chartY + chartHeight + box.b + 5;
} else {
titleY = chartY + box.t - 10;
}
titleX = chartX + (chartWidth / 2);
} else if (axis.position === "y") {
if (axis === firstY) {
titleX = chartX + box.l - 10;
} else {
titleX = chartX + chartWidth + box.r + 20;
}
titleY = chartY + (chartHeight / 2);
rotate = "rotate(270, " + titleX + ", " + titleY + ")";
}
// Add a title for the axis - NB check for null here, by default the title is undefined, in which case
// use the dimension name
if (!axis.hidden && (axis.position === "x" || axis.position === "y") && axis.title !== null) {
axis.titleShape = this._group.append("text")
.attr("class", "dimple-axis dimple-title " + chart.customClassList.axisTitle + " dimple-axis-" + axis.position);
axis.titleShape
.attr("x", titleX)
.attr("y", titleY)
.attr("text-anchor", "middle")
.attr("transform", rotate)
.text(axis.title !== undefined ? axis.title : (axis.categoryFields === null || axis.categoryFields === undefined || axis.categoryFields.length === 0 ? axis.measure : axis.categoryFields.join("/")))
.each(function () {
if (!chart.noFormats) {
d3.select(this)
.style("font-family", axis.fontFamily)
.style("font-size", axis._getFontSize());
}
});
// Offset Y position to baseline. This previously used dominant-baseline but this caused
// browser inconsistency
if (axis === firstX) {
axis.titleShape.each(function () {
d3.select(this).attr("y", titleY + this.getBBox().height / 1.65);
});
} else if (axis === firstY) {
axis.titleShape.each(function () {
d3.select(this).attr("x", titleX + this.getBBox().height / 1.65);
});
}
}
// }
}, this);
// Iterate the series
this.series.forEach(function (series) {
series.plot.draw(this, series, duration);
this._registerEventHandlers(series);
}, this);
// Iterate the legends
this.legends.forEach(function (legend) {
legend._draw();
}, this);
// If the chart has a storyboard
if (this.storyboard) {
this.storyboard._drawText();
if (this.storyboard.autoplay) {
this.storyboard.startAnimation();
}
}
// Return the chart for chaining
return this;
};