Skip to content

Instantly share code, notes, and snippets.

@gerbal
Last active August 29, 2015 13:57
Show Gist options
  • Save gerbal/9631405 to your computer and use it in GitHub Desktop.
Save gerbal/9631405 to your computer and use it in GitHub Desktop.
// Create a 3d scatter plot within d3 selection parent.
function scatterPlot3d(parent) {
var x3d = parent
.append("x3d")
.style("width", parseInt(parent.style("width")) + "px")
.style("height", parseInt(parent.style("height")) + "px")
.style("border", "none")
var scene = x3d.append("scene")
scene.append("orthoviewpoint")
.attr("centerOfRotation", [2000, 2000, 2000])
.attr("fieldOfView", [-2500, -2500, 7200, 7200])
.attr("orientation", [-0.5, 1, 0.2, 1.12 * Math.PI / 4])
.attr("position", [4000, 2000, 7500])
scene.append("navigationInfo")
.attr("headlight", 'true')
.attr("type", '"EXAMINE"');
scene.append("directionalLight")
.attr("on", "true")
.attr("ambientIntensity", '1');
//var rows = initializeDataGrid();
var axisRange = [0, 4500];
var scales = [];
var initialDuration = 0;
var defaultDuration = 0;
var ease = 'linear';
var time = 0;
var axisKeys = ["x", "y", "z"]
// Helper functions for initializeAxis() and drawAxis()
function axisName(name, axisIndex) {
return ["Democratic", "Republican", "Independent"][axisIndex] + name;
}
function constVecWithAxisValue(otherValue, axisValue, axisIndex) {
var result = [otherValue, otherValue, otherValue];
result[axisIndex] = axisValue;
return result;
}
// Used to make 2d elements visible
function makeSolid(selection, color) {
selection.append("appearance")
.append("material")
.attr("diffuseColor", color || "black")
return selection;
}
// Initialize the axes lines and labels.
function initializePlot() {
initializeAxis(0);
initializeAxis(1);
initializeAxis(2);
}
function initializeAxis(axisIndex) {
var key = axisKeys[axisIndex];
drawAxis(axisIndex, key, initialDuration);
var scaleMin = axisRange[0];
var scaleMax = axisRange[1];
// the axis line
var newAxisLine = scene.append("transform")
.attr("class", axisName("Axis", axisIndex))
.attr("rotation", ([
[0, 0, 0, 0],
[0, 0, 1, Math.PI / 2],
[0, 1, 0, -Math.PI / 2]
][axisIndex]))
.append("shape")
newAxisLine
.append("appearance")
.append("material")
.attr("emissiveColor", "black")
newAxisLine
.append("polyline2d")
// Line drawn along y axis does not render in Firefox, so draw one
// along the x axis instead and rotate it (above).
.attr("lineSegments", "0 0, 200 0")
// axis labels
var newAxisLabel = scene.append("transform")
.attr("class", axisName("AxisLabel", axisIndex))
.attr("translation", constVecWithAxisValue(0, scaleMin + 1.1 * (scaleMax - scaleMin), axisIndex))
.attr("scale", "200 200 200")
var newAxisLabelShape = newAxisLabel
.append("billboard")
.attr("axisOfRotation", "0 0 0") // face viewer
.append("shape")
.call(makeSolid)
var labelFontSize = 2.3;
newAxisLabelShape
.append("text")
.attr("string", axisName("", axisIndex))
.attr("class", axisName("AxisLabelText", axisIndex))
.attr("solid", "true")
.append("fontstyle")
.attr("size", labelFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE")
}
// Assign key to axis, creating or updating its ticks, grid lines, and labels.
function drawAxis(axisIndex, key, duration) {
var scale = d3.scale.linear()
.domain([0, 4500]) // demo data range
.range(axisRange)
scales[axisIndex] = scale;
var numTicks = 8;
var tickSize = 50;
var tickFontSize = 0.8;
// ticks along each axis
var ticks = scene.selectAll("." + axisName("Tick", axisIndex))
.data(scale.ticks(numTicks));
var newTicks = ticks.enter()
.append("transform")
.attr("class", axisName("Tick", axisIndex))
newTicks.append("shape")
.call(makeSolid)
.append("box")
.attr("size", tickSize + " " + tickSize + " " + tickSize);
// enter + update
ticks.transition()
.duration(duration)
.attr("translation", function (tick) {
return constVecWithAxisValue(0, scale(tick), axisIndex);
})
ticks.exit()
.remove();
// tick labels
var tickLabels = ticks.selectAll("billboard shape text")
.data(function (d) {
return [d];
});
var newTickLabels = tickLabels.enter()
.append("transform")
.attr("scale", "200 200 200")
.append("billboard")
.attr("axisOfRotation", "0 0 0")
.append("shape")
.call(makeSolid)
newTickLabels.append("text")
.attr("string", scale.tickFormat(10))
.attr("solid", "true")
.append("fontstyle")
.attr("size", tickFontSize)
.attr("family", "SANS")
.attr("justify", "END MIDDLE");
tickLabels // enter + update
.attr("string", scale.tickFormat(10))
tickLabels.exit()
.remove();
// base grid lines
var gridLines = scene.selectAll("." + axisName("GridLine", axisIndex))
.data(scale.ticks(numTicks));
gridLines.exit()
.remove();
var newGridLines = gridLines.enter()
.append("transform")
.attr("class", axisName("GridLine", axisIndex))
.attr("rotation", [0, 0, 0, 0])
.append("shape");
newGridLines.append("appearance")
.append("material")
.attr("emissiveColor", "gray");
newGridLines.append("polyline2d");
gridLines.selectAll("shape polyline2d")
.transition()
.duration(duration)
.attr("lineSegments", "0 0, " + axisRange[1] + " 0");
gridLines.transition()
.duration(duration)
.attr("translation", function (d) {
return "0 0 0 0";
});
}
// Update the data points (spheres) and stems.
function plotData(duration) {
if (!rows) {
console.log("no rows to plot.")
return;
}
var x = scales[0],
y = scales[1],
z = scales[2];
var sphereRadius = 50;
// Draw a sphere at each x,y,z coordinate.
var datapoints = scene.selectAll(".datapoint")
.data(rows);
datapoints.exit()
.remove();
var newDatapoints = datapoints.enter()
.append("transform")
.attr("class", "datapoint")
.attr("scale", [sphereRadius, sphereRadius, sphereRadius])
.append("shape");
newDatapoints
.append("appearance")
.append("material");
newDatapoints
.append("sphere");
// Does not work on Chrome; use transform instead
//.attr("radius", sphereRadius)
datapoints.selectAll("shape appearance material")
.attr("diffuseColor", 'steelblue');
var tmpDatapoints = scene.selectAll(".datapoint");
tmpDatapoints.selectAll("material")
.each(function (d, i) {
if (d.x > d.y && d.x > d.z) {
d3.select(this)
.attr("diffuseColor", '#0000ff')
} else if (d.y > d.x && d.y > d.z) {
d3.select(this)
.attr("diffuseColor", '#ff0000')
} else if (d.z > d.x && d.z > d.y) {
d3.select(this)
.attr("diffuseColor", '#00ff00')
} else {
d3.select(this)
.attr("diffuseColor", '#888888')
}
});
var datapointsXY = scene.selectAll(".datapointXY")
.data(rows);
datapointsXY.exit()
.remove();
var newDatapointsXY = datapointsXY.enter()
.append("transform")
.attr("class", "datapointXY")
.attr("scale", [sphereRadius, sphereRadius, 0])
.append("shape");
newDatapointsXY
.append("appearance")
.append("material");
newDatapointsXY
.append("sphere");
var datapointsYZ = scene.selectAll(".datapointYZ")
.data(rows);
datapointsYZ.exit()
.remove();
var newDatapointsYZ = datapointsYZ.enter()
.append("transform")
.attr("class", "datapointYZ")
.attr("scale", [0, sphereRadius, sphereRadius])
.append("shape");
newDatapointsYZ
.append("appearance")
.append("material");
newDatapointsYZ
.append("sphere");
var datapointsXZ = scene.selectAll(".datapointXZ")
.data(rows);
datapointsXZ.exit()
.remove();
var newDatapointsXZ = datapointsXZ.enter()
.append("transform")
.attr("class", "datapointXZ")
.attr("scale", [sphereRadius, 0, sphereRadius])
.append("shape");
newDatapointsXZ
.append("appearance")
.append("material");
newDatapointsXZ
.append("sphere");
var tmpDatapoints = scene.selectAll(".datapointXY");
tmpDatapoints.selectAll("material")
.each(function (d, i) {
var tmpTotal = d.x + d.y;
var xRed = Math.round(((d.y) / tmpTotal) * 255);
var yBlue = Math.round(((d.x) / tmpTotal) * 255);
d3.select(this)
.attr("shininess", '1')
.attr("ambientIntensity", '1')
.attr("diffuseColor", rgbToHex(xRed, 0, yBlue))
});
var tmpDatapoints = scene.selectAll(".datapointXZ");
tmpDatapoints.selectAll("material")
.each(function (d, i) {
var tmpTotal = d.x + d.z;
var xRed = Math.round(((d.z) / tmpTotal) * 255);
var yBlue = Math.round(((d.x) / tmpTotal) * 255);
d3.select(this)
.attr("shininess", '1')
.attr("ambientIntensity", '1')
.attr("diffuseColor", rgbToHex(0, xRed, yBlue))
});
var tmpDatapoints = scene.selectAll(".datapointYZ");
tmpDatapoints.selectAll("material")
.each(function (d, i) {
var tmpTotal = d.z + d.y;
var xRed = Math.round(((d.y) / tmpTotal) * 255);
var yBlue = Math.round(((d.z) / tmpTotal) * 255);
d3.select(this)
.attr("shininess", "1")
.attr("ambientIntensity", "1")
.attr("diffuseColor", rgbToHex(xRed, yBlue, 0))
.attr("specularColor", rgbToHex(xRed, yBlue, 0))
});
//hacking in the 2d planes
datapointsXY.transition()
.ease(ease)
.duration(duration)
.attr("translation", function (row) {
return x(row[axisKeys[0]]) + " " + y(row[axisKeys[1]]) + " 0"
})
datapointsYZ.transition()
.ease(ease)
.duration(duration)
.attr("translation", function (row) {
return "0 " + y(row[axisKeys[1]]) + " " + z(row[axisKeys[2]])
})
datapointsXZ.transition()
.ease(ease)
.duration(duration)
.attr("translation", function (row) {
return x(row[axisKeys[0]]) + " 0 " + z(row[axisKeys[2]])
})
datapoints.transition()
.ease(ease)
.duration(duration)
.attr("translation", function (row) {
return x(row[axisKeys[0]]) + " " + y(row[axisKeys[1]]) + " " + z(row[axisKeys[2]])
})
// Draw a stem from the x-z plane to each sphere at elevation y.
// This convention was chosen to be consistent with x3d primitive ElevationGrid.
// var stems = scene.selectAll(".stem").data( rows );
// stems.exit().remove();
// var newStems = stems.enter()
// .append("transform")
// .attr("class", "stem")
// .append("shape");
// newStems
// .append("appearance")
// .append("material")
// .attr("emissiveColor", "gray")
// newStems
// .append("polyline2d")
// .attr("lineSegments", function(row) { return "0 1, 0 0"; })
// stems.transition().ease(ease).duration(duration)
// .attr("translation",
// function(row) { return x(row[axisKeys[0]]) + " 0 " + z(row[axisKeys[2]]); })
// .attr("scale",
// function(row) { return [1, y(row[axisKeys[1]])]; })
}
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
initializePlot();
plotData(defaultDuration);
}
var rows = [{x:831,y:667,z:340},
{x:868,y:683,z:474},
{x:723,y:444,z:704},
{x:436,y:340,z:401},
{x:427,y:221,z:384},
{x:1021,y:791,z:525},
{x:708,y:466,z:289},
{x:215,y:55,z:26},
{x:491,y:328,z:239},
{x:614,y:371,z:429},
{x:678,y:421,z:1014},
{x:677,y:455,z:411},
{x:779,y:468,z:199},
{x:1763,y:1073,z:438},
{x:711,y:390,z:491},
{x:1200,y:571,z:571},
{x:384,y:273,z:386},
{x:1237,y:794,z:736},
{x:1636,y:316,z:49},
{x:1604,y:474,z:136},
{x:1818,y:1050,z:700},
{x:2783,y:592,z:92},
{x:1780,y:3057,z:1264},
{x:297,y:92,z:23},
{x:1914,y:675,z:99},
{x:1021,y:622,z:188},
{x:2631,y:937,z:360},
{x:901,y:485,z:838},
{x:485,y:295,z:477},
{x:1555,y:1394,z:478},
{x:1079,y:821,z:467},
{x:699,y:389,z:388},
{x:1612,y:817,z:60},
{x:2053,y:347,z:63},
{x:577,y:403,z:710},
{x:769,y:511,z:545},
{x:1222,y:628,z:445},
{x:1194,y:782,z:559},
{x:3471,y:709,z:169},
{x:794,y:768,z:375},
{x:877,y:761,z:1146},
{x:1067,y:637,z:459},
{x:1700,y:929,z:784},
{x:661,y:493,z:516},
{x:2000,y:787,z:257},
{x:762,y:566,z:876},
{x:994,y:707,z:370},
{x:1339,y:1073,z:546},
{x:2060,y:369,z:93},
{x:464,y:318,z:356},
{x:886,y:950,z:1247},
{x:1294,y:1567,z:2101},
{x:692,y:900,z:1333},
{x:577,y:693,z:1102},
{x:274,y:353,z:418},
{x:660,y:729,z:1224},
{x:784,y:798,z:949},
{x:896,y:636,z:563},
{x:977,y:859,z:661},
{x:812,y:677,z:485},
{x:690,y:566,z:413},
{x:1107,y:1094,z:900},
{x:972,y:976,z:917},
{x:814,y:822,z:1207},
{x:754,y:828,z:508},
{x:617,y:1030,z:968},
{x:812,y:1020,z:899},
{x:624,y:615,z:366},
{x:967,y:951,z:893},
{x:1132,y:1657,z:1262},
{x:604,y:575,z:524},
{x:752,y:921,z:690},
{x:713,y:770,z:568},
{x:686,y:747,z:527},
{x:786,y:1071,z:710},
{x:539,y:736,z:608},
{x:1123,y:1057,z:723},
{x:690,y:646,z:316},
{x:1240,y:1463,z:1166},
{x:644,y:1054,z:1021},
{x:2137,y:2128,z:1644},
{x:1209,y:1933,z:1564},
{x:1934,y:2795,z:1336},
{x:779,y:897,z:536},
{x:979,y:841,z:692},
{x:744,y:940,z:1409},
{x:1333,y:1539,z:1487},
{x:1012,y:1151,z:1058},
{x:869,y:679,z:683},
{x:925,y:1318,z:1306},
{x:753,y:741,z:835},
{x:704,y:501,z:457},
{x:570,y:404,z:474},
{x:564,y:474,z:544},
{x:1717,y:1371,z:954},
{x:686,y:514,z:553},
{x:1068,y:992,z:1230},
{x:1157,y:1120,z:1101},
{x:283,y:92,z:170},
{x:1110,y:952,z:812},
{x:1694,y:1506,z:1249},
{x:425,y:307,z:342},
{x:1126,y:716,z:690},
{x:744,y:595,z:562},
{x:1215,y:1121,z:1203},
{x:1401,y:1459,z:1333},
{x:496,y:563,z:672},
{x:671,y:660,z:770},
{x:979,y:763,z:448},
{x:684,y:860,z:1047},
{x:1172,y:1447,z:1515},
{x:757,y:681,z:504},
{x:829,y:792,z:775},
{x:1160,y:1302,z:1035},
{x:1042,y:521,z:638},
{x:1944,y:761,z:704},
{x:919,y:483,z:844},
{x:443,y:290,z:532},
{x:1668,y:916,z:1005},
{x:1282,y:614,z:719},
{x:2958,y:1437,z:1540},
{x:1534,y:1359,z:1002},
{x:1422,y:1082,z:1173},
{x:1355,y:1348,z:2011},
{x:1354,y:1157,z:1327},
{x:1009,y:1100,z:1548},
{x:1720,y:2135,z:2609},
{x:965,y:829,z:1158},
{x:803,y:699,z:948},
{x:1667,y:1119,z:1253},
{x:1025,y:1029,z:1295},
{x:2556,y:1163,z:546},
{x:608,y:610,z:723},
{x:1006,y:544,z:378},
{x:996,y:888,z:971},
{x:979,y:529,z:426},
{x:1499,y:611,z:334},
{x:2033,y:1106,z:662},
{x:1629,y:1673,z:1942},
{x:830,y:935,z:1222},
{x:217,y:175,z:271},
{x:1132,y:1416,z:2260},
{x:1287,y:1076,z:1651},
{x:1732,y:1386,z:1884},
{x:1311,y:1163,z:1851},
{x:719,y:512,z:866},
{x:900,y:653,z:794},
{x:3218,y:1056,z:562},
{x:805,y:437,z:344},
{x:1092,y:635,z:766},
{x:1196,y:665,z:765},
{x:813,y:310,z:311},
{x:1511,y:874,z:949},
{x:4532,y:1424,z:664},
{x:1491,y:811,z:771},
{x:1575,y:654,z:470},
{x:447,y:260,z:272},
{x:2902,y:1718,z:1396},
{x:2378,y:1296,z:1282},
{x:2086,y:1011,z:716},
{x:770,y:517,z:616},
{x:2382,y:840,z:379},
{x:3330,y:1848,z:1626},
{x:2270,y:603,z:176},
{x:714,y:293,z:273},
{x:2050,y:1099,z:615},
{x:1697,y:1556,z:891},
{x:1146,y:1446,z:1882},
{x:635,y:734,z:778},
{x:1345,y:1113,z:1366},
{x:1263,y:1638,z:1882},
{x:1399,y:1317,z:814},
{x:946,y:910,z:1309},
{x:1170,y:1418,z:1142},
{x:855,y:900,z:1098},
{x:1993,y:1827,z:2169},
{x:745,y:897,z:1427},
{x:953,y:507,z:681},
{x:1298,y:1009,z:1456},
{x:1807,y:2042,z:2582},
{x:763,y:830,z:956},
{x:1351,y:1448,z:1659},
{x:699,y:698,z:642},
{x:642,y:765,z:991},
{x:692,y:733,z:945},
{x:1135,y:690,z:884},
{x:1910,y:1239,z:979},
{x:1164,y:1012,z:866},
{x:820,y:1043,z:1131},
{x:872,y:1417,z:1058},
{x:916,y:1260,z:1381},
{x:1280,y:1214,z:1078},
{x:850,y:1249,z:1226},
{x:1276,y:1547,z:1520},
{x:701,y:958,z:892},
{x:816,y:1375,z:1095},
{x:836,y:1224,z:1217},
{x:674,y:901,z:928},
{x:2238,y:3037,z:2145},
{x:1230,y:1966,z:1348}]
<!DOCTYPE html >
<script type="text/javascript" src="data.js"></script>
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js"></script>
<script type="text/javascript" src="http://x3dom.org/x3dom/dist/x3dom-full.js"></script>
<script type="text/javascript" src="3dscatter.js"></script>
<link rel="stylesheet" type="text/css" href="http://www.x3dom.org/download/dev/x3dom.css"/>
<div id="divPlot"></div>
<script>
d3.select('html').style('height','100%').style('width','100%')
d3.select('body').style('height','100%').style('width','100%')
d3.select('#divPlot').style('width', "100%").style('height', "100%")
scatterPlot3d( d3.select('#divPlot'));
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment