snMapbox = {
let maxBarΝumberFromData = 0;
let maxNumberOfBars = 0;
let map;
let camera;
let scene;
let renderer;
const barWidth = 100;
const barOpacity = 1;
const modelOrigin = [-30, 55];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude,
);
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits(),
};
const options = {
accessToken: 'pk.eyJ1IjoiYXJ0dXJvbXVub3oiLCJhIjoiY2swODR2NmlhNDYwaDNicDBlcnB6YmR0OSJ9.AgG7MN8DX1aFuG1DfbFr_Q',
style: 'mapbox://styles/arturomunoz/ck18elevs5kch1dpnwrwa76ox',
center: [-140, 50],
zoom: 3,
pitch: 90,
bearing: 0,
circleRadius: 8,
circleOpacity: 1,
createLayers: true,
// Add a flying point for entry animation
flyTo: {
center: [-94.39962116967581, 40.61298086159351],
zoom: 3.5,
bearing: -8,
speed: 0.3,
curve: 1, // change the speed at which it zooms out
pitch: 58,
easing(t) {
return t;
},
essential: true,
},
// The colors for the dots
palette: [
'#3399CC', // Light Blue
'#CC6666', // Light Red
]
};
return {
// Define the Engine API HyperCube
qae: {
properties: {
qHyperCubeDef: {
qDimensions: [],
qMeasures: [],
qInitialDataFetch: [{ qWidth: 6, qHeight: 500 }],
qSuppressZero: true,
qSuppressMissing: true,
},
showTitles: true,
title: 'US Data',
subtitle: 'Random gender / age buckets',
footnote: 'Data is random, for this example only.',
},
data: {
targets: [
{
path: '/qHyperCubeDef',
dimensions: {
min: 1,
max: 6,
},
measures: {
min: 0,
max: 0,
},
},
],
},
},
component() {
const element = stardust.useElement();
const layout = stardust.useLayout();
const qData = layout.qHyperCube?.qDataPages[0];
const qMatrix = qData.qMatrix.filter(row => row.some(el => el.qNum !== "NaN"))
const property = layout.qHyperCube?.qDimensionInfo[3]?.qFallbackTitle;
const property2 = layout.qHyperCube?.qDimensionInfo[4]?.qFallbackTitle;
const [instance, setInstance] = stardust.useState();
let GeoJSON, map = null;
let mapData = [];
const propertyChildren = [...new Set(qMatrix.map((array) => array[3].qText))];
// Create the Mapbox features based on our HyperCube data
const buildFeatures = (obj) => {
const featureObj = {
type: 'Feature',
properties: {
count: 1,
userID: obj.id,
[property]: obj[property],
},
geometry: {
type: 'Point',
coordinates: [obj.lng, obj.lat],
},
};
return featureObj;
}
// Convert our HyperCube data into a GeoJSON for Mapbox
const buildGeoJSON = () => {
const goodGeoJSON = {
type: 'FeatureCollection',
features: [],
};
qMatrix.map((array) => {
if (typeof array[1].qNum !== 'number' || typeof array[2].qNum !== 'number') return false;
const obj = {
id: Number(array[0].qNum),
lat: Number(array[1].qNum),
lng: Number(array[2].qNum),
};
obj[property] = array[3].qText;
obj[property2] = array[4].qText;
const feature = buildFeatures(obj);
goodGeoJSON.features.push(feature);
return obj;
});
return goodGeoJSON;
}
const createBar = (posx, posz, posy, order) => {
const max = 3000;
const ratio = Number(posy) / Number(maxBarΝumberFromData);
const y = max * ratio;
const _posy = 1;
const geometry = new THREE.BoxGeometry(barWidth, 1, barWidth, 1, 1, 1);
const material = new THREE.MeshLambertMaterial({ color: 0xfffff, transparent: true });
const bar = new THREE.Mesh(geometry, material);
bar.position.set(posx, _posy, posz);
bar.name = `bar-${order}`;
bar.userData.y = y;
bar.material.opacity = barOpacity;
scene.add(bar);
// Animate
TweenMax.to(bar.scale, 1, { y, delay: order * 0.01 });
TweenMax.to(bar.position, 1, { y: y / 2, delay: order * 0.01 });
maxNumberOfBars = order;
};
// Create the layer that will hold the dots
const buildLayer = () => {
// const match = ['match', ['get', property], ...propertyChildrenWithColors, '#FFF'];
const layer = {
id: '3d-model',
type: 'custom',
renderingMode: '3d',
onAdd(_map, gl) {
camera = new THREE.Camera();
scene = new THREE.Scene();
// create two three.js lights to illuminate the model
const directionalLight = new THREE.DirectionalLight(0xffffff);
directionalLight.position.set(-90, 200, 130).normalize();
scene.add(directionalLight);
// sky color ground color intensity
const directionalLight2 = new THREE.DirectionalLight(0xffffff, 0.3);
directionalLight2.position.set(90, 20, -100).normalize();
scene.add(directionalLight2);
qMatrix.forEach((row, index) => {
maxBarΝumberFromData = (maxBarΝumberFromData < row[1].qNum) ? row[1].qNum : maxBarΝumberFromData;
})
qMatrix.forEach((row, index) => {
createBar(row[2].qNum * 150, row[1].qNum * 150, row[5].qNum, index);
})
// scale up geometry
scene.scale.set(300, 300, 300);
// use the Mapbox GL JS map canvas for three.js
renderer = new THREE.WebGLRenderer({
canvas: _map.getCanvas(),
context: gl,
antialias: true,
});
renderer.autoClear = false;
},
render(gl, matrix) {
const rotationX = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(1, 0, 0),
modelTransform.rotateX,
);
const rotationY = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 1, 0),
modelTransform.rotateY,
);
const rotationZ = new THREE.Matrix4().makeRotationAxis(
new THREE.Vector3(0, 0, 1),
modelTransform.rotateZ,
);
const m = new THREE.Matrix4().fromArray(matrix);
const l = new THREE.Matrix4()
.makeTranslation(
modelTransform.translateX,
modelTransform.translateY,
modelTransform.translateZ,
)
.scale(
new THREE.Vector3(
modelTransform.scale,
-modelTransform.scale,
modelTransform.scale,
),
)
.multiply(rotationX)
.multiply(rotationY)
.multiply(rotationZ);
camera.projectionMatrix = m.multiply(l);
renderer.state.reset();
renderer.render(scene, camera);
map.triggerRepaint();
},
};
return layer;
}
// Create the map
const buildMap = () => {
// Add HyperCube data as GeoJSON
map.addSource('hyperCubeData', {
type: 'geojson',
data: GeoJSON,
});
// Create the layer
const layer = buildLayer();
map.addLayer(layer);
if (options.extraLayers && options.extraLayers.length) {
options.extraLayers.map((_layer) => map.addLayer(_layer));
}
};
// Update layer data upon HyperCube change
const updateLayers = () => {
const nextChunk = qMatrix.map((array) => {
const obj = {
id: Number(array[0].qNum),
lat: Number(array[1].qNum),
lng: Number(array[2].qNum),
[property]: array[3].qText,
};
return buildFeatures(obj);
});
if (GeoJSON) {
GeoJSON = { ...GeoJSON, features: [...GeoJSON.features, ...nextChunk] };
map.getSource('hyperCubeData').setData(GeoJSON);
} else {
GeoJSON = buildGeoJSON();
buildMap();
}
};
stardust.useEffect(() => {
mapboxgl.accessToken = options.accessToken;
if (!map) {
// Initialize mapbox GL
map = new mapboxgl.Map({
container: element,
...options,
});
// Add layer with data
map.on('load', () => {
updateLayers(qData); // Draw the first set of data, in case we load all
mapData = [...mapData, ...qMatrix];
});
map.flyTo(options.flyTo);
}
}, [layout]);
},
};
}