var size = 600; d3.csv("flowers.csv", function (row) { for (var key in row) { var floatValue = parseFloat(row[key]); if (!isNaN(floatValue)) row[key] = floatValue; } return row; }, function (error, rows) { if (error !== null) throw error; var svg = d3.select('body').append('svg') .attr('width',size) .attr('height',size); var pentRad = size/(4*Math.sin(3*Math.PI/10) + 2), baseToCenter = pentRad*Math.sin(3*Math.PI/10); var pentPoints = [0,1,2,3,4].map(function (i) { var a = ((2*Math.PI)*(i/5) + (Math.PI/2)); return [Math.cos(a)*pentRad, -Math.sin(a)*pentRad].join(",") }).join(" ") var frames = svg.append('g') .attr('transform', translate(size/2, size/2)) .selectAll('.frame') .data([-1,0,1,2,3,4]) .enter().append('g') .attr('class', 'frame') .attr('transform', function (index) { if (index === -1) return ''; // the center pentagon isn't transformed var moveAngle = (1.5 + 2*index/5)*Math.PI; // this starts pointing down and rotates 1/5 of a circle each return [rotate(moveAngle), translate(2*baseToCenter,0), rotate(-Math.PI/2)].join(' ') }); frames.append('polygon') .attr('points', pentPoints) .style('stroke', 'black') .style('stroke-width', '1px') .style('fill', 'none') var scaledData = (function () { var data = rows.map(function (d) { return { x: d['petal length'], y: d['petal width'], z: d['sepal width'] } }); var range = [-pentRad/2, pentRad/2]; var x = d3.scaleLinear().range(range).domain(d3.extent(data, function (d) { return d.x; })); var y = d3.scaleLinear().range(range).domain(d3.extent(data, function (d) { return d.y; })); var z = d3.scaleLinear().range(range).domain(d3.extent(data, function (d) { return d.z })); return data.map(function (d,i) { return {x: x(d.x), y: y(d.y), z: z(d.z), species: rows[i].species} }) })() var color = d3.scaleOrdinal(d3.schemeCategory10); var baseRotation = rotate3DPoint(0,1,0,0); function plotCircles(g) { var circles = d3.select(this).selectAll('circle') .data(function (index) { var p1 = rotate3DPoint(2*Math.PI*(index/5),0,0,-1); var p2 = rotate3DPoint(Math.PI/2,1,0,0); var _data = scaledData.map(function (d) { d = baseRotation(d); if (index !== -1) { d = p2(p1(d)); } return d }); scaledData.forEach(function (d, i) { _data[i].species = d.species; }); return _data }) circles .enter().append('circle') .merge(circles) .attr('cx', function (d) {return d.x}) .attr('cy', function (d) {return d.y}) .attr('r', 4) .style('fill', function (d) {return color(d.species)}) .sort(function (a, b) { return a.z > b.z ? -1 : 1; }) } var circleGroups = frames.append('g') .attr('class', 'circles') .attr('transform', function (index) { return index === -1 ? '' : 'scale(-1,1)'; }).each(plotCircles); svg.on('mousemove', function (e) { var mouse = d3.mouse(this); var pt = {x: mouse[0]-size/2, y: mouse[1]-size/2} var dist = Math.sqrt(Math.pow(pt.x, 2) + Math.pow(pt.y, 2)); var angle = Math.PI * dist/(size/2); var perpUnitVec = { x: -pt.y / dist, y: pt.x / dist, } baseRotation = rotate3DPoint(angle, perpUnitVec.x, perpUnitVec.y, 0) circleGroups.each(plotCircles); }) }) function rotate(a) { return 'rotate('+(-a*180/Math.PI)+', 0, 0)'; } function translate(x, y) { return 'translate('+x+','+y+')'; } // returns a function that rotates points by theta around the axis defined by unit vector (a,b,c) // https://en.wikipedia.org/wiki/Transformation_matrix#Rotation_2 function rotate3DPoint(theta, a, b, c) { var cosTheta = Math.cos(theta), sinTheta = Math.sin(theta); return function (d) { return { x: d.x * (a*a*(1-cosTheta) + cosTheta) + d.y * (a*b*(1-cosTheta) + c*sinTheta) + d.z * (a*c*(1-cosTheta) - b*sinTheta), y: d.x * (b*a*(1-cosTheta) - c*sinTheta) + d.y * (b*b*(1-cosTheta) + cosTheta) + d.z * (b*c*(1-cosTheta) + a*sinTheta), z: d.x * (c*a*(1-cosTheta) + b*sinTheta) + d.y * (c*b*(1-cosTheta) - a*sinTheta) + d.z * (c*c*(1-cosTheta) + cosTheta), } } }