/**
* D3 Geometer - a utility library for d3 that allows drawing
* of geometric primitives, labels, connections and more.
* Uses semantic versioning. http://semver.org/.
* Licensed under MIT.
* @copyright Copyright (c) Chris Tabor. 2014 All rights reserved.
* @license MIT
* @author Chris Tabor
**/
/**
* @namespace d3_geometer
*/
var d3_geometer = {
/** @version 1.0.0 */
'version': '1.0.0'
};
/**
* @namespace d3_geometer.utils
*/
d3_geometer.utils = {};
/**
* Converts degrees to radians.
* @param {Number} degree - the degrees to convert - up to 360
* @returns {Number}
*/
d3_geometer.utils.toRadian = function(deg) {
if(deg > 360) deg = 360;
if(isNaN(deg)) return deg;
return deg * (Math.PI / 180);
};
/**
* Calculates the sum of interior angles of a given number of sides
* @param {Number} sides - number of sides to calculate from.
* @returns {Number}
*/
d3_geometer.utils.calculateAngleSum = function(sides) {
// http://www.regentsprep.org/Regents/math/geometry/GG3/LPoly1.htm
return 180 * (sides - 2);
};
/**
* Calculates the angle of a given side,
* given a number of equal sides.
* @param {Number} sides - number of sides to calculate.
* @param {Boolean} round - whether or not to round the calculation.
* @returns {Number}
*/
d3_geometer.utils.calculateAngles = function(sides, round) {
var deg = d3_geometer.utils.calculateAngleSum(sides) / sides;
return round ? Math.round(deg) : deg;
};
/**
* Generates a visual protractor to use in angle measurement
* and interactive contexts
* @constructor
* @param {Object} group - d3 data selection.
* @param {Number} size - size of protractor.
* @returns {Object} - public rotation method and reference to protractor selection.
*/
d3_geometer.Protractor = function(group, size) {
var SIZE = size || 200;
var end_angle = d3_geometer.utils.toRadian(180);
var drag = d3.behavior.drag().on('drag', _move);
var BAR_THICKNESS = 1;
var HIGHLIGHT_BAR_THICKNESS = 4;
var protractor = group.attr('id', 'protractor').call(drag);
var angle = 0; // protractor angle -- part of interactivity.
var arc_bottom;
var arc_bg;
arc_bg = d3.svg.arc()
.innerRadius(20)
.outerRadius(SIZE)
.startAngle(0)
.endAngle(end_angle);
arc_bottom = d3.svg.arc()
.innerRadius(10)
.outerRadius(20)
.startAngle(0)
.endAngle(end_angle);
function _move() {
var trans = 'translate(' + d3.event.x + ',' + d3.event.y + ')';
d3.select(this).attr('transform', trans);
}
function _updateAngle(e) {
var key = d3.event.keyCode;
// reset angle
if(angle > 360) {
angle = 0;
}
// up key
if(key === 38) {
angle -= 1;
// down key
} else if(key === 40) {
angle += 1;
}
// recalculate
rotate();
}
/**
* Updates the angle of the protractor based on
* the current value of the angle property
*/
function rotate() {
var trans = group.attr('transform');
var ang = ',rotate(' + Math.abs(angle) + ')';
// trans = trans.replace(/,rotate\(+[0-9]+\)/g, '');
// trans += ang;
// protractor.attr('transform', trans);
}
protractor.rotate = rotate;
/**
* Sets angle and rotates
* @param {Number} ang - the angle to set
* @returns none
*/
function updateAngle(ang) {
angle = ang;
rotate();
}
protractor.updateAngle = updateAngle;
// drag bg
protractor
.append('path')
.classed('protractor-bg', true)
.attr('fill', 'white')
.attr('opacity', 0.3)
.attr('stroke', 'black')
.attr('stroke-width', HIGHLIGHT_BAR_THICKNESS / 2)
.attr('d', arc_bg);
// 0, 180 degree line -----
protractor
.selectAll('.protractor-angle-0')
.data(d3.range(1))
.enter()
.append('rect')
.classed('protractor-angle-0', true)
.attr('fill', 'black')
.attr('width', HIGHLIGHT_BAR_THICKNESS)
.attr('height', SIZE * 2)
.attr('y', -SIZE)
.attr('x', -HIGHLIGHT_BAR_THICKNESS / 2);
// 90 degree line -----
protractor
.selectAll('.protractor-angle-90')
.data(d3.range(89, 90))
.enter()
.append('rect')
.classed('protractor-angle-90', true)
.attr('fill', 'red')
.attr('width', SIZE / 1.4)
.attr('height', HIGHLIGHT_BAR_THICKNESS)
.attr('y', -HIGHLIGHT_BAR_THICKNESS / 2)
.attr('x', 0);
// all angles -----
protractor
.selectAll('.protractor-angle')
.data(d3.range(1, 180))
.enter()
.append('rect')
.classed('protractor-angle', true)
.attr('fill', function(d){return d === 90 ? 'red' : 'black';})
.attr('width', BAR_THICKNESS)
.attr('opacity', 0.6)
.attr('height', function(d){
if(d % 10 === 0) {
if(d === 90) {
return SIZE / 6;
}
return SIZE;
} else if(d % 5 === 0) {
return SIZE / 6;
}
return SIZE / 14;
})
.attr('y', -SIZE)
.attr('transform', function(d){return 'rotate(' + d + ')';});
// bottom arc overlay
protractor
.append('path')
.classed('protractor-arc-bottom', true)
.attr('fill', 'black')
.attr('stroke-width', 0)
.attr('stroke', 'none')
.attr('d', arc_bottom);
// text labels - TOP -----
protractor
.selectAll('.protractor-text-top')
.data(d3.range(19)) // 0 + 18 * 10 = 180
.enter()
.append('text')
.classed('protractor-text-top', true)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.attr('x', 0)
.attr('y', -SIZE - 8)
.attr('font-size', function(d){return d === 90 ? 30 : 15;})
.attr('transform', function(d){return 'rotate(' + (d * 10)+ ')';})
.text(function(d){return d * 10;});
// text labels - BOTTOM -----
protractor
.selectAll('.protractor-text-bottom')
.data(d3.range(1, 18)) // 0 + 18 * 10 = 180
.enter()
.append('text')
.classed('protractor-text-bottom', true)
.attr('fill', 'black')
.attr('text-anchor', 'middle')
.attr('x', 0)
.attr('y', -SIZE / 1.3)
.attr('font-size', 11)
.attr('transform', function(d){return 'rotate(' + (d * 10)+ ')';})
.text(function(d){return d !== 90 ? 180 - d * 10 : '';});
d3.select('body').on('keydown', _updateAngle);
// trigger update angle first time, so user set angle is rendered.
rotate();
return protractor;
};
/**
* Generates a coordinate space for overlaying elements
* @constructor
* @param {Object} group - A d3 group selection.
* @param {Object} dims - A dimensions object with (width, height) keys.
* @param {Number} max_coords - Max number of coordinate spaces per axis.
*/
d3_geometer.CoordSpace = function(group, dims, max_coords) {
var PADDING = 10;
var LINE_THICKNESS = 1;
var x_scale = null;
var y_scale = null;
var x_axis = null;
var y_axis = null;
var coords = d3.range(-max_coords, max_coords + 1);
x_scale = d3.scale.linear()
.domain([d3.min(coords), d3.max(coords)])
.range([PADDING, dims.width - PADDING]);
y_scale = d3.scale.linear()
.domain([d3.min(coords), d3.max(coords)])
.range([dims.height - PADDING, PADDING]);
x_axis = d3.svg.axis()
.tickValues(coords)
.scale(x_scale)
.orient('bottom');
y_axis = d3.svg.axis()
.tickValues(coords)
.scale(y_scale)
.orient('left');
// add x-axis
group.append('g')
.attr('transform', 'translate(0,' + dims.height / 2 + ')')
.attr('id', 'xCoords').call(x_axis);
// add y-axis
group.append('g')
.attr('transform', 'translate(' + dims.width / 2 + ', 0)')
.attr('id', 'yCoords').call(y_axis);
// add bg lines
group.append('g').attr('id', 'yCoordsLines')
.selectAll('.ycoord-line')
.data(coords)
.enter()
.append('rect')
.attr('opacity', function(d){
return d === 0 ? 1 : 0.1;
})
.attr('y', dims.width / 2)
.attr('width', function(d){
return d === 0 ? LINE_THICKNESS * 3 : LINE_THICKNESS;
})
.attr('height', dims.height)
.attr('x', x_scale)
.attr('y', 0);
group.append('g').attr('id', 'xCoordsLines')
.selectAll('.xcoord-line')
.data(coords)
.enter()
.append('rect')
.attr('y', dims.height / 2)
.attr('opacity', function(d){
return d === 0 ? 1 : 0.1;
})
.attr('width', dims.width)
.attr('height', function(d){
return d === 0 ? LINE_THICKNESS * 3 : LINE_THICKNESS;
})
.attr('x', 0)
.attr('y', y_scale);
// hide domain paths
group.select('#xCoords')
.select('.domain').style('display', 'none');
group.select('#yCoords')
.select('.domain').style('display', 'none');
};
/**
* Represents a polygon of n sides. Always returns the nGon selection which mimics
* the chainable d3 style interface.
* @constructor
* @param {Object} group - A d3 group selection.
*/
d3_geometer.NGon = function(group) {
if(!d3) return console.error('d3 library not found :sadface:');
var element = null;
var _radius = null;
var _sides = null;
var _coords = null;
var _connections = [];
var GLOBAL_CLASS = 'd3_geometer';
var TAU = Math.PI * 2;
var STROKE = '#b66d48';
var FILL = '#f4eae5';
var STROKE_WIDTH = 3;
var DOT_SIZE = 5;
var RADIUS = 6;
var OFFSET = 50;
var DASHARRAY = 5;
var ARC_I_RADIUS = 10;
var ARC_O_RADIUS = RADIUS * 4;
var ANG_OPACITY = 0.8;
var ANG_STROKE = '#689452';
var ANG_FILL = '#acf287';
var line = d3.svg.line()
.x(function(d){return d.x;})
.y(function(d){return d.y;});
// A single source to set up parent groups
// for each type of element.
function _setupGroups() {
var classes = [
'ngon-labels',
'ngon-dash-edges',
'ngon-angles',
'ngon-vertices',
'ngon-right-angles'
];
for(var i = 0, len = classes.length; i < len; i ++) {
group.append('g')
.attr('class', [GLOBAL_CLASS, classes[i]].join(' '));
}
}
/**
* Inner "parent" function that is always returned
* in each method, which allows for chaining.
* @param {Number} radius - Size of entire shape
* - calculated using the unit circle
* @param {Number} sides - Number of sides.
*/
function _nGon(radius, sides) {
group = group || d3.select('svg').append('g');
// expose values
_radius = radius;
_sides = sides;
_coords = _nGon.getCoords(radius, sides);
// Initialize element for later reference
// This is important!
element = group.selectAll('.ngon')
.data([_coords])
.enter()
.append('path')
.attr('stroke-width', STROKE_WIDTH)
.attr('stroke', STROKE)
.attr('fill', FILL)
.attr('opacity', 1)
.attr('d', function(d){return line(d) + 'Z';});
_setupGroups();
return _nGon;
}
/**
* Move the entire nGon.
* @param {Number} x - the x position to move to
* @param {Number} y - the y position to move to
* @returns {Object} - nGon selection
*/
function move(x, y) {
var trans = 'translate(' + [x, y].join(', ') + ')';
group.attr('transform', trans);
return _nGon;
}
_nGon.move = move;
/**
* Rotate a given element.
* @param {Number} deg - the amount, in degrees, to rotate by
* @param {String} el - the d3 selector (class, tag, etc..) to rotate.
* @returns {Object} - nGon selection
*/
function rotate(deg, el) {
group.select(el)
.attr('transform', 'rotate(' + deg + ')');
return _nGon;
}
_nGon.rotate = rotate;
function _destroy() {
element.remove();
// One reason for keeping all elements
// in a group is that it makes cleanup easier.
group.selectAll('g').remove();
}
_nGon._destroy = _destroy;
/**
* Calculates the sum of all interior angles.
* @returns {Number} result - the result of calculating all interior angles
*/
function sumTotalInteriorAngles() {
var res = 0;
var deg = d3_geometer.utils.calculateAngles(_sides, true);
d3.range(_sides).map(function(d){
return res += deg;
});
return res;
}
_nGon.sumTotalInteriorAngles = sumTotalInteriorAngles;
/**
* Generate the coordinates for
* each point based on number of sides of the nGon.
* @param {Number} radius - Size of entire shape
* - calculated using the unit circle
* @param {Number} sides - Number of sides.
* returns {Array} - list of objects with
* coordinates and label of each vertex
*/
function getCoords(radius, sides) {
return d3.range(sides).map(function(d){
var cx = 0; // center x
var cy = 0; // center y
var start_angle = 0;
var center_angle = TAU / sides;
var curr_angle = start_angle + (d * center_angle);
var vx = Math.round(cx + radius * Math.cos(curr_angle));
var vy = Math.round(cy - radius * Math.sin(curr_angle));
return {
'label': 'L' + d,
'x': vx,
'y': vy
};
});
}
_nGon.getCoords = getCoords;
/**
* Draw a custom label, wherever.
* @param {String} text - the label text.
* @param {Number} x - x position
* @param {Number} y - y position
* @returns {Object} - nGon selection
*/
function label(text, x, y) {
group.select('.ngon-labels')
.append('text')
.classed('custom-label', true)
.text(text)
.attr('x', x || 0)
.attr('y', y || 0);
return _nGon;
}
_nGon.label = label;
/**
* Draws labels for all vertices on the nGon.
* @returns {Object} - nGon selection
*/
function drawLabels() {
// Draw some labels on the vertices.
group.select('.ngon-labels')
.selectAll('.label')
.data(element.datum())
.enter()
.append('text')
.classed('label', true)
.text(function(d){return d.label;})
.attr('x', function(d){return d.x - (RADIUS + RADIUS / 2);})
.attr('y', function(d){return d.y - (RADIUS + RADIUS / 2);});
return _nGon;
}
_nGon.drawLabels = drawLabels;
/**
* Draws vertices for all points on the nGon.
* @returns {Object} - nGon selection
*/
function drawVertices() {
// Draw vertices for each edge.
group.select('.ngon-vertices')
.selectAll('.vertex')
.data(element.datum())
.enter()
.append('circle')
.classed('vertex', true)
.attr('r', 0)
.attr('cx', function(d){return d.x;})
.attr('cy', function(d){return d.y;})
.transition()
.delay(function(d, i){return i * 100;})
.attr('r', RADIUS);
return _nGon;
}
_nGon.drawVertices = drawVertices;
/**
* Draws a right angle given coordinates.
* @param {Number} x - the x position
* @param {Number} y - the y position
* @returns {Object} - nGon selection
*/
function drawRightAngle(x, y) {
group.select('.ngon-right-angles')
.append('rect')
.attr('class', 'right-angle')
.attr('x', x)
.attr('y', y)
.attr('stroke-width', STROKE_WIDTH / 2)
.attr('stroke', ANG_STROKE)
.attr('fill', ANG_FILL)
.attr('opacity', ANG_OPACITY)
.attr('width', RADIUS * 3)
.attr('height', RADIUS * 3);
return _nGon;
}
_nGon.drawRightAngle = drawRightAngle;
/**
* Draws any arbitrary angle given coordinates, degree and rotation.
* @param {Number} deg - the degree of rotation
* @param {Number} x - the x position
* @param {Number} y - the y position
* @param {Number} rotation - the angle
* @returns {Object} - nGon selection
*/
function drawAngle(deg, x, y, rotation) {
if(!rotation) rotation = 0;
var _arc = d3.svg.arc()
.innerRadius(ARC_I_RADIUS)
.outerRadius(ARC_O_RADIUS)
.startAngle(0)
.endAngle(d3_geometer.utils.toRadian(deg));
group.select('.ngon-angles')
.append('g')
.attr('transform', 'translate(' + x + ',' + y +'), rotate(' + rotation + ')')
.append('path')
.attr('class', 'ngon-angle')
.attr('stroke-width', STROKE_WIDTH / 2)
.attr('stroke', ANG_STROKE)
.attr('fill', ANG_FILL)
.attr('opacity', ANG_OPACITY)
.attr('d', _arc);
// add label
group.select('.ngon-angles')
.append('text')
.classed('ngon-angle-text', true)
.attr('x', x + 5)
.attr('y', y + 20)
.attr('fill', 'black')
.attr('font-size', 10)
.text(Math.abs(deg) + '°');
return _nGon;
}
_nGon.drawAngle = drawAngle;
/**
* Uses util function to get all interior angles
* on the outer perimeter and then draws it for each vertex
* @throws Error
* @returns {Object} - nGon selection
*/
function drawPerimeterInteriorAngles() {
var deg = d3_geometer.utils.calculateAngles(_sides, true);
var vertices = d3.select('.ngon-vertices').selectAll('.vertex');
var angle_increment = 360 / _sides;
if(!vertices) throw new Error('Must apply vertices before angles are calculated.');
vertices.each(function(v, k){
// The angle arc may be the same length for all vertices,
// but the rotation for each one is different, and must be
// calculated by getting the standard inverse angle (360/sides)
// as well as the offset, calculated for each vertex, in a series
// based on the vertex order, starting at 90 and going in increments
// of angle_increment.
var rotation_offset = k + (_sides / 2 - 0.5);
var aor = -angle_increment * rotation_offset;
_nGon.drawAngle(deg, v.x, v.y, aor);
});
return _nGon;
}
_nGon.drawPerimeterInteriorAngles = drawPerimeterInteriorAngles;
/**
* Calculates all positions for each
* vertex to connect to - offset by one,
* since adjacent vertices are already connected,
* @param {Number} modulo - A modulo to allow custom numerical offsets.
* @returns {Object} - nGon selection
*/
function calculateEdgeOffsets(modulo) {
var datum = element.datum();
var len = datum.length;
if(len < 3) return;
function _push(inner, sub_index, index) {
inner.push({'x': datum[index].x, 'y': datum[index].y, 'label': 'C' + index});
inner.push({'x': datum[len - sub_index].x, 'y': datum[len - sub_index].y, 'label': 'C' + (len - sub_index)});
}
// store connections state
_connections = d3.range(len).map(function(d, i){
var inner = [];
for(var j = 1; j < len - 1; j++) {
if(modulo) {
if(j % modulo === 0) {
_push(inner, j, i);
}
} else {
_push(inner, j, i);
}
}
return inner;
});
return _nGon;
}
_nGon.calculateEdgeOffsets = calculateEdgeOffsets;
/**
* Simply draws the center vertex
* @param {Boolean} use_labels - Add a label to center point
* @returns {Object} - nGon selection
*/
function drawCenterPoint(use_labels) {
group.select('.ngon-vertices')
.append('circle')
.attr('class', 'center-vertex')
.attr('r', 0)
.attr('cx', 0)
.attr('cy', 0)
.transition()
.delay(function(d, i){return i * 100;})
.attr('r', RADIUS);
if(use_labels) {
// Draw some labels on the vertices.
group.select('.ngon-labels')
.append('text')
.attr('class', 'center-vertex-label')
.text(function(d){return 'O1';})
.attr('x', RADIUS + RADIUS / 2)
.attr('y', RADIUS);
}
return _nGon;
}
_nGon.drawCenterPoint = drawCenterPoint;
/**
* Draws edges directly adjacent + 1 to each vertex.
* see calculateEdgeOffsets for details.
* @param {Array} connections - A list of custom connections
* -must be an array of objects with x and y accessors
* @returns {Object} - nGon selection
*/
function drawNearAdjacentEdges(connections, modulo) {
if(!connections) {
_nGon.calculateEdgeOffsets(modulo);
connections = _connections;
}
group.select('.ngon-dash-edges')
.selectAll('.dashed')
.data(connections)
.enter()
.append('path')
.attr('stroke-width', STROKE_WIDTH / 2)
.attr('stroke', STROKE)
.attr('fill', 'none')
.attr('d', line);
return _nGon;
}
_nGon.drawNearAdjacentEdges = drawNearAdjacentEdges;
return _nGon;
};