blob: 884973dad0604e656152bd6bfe425c7cdc224003 [file] [log] [blame]
/* [LICENSE TBD] */
/* eslint-disable */
export default function (config) {
var __ = {
data: [],
highlighted: [],
dimensions: [],
dimensionTitles: {},
dimensionTitleRotation: 0,
types: {},
brushed: false,
brushedColor: null,
alphaOnBrushed: 0.0,
mode: 'default',
rate: 20,
width: 600,
height: 300,
margin: { top: 24, right: 0, bottom: 12, left: 0 },
nullValueSeparator: 'undefined', // set to "top" or "bottom"
nullValueSeparatorPadding: { top: 8, right: 0, bottom: 8, left: 0 },
color: '#069',
composite: 'source-over',
alpha: 0.7,
bundlingStrength: 0.5,
bundleDimension: null,
smoothness: 0.0,
showControlPoints: false,
hideAxis: [],
};
extend(__, config);
var pc = function (selection) {
selection = pc.selection = d3.select(selection);
__.width = selection[0][0].clientWidth;
__.height = selection[0][0].clientHeight;
// canvas data layers
['marks', 'foreground', 'brushed', 'highlight'].forEach(function (layer) {
canvas[layer] = selection.append('canvas').attr('class', layer)[0][0];
ctx[layer] = canvas[layer].getContext('2d');
});
// svg tick and brush layers
pc.svg = selection
.append('svg')
.attr('width', __.width)
.attr('height', __.height)
.append('svg:g')
.attr('transform', 'translate(' + __.margin.left + ',' + __.margin.top + ')');
return pc;
};
var events = d3.dispatch.apply(
this,
['render', 'resize', 'highlight', 'brush', 'brushend', 'axesreorder'].concat(d3.keys(__)),
),
w = function () {
return __.width - __.margin.right - __.margin.left;
},
h = function () {
return __.height - __.margin.top - __.margin.bottom;
},
flags = {
brushable: false,
reorderable: false,
axes: false,
interactive: false,
debug: false,
},
xscale = d3.scale.ordinal(),
yscale = {},
dragging = {},
line = d3.svg.line(),
axis = d3.svg.axis().orient('left').ticks(5),
g, // groups for axes, brushes
ctx = {},
canvas = {},
clusterCentroids = [];
// side effects for setters
var side_effects = d3.dispatch
.apply(this, d3.keys(__))
.on('composite', function (d) {
ctx.foreground.globalCompositeOperation = d.value;
ctx.brushed.globalCompositeOperation = d.value;
})
.on('alpha', function (d) {
ctx.foreground.globalAlpha = d.value;
ctx.brushed.globalAlpha = d.value;
})
.on('brushedColor', function (d) {
ctx.brushed.strokeStyle = d.value;
})
.on('width', function (d) {
pc.resize();
})
.on('height', function (d) {
pc.resize();
})
.on('margin', function (d) {
pc.resize();
})
.on('rate', function (d) {
brushedQueue.rate(d.value);
foregroundQueue.rate(d.value);
})
.on('dimensions', function (d) {
xscale.domain(__.dimensions);
if (flags.interactive) {
pc.render().updateAxes();
}
})
.on('bundleDimension', function (d) {
if (!__.dimensions.length) pc.detectDimensions();
if (!(__.dimensions[0] in yscale)) pc.autoscale();
if (typeof d.value === 'number') {
if (d.value < __.dimensions.length) {
__.bundleDimension = __.dimensions[d.value];
} else if (d.value < __.hideAxis.length) {
__.bundleDimension = __.hideAxis[d.value];
}
} else {
__.bundleDimension = d.value;
}
__.clusterCentroids = compute_cluster_centroids(__.bundleDimension);
})
.on('hideAxis', function (d) {
if (!__.dimensions.length) pc.detectDimensions();
pc.dimensions(without(__.dimensions, d.value));
});
// expose the state of the chart
pc.state = __;
pc.flags = flags;
// create getter/setters
getset(pc, __, events);
// expose events
d3.rebind(pc, events, 'on');
// getter/setter with event firing
function getset(obj, state, events) {
d3.keys(state).forEach(function (key) {
obj[key] = function (x) {
if (!arguments.length) {
return state[key];
}
var old = state[key];
state[key] = x;
side_effects[key].call(pc, { value: x, previous: old });
events[key].call(pc, { value: x, previous: old });
return obj;
};
});
}
function extend(target, source) {
for (var key in source) {
target[key] = source[key];
}
return target;
}
function without(arr, item) {
return arr.filter(function (elem) {
return item.indexOf(elem) === -1;
});
}
/** adjusts an axis' default range [h()+1, 1] if a NullValueSeparator is set */
function getRange() {
if (__.nullValueSeparator == 'bottom') {
return [h() + 1 - __.nullValueSeparatorPadding.bottom - __.nullValueSeparatorPadding.top, 1];
} else if (__.nullValueSeparator == 'top') {
return [h() + 1, 1 + __.nullValueSeparatorPadding.bottom + __.nullValueSeparatorPadding.top];
}
return [h() + 1, 1];
}
pc.autoscale = function () {
// yscale
var defaultScales = {
date: function (k) {
var extent = d3.extent(__.data, function (d) {
return d[k] ? d[k].getTime() : null;
});
// special case if single value
if (extent[0] === extent[1]) {
return d3.scale.ordinal().domain([extent[0]]).rangePoints(getRange());
}
return d3.time.scale().domain(extent).range(getRange());
},
number: function (k) {
var extent = d3.extent(__.data, function (d) {
return +d[k];
});
// special case if single value
if (extent[0] === extent[1]) {
return d3.scale.ordinal().domain([extent[0]]).rangePoints(getRange());
}
return d3.scale.linear().domain(extent).range(getRange());
},
string: function (k) {
var counts = {},
domain = [];
// Let's get the count for each value so that we can sort the domain based
// on the number of items for each value.
__.data.map(function (p) {
if (p[k] === undefined && __.nullValueSeparator !== 'undefined') {
return; // null values will be drawn beyond the horizontal null value separator!
}
if (counts[p[k]] === undefined) {
counts[p[k]] = 1;
} else {
counts[p[k]] = counts[p[k]] + 1;
}
});
domain = Object.getOwnPropertyNames(counts).sort(function (a, b) {
return counts[a] - counts[b];
});
return d3.scale.ordinal().domain(domain).rangePoints(getRange());
},
};
__.dimensions.forEach(function (k) {
yscale[k] = defaultScales[__.types[k]](k);
});
__.hideAxis.forEach(function (k) {
yscale[k] = defaultScales[__.types[k]](k);
});
// xscale
xscale.rangePoints([0, w()], 1);
// canvas sizes
pc.selection
.selectAll('canvas')
.style('margin-top', __.margin.top + 'px')
.style('margin-left', __.margin.left + 'px')
.attr('width', w() + 2)
.attr('height', h() + 2);
// default styles, needs to be set when canvas width changes
ctx.foreground.strokeStyle = __.color;
ctx.foreground.lineWidth = 1.4;
ctx.foreground.globalCompositeOperation = __.composite;
ctx.foreground.globalAlpha = __.alpha;
ctx.brushed.strokeStyle = __.brushedColor;
ctx.brushed.lineWidth = 1.4;
ctx.brushed.globalCompositeOperation = __.composite;
ctx.brushed.globalAlpha = __.alpha;
ctx.highlight.lineWidth = 3;
return this;
};
pc.scale = function (d, domain) {
yscale[d].domain(domain);
return this;
};
pc.flip = function (d) {
//yscale[d].domain().reverse(); // does not work
yscale[d].domain(yscale[d].domain().reverse()); // works
return this;
};
pc.commonScale = function (global, type) {
var t = type || 'number';
if (typeof global === 'undefined') {
global = true;
}
// scales of the same type
var scales = __.dimensions.concat(__.hideAxis).filter(function (p) {
return __.types[p] == t;
});
if (global) {
var extent = d3.extent(
scales
.map(function (p, i) {
return yscale[p].domain();
})
.reduce(function (a, b) {
return a.concat(b);
}),
);
scales.forEach(function (d) {
yscale[d].domain(extent);
});
} else {
scales.forEach(function (k) {
yscale[k].domain(
d3.extent(__.data, function (d) {
return +d[k];
}),
);
});
}
// update centroids
if (__.bundleDimension !== null) {
pc.bundleDimension(__.bundleDimension);
}
return this;
};
pc.detectDimensions = function () {
pc.types(pc.detectDimensionTypes(__.data));
pc.dimensions(d3.keys(pc.types()));
return this;
};
// a better "typeof" from this post: http://stackoverflow.com/questions/7390426/better-way-to-get-type-of-a-javascript-variable
pc.toType = function (v) {
return {}.toString
.call(v)
.match(/\s([a-zA-Z]+)/)[1]
.toLowerCase();
};
// try to coerce to number before returning type
pc.toTypeCoerceNumbers = function (v) {
if (parseFloat(v) == v && v != null) {
return 'number';
}
return pc.toType(v);
};
// attempt to determine types of each dimension based on first row of data
pc.detectDimensionTypes = function (data) {
var types = {};
d3.keys(data[0]).forEach(function (col) {
types[col] = pc.toTypeCoerceNumbers(data[0][col]);
});
return types;
};
pc.render = function () {
// try to autodetect dimensions and create scales
if (!__.dimensions.length) pc.detectDimensions();
if (!(__.dimensions[0] in yscale)) pc.autoscale();
pc.render[__.mode]();
events.render.call(this);
return this;
};
pc.renderBrushed = function () {
if (!__.dimensions.length) pc.detectDimensions();
if (!(__.dimensions[0] in yscale)) pc.autoscale();
pc.renderBrushed[__.mode]();
events.render.call(this);
return this;
};
function isBrushed() {
if (__.brushed && __.brushed.length !== __.data.length) return true;
var object = brush.currentMode().brushState();
for (var key in object) {
if (object.hasOwnProperty(key)) {
return true;
}
}
return false;
}
pc.render.default = function () {
pc.clear('foreground');
pc.clear('highlight');
pc.renderBrushed.default();
__.data.forEach(path_foreground);
};
var foregroundQueue = d3
.renderQueue(path_foreground)
.rate(50)
.clear(function () {
pc.clear('foreground');
pc.clear('highlight');
});
pc.render.queue = function () {
pc.renderBrushed.queue();
foregroundQueue(__.data);
};
pc.renderBrushed.default = function () {
pc.clear('brushed');
if (isBrushed()) {
__.brushed.forEach(path_brushed);
}
};
var brushedQueue = d3
.renderQueue(path_brushed)
.rate(50)
.clear(function () {
pc.clear('brushed');
});
pc.renderBrushed.queue = function () {
if (isBrushed()) {
brushedQueue(__.brushed);
} else {
brushedQueue([]); // This is needed to clear the currently brushed items
}
};
function compute_cluster_centroids(d) {
var clusterCentroids = d3.map();
var clusterCounts = d3.map();
// determine clusterCounts
__.data.forEach(function (row) {
var scaled = yscale[d](row[d]);
if (!clusterCounts.has(scaled)) {
clusterCounts.set(scaled, 0);
}
var count = clusterCounts.get(scaled);
clusterCounts.set(scaled, count + 1);
});
__.data.forEach(function (row) {
__.dimensions.map(function (p, i) {
var scaled = yscale[d](row[d]);
if (!clusterCentroids.has(scaled)) {
var map = d3.map();
clusterCentroids.set(scaled, map);
}
if (!clusterCentroids.get(scaled).has(p)) {
clusterCentroids.get(scaled).set(p, 0);
}
var value = clusterCentroids.get(scaled).get(p);
value += yscale[p](row[p]) / clusterCounts.get(scaled);
clusterCentroids.get(scaled).set(p, value);
});
});
return clusterCentroids;
}
function compute_centroids(row) {
var centroids = [];
var p = __.dimensions;
var cols = p.length;
var a = 0.5; // center between axes
for (var i = 0; i < cols; ++i) {
// centroids on 'real' axes
var x = position(p[i]);
var y = yscale[p[i]](row[p[i]]);
centroids.push($V([x, y]));
// centroids on 'virtual' axes
if (i < cols - 1) {
var cx = x + a * (position(p[i + 1]) - x);
var cy = y + a * (yscale[p[i + 1]](row[p[i + 1]]) - y);
if (__.bundleDimension !== null) {
var leftCentroid = __.clusterCentroids
.get(yscale[__.bundleDimension](row[__.bundleDimension]))
.get(p[i]);
var rightCentroid = __.clusterCentroids
.get(yscale[__.bundleDimension](row[__.bundleDimension]))
.get(p[i + 1]);
var centroid = 0.5 * (leftCentroid + rightCentroid);
cy = centroid + (1 - __.bundlingStrength) * (cy - centroid);
}
centroids.push($V([cx, cy]));
}
}
return centroids;
}
function compute_control_points(centroids) {
var cols = centroids.length;
var a = __.smoothness;
var cps = [];
cps.push(centroids[0]);
cps.push(
$V([centroids[0].e(1) + a * 2 * (centroids[1].e(1) - centroids[0].e(1)), centroids[0].e(2)]),
);
for (var col = 1; col < cols - 1; ++col) {
var mid = centroids[col];
var left = centroids[col - 1];
var right = centroids[col + 1];
var diff = left.subtract(right);
cps.push(mid.add(diff.x(a)));
cps.push(mid);
cps.push(mid.subtract(diff.x(a)));
}
cps.push(
$V([
centroids[cols - 1].e(1) + a * 2 * (centroids[cols - 2].e(1) - centroids[cols - 1].e(1)),
centroids[cols - 1].e(2),
]),
);
cps.push(centroids[cols - 1]);
return cps;
}
pc.shadows = function () {
flags.shadows = true;
pc.alphaOnBrushed(0.1);
pc.render();
return this;
};
// draw dots with radius r on the axis line where data intersects
pc.axisDots = function (r) {
var r = r || 0.1;
var ctx = pc.ctx.marks;
var startAngle = 0;
var endAngle = 2 * Math.PI;
ctx.globalAlpha = d3.min([1 / Math.pow(__.data.length, 1 / 2), 1]);
__.data.forEach(function (d) {
__.dimensions.map(function (p, i) {
ctx.beginPath();
ctx.arc(position(p), yscale[p](d[p]), r, startAngle, endAngle);
ctx.stroke();
ctx.fill();
});
});
return this;
};
// draw single cubic bezier curve
function single_curve(d, ctx) {
var centroids = compute_centroids(d);
var cps = compute_control_points(centroids);
ctx.moveTo(cps[0].e(1), cps[0].e(2));
for (var i = 1; i < cps.length; i += 3) {
if (__.showControlPoints) {
for (var j = 0; j < 3; j += 1) {
ctx.fillRect(cps[i + j].e(1), cps[i + j].e(2), 2, 2);
}
}
ctx.bezierCurveTo(
cps[i].e(1),
cps[i].e(2),
cps[i + 1].e(1),
cps[i + 1].e(2),
cps[i + 2].e(1),
cps[i + 2].e(2),
);
}
}
// draw single polyline
function color_path(d, ctx) {
ctx.beginPath();
if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
single_curve(d, ctx);
} else {
single_path(d, ctx);
}
ctx.stroke();
}
// draw many polylines of the same color
function paths(data, ctx) {
ctx.clearRect(-1, -1, w() + 2, h() + 2);
ctx.beginPath();
data.forEach(function (d) {
if ((__.bundleDimension !== null && __.bundlingStrength > 0) || __.smoothness > 0) {
single_curve(d, ctx);
} else {
single_path(d, ctx);
}
});
ctx.stroke();
}
// returns the y-position just beyond the separating null value line
function getNullPosition() {
if (__.nullValueSeparator == 'bottom') {
return h() + 1;
} else if (__.nullValueSeparator == 'top') {
return 1;
} else {
console.log(
"A value is NULL, but nullValueSeparator is not set; set it to 'bottom' or 'top'.",
);
}
return h() + 1;
}
function single_path(d, ctx) {
__.dimensions.map(function (p, i) {
if (i == 0) {
ctx.moveTo(position(p), typeof d[p] == 'undefined' ? getNullPosition() : yscale[p](d[p]));
} else {
ctx.lineTo(position(p), typeof d[p] == 'undefined' ? getNullPosition() : yscale[p](d[p]));
}
});
}
function path_brushed(d, i) {
if (__.brushedColor !== null) {
ctx.brushed.strokeStyle = d3.functor(__.brushedColor)(d, i);
} else {
ctx.brushed.strokeStyle = d3.functor(__.color)(d, i);
}
return color_path(d, ctx.brushed);
}
function path_foreground(d, i) {
ctx.foreground.strokeStyle = d3.functor(__.color)(d, i);
return color_path(d, ctx.foreground);
}
function path_highlight(d, i) {
ctx.highlight.strokeStyle = d3.functor(__.color)(d, i);
return color_path(d, ctx.highlight);
}
pc.clear = function (layer) {
ctx[layer].clearRect(0, 0, w() + 2, h() + 2);
// This will make sure that the foreground items are transparent
// without the need for changing the opacity style of the foreground canvas
// as this would stop the css styling from working
if (layer === 'brushed' && isBrushed()) {
ctx.brushed.fillStyle = pc.selection.style('background-color');
ctx.brushed.globalAlpha = 1 - __.alphaOnBrushed;
ctx.brushed.fillRect(0, 0, w() + 2, h() + 2);
ctx.brushed.globalAlpha = __.alpha;
}
return this;
};
d3.rebind(
pc,
axis,
'ticks',
'orient',
'tickValues',
'tickSubdivide',
'tickSize',
'tickPadding',
'tickFormat',
);
function flipAxisAndUpdatePCP(dimension) {
var g = pc.svg.selectAll('.dimension');
pc.flip(dimension);
d3.select(this.parentElement).transition().duration(1100).call(axis.scale(yscale[dimension]));
pc.render();
}
function rotateLabels() {
var delta = d3.event.deltaY;
delta = delta < 0 ? -5 : delta;
delta = delta > 0 ? 5 : delta;
__.dimensionTitleRotation += delta;
pc.svg
.selectAll('text.label')
.attr('transform', 'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')');
d3.event.preventDefault();
}
function dimensionLabels(d) {
return d in __.dimensionTitles ? __.dimensionTitles[d] : d; // dimension display names
}
pc.createAxes = function () {
if (g) pc.removeAxes();
// Add a group element for each dimension.
g = pc.svg
.selectAll('.dimension')
.data(__.dimensions, function (d) {
return d;
})
.enter()
.append('svg:g')
.attr('class', 'dimension')
.attr('transform', function (d) {
return 'translate(' + xscale(d) + ')';
});
// Add an axis and title.
g.append('svg:g')
.attr('class', 'axis')
.attr('transform', 'translate(0,0)')
.each(function (d) {
d3.select(this).call(axis.scale(yscale[d]));
})
.append('svg:text')
.attr({
'text-anchor': 'middle',
y: 0,
transform: 'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')',
x: 0,
class: 'label',
})
.text(dimensionLabels)
.on('dblclick', flipAxisAndUpdatePCP)
.on('wheel', rotateLabels);
if (__.nullValueSeparator == 'top') {
pc.svg
.append('line')
.attr('x1', 0)
.attr('y1', 1 + __.nullValueSeparatorPadding.top)
.attr('x2', w())
.attr('y2', 1 + __.nullValueSeparatorPadding.top)
.attr('stroke-width', 1)
.attr('stroke', '#777')
.attr('fill', 'none')
.attr('shape-rendering', 'crispEdges');
} else if (__.nullValueSeparator == 'bottom') {
pc.svg
.append('line')
.attr('x1', 0)
.attr('y1', h() + 1 - __.nullValueSeparatorPadding.bottom)
.attr('x2', w())
.attr('y2', h() + 1 - __.nullValueSeparatorPadding.bottom)
.attr('stroke-width', 1)
.attr('stroke', '#777')
.attr('fill', 'none')
.attr('shape-rendering', 'crispEdges');
}
flags.axes = true;
return this;
};
pc.removeAxes = function () {
g.remove();
return this;
};
pc.updateAxes = function () {
var g_data = pc.svg.selectAll('.dimension').data(__.dimensions);
// Enter
g_data
.enter()
.append('svg:g')
.attr('class', 'dimension')
.attr('transform', function (p) {
return 'translate(' + position(p) + ')';
})
.style('opacity', 0)
.append('svg:g')
.attr('class', 'axis')
.attr('transform', 'translate(0,0)')
.each(function (d) {
d3.select(this).call(axis.scale(yscale[d]));
})
.append('svg:text')
.attr({
'text-anchor': 'middle',
y: 0,
transform: 'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')',
x: 0,
class: 'label',
})
.text(dimensionLabels)
.on('dblclick', flipAxisAndUpdatePCP)
.on('wheel', rotateLabels);
// Update
g_data.attr('opacity', 0);
g_data
.select('.axis')
.transition()
.duration(1100)
.each(function (d) {
d3.select(this).call(axis.scale(yscale[d]));
});
g_data
.select('.label')
.transition()
.duration(1100)
.text(dimensionLabels)
.attr('transform', 'translate(0,-5) rotate(' + __.dimensionTitleRotation + ')');
// Exit
g_data.exit().remove();
g = pc.svg.selectAll('.dimension');
g.transition()
.duration(1100)
.attr('transform', function (p) {
return 'translate(' + position(p) + ')';
})
.style('opacity', 1);
pc.svg
.selectAll('.axis')
.transition()
.duration(1100)
.each(function (d) {
d3.select(this).call(axis.scale(yscale[d]));
});
if (flags.brushable) pc.brushable();
if (flags.reorderable) pc.reorderable();
if (pc.brushMode() !== 'None') {
var mode = pc.brushMode();
pc.brushMode('None');
pc.brushMode(mode);
}
return this;
};
// Jason Davies, http://bl.ocks.org/1341281
pc.reorderable = function () {
if (!g) pc.createAxes();
g.style('cursor', 'move').call(
d3.behavior
.drag()
.on('dragstart', function (d) {
dragging[d] = this.__origin__ = xscale(d);
})
.on('drag', function (d) {
dragging[d] = Math.min(w(), Math.max(0, (this.__origin__ += d3.event.dx)));
__.dimensions.sort(function (a, b) {
return position(a) - position(b);
});
xscale.domain(__.dimensions);
pc.render();
g.attr('transform', function (d) {
return 'translate(' + position(d) + ')';
});
})
.on('dragend', function (d) {
// Let's see if the order has changed and send out an event if so.
var i = 0,
j = __.dimensions.indexOf(d),
elem = this,
parent = this.parentElement;
while ((elem = elem.previousElementSibling) != null) ++i;
if (i !== j) {
events.axesreorder.call(pc, __.dimensions);
// We now also want to reorder the actual dom elements that represent
// the axes. That is, the g.dimension elements. If we don't do this,
// we get a weird and confusing transition when updateAxes is called.
// This is due to the fact that, initially the nth g.dimension element
// represents the nth axis. However, after a manual reordering,
// without reordering the dom elements, the nth dom elements no longer
// necessarily represents the nth axis.
//
// i is the original index of the dom element
// j is the new index of the dom element
if (i > j) {
// Element moved left
parent.insertBefore(this, parent.children[j - 1]);
} else {
// Element moved right
if (j + 1 < parent.children.length) {
parent.insertBefore(this, parent.children[j + 1]);
} else {
parent.appendChild(this);
}
}
}
delete this.__origin__;
delete dragging[d];
d3.select(this)
.transition()
.attr('transform', 'translate(' + xscale(d) + ')');
pc.render();
}),
);
flags.reorderable = true;
return this;
};
// Reorder dimensions, such that the highest value (visually) is on the left and
// the lowest on the right. Visual values are determined by the data values in
// the given row.
pc.reorder = function (rowdata) {
var dims = __.dimensions.slice(0);
__.dimensions.sort(function (a, b) {
var pixelDifference = yscale[a](rowdata[a]) - yscale[b](rowdata[b]);
// Array.sort is not necessarily stable, this means that if pixelDifference is zero
// the ordering of dimensions might change unexpectedly. This is solved by sorting on
// variable name in that case.
if (pixelDifference === 0) {
return a.localeCompare(b);
} // else
return pixelDifference;
});
// NOTE: this is relatively cheap given that:
// number of dimensions < number of data items
// Thus we check equality of order to prevent rerendering when this is the case.
var reordered = false;
dims.some(function (val, index) {
reordered = val !== __.dimensions[index];
return reordered;
});
if (reordered) {
xscale.domain(__.dimensions);
var highlighted = __.highlighted.slice(0);
pc.unhighlight();
g.transition()
.duration(1500)
.attr('transform', function (d) {
return 'translate(' + xscale(d) + ')';
});
pc.render();
// pc.highlight() does not check whether highlighted is length zero, so we do that here.
if (highlighted.length !== 0) {
pc.highlight(highlighted);
}
}
};
// pairs of adjacent dimensions
pc.adjacent_pairs = function (arr) {
var ret = [];
for (var i = 0; i < arr.length - 1; i += 1) {
ret.push([arr[i], arr[i + 1]]);
}
return ret;
};
var brush = {
modes: {
None: {
install: function (pc) {}, // Nothing to be done.
uninstall: function (pc) {}, // Nothing to be done.
selected: function () {
return [];
}, // Nothing to return
brushState: function () {
return {};
},
},
},
mode: 'None',
predicate: 'AND',
currentMode: function () {
return this.modes[this.mode];
},
};
// This function can be used for 'live' updates of brushes. That is, during the
// specification of a brush, this method can be called to update the view.
//
// @param newSelection - The new set of data items that is currently contained
// by the brushes
function brushUpdated(newSelection) {
__.brushed = newSelection;
events.brush.call(pc, __.brushed);
pc.renderBrushed();
}
function brushPredicate(predicate) {
if (!arguments.length) {
return brush.predicate;
}
predicate = String(predicate).toUpperCase();
if (predicate !== 'AND' && predicate !== 'OR') {
throw 'Invalid predicate ' + predicate;
}
brush.predicate = predicate;
__.brushed = brush.currentMode().selected();
pc.renderBrushed();
return pc;
}
pc.brushModes = function () {
return Object.getOwnPropertyNames(brush.modes);
};
pc.brushMode = function (mode) {
if (arguments.length === 0) {
return brush.mode;
}
if (pc.brushModes().indexOf(mode) === -1) {
throw 'pc.brushmode: Unsupported brush mode: ' + mode;
}
// Make sure that we don't trigger unnecessary events by checking if the mode
// actually changes.
if (mode !== brush.mode) {
// When changing brush modes, the first thing we need to do is clearing any
// brushes from the current mode, if any.
if (brush.mode !== 'None') {
pc.brushReset();
}
// Next, we need to 'uninstall' the current brushMode.
brush.modes[brush.mode].uninstall(pc);
// Finally, we can install the requested one.
brush.mode = mode;
brush.modes[brush.mode].install();
if (mode === 'None') {
delete pc.brushPredicate;
} else {
pc.brushPredicate = brushPredicate;
}
}
return pc;
};
// brush mode: 1D-Axes
(function () {
var brushes = {};
function is_brushed(p) {
return !brushes[p].empty();
}
// data within extents
function selected() {
var actives = __.dimensions.filter(is_brushed),
extents = actives.map(function (p) {
return brushes[p].extent();
});
// We don't want to return the full data set when there are no axes brushed.
// Actually, when there are no axes brushed, by definition, no items are
// selected. So, let's avoid the filtering and just return false.
//if (actives.length === 0) return false;
// Resolves broken examples for now. They expect to get the full dataset back from empty brushes
if (actives.length === 0) return __.data;
// test if within range
var within = {
date: function (d, p, dimension) {
if (typeof yscale[p].rangePoints === 'function') {
// if it is ordinal
return (
extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
);
} else {
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1];
}
},
number: function (d, p, dimension) {
if (typeof yscale[p].rangePoints === 'function') {
// if it is ordinal
return (
extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
);
} else {
return extents[dimension][0] <= d[p] && d[p] <= extents[dimension][1];
}
},
string: function (d, p, dimension) {
return (
extents[dimension][0] <= yscale[p](d[p]) && yscale[p](d[p]) <= extents[dimension][1]
);
},
};
return __.data.filter(function (d) {
switch (brush.predicate) {
case 'AND':
return actives.every(function (p, dimension) {
return within[__.types[p]](d, p, dimension);
});
case 'OR':
return actives.some(function (p, dimension) {
return within[__.types[p]](d, p, dimension);
});
default:
throw 'Unknown brush predicate ' + __.brushPredicate;
}
});
}
function brushExtents(extents) {
if (typeof extents === 'undefined') {
var extents = {};
__.dimensions.forEach(function (d) {
var brush = brushes[d];
if (brush !== undefined && !brush.empty()) {
var extent = brush.extent();
extent.sort(d3.ascending);
extents[d] = extent;
}
});
return extents;
} else {
//first get all the brush selections
var brushSelections = {};
g.selectAll('.brush').each(function (d) {
brushSelections[d] = d3.select(this);
});
// loop over each dimension and update appropriately (if it was passed in through extents)
__.dimensions.forEach(function (d) {
if (extents[d] === undefined) {
return;
}
var brush = brushes[d];
if (brush !== undefined) {
//update the extent
brush.extent(extents[d]);
//redraw the brush
brush(brushSelections[d]);
//fire some events
brush.event(brushSelections[d]);
}
});
//redraw the chart
pc.renderBrushed();
}
}
function brushFor(axis) {
var brush = d3.svg.brush();
brush
.y(yscale[axis])
.on('brushstart', function () {
if (d3.event.sourceEvent !== null) {
d3.event.sourceEvent.stopPropagation();
}
})
.on('brush', function () {
brushUpdated(selected());
})
.on('brushend', function () {
events.brushend.call(pc, __.brushed);
});
brushes[axis] = brush;
return brush;
}
function brushReset(dimension) {
__.brushed = false;
if (g) {
g.selectAll('.brush').each(function (d) {
d3.select(this).call(brushes[d].clear());
});
pc.renderBrushed();
}
return this;
}
function install() {
if (!g) pc.createAxes();
// Add and store a brush for each axis.
g.append('svg:g')
.attr('class', 'brush')
.each(function (d) {
d3.select(this).call(brushFor(d));
})
.selectAll('rect')
.style('visibility', null)
.attr('x', -15)
.attr('width', 30);
pc.brushExtents = brushExtents;
pc.brushReset = brushReset;
return pc;
}
brush.modes['1D-axes'] = {
install: install,
uninstall: function () {
g.selectAll('.brush').remove();
brushes = {};
delete pc.brushExtents;
delete pc.brushReset;
},
selected: selected,
brushState: brushExtents,
};
})();
// brush mode: 2D-strums
// bl.ocks.org/syntagmatic/5441022
(function () {
var strums = {},
strumRect;
function drawStrum(strum, activePoint) {
var svg = pc.selection.select('svg').select('g#strums'),
id = strum.dims.i,
points = [strum.p1, strum.p2],
line = svg.selectAll('line#strum-' + id).data([strum]),
circles = svg.selectAll('circle#strum-' + id).data(points),
drag = d3.behavior.drag();
line
.enter()
.append('line')
.attr('id', 'strum-' + id)
.attr('class', 'strum');
line
.attr('x1', function (d) {
return d.p1[0];
})
.attr('y1', function (d) {
return d.p1[1];
})
.attr('x2', function (d) {
return d.p2[0];
})
.attr('y2', function (d) {
return d.p2[1];
})
.attr('stroke', 'black')
.attr('stroke-width', 2);
drag
.on('drag', function (d, i) {
var ev = d3.event;
i = i + 1;
strum['p' + i][0] = Math.min(Math.max(strum.minX + 1, ev.x), strum.maxX);
strum['p' + i][1] = Math.min(Math.max(strum.minY, ev.y), strum.maxY);
drawStrum(strum, i - 1);
})
.on('dragend', onDragEnd());
circles
.enter()
.append('circle')
.attr('id', 'strum-' + id)
.attr('class', 'strum');
circles
.attr('cx', function (d) {
return d[0];
})
.attr('cy', function (d) {
return d[1];
})
.attr('r', 5)
.style('opacity', function (d, i) {
return activePoint !== undefined && i === activePoint ? 0.8 : 0;
})
.on('mouseover', function () {
d3.select(this).style('opacity', 0.8);
})
.on('mouseout', function () {
d3.select(this).style('opacity', 0);
})
.call(drag);
}
function dimensionsForPoint(p) {
var dims = { i: -1, left: undefined, right: undefined };
__.dimensions.some(function (dim, i) {
if (xscale(dim) < p[0]) {
var next = __.dimensions[i + 1];
dims.i = i;
dims.left = dim;
dims.right = next;
return false;
}
return true;
});
if (dims.left === undefined) {
// Event on the left side of the first axis.
dims.i = 0;
dims.left = __.dimensions[0];
dims.right = __.dimensions[1];
} else if (dims.right === undefined) {
// Event on the right side of the last axis
dims.i = __.dimensions.length - 1;
dims.right = dims.left;
dims.left = __.dimensions[__.dimensions.length - 2];
}
return dims;
}
function onDragStart() {
// First we need to determine between which two axes the sturm was started.
// This will determine the freedom of movement, because a strum can
// logically only happen between two axes, so no movement outside these axes
// should be allowed.
return function () {
var p = d3.mouse(strumRect[0][0]),
dims,
strum;
p[0] = p[0] - __.margin.left;
p[1] = p[1] - __.margin.top;
(dims = dimensionsForPoint(p)),
(strum = {
p1: p,
dims: dims,
minX: xscale(dims.left),
maxX: xscale(dims.right),
minY: 0,
maxY: h(),
});
strums[dims.i] = strum;
strums.active = dims.i;
// Make sure that the point is within the bounds
strum.p1[0] = Math.min(Math.max(strum.minX, p[0]), strum.maxX);
strum.p2 = strum.p1.slice();
};
}
function onDrag() {
return function () {
var ev = d3.event,
strum = strums[strums.active];
// Make sure that the point is within the bounds
strum.p2[0] = Math.min(Math.max(strum.minX + 1, ev.x - __.margin.left), strum.maxX);
strum.p2[1] = Math.min(Math.max(strum.minY, ev.y - __.margin.top), strum.maxY);
drawStrum(strum, 1);
};
}
function containmentTest(strum, width) {
var p1 = [strum.p1[0] - strum.minX, strum.p1[1] - strum.minX],
p2 = [strum.p2[0] - strum.minX, strum.p2[1] - strum.minX],
m1 = 1 - width / p1[0],
b1 = p1[1] * (1 - m1),
m2 = 1 - width / p2[0],
b2 = p2[1] * (1 - m2);
// test if point falls between lines
return function (p) {
var x = p[0],
y = p[1],
y1 = m1 * x + b1,
y2 = m2 * x + b2;
if (y > Math.min(y1, y2) && y < Math.max(y1, y2)) {
return true;
}
return false;
};
}
function selected() {
var ids = Object.getOwnPropertyNames(strums),
brushed = __.data;
// Get the ids of the currently active strums.
ids = ids.filter(function (d) {
return !isNaN(d);
});
function crossesStrum(d, id) {
var strum = strums[id],
test = containmentTest(strum, strums.width(id)),
d1 = strum.dims.left,
d2 = strum.dims.right,
y1 = yscale[d1],
y2 = yscale[d2],
point = [y1(d[d1]) - strum.minX, y2(d[d2]) - strum.minX];
return test(point);
}
if (ids.length === 0) {
return brushed;
}
return brushed.filter(function (d) {
switch (brush.predicate) {
case 'AND':
return ids.every(function (id) {
return crossesStrum(d, id);
});
case 'OR':
return ids.some(function (id) {
return crossesStrum(d, id);
});
default:
throw 'Unknown brush predicate ' + __.brushPredicate;
}
});
}
function removeStrum() {
var strum = strums[strums.active],
svg = pc.selection.select('svg').select('g#strums');
delete strums[strums.active];
strums.active = undefined;
svg.selectAll('line#strum-' + strum.dims.i).remove();
svg.selectAll('circle#strum-' + strum.dims.i).remove();
}
function onDragEnd() {
return function () {
var brushed = __.data,
strum = strums[strums.active];
// Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
// considered a drag without move. So we have to deal with that case
if (strum && strum.p1[0] === strum.p2[0] && strum.p1[1] === strum.p2[1]) {
removeStrum(strums);
}
brushed = selected(strums);
strums.active = undefined;
__.brushed = brushed;
pc.renderBrushed();
events.brushend.call(pc, __.brushed);
};
}
function brushReset(strums) {
return function () {
var ids = Object.getOwnPropertyNames(strums).filter(function (d) {
return !isNaN(d);
});
ids.forEach(function (d) {
strums.active = d;
removeStrum(strums);
});
onDragEnd(strums)();
};
}
function install() {
var drag = d3.behavior.drag();
// Map of current strums. Strums are stored per segment of the PC. A segment,
// being the area between two axes. The left most area is indexed at 0.
strums.active = undefined;
// Returns the width of the PC segment where currently a strum is being
// placed. NOTE: even though they are evenly spaced in our current
// implementation, we keep for when non-even spaced segments are supported as
// well.
strums.width = function (id) {
var strum = strums[id];
if (strum === undefined) {
return undefined;
}
return strum.maxX - strum.minX;
};
pc.on('axesreorder.strums', function () {
var ids = Object.getOwnPropertyNames(strums).filter(function (d) {
return !isNaN(d);
});
// Checks if the first dimension is directly left of the second dimension.
function consecutive(first, second) {
var length = __.dimensions.length;
return __.dimensions.some(function (d, i) {
return d === first ? i + i < length && __.dimensions[i + 1] === second : false;
});
}
if (ids.length > 0) {
// We have some strums, which might need to be removed.
ids.forEach(function (d) {
var dims = strums[d].dims;
strums.active = d;
// If the two dimensions of the current strum are not next to each other
// any more, than we'll need to remove the strum. Otherwise we keep it.
if (!consecutive(dims.left, dims.right)) {
removeStrum(strums);
}
});
onDragEnd(strums)();
}
});
// Add a new svg group in which we draw the strums.
pc.selection
.select('svg')
.append('g')
.attr('id', 'strums')
.attr('transform', 'translate(' + __.margin.left + ',' + __.margin.top + ')');
// Install the required brushReset function
pc.brushReset = brushReset(strums);
drag
.on('dragstart', onDragStart(strums))
.on('drag', onDrag(strums))
.on('dragend', onDragEnd(strums));
// NOTE: The styling needs to be done here and not in the css. This is because
// for 1D brushing, the canvas layers should not listen to
// pointer-events.
strumRect = pc.selection
.select('svg')
.insert('rect', 'g#strums')
.attr('id', 'strum-events')
.attr('x', __.margin.left)
.attr('y', __.margin.top)
.attr('width', w())
.attr('height', h() + 2)
.style('opacity', 0)
.call(drag);
}
brush.modes['2D-strums'] = {
install: install,
uninstall: function () {
pc.selection.select('svg').select('g#strums').remove();
pc.selection.select('svg').select('rect#strum-events').remove();
pc.on('axesreorder.strums', undefined);
delete pc.brushReset;
strumRect = undefined;
},
selected: selected,
brushState: function () {
return strums;
},
};
})();
// brush mode: 1D-Axes with multiple extents
// requires d3.svg.multibrush
(function () {
if (typeof d3.svg.multibrush !== 'function') {
return;
}
var brushes = {};
function is_brushed(p) {
return !brushes[p].empty();
}
// data within extents
function selected() {
var actives = __.dimensions.filter(is_brushed),
extents = actives.map(function (p) {
return brushes[p].extent();
});
// We don't want to return the full data set when there are no axes brushed.
// Actually, when there are no axes brushed, by definition, no items are
// selected. So, let's avoid the filtering and just return false.
//if (actives.length === 0) return false;
// Resolves broken examples for now. They expect to get the full dataset back from empty brushes
if (actives.length === 0) return __.data;
// test if within range
var within = {
date: function (d, p, dimension, b) {
if (typeof yscale[p].rangePoints === 'function') {
// if it is ordinal
return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1];
} else {
return b[0] <= d[p] && d[p] <= b[1];
}
},
number: function (d, p, dimension, b) {
if (typeof yscale[p].rangePoints === 'function') {
// if it is ordinal
return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1];
} else {
return b[0] <= d[p] && d[p] <= b[1];
}
},
string: function (d, p, dimension, b) {
return b[0] <= yscale[p](d[p]) && yscale[p](d[p]) <= b[1];
},
};
return __.data.filter(function (d) {
switch (brush.predicate) {
case 'AND':
return actives.every(function (p, dimension) {
return extents[dimension].some(function (b) {
return within[__.types[p]](d, p, dimension, b);
});
});
case 'OR':
return actives.some(function (p, dimension) {
return extents[dimension].some(function (b) {
return within[__.types[p]](d, p, dimension, b);
});
});
default:
throw 'Unknown brush predicate ' + __.brushPredicate;
}
});
}
function brushExtents() {
var extents = {};
__.dimensions.forEach(function (d) {
var brush = brushes[d];
if (brush !== undefined && !brush.empty()) {
var extent = brush.extent();
extents[d] = extent;
}
});
return extents;
}
function brushFor(axis) {
var brush = d3.svg.multibrush();
brush
.y(yscale[axis])
.on('brushstart', function () {
if (d3.event.sourceEvent !== null) {
d3.event.sourceEvent.stopPropagation();
}
})
.on('brush', function () {
brushUpdated(selected());
})
.on('brushend', function () {
// d3.svg.multibrush clears extents just before calling 'brushend'
// so we have to update here again.
// This fixes issue #103 for now, but should be changed in d3.svg.multibrush
// to avoid unnecessary computation.
brushUpdated(selected());
events.brushend.call(pc, __.brushed);
})
.extentAdaption(function (selection) {
selection.style('visibility', null).attr('x', -15).attr('width', 30);
})
.resizeAdaption(function (selection) {
selection.selectAll('rect').attr('x', -15).attr('width', 30);
});
brushes[axis] = brush;
return brush;
}
function brushReset(dimension) {
__.brushed = false;
if (g) {
g.selectAll('.brush').each(function (d) {
d3.select(this).call(brushes[d].clear());
});
pc.renderBrushed();
}
return this;
}
function install() {
if (!g) pc.createAxes();
// Add and store a brush for each axis.
g.append('svg:g')
.attr('class', 'brush')
.each(function (d) {
d3.select(this).call(brushFor(d));
})
.selectAll('rect')
.style('visibility', null)
.attr('x', -15)
.attr('width', 30);
pc.brushExtents = brushExtents;
pc.brushReset = brushReset;
return pc;
}
brush.modes['1D-axes-multi'] = {
install: install,
uninstall: function () {
g.selectAll('.brush').remove();
brushes = {};
delete pc.brushExtents;
delete pc.brushReset;
},
selected: selected,
brushState: brushExtents,
};
})();
// brush mode: angular
// code based on 2D.strums.js
(function () {
var arcs = {},
strumRect;
function drawStrum(arc, activePoint) {
var svg = pc.selection.select('svg').select('g#arcs'),
id = arc.dims.i,
points = [arc.p2, arc.p3],
line = svg.selectAll('line#arc-' + id).data([
{ p1: arc.p1, p2: arc.p2 },
{ p1: arc.p1, p2: arc.p3 },
]),
circles = svg.selectAll('circle#arc-' + id).data(points),
drag = d3.behavior.drag(),
path = svg.selectAll('path#arc-' + id).data([arc]);
path
.enter()
.append('path')
.attr('id', 'arc-' + id)
.attr('class', 'arc')
.style('fill', 'orange')
.style('opacity', 0.5);
path.attr('d', arc.arc).attr('transform', 'translate(' + arc.p1[0] + ',' + arc.p1[1] + ')');
line
.enter()
.append('line')
.attr('id', 'arc-' + id)
.attr('class', 'arc');
line
.attr('x1', function (d) {
return d.p1[0];
})
.attr('y1', function (d) {
return d.p1[1];
})
.attr('x2', function (d) {
return d.p2[0];
})
.attr('y2', function (d) {
return d.p2[1];
})
.attr('stroke', 'black')
.attr('stroke-width', 2);
drag
.on('drag', function (d, i) {
var ev = d3.event,
angle = 0;
i = i + 2;
arc['p' + i][0] = Math.min(Math.max(arc.minX + 1, ev.x), arc.maxX);
arc['p' + i][1] = Math.min(Math.max(arc.minY, ev.y), arc.maxY);
angle = i === 3 ? arcs.startAngle(id) : arcs.endAngle(id);
if (
(arc.startAngle < Math.PI && arc.endAngle < Math.PI && angle < Math.PI) ||
(arc.startAngle >= Math.PI && arc.endAngle >= Math.PI && angle >= Math.PI)
) {
if (i === 2) {
arc.endAngle = angle;
arc.arc.endAngle(angle);
} else if (i === 3) {
arc.startAngle = angle;
arc.arc.startAngle(angle);
}
}
drawStrum(arc, i - 2);
})
.on('dragend', onDragEnd());
circles
.enter()
.append('circle')
.attr('id', 'arc-' + id)
.attr('class', 'arc');
circles
.attr('cx', function (d) {
return d[0];
})
.attr('cy', function (d) {
return d[1];
})
.attr('r', 5)
.style('opacity', function (d, i) {
return activePoint !== undefined && i === activePoint ? 0.8 : 0;
})
.on('mouseover', function () {
d3.select(this).style('opacity', 0.8);
})
.on('mouseout', function () {
d3.select(this).style('opacity', 0);
})
.call(drag);
}
function dimensionsForPoint(p) {
var dims = { i: -1, left: undefined, right: undefined };
__.dimensions.some(function (dim, i) {
if (xscale(dim) < p[0]) {
var next = __.dimensions[i + 1];
dims.i = i;
dims.left = dim;
dims.right = next;
return false;
}
return true;
});
if (dims.left === undefined) {
// Event on the left side of the first axis.
dims.i = 0;
dims.left = __.dimensions[0];
dims.right = __.dimensions[1];
} else if (dims.right === undefined) {
// Event on the right side of the last axis
dims.i = __.dimensions.length - 1;
dims.right = dims.left;
dims.left = __.dimensions[__.dimensions.length - 2];
}
return dims;
}
function onDragStart() {
// First we need to determine between which two axes the arc was started.
// This will determine the freedom of movement, because a arc can
// logically only happen between two axes, so no movement outside these axes
// should be allowed.
return function () {
var p = d3.mouse(strumRect[0][0]),
dims,
arc;
p[0] = p[0] - __.margin.left;
p[1] = p[1] - __.margin.top;
(dims = dimensionsForPoint(p)),
(arc = {
p1: p,
dims: dims,
minX: xscale(dims.left),
maxX: xscale(dims.right),
minY: 0,
maxY: h(),
startAngle: undefined,
endAngle: undefined,
arc: d3.svg.arc().innerRadius(0),
});
arcs[dims.i] = arc;
arcs.active = dims.i;
// Make sure that the point is within the bounds
arc.p1[0] = Math.min(Math.max(arc.minX, p[0]), arc.maxX);
arc.p2 = arc.p1.slice();
arc.p3 = arc.p1.slice();
};
}
function onDrag() {
return function () {
var ev = d3.event,
arc = arcs[arcs.active];
// Make sure that the point is within the bounds
arc.p2[0] = Math.min(Math.max(arc.minX + 1, ev.x - __.margin.left), arc.maxX);
arc.p2[1] = Math.min(Math.max(arc.minY, ev.y - __.margin.top), arc.maxY);
arc.p3 = arc.p2.slice();
drawStrum(arc, 1);
};
}
// some helper functions
function hypothenuse(a, b) {
return Math.sqrt(a * a + b * b);
}
var rad = (function () {
var c = Math.PI / 180;
return function (angle) {
return angle * c;
};
})();
var deg = (function () {
var c = 180 / Math.PI;
return function (angle) {
return angle * c;
};
})();
// [0, 2*PI] -> [-PI/2, PI/2]
var signedAngle = function (angle) {
var ret = angle;
if (angle > Math.PI) {
ret = angle - 1.5 * Math.PI;
ret = angle - 1.5 * Math.PI;
} else {
ret = angle - 0.5 * Math.PI;
ret = angle - 0.5 * Math.PI;
}
return -ret;
};
/**
* angles are stored in radians from in [0, 2*PI], where 0 in 12 o'clock.
* However, one can only select lines from 0 to PI, so we compute the
* 'signed' angle, where 0 is the horizontal line (3 o'clock), and +/- PI/2
* are 12 and 6 o'clock respectively.
*/
function containmentTest(arc) {
var startAngle = signedAngle(arc.startAngle);
var endAngle = signedAngle(arc.endAngle);
if (startAngle > endAngle) {
var tmp = startAngle;
startAngle = endAngle;
endAngle = tmp;
}
// test if segment angle is contained in angle interval
return function (a) {
if (a >= startAngle && a <= endAngle) {
return true;
}
return false;
};
}
function selected() {
var ids = Object.getOwnPropertyNames(arcs),
brushed = __.data;
// Get the ids of the currently active arcs.
ids = ids.filter(function (d) {
return !isNaN(d);
});
function crossesStrum(d, id) {
var arc = arcs[id],
test = containmentTest(arc),
d1 = arc.dims.left,
d2 = arc.dims.right,
y1 = yscale[d1],
y2 = yscale[d2],
a = arcs.width(id),
b = y1(d[d1]) - y2(d[d2]),
c = hypothenuse(a, b),
angle = Math.asin(b / c); // rad in [-PI/2, PI/2]
return test(angle);
}
if (ids.length === 0) {
return brushed;
}
return brushed.filter(function (d) {
switch (brush.predicate) {
case 'AND':
return ids.every(function (id) {
return crossesStrum(d, id);
});
case 'OR':
return ids.some(function (id) {
return crossesStrum(d, id);
});
default:
throw 'Unknown brush predicate ' + __.brushPredicate;
}
});
}
function removeStrum() {
var arc = arcs[arcs.active],
svg = pc.selection.select('svg').select('g#arcs');
delete arcs[arcs.active];
arcs.active = undefined;
svg.selectAll('line#arc-' + arc.dims.i).remove();
svg.selectAll('circle#arc-' + arc.dims.i).remove();
svg.selectAll('path#arc-' + arc.dims.i).remove();
}
function onDragEnd() {
return function () {
var brushed = __.data,
arc = arcs[arcs.active];
// Okay, somewhat unexpected, but not totally unsurprising, a mousclick is
// considered a drag without move. So we have to deal with that case
if (arc && arc.p1[0] === arc.p2[0] && arc.p1[1] === arc.p2[1]) {
removeStrum(arcs);
}
if (arc) {
var angle = arcs.startAngle(arcs.active);
arc.startAngle = angle;
arc.endAngle = angle;
arc.arc.outerRadius(arcs.length(arcs.active)).startAngle(angle).endAngle(angle);
}
brushed = selected(arcs);
arcs.active = undefined;
__.brushed = brushed;
pc.renderBrushed();
events.brushend.call(pc, __.brushed);
};
}
function brushReset(arcs) {
return function () {
var ids = Object.getOwnPropertyNames(arcs).filter(function (d) {
return !isNaN(d);
});
ids.forEach(function (d) {
arcs.active = d;
removeStrum(arcs);
});
onDragEnd(arcs)();
};
}
function install() {
var drag = d3.behavior.drag();
// Map of current arcs. arcs are stored per segment of the PC. A segment,
// being the area between two axes. The left most area is indexed at 0.
arcs.active = undefined;
// Returns the width of the PC segment where currently a arc is being
// placed. NOTE: even though they are evenly spaced in our current
// implementation, we keep for when non-even spaced segments are supported as
// well.
arcs.width = function (id) {
var arc = arcs[id];
if (arc === undefined) {
return undefined;
}
return arc.maxX - arc.minX;
};
// returns angles in [-PI/2, PI/2]
angle = function (p1, p2) {
var a = p1[0] - p2[0],
b = p1[1] - p2[1],
c = hypothenuse(a, b);
return Math.asin(b / c);
};
// returns angles in [0, 2 * PI]
arcs.endAngle = function (id) {
var arc = arcs[id];
if (arc === undefined) {
return undefined;
}
var sAngle = angle(arc.p1, arc.p2),
uAngle = -sAngle + Math.PI / 2;
if (arc.p1[0] > arc.p2[0]) {
uAngle = 2 * Math.PI - uAngle;
}
return uAngle;
};
arcs.startAngle = function (id) {
var arc = arcs[id];
if (arc === undefined) {
return undefined;
}
var sAngle = angle(arc.p1, arc.p3),
uAngle = -sAngle + Math.PI / 2;
if (arc.p1[0] > arc.p3[0]) {
uAngle = 2 * Math.PI - uAngle;
}
return uAngle;
};
arcs.length = function (id) {
var arc = arcs[id];
if (arc === undefined) {
return undefined;
}
var a = arc.p1[0] - arc.p2[0],
b = arc.p1[1] - arc.p2[1],
c = hypothenuse(a, b);
return c;
};
pc.on('axesreorder.arcs', function () {
var ids = Object.getOwnPropertyNames(arcs).filter(function (d) {
return !isNaN(d);
});
// Checks if the first dimension is directly left of the second dimension.
function consecutive(first, second) {
var length = __.dimensions.length;
return __.dimensions.some(function (d, i) {
return d === first ? i + i < length && __.dimensions[i + 1] === second : false;
});
}
if (ids.length > 0) {
// We have some arcs, which might need to be removed.
ids.forEach(function (d) {
var dims = arcs[d].dims;
arcs.active = d;
// If the two dimensions of the current arc are not next to each other
// any more, than we'll need to remove the arc. Otherwise we keep it.
if (!consecutive(dims.left, dims.right)) {
removeStrum(arcs);
}
});
onDragEnd(arcs)();
}
});
// Add a new svg group in which we draw the arcs.
pc.selection
.select('svg')
.append('g')
.attr('id', 'arcs')
.attr('transform', 'translate(' + __.margin.left + ',' + __.margin.top + ')');
// Install the required brushReset function
pc.brushReset = brushReset(arcs);
drag
.on('dragstart', onDragStart(arcs))
.on('drag', onDrag(arcs))
.on('dragend', onDragEnd(arcs));
// NOTE: The styling needs to be done here and not in the css. This is because
// for 1D brushing, the canvas layers should not listen to
// pointer-events.
strumRect = pc.selection
.select('svg')
.insert('rect', 'g#arcs')
.attr('id', 'arc-events')
.attr('x', __.margin.left)
.attr('y', __.margin.top)
.attr('width', w())
.attr('height', h() + 2)
.style('opacity', 0)
.call(drag);
}
brush.modes['angular'] = {
install: install,
uninstall: function () {
pc.selection.select('svg').select('g#arcs').remove();
pc.selection.select('svg').select('rect#arc-events').remove();
pc.on('axesreorder.arcs', undefined);
delete pc.brushReset;
strumRect = undefined;
},
selected: selected,
brushState: function () {
return arcs;
},
};
})();
pc.interactive = function () {
flags.interactive = true;
return this;
};
// expose a few objects
pc.xscale = xscale;
pc.yscale = yscale;
pc.ctx = ctx;
pc.canvas = canvas;
pc.g = function () {
return g;
};
// rescale for height, width and margins
// TODO currently assumes chart is brushable, and destroys old brushes
pc.resize = function () {
// selection size
pc.selection.select('svg').attr('width', __.width).attr('height', __.height);
pc.svg.attr('transform', 'translate(' + __.margin.left + ',' + __.margin.top + ')');
// FIXME: the current brush state should pass through
if (flags.brushable) pc.brushReset();
// scales
pc.autoscale();
// axes, destroys old brushes.
if (g) pc.createAxes();
if (flags.brushable) pc.brushable();
if (flags.reorderable) pc.reorderable();
events.resize.call(this, { width: __.width, height: __.height, margin: __.margin });
return this;
};
// highlight an array of data
pc.highlight = function (data) {
if (arguments.length === 0) {
return __.highlighted;
}
__.highlighted = data;
pc.clear('highlight');
d3.selectAll([canvas.foreground, canvas.brushed]).classed('faded', true);
data.forEach(path_highlight);
events.highlight.call(this, data);
return this;
};
// clear highlighting
pc.unhighlight = function () {
__.highlighted = [];
pc.clear('highlight');
d3.selectAll([canvas.foreground, canvas.brushed]).classed('faded', false);
return this;
};
// calculate 2d intersection of line a->b with line c->d
// points are objects with x and y properties
pc.intersection = function (a, b, c, d) {
return {
x:
((a.x * b.y - a.y * b.x) * (c.x - d.x) - (a.x - b.x) * (c.x * d.y - c.y * d.x)) /
((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
y:
((a.x * b.y - a.y * b.x) * (c.y - d.y) - (a.y - b.y) * (c.x * d.y - c.y * d.x)) /
((a.x - b.x) * (c.y - d.y) - (a.y - b.y) * (c.x - d.x)),
};
};
function position(d) {
var v = dragging[d];
return v == null ? xscale(d) : v;
}
pc.version = '0.7.0';
// this descriptive text should live with other introspective methods
pc.toString = function () {
return (
'Parallel Coordinates: ' +
__.dimensions.length +
' dimensions (' +
d3.keys(__.data[0]).length +
' total) , ' +
__.data.length +
' rows'
);
};
return pc;
}
d3.renderQueue = function (func) {
var _queue = [], // data to be rendered
_rate = 10, // number of calls per frame
_clear = function () {}, // clearing function
_i = 0; // current iteration
var rq = function (data) {
if (data) rq.data(data);
rq.invalidate();
_clear();
rq.render();
};
rq.render = function () {
_i = 0;
var valid = true;
rq.invalidate = function () {
valid = false;
};
function doFrame() {
if (!valid) return true;
if (_i > _queue.length) return true;
// Typical d3 behavior is to pass a data item *and* its index. As the
// render queue splits the original data set, we'll have to be slightly
// more carefull about passing the correct index with the data item.
var end = Math.min(_i + _rate, _queue.length);
for (var i = _i; i < end; i += 1) {
func(_queue[i], i);
}
_i += _rate;
}
d3.timer(doFrame);
};
rq.data = function (data) {
rq.invalidate();
_queue = data.slice(0);
return rq;
};
rq.rate = function (value) {
if (!arguments.length) return _rate;
_rate = value;
return rq;
};
rq.remaining = function () {
return _queue.length - _i;
};
// clear the canvas
rq.clear = function (func) {
if (!arguments.length) {
_clear();
return rq;
}
_clear = func;
return rq;
};
rq.invalidate = function () {};
return rq;
};