Skip to content

Instantly share code, notes, and snippets.

@vishaalagartha
Last active April 24, 2019 19:59
Show Gist options
  • Save vishaalagartha/c772eb4973f68527e7d2ee46b0e12515 to your computer and use it in GitHub Desktop.
Save vishaalagartha/c772eb4973f68527e7d2ee46b0e12515 to your computer and use it in GitHub Desktop.
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
typeof define === 'function' && define.amd ? define(['exports'], factory) :
(global = global || self, factory(global.d3 = {}));
}(this, function (exports) { 'use strict';
function projection({p, settings}){
const {yaw, pitch, roll, scale, origin, shift, rotationFactor, zoomFactor} = settings;
const cosA = Math.cos(yaw);
const sinA = Math.sin(yaw);
const cosB = Math.cos(pitch);
const sinB = Math.sin(pitch);
const cosC = Math.cos(roll);
const sinC = Math.sin(roll);
const zoomedP = {x: (p.x-origin.x)*zoomFactor+origin.x, y: (p.y-origin.y)*zoomFactor+origin.y, z: (p.z-origin.z)*zoomFactor+origin.z};
const shiftedP = {x: scale*(zoomedP.x-origin.x), y: scale*(zoomedP.y-origin.y), z: scale*(zoomedP.z-origin.z)};
const xVec = {x: cosA*cosC-sinC*sinA*sinB, y: cosC*sinA*sinB+sinC*cosA};
const yVec = {x: -cosC*sinA-sinC*cosA*sinB, y: cosC*cosA*sinB-sinC*sinA};
const zVec = {x: -sinC*cosB, y: cosC*cosB};
const x = shiftedP.x*xVec.x+shiftedP.y*yVec.x+shiftedP.z*zVec.x;
const y = shiftedP.x*xVec.y+shiftedP.y*yVec.y+shiftedP.z*zVec.y;
return {x: x + shift.x, y: -y + shift.y}
}
const drawPoints = ({points, context, settings}) => {
const transformedData = points.map(d =>
{ return {...d, data: d.data.map(p => projection({p, settings}))} }
);
let pointsDataJoin = context.selectAll('circle.data').data(transformedData);
pointsDataJoin.enter().append('circle')
.merge(pointsDataJoin)
.transition()
.duration(() => settings.dragging ? 0 : 1000)
.attr('class', d => `data ${d.attributes.class}`)
.attr('cx', d => d.data[0].x)
.attr('cy', d => d.data[0].y)
.attr('r', d => d.attributes.radius ? d.attributes.radius : 3)
.attr('fill', d => d.attributes.fill ? d.attributes.fill : 'black');
pointsDataJoin.exit().remove();
};
const drawLines = ({lines, context, settings}) => {
const transformedData = lines.map(d =>
{ return {...d, data: d.data.map(p => projection({p, settings}))} }
);
let linesDataJoin = context.selectAll('line.data').data(transformedData);
linesDataJoin.enter().append('line')
.merge(linesDataJoin)
.transition()
.duration(() => settings.dragging ? 0 : 200)
.attr('class', d => `data ${d.attributes.class}`)
.attr('x1', d => d.data[0].x)
.attr('y1', d => d.data[0].y)
.attr('x2', d => d.data[1].x)
.attr('y2', d => d.data[1].y)
.attr('stroke-width', d => d.attributes.stroke_width ? d.attributes.stroke_width : 0.3)
.attr('stroke', d => d.attributes.stroke ? d.attributes.stroke : 'black');
linesDataJoin.exit().remove();
};
const drawPolygons = ({polygons, context, settings}) => {
const transformedData = polygons.map(d =>
{ return {...d, data: d.data.map(p => projection({p, settings}))} }
);
let polygonsDataJoin = context.selectAll('polygon.data').data(transformedData);
polygonsDataJoin.enter().append('polygon')
.merge(polygonsDataJoin)
.transition()
.duration(() => settings.dragging ? 0 : 200)
.attr('class', d => `data ${d.attributes.class}`)
.attr('points', d => d.data.map(p => [p.x, p.y].join(',')))
.attr('fill', d => d.attributes.fill ? d.attributes.fill : 'black')
.attr('opacity', d => d.attributes.opacity ? d.attributes.opacity : 0.8);
polygonsDataJoin.exit().remove();
};
const drawCurves = ({curves, context, settings}) => {
let computedCurves = [];
curves.forEach(plot => {
let computedPoints = [];
let t;
for(t=plot.data.tMin; t<=plot.data.tMax; t+=plot.data.tStep) {
computedPoints.push({x: plot.data.x(t), y: plot.data.y(t), z: plot.data.z(t)});
}
computedCurves.push({...plot, data: computedPoints});
});
const transformedData = computedCurves.map(d =>
{ return {...d, data: d.data.map(p => projection({p, settings}))} }
);
let curveGenerator = d3.line()
.curve(d3.curveCardinal);
let curvesDataJoin = context.selectAll('path.data').data(transformedData);
curvesDataJoin.enter().append('path')
.merge(curvesDataJoin)
.transition()
.attr('class', d => `data ${d.attributes.class}`)
.duration(() => settings.dragging ? 0 : 200)
.attr('d', d => curveGenerator(d.data.map(p => [p.x, p.y])))
.attr('fill', 'none')
.attr('stroke-width', d => d.attributes.stroke_width ? d.attributes.stroke_width : 0.3)
.attr('stroke', d => d.attributes.stroke ? d.attributes.stroke : 'black');
curvesDataJoin.exit().remove();
};
const drawSurfaces = ({surfaces, context, settings}) => {
let polygons = [];
const colorSchemes = ['Blues', 'Greens', 'Greys', 'Oranges', 'Purples', 'Reds'];
surfaces.forEach((surface, i) => {
polygons.push({data: []});
let x = -settings.scale+settings.origin.x;
let minZ = Infinity;
let maxZ = -Infinity;
const increment = 1/settings.zoomFactor;
while(x<=settings.scale+settings.origin.x){
let y = -settings.scale+settings.origin.y;
while(y<=settings.scale+settings.origin.y){
const z1 = surface.data.z(x, y);
const z2 = surface.data.z(x+increment, y);
const z3 = surface.data.z(x, y+increment);
const z4 = surface.data.z(x+increment, y+increment);
const p1 = projection({p: {x, y, z: z1}, settings});
if(z1<minZ) minZ = z1;
if(z1>maxZ) maxZ = z1;
const p2 = projection({p: {x: x+increment, y, z: z2}, settings});
if(z2<minZ) minZ = z2;
if(z2>maxZ) maxZ = z2;
const p3 = projection({p: {x, y: y+increment, z: z3}, settings});
if(z3<minZ) minZ = z3;
if(z3>maxZ) maxZ = z3;
const p4 = projection({p: {x: x+increment, y: y+increment, z: z4}, settings});
if(z4<minZ) minZ = z4;
if(z4>maxZ) maxZ = z4;
const avgZ = (z1+z2+z3+z4)/4.0;
polygons[polygons.length-1].data.push({points: [p1, p2, p4, p3], avgZ});
y+=increment;
}
x+=increment;
}
const selectedScheme = surface.attributes.fill ? `${surface.attributes.fill.charAt(0).toUpperCase() + surface.attributes.fill.slice(1)}s` : colorSchemes[i];
const colorScheme = d3.scaleSequential(d3[`interpolate${selectedScheme}`]).domain([maxZ, minZ]);
polygons[polygons.length-1].colorScheme = colorScheme;
polygons[polygons.length-1].opacity = surface.attributes.opacity ? surface.attributes.opacity : null;
polygons[polygons.length-1].class = surface.attributes.class ? surface.attributes.class : null;
});
polygons.forEach((surface, i) => {
let polygonsDataJoin = context.selectAll(`polygon.surface${i}`).data(surface.data);
polygonsDataJoin.enter().append('polygon')
.merge(polygonsDataJoin)
.transition()
.duration(() => settings.dragging ? 0 : 200)
.attr('class', d => `surface${i} ${surface.class}`)
.attr('points', d => d.points.map(p => [p.x, p.y].join(',')))
.attr('fill', d => surface.colorScheme(d.avgZ))
.attr('opacity', d => surface.opacity ? surface.opacity : 0.8);
polygonsDataJoin.exit().remove();
});
};
function d3Plot3d(){
// Default settings
let settings = {
origin: {x: 0, y: 0, z:0},
scale: 100,
yaw: -2,
pitch: 0.66,
roll: 0,
dragging: false,
rotationFactor: 1000,
zoomFactor: 1,
shift: {x: 0, y: 0}
};
let context = null;
let data = [];
let plot3d = (parentContext, rotate = true, zoom = true) => {
const createContext = ({parentContext}) => {
const parentWidth = parentContext.node().getBoundingClientRect().width;
const parentHeight = parentContext.node().getBoundingClientRect().height;
settings.shift = {x: parentWidth/2, y: parentHeight/2};
context = parentContext.append('g')
.attr('class', 'plot3d');
};
createContext({parentContext});
if(rotate) {
let mouse;
parentContext.on('mousedown', () => {
settings.dragging = true;
mouse = {x: d3.event.pageX, y: d3.event.pageY};
})
.on('mouseup', () => {
settings.dragging = false;
})
.on('mousemove', () => {
if(settings.dragging){
settings.yaw += (d3.event.pageX - mouse.x)/settings.rotationFactor;
settings.pitch += (d3.event.pageY - mouse.y)/settings.rotationFactor;
plot3d.draw();
}
});
}
if(zoom) {
const zoomIn = () => {
settings.zoomFactor*=1.1;
plot3d.draw();
};
const zoomOut = () => {
settings.zoomFactor/=1.1;
plot3d.draw();
};
const zoomInButtonGroup = context.append('g')
.style('cursor', 'pointer');
zoomInButtonGroup.append('rect')
.attr('x', 20)
.attr('y', 20)
.attr('width', 20)
.attr('height', 20)
.style('stroke', 'black');
zoomInButtonGroup.append('text')
.text('+')
.attr('dx', 25)
.attr('dy', 35)
.style('fill', 'white')
.style('user-select', 'none');
zoomInButtonGroup.on('click', () => zoomIn());
const zoomOutButtonGroup = context.append('g')
.style('cursor', 'pointer');
zoomOutButtonGroup.append('rect')
.attr('x', 20)
.attr('y', 50)
.attr('width', 20)
.attr('height', 20);
zoomOutButtonGroup.append('text')
.text('-')
.attr('dx', 25)
.attr('dy', 65)
.style('fill', 'white')
.style('font-size', 25)
.style('user-select', 'none');
zoomOutButtonGroup.on('click', () => zoomOut());
}
return plot3d
};
plot3d.axes = ({xRange=1, yRange=1, zRange=1, xAttr={}, yAttr={}, zAttr={}}) => {
const origin = settings.origin;
const xAxis = {type: 'line', data: [origin, {...origin, x: origin.x+xRange}], attributes: {stroke_width: 1, stroke: 'blue', ...xAttr}};
const yAxis = {type: 'line', data: [origin, {...origin, y: origin.y+yRange}], attributes: {stroke_width: 1, stroke: 'red', ...yAttr}};
const zAxis = {type: 'line', data: [origin, {...origin, z: origin.z+zRange}], attributes: {stroke_width: 1, stroke: 'green', ...zAttr}};
data.push(xAxis);
data.push(yAxis);
data.push(zAxis);
return plot3d
};
plot3d.scale = ({scale=100}) => {
settings.scale = scale;
return plot3d
};
plot3d.origin = ({origin={x:0, y:0, z:0}}) => {
settings.origin = origin;
return plot3d
};
plot3d.rotation = ({yaw=-2, pitch=0.66, roll=0}) => {
settings.yaw = yaw;
settings.pitch = pitch;
settings.roll = roll;
return plot3d
};
plot3d.rotationFactor= ({rotationFactor=1000}) => {
settings.rotationFactor = rotationFactor;
return plot3d
};
plot3d.zoomFactor = ({zoomFactor=1}) => {
settings.zoomFactor = zoomFactor;
return plot3d
};
plot3d.plot = (plot) => {
data.push(plot);
return plot3d
};
plot3d.draw = () => {
const points = data.filter(d => d.type==='point');
const lines = data.filter(d => d.type==='line');
const polygons = data.filter(d => d.type==='polygon');
const curves = data.filter(d => d.type==='curve');
const surfaces = data.filter(d => d.type==='surface');
drawPoints({points, context, settings});
drawLines({lines, context, settings});
drawPolygons({polygons, context, settings});
drawCurves({curves, context, settings});
drawSurfaces({surfaces, context, settings});
return plot3d
};
return plot3d
}
exports.plot3d = d3Plot3d;
Object.defineProperty(exports, '__esModule', { value: true });
}));
<!DOCTYPE html>
<meta charset="utf-8">
<body>
<script src="d3-plot3d.js"></script>
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="main.js"></script>
</body>
const margin = {top: 20, bottom: 20, left: 30, right: 30}
const width = 1024
const height = 1024
let svg = d3.select('body').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
let plot = d3.plot3d()
.scale({scale: 10})
.origin({origin: {x: 0, y: 0, z: 0}})
.axes({xRange: 10, yRange: 10, zRange: 10})
.plot({type: 'line', data: [{x: 5, y: 10, z: 10}, {x: 10, y: 15, z: 10}], attributes: {stroke: 'purple'}})
.plot({type: 'polygon', data: [{x: 0, y: 0, z: 0}, {x: 10, y: 10, z: 10}, {x: 0, y: 10, z: 0}], attributes: {fill: 'orange'}})
.plot({type: 'curve', data: {x: (t) => 3*Math.cos(t), y: (t) => 3*Math.sin(t), z: (t) => t, tMin: -10, tMax: 10, tStep: 0.1}, attributes: {stroke: 'brown'}})
.plot({type: 'curve', data: {x: (t) => -3*Math.cos(t), y: (t) => 3*Math.sin(t), z: (t) => t, tMin: -10, tMax: 10, tStep: 0.1}, attributes: {stroke: 'maroon'}})
.plot({type: 'point', data: [{x: 10, y:0 , z:0}], attributes: {stroke: 'maroon'}})
.plot({type: 'surface', data: {z: (x, y) => Math.sin((x*x+y*y))}, attributes: {fill: 'purple'}})
svg.call(plot)
plot.draw()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment