Public
Edited
Mar 10
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable stepCount = null
Insert cell
play0 = {
console.log("play", play);
if (model.markers.bubble.state !== "fulfilled") return;
const playing = model.markers.bubble.encoding.frame.playing;
model.markers.bubble.encoding.frame[playing ? "stopPlaying" : "startPlaying"]();
}
Insert cell
viewof play = Inputs.button("Play")
Insert cell
viewof step = Inputs.range([0, mutable stepCount], {label: "Step", value: mutable stepCount, step: 1, width})
Insert cell
Insert cell
deck.log.level = 0
Insert cell
Insert cell
Insert cell
getViews = (controllerProps = true) => {
return [
new deck.OrthographicView({id: 'axis',
controller: false,
//padding: { left: 50, top: 50, bottom: 50, right: 50 },
flipY: false, far: 1000}),
new deck.OrthographicView({id: 'bubble',
controller: controllerProps,
flipY: false, far: 1000})
]
}
Insert cell
options = ({
opacityNormal: 180,
opacitySelected: 200,
opacityNonSelected: 10,
speed: 12000,
minRadiusPx: 2,
maxRadiusPx: 50
})
Insert cell
Insert cell
resortData = function(data, newData, trailsCount) {
const keyToIndex = {};
const trailsStartIndex = {};
let i = 0, index;
data.forEach((d, i) => {
if (d[TRAIL_KEY]) {
if (trailsStartIndex[d[TRAIL_KEY]] === undefined) trailsStartIndex[d[TRAIL_KEY]] = i;
} else {
keyToIndex[d[KEY]] = d;
}
});
//console.log("trailsStartIndex", trailsStartIndex);
return newData.map(d => {
if (d[TRAIL_KEY]) {
index = i++;
if (i > trailsCount) i = 0;
return data[trailsStartIndex[d[TRAIL_KEY]] + index];
}
return keyToIndex[d[KEY]];
});
}
Insert cell
model = {
const ddfReader = new DDFCsvReader.getDDFCsvReaderObject();
Vizabi.stores.dataSources.createAndAddType("ddfcsv", ddfReader);
const model = Vizabi(data_config(url, {
markers: {
bubble: {
encoding: {
y: {data: {concept: "m_efterg_25_64"}},
x: {data: {concept: "m_dispin_20_64"}, scale: {type: "log"}},
z: {data: {concept: "antal"}},
size: {data: {concept: "antal"}},
color: {data: {
space: ["geo"],
concept: "region"
},
scale: {
modelType: "color",
type: "ordinal"
}
}
}
}
}
}));
const fullMarker = model.markers.bubble;
Vizabi.utils.applyDefaults(model.markers.bubble.config, DEFAULT_CORE("bubble"));
const frameType = Vizabi.stores.encodings.modelTypes.frame;
const { marker, splashMarker } = frameType.splashMarker(fullMarker);
model.markers.bubble = marker;

return model;
}
Insert cell
disposer = []
Insert cell
KEY = Symbol.for("key")
Insert cell
TRAIL_KEY = Symbol.for("trailHeadKey")
Insert cell
year = d3.utcFormat("%Y");

Insert cell
size = ({
w: 700,
h: 500
})
Insert cell
mutable data = []
Insert cell
mobx.autorun(() => {
model.markers.bubble.state == "fulfilled" && (mutable stepCount = model.markers.bubble.encoding.frame.stepCount - 1);
})
Insert cell
mobx.autorun(() => {
if (model.markers.bubble.state !== "fulfilled") return;

mutable data = model.markers.bubble;
})
Insert cell
mobx.autorun(() => {
model.markers.bubble.state == "fulfilled" && model.markers.bubble.encoding.frame.setStep(step);
})
Insert cell
Insert cell
class LabelMultiIconLayer extends deck._MultiIconLayer {
static defaultProps = {
getBoundingRect: {type: 'accessor', value: [0, 0, 0, 0]},
getDragged: {type: 'accessor', value: 0.0},
};

initializeState() {
super.initializeState();

this.getAttributeManager().addInstanced({
instanceRects: {
size: 4,
//accessor: 'getBoundingRect'
accessor: (object, info) => {
return this.parent.getBoundingRect(object, info);
}
},
instanceDragged: {
size: 1,
transition: false,
accessor: "getDragged",
//accessor: (object, info) => {
// console.log("dragged",this.id, this.parent.props.getDragged(object, info));
// return this.parent.props.getDragged(object, info);
//}
},
});

}
draw(params) {
const {edgeMaxCoord = 1.0 } = this.parent.props;
let { backgroundPadding: padding = [0.0, 0.0, 0.0, 0.0] } = this.parent.props;
if (padding.length < 4) {
padding = [padding[0], padding[1], padding[0], padding[1]];
}
params.uniforms.padding = padding;
params.uniforms.edgeMaxCoord = edgeMaxCoord;
super.draw(params);
}

getShaders() {
return { ...super.getShaders(), vs: `\
#version 300 es
#define SHADER_NAME label-icon-layer-vertex-shader

in vec2 positions;

in vec3 instancePositions;
in vec3 instancePositions64Low;
in float instanceSizes;
in float instanceAngles;
in vec4 instanceColors;
in vec3 instancePickingColors;
in vec4 instanceIconFrames;
in float instanceColorModes;
in vec2 instanceOffsets;
in vec2 instancePixelOffset;
in vec4 instanceRects;
in float instanceDragged;

uniform float sizeScale;
uniform vec2 iconsTextureDim;
uniform float sizeMinPixels;
uniform float sizeMaxPixels;
uniform bool billboard;
uniform int sizeUnits;
uniform vec4 padding;
uniform float edgeMaxCoord;

out float vColorMode;
out vec4 vColor;
out vec2 vTextureCoords;
out vec2 uv;

vec2 rotate_by_angle(vec2 vertex, float angle) {
float angle_radian = angle * PI / 180.0;
float cos_angle = cos(angle_radian);
float sin_angle = sin(angle_radian);
mat2 rotationMatrix = mat2(cos_angle, -sin_angle, sin_angle, cos_angle);
return rotationMatrix * vertex;
}

void main(void) {
geometry.worldPosition = instancePositions;
geometry.uv = positions;
geometry.pickingColor = instancePickingColors;
uv = positions;

vec2 iconSize = instanceIconFrames.zw;
// convert size in meters to pixels, then scaled and clamp
// project meters to pixels and clamp to limits
float sizePixels = clamp(
project_size_to_pixel(instanceSizes * sizeScale, sizeUnits),
sizeMinPixels, sizeMaxPixels
);

// scale icon height to match instanceSize
float instanceScale = iconSize.y == 0.0 ? 0.0 : sizePixels / iconSize.y;

// scale and rotate vertex in "pixel" value and convert back to fraction in clipspace
vec2 pixelOffset = positions / 2.0 * iconSize + instanceOffsets;
pixelOffset = rotate_by_angle(pixelOffset, instanceAngles) * instanceScale;
vec2 offset_icon = pixelOffset;
pixelOffset += instancePixelOffset;
pixelOffset.y *= -1.0;

if (billboard) {
gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, vec3(0.0), geometry.position);
DECKGL_FILTER_GL_POSITION(gl_Position, geometry);
vec3 offset = vec3(pixelOffset, 0.0);
DECKGL_FILTER_SIZE(offset, geometry);
gl_Position.xy += project_pixel_size_to_clipspace(offset.xy);

vec2 dimensions_wo_padd = instanceRects.zw * instanceSizes;
vec2 clip_paddLT = project_pixel_size_to_clipspace(padding.xy);
vec2 clip_paddRB = project_pixel_size_to_clipspace(padding.zw);
vec2 positions0 = (positions + vec2(1.)) * 0.5;
vec2 clip_offset_icon = project_pixel_size_to_clipspace(offset_icon);
vec2 clip_dimensions_wo_padd = project_pixel_size_to_clipspace(dimensions_wo_padd);
//default pos switch on edge
if (instanceDragged < 0.5) {
vec2 clip_offset = project_pixel_size_to_clipspace(abs(instancePixelOffset));
gl_Position.x += (1.0 - step(-edgeMaxCoord + clip_paddLT.x, gl_Position.x - clip_dimensions_wo_padd.x - clip_offset_icon.x)) * (clip_offset.x * 2.0 + clip_dimensions_wo_padd.x);
gl_Position.y += (-step(edgeMaxCoord - clip_paddLT.y, gl_Position.y + clip_dimensions_wo_padd.y + clip_offset_icon.y)) * (clip_offset.y * 2.0 + clip_dimensions_wo_padd.y);
}

//edge check

vec2 a = vec2(0.);
vec2 b = vec2(0.);

a.x = clamp(gl_Position.x, -edgeMaxCoord + clip_paddLT.x + clip_dimensions_wo_padd.x + clip_offset_icon.x , edgeMaxCoord - clip_paddRB.x + clip_offset_icon.x);
b.x = clamp(gl_Position.x, -edgeMaxCoord + clip_paddLT.x + clip_dimensions_wo_padd.x + clip_offset_icon.x , edgeMaxCoord - clip_paddRB.x + clip_offset_icon.x);

a.y = clamp(gl_Position.y, -edgeMaxCoord + clip_paddRB.y - clip_offset_icon.y, edgeMaxCoord - clip_paddLT.y - clip_dimensions_wo_padd.y - clip_offset_icon.y);
b.y = clamp(gl_Position.y, -edgeMaxCoord + clip_paddRB.y - clip_offset_icon.y, edgeMaxCoord - clip_paddLT.y - clip_dimensions_wo_padd.y - clip_offset_icon.y);

gl_Position.x = mix(a.x, b.x, positions0.x);
gl_Position.y = mix(a.y, b.y, 1. - positions0.y);
} else {
vec3 offset_common = vec3(project_pixel_size(pixelOffset), 0.0);
DECKGL_FILTER_SIZE(offset_common, geometry);
gl_Position = project_position_to_clipspace(instancePositions, instancePositions64Low, offset_common, geometry.position);
DECKGL_FILTER_GL_POSITION(gl_Position, geometry);
}

vTextureCoords = mix(
instanceIconFrames.xy,
instanceIconFrames.xy + iconSize,
(positions.xy + 1.0) / 2.0
) / iconsTextureDim;

vColor = instanceColors;
DECKGL_FILTER_COLOR(vColor, geometry);

vColorMode = instanceColorModes;
}
`
}
}
}
Insert cell
Insert cell
class LabelLayer extends deck.TextLayer {
static defaultProps = {
getLineSourceFillOffset: {type: 'accessor', value: 0},
getRadius: {type: 'accessor', value: 0},
getDragged: {type: 'accessor', value: 0.0},
}
getPickingInfo(e) {
const {info, sourceLayer} = e;
//console.log("bounds", this.getBoundingRect(info.object, info));
if (info.index !== -1) {
if (this.state.closeData?.[0]?.dataIndex !== info.index) {
const labelSize = this.getBoundingRect(info.object, info).slice(2).map(b => b * this.props.getSize);
this.setState({ labelSize, closeData: [Object.assign({labelSize, viewportW: info.viewport.width, viewportH: info.viewport.height, dataIndex: info.index }, info.object)]});
}
} else {
this.setState({closeData: []})
}
return info;
}

//shouldUpdateState({props, oldProps, context, changeFlags}) {
// console.log("label shouldUpdateState", props, oldProps, context, changeFlags);
//}
/*
updateState(e) {
const {props, changeFlags} = e;
if (changeFlags.dataChanged) {
}
console.log("updateState", props, changeFlags);
return super.updateState(e);
}
*/
renderLayers() {
/*const sLayers = super.renderLayers();
const layers = sLayers.map(l => {
return l && l.clone({
updateTriggers: {...l.props.updateTriggers, getDragged: this.props.updateTriggers["getDragged"]},
getDragged: this.props.getDragged
})
});
*/
//console.log("LabelLayer", sLayers, layers);

return [
new LabelLineLayer(this.getSubLayerProps({
id: 'labelLineLayer',
data: this.props.data,
getColor: [102, 102, 102],
getSourcePosition: this.props.getPosition,
getTargetPosition: this.props.getPosition,
getTargetPixelOffset: this.props.getPixelOffset,
getSourceDashOffset: this.props.getLineSourceFillOffset,
getBoundingRect: this.getBoundingRect,
getRadius: this.props.getRadius,
getSize: this.props.getSize,
getWidth: 1,
getDragged: this.props.getDragged,
padding: this.props.backgroundPadding,
edgeMaxCoord: this.props.edgeMaxCoord,
updateTriggers: {
//getColor: [activeObject],
getDragged: this.props.updateTriggers.getDragged,
getTargetPixelOffset: this.props.updateTriggers.getPixelOffset//[dragX, dragY]
},
transitions: {
getSourcePosition: this.props.transitions?.getPosition || {},
getTargetPosition: this.props.transitions?.getPosition || {},
getTargetPixelOffset: this.props.transitions?.getPosition || {},
}
//pickable: true,
//_dataDiff: (newData, oldData) => {
// console.log("_datediff line", newData, oldData, _updateRangesLine);
// return dataDiff ? playing ? _updateRangesLine : null : null;
//}
})),
//...layers,
...super.renderLayers(),
this.state.closeData?.length && new deck.TextLayer({
id: 'labelCloseTextLayer',
_subLayerProps: {
background: {
type: LabelBackgroundLayer,
cornerRadius: 12,
getDragged: 1,
},
characters: {
type: LabelMultiIconLayer,
getDragged: 1,
}
},
data: this.state.closeData || [],
getPosition: (d) => {
//console.log("close pos", d);
return this.props.getPosition(d, {index: d.dataIndex});
},
getPixelOffset: (d) => {
const offset = typeof this.props.getPixelOffset == "function" ? this.props.getPixelOffset(d) : this.props.getPixelOffset.slice(0);

//adjust label offset if label with current offset located outside of vieport
//const offset = labelOffset[key];
const pPos = this.context.viewport.project(this.props.getPosition(d, {index: d.dataIndex}).slice(0,2));
const vW = d.viewportW;
const vH = d.viewportH;
const [lW, lH] = d.labelSize;
const [lPaddL, lPaddT, lPaddR = lPaddL, lPaddB = lPaddT] = this.props.backgroundPadding;
const dragged = this.props.getDragged.name ? this.props.getDragged(d) : this.props.getDragged;
if (!dragged) {
if(pPos[0] + offset[0] < lW + lPaddL) {
offset[0] = lW - offset[0];
}
if(pPos[1] + offset[1] < lH + lPaddT) {
offset[1] = lH - offset[1];
}
}
if(pPos[0] + offset[0] < lW + lPaddL) {
offset[0] = lW + lPaddL - pPos[0];
} else if(pPos[0] + offset[0] > vW - lPaddR) {
offset[0] = vW - lPaddR - pPos[0];
}
if(pPos[1] + offset[1] < lH + lPaddT) {
offset[1] = lH + lPaddT - pPos[1];
} else if(pPos[1] + offset[1] > vH - lPaddB) {
offset[1] = vH - lPaddB - pPos[1];
}

offset[0] += lPaddR + 9*0.5;//half size closecross .getSize
offset[1] -= lH + lPaddT - 9*0.5;//half size closecross .getSize
return offset;
},
getText: x=>"❌",
getColor: [255, 255, 255],
getSize: 9,
getTextAnchor: 'end',
getAlignmentBaseline: 'bottom',
getDragged: x=>1,
edgeMaxCoord: this.props.edgeMaxCoord,
fontSettings: {
sdf: true,
fontSize: 24,
},
characterSet:["❌"],
//getPolygonOffset: null,//({layerIndex}) => [0, layerIndex * 100],
pickable: true,
background: true,
backgroundPadding: [8, 9, 8, 8],
getBackgroundColor: [0x60, 0x78, 0x89],
getBorderColor: [255, 255, 255],
getBorderWidth: 2,
outlineColor: [255, 255, 255],
outlineWidth: 2,
updateTriggers: {
getPixelOffset: this.props.updateTriggers.getPixelOffset
},
})]
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
data_config = (url, custom = {}) => {
const config = deepmerge({
markers: {
bubble: {
data: { source: "data1",
space: ["geo", "year"],
filter: {
dimensions: { "geo": { "$or": [{ "is--deso": true }] } }
//dimensions: { "geo": { "$or": [{"geo": { "$in": ["0180C2840", "1280C2080" ,"1230", "0687","2303", "1814" ]}}] } }
}
//filter: {
// dimensions: {
// "country": {
// "country": {
// "$in":["usa", "nga", "swe", "rus", "_csk"]
// }
// }
// }
//}
}
}
},
dataSources: {
data1: {
path: url,
modelType: "ddfcsv",
locale: "en"
}
}
}, custom)
console.log("config", config)
return config;
}

Insert cell
Insert cell
features = {
const config = {
path: "assets/shapes.json",
objects: {
areas: "shapes",
boundaries: false
},
projection: "geoMercator",
}
const map = {
"missingDataColor": "none", //"#999" or "none" for transparent. "none" makes it faster
"scale": 1,
"preserveAspectRatio": true,
"mapEngine": "mapbox",
"mapStyle": "mapbox://styles/mapbox/light-v9",
"showBubbles": false,
"showAreas": true,
"showMap": true,
"offset": {
"top": 0.05,
"bottom": -0.12,
"left": 0,
"right": 0
},
"path": null,
"bounds": {
west: 14.91, north: 59.72, east: 19.71, south: 57.26
},
"projection": "mercator",
topology: {
path: "assets/shapes.json",
objects: {
areas: "shapes",
boundaries: false
},
geoIdProperty: "id",
}
};
const shapes = await model.markers.bubble.data.source.reader.getAsset(config.path);
const _features = topojson.feature(shapes, shapes.objects[config.objects.areas]);
const projection = d3[config.projection]();
projection.scale(1).translate([0, 0]);
const d3GeoPath = d3.geoPath(projection);
_features.features.forEach(f => {
f.properties.centroid = projection.invert(d3GeoPath.centroid(f));
})
console.log(_features);
return _features;
}
Insert cell
Insert cell
{
document.head.appendChild(html`
<link rel='stylesheet' href='https://www.unpkg.com/@vizabi/shared-components@1.40.0/build/VizabiSharedComponents.css'/>
<link rel='stylesheet' href='https://api.tiles.mapbox.com/mapbox-gl-js/v1.13.1/mapbox-gl.css'/>
`);
return "css imports"
}
Insert cell
Insert cell
CHARACTER_SET =
'ABCDEFGHIJKLMNOPQRSTUVWXYZÅÄÖabcdefghijklmnopqrstuvwxyzåäéö0123456789+-−–*/%,.²:() '.split('');
Insert cell
Vizabi = require("@vizabi/core@1.32.2")
Insert cell
VizabiSharedComponents = require("@vizabi/shared-components@1.43.2")
Insert cell
deepmerge = (await require("lodash@4")).merge
Insert cell
mobx = require('mobx@5.15.7/lib/mobx.umd.min.js')
Insert cell
d3 = require("d3@latest")
Insert cell
deck = require("https://unpkg.com/deck.gl@9.0.40/dist.min.js")
Insert cell
DDFCsvReader = require('https://unpkg.com/@vizabi/reader-ddfcsv@latest')
Insert cell
mapboxgl = require('mapbox-gl@latest')
Insert cell
GL = (await require("https://unpkg.com/@luma.gl/constants/dist/dist.dev.js")).GL
Insert cell
m = import("https://unpkg.com/@math.gl/core@latest/dist/index.js")
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