'How can I integrate a D3 & Three.js object as a Vue 3 component?
I am trying to create a Vue 3 component based off of a D3 & Three.js object that I found online written by Mike Bostock.
This is the object that I would like to use: https://bl.ocks.org/mbostock/2b85250396c17a79155302f91ec21224
It's given as HTML code that works right away when copied into an html file.
I've downloaded d3, three, and other dependencies with npm and used loadScript to load the other scripts. I'm using Vite for tooling. Nothing appears when I load the component on a page. There are also no errors in the console.
Here is the Vue code that I have so far. Is this a solid approach? Is there a better way of integrating this object? I'm still very new to Vue and any help is appreciated.
Thank you.
<template>
<div id="globe" style="height:500px; width:700px"></div>
</template>
<script>
import * as THREE from 'three';
import * as d3 from 'd3'
import { loadScript } from "vue-plugin-load-script";
import { onMounted } from 'vue';
loadScript("https://unpkg.com/[email protected]");
loadScript("https://unpkg.com/topojson-client@3");
loadScript("https://unpkg.com/d3-array@1");
loadScript("https://unpkg.com/d3-collection@1");
loadScript("https://unpkg.com/d3-dispatch@1");
loadScript("https://unpkg.com/d3-request@1");
loadScript("https://unpkg.com/d3-timer@1");
var globe = document.getElementById('globe');
onMounted(() => {
console.log('mounted!')
var radius = 228,
mesh,
graticule,
scene = new THREE.Scene,
camera = new THREE.PerspectiveCamera(70, globe.clientWidth/globe.clientHeight, 1, 1000),
renderer = new THREE.WebGLRenderer({alpha: true});
camera.position.z = 400;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(globe.clientWidth, globe.clientHeight);
globe.appendChild(renderer.domElement);
d3.json("https://unpkg.com/world-atlas@1/world/50m.json", function(error, topology) {
if (error) throw error;
scene.add(graticule = wireframe(graticule10(), new THREE.LineBasicMaterial({color: 0xaaaaaa})));
scene.add(mesh = wireframe(topojson.mesh(topology, topology.objects.land), new THREE.LineBasicMaterial({color: 0xff0000})));
d3.timer(function(t) {
graticule.rotation.x = mesh.rotation.x = Math.sin(t / 11000) * Math.PI / 3 - Math.PI / 2;
graticule.rotation.z = mesh.rotation.z = t / 10000;
renderer.render(scene, camera);
});
});
// Converts a point [longitude, latitude] in degrees to a THREE.Vector3.
function vertex(point) {
var lambda = point[0] * Math.PI / 180,
phi = point[1] * Math.PI / 180,
cosPhi = Math.cos(phi);
return new THREE.Vector3(
radius * cosPhi * Math.cos(lambda),
radius * cosPhi * Math.sin(lambda),
radius * Math.sin(phi)
);
}
// Converts a GeoJSON MultiLineString in spherical coordinates to a THREE.LineSegments.
function wireframe(multilinestring, material) {
var geometry = new THREE.Geometry;
multilinestring.coordinates.forEach(function(line) {
d3.pairs(line.map(vertex), function(a, b) {
geometry.vertices.push(a, b);
});
});
return new THREE.LineSegments(geometry, material);
}
// See https://github.com/d3/d3-geo/issues/95
function graticule10() {
var epsilon = 1e-6,
x1 = 180, x0 = -x1, y1 = 80, y0 = -y1, dx = 10, dy = 10,
X1 = 180, X0 = -X1, Y1 = 90, Y0 = -Y1, DX = 90, DY = 360,
x = graticuleX(y0, y1, 2.5), y = graticuleY(x0, x1, 2.5),
X = graticuleX(Y0, Y1, 2.5), Y = graticuleY(X0, X1, 2.5);
function graticuleX(y0, y1, dy) {
var y = d3.range(y0, y1 - epsilon, dy).concat(y1);
return function(x) { return y.map(function(y) { return [x, y]; }); };
}
function graticuleY(x0, x1, dx) {
var x = d3.range(x0, x1 - epsilon, dx).concat(x1);
return function(y) { return x.map(function(x) { return [x, y]; }); };
}
return {
type: "MultiLineString",
coordinates: d3.range(Math.ceil(X0 / DX) * DX, X1, DX).map(X)
.concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y))
.concat(d3.range(Math.ceil(x0 / dx) * dx, x1, dx).filter(function(x) { return Math.abs(x % DX) > epsilon; }).map(x))
.concat(d3.range(Math.ceil(y0 / dy) * dy, y1 + epsilon, dy).filter(function(y) { return Math.abs(y % DY) > epsilon; }).map(y))
};
}
})
</script>
Solution 1:[1]
I don't think you need to use loadscript to load d3 & THREEjs dependencies because the npm packages you are importing already includes those.
you also need to install topojson-client and import it as well.
npm i topojson-client
Below is the code working for me. screenshot
Some notes of what I changed:
When using composition api the
<script>tag needs a setup attribute.<script setup>THREE.Geometry used in the wireframe function is deprecated in newer THREEjs version, you may need to use THREE.BufferGeometry instead.
d3.json() didn't work for me and I'm not that familiar with d3 so I just use fetch to get the json.
I use Template Refs to get the div element but your original way should work too.
<script setup>
import * as THREE from 'three'
import * as d3 from 'd3'
import * as topojson from 'topojson-client'
import { onMounted, ref } from 'vue'
const globe = ref(null)
onMounted(() => {
var radius = 228,
mesh,
graticule,
scene = new THREE.Scene(),
camera = new THREE.PerspectiveCamera(70, globe.value.clientWidth/globe.value.clientHeight, 1, 1000),
renderer = new THREE.WebGLRenderer({ alpha: true })
camera.position.z = 400
renderer.setPixelRatio(window.devicePixelRatio)
renderer.setSize(globe.value.clientWidth, globe.value.clientHeight)
globe.value.appendChild(renderer.domElement)
fetch('https://unpkg.com/world-atlas@1/world/50m.json')
.then(res => res.json())
.then(topology => {
scene.add(
(graticule = wireframe(
graticule10(),
new THREE.LineBasicMaterial({ color: 0xaaaaaa })
))
)
scene.add(
(mesh = wireframe(
topojson.mesh(topology, topology.objects.land),
new THREE.LineBasicMaterial({ color: 0xff0000 })
))
)
d3.timer(function (t) {
graticule.rotation.x = mesh.rotation.x =
(Math.sin(t / 11000) * Math.PI) / 3 - Math.PI / 2
graticule.rotation.z = mesh.rotation.z = t / 10000
renderer.render(scene, camera)
})
})
// Converts a point [longitude, latitude] in degrees to a THREE.Vector3.
function vertex (point) {
var lambda = (point[0] * Math.PI) / 180,
phi = (point[1] * Math.PI) / 180,
cosPhi = Math.cos(phi)
return new THREE.Vector3(
radius * cosPhi * Math.cos(lambda),
radius * cosPhi * Math.sin(lambda),
radius * Math.sin(phi)
)
}
// Converts a GeoJSON MultiLineString in spherical coordinates to a THREE.LineSegments.
function wireframe (multilinestring, material) {
var points = []
multilinestring.coordinates.forEach(function (line) {
d3.pairs(line.map(vertex), function (a, b) {
points.push(a, b)
})
})
var geometry = new THREE.BufferGeometry().setFromPoints(points)
return new THREE.LineSegments(geometry, material)
}
// See https://github.com/d3/d3-geo/issues/95
function graticule10 () {
var epsilon = 1e-6,
x1 = 180,
x0 = -x1,
y1 = 80,
y0 = -y1,
dx = 10,
dy = 10,
X1 = 180,
X0 = -X1,
Y1 = 90,
Y0 = -Y1,
DX = 90,
DY = 360,
x = graticuleX(y0, y1, 2.5),
y = graticuleY(x0, x1, 2.5),
X = graticuleX(Y0, Y1, 2.5),
Y = graticuleY(X0, X1, 2.5)
function graticuleX (y0, y1, dy) {
var y = d3.range(y0, y1 - epsilon, dy).concat(y1)
return function (x) {
return y.map(function (y) {
return [x, y]
})
}
}
function graticuleY (x0, x1, dx) {
var x = d3.range(x0, x1 - epsilon, dx).concat(x1)
return function (y) {
return x.map(function (x) {
return [x, y]
})
}
}
return {
type: 'MultiLineString',
coordinates: d3
.range(Math.ceil(X0 / DX) * DX, X1, DX)
.map(X)
.concat(d3.range(Math.ceil(Y0 / DY) * DY, Y1, DY).map(Y))
.concat(
d3
.range(Math.ceil(x0 / dx) * dx, x1, dx)
.filter(function (x) {
return Math.abs(x % DX) > epsilon
})
.map(x)
)
.concat(
d3
.range(Math.ceil(y0 / dy) * dy, y1 + epsilon, dy)
.filter(function (y) {
return Math.abs(y % DY) > epsilon
})
.map(y)
)
}
}
})
</script>
<template>
<div ref="globe" style="height:500px; width:700px"></div>
</template>
my package.json
"dependencies": {
"d3": "^7.4.4",
"three": "^0.139.2",
"topojson-client": "^3.1.0",
"vue": "^3.2.25"
},
"devDependencies": {
"@vitejs/plugin-vue": "^2.3.1",
"vite": "^2.9.2"
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 |
