Published
Edited
Sep 3, 2021
1 fork
6 stars
Insert cell
Insert cell
chartElement = html`<div style="width: 100%; height: 700px; position: relative;"></div>`
Insert cell
snMapbox = {
let maxBarΝumberFromData = 0;
let maxNumberOfBars = 0;
let map;
let camera;
let scene;
let renderer;
const barWidth = 100;
const barOpacity = 1;
// parameters to ensure the model is georeferenced correctly on the map
const modelOrigin = [-30, 55];
const modelAltitude = 0;
const modelRotate = [Math.PI / 2, 0, 0];
const modelAsMercatorCoordinate = mapboxgl.MercatorCoordinate.fromLngLat(
modelOrigin,
modelAltitude,
);

// transformation parameters to position, rotate and scale the 3D model onto the map
const modelTransform = {
translateX: modelAsMercatorCoordinate.x,
translateY: modelAsMercatorCoordinate.y,
translateZ: modelAsMercatorCoordinate.z,
rotateX: modelRotate[0],
rotateY: modelRotate[1],
rotateZ: modelRotate[2],
/* Since our 3D model is in real world meters, a scale transform needs to be
* applied since the CustomLayerInterface expects units in MercatorCoordinates.
*/
scale: modelAsMercatorCoordinate.meterInMercatorCoordinateUnits(),
};
const options = {
accessToken: 'pk.eyJ1IjoiYXJ0dXJvbXVub3oiLCJhIjoiY2swODR2NmlhNDYwaDNicDBlcnB6YmR0OSJ9.AgG7MN8DX1aFuG1DfbFr_Q', // Change this to your free personal token
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]);

},
};
}
Insert cell
nebula.render({
element: chartElement,
type: 'sn-mapbox',
fields: [ 'ID', 'lat', 'lon', 'gender', 'AgeBucket', 'age'],
});
Insert cell
qlikApp = {
const config = {
host: 'sense-demo.qlik.com',
appId: '4052680c-fd97-4f49-ac83-e026cdd26d65',
};
const url = SenseUtilities.buildUrl(config);
const session = enigma.create({ schema, url });

const qlikApp = await session.open().then((global) => global.openDoc(config.appId));
return qlikApp;
}
Insert cell
enigma = require('enigma.js');
Insert cell
nebula = {
const nebula = await stardust.embed(qlikApp, {
types: [{
name: 'sn-mapbox',
load: () => snMapbox,
}],
});
return nebula;
};
Insert cell
schema = FileAttachment("12.170.2.json").json()
Insert cell
SenseUtilities = require('enigma.js/sense-utilities');
Insert cell
stardust = require('@nebula.js/stardust');
Insert cell
mapboxgl = require('mapbox-gl');
Insert cell
THREE = require('three');
Insert cell
GSAP = require('gsap');
Insert cell
TweenMax = GSAP.TweenMax;
Insert cell
html`<link href="https://api.mapbox.com/mapbox-gl-js/v2.3.0/mapbox-gl.css" rel="stylesheet">`
Insert cell
html`<style>
.mapboxgl-map {
background-color: #0f1534;
}
.mapboxgl-popup-tip {
border-top-color: rgba(0,0,0,0.8) !important;
}
.mapboxgl-popup-content {
color: white;
background: rgba(0,0,0,0.8);
box-shadow: 0 1px 2px rgb(0 0 0 / 10%);
}

</style>`
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more