Last active
April 24, 2019 19:59
-
-
Save vishaalagartha/c772eb4973f68527e7d2ee46b0e12515 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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 }); | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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