Public
Edited
Oct 10, 2024
Importers
Insert cell
Insert cell
Insert cell
Insert cell
{
if (display == "yes") {
render(true)
return ctx.canvas}
else {
//render(false)
}
return "Done"
}
Insert cell
Insert cell
Insert cell
import { radio } from '@jashkenas/inputs'
Insert cell
md`# Data Converter

You can convert any geojson file into this format and download it using the button below; just change the source.

You can also live-transform by importing the feature_collection_to_feather_frame function below.
`
Insert cell
url = 'https://raw.githubusercontent.com/martynafford/natural-earth-geojson/master/110m/cultural/ne_110m_admin_0_countries_lakes.json'
Insert cell
raw_geojson = d3.json(url)
Insert cell
raw_geojson.features.filter(d => d.properties.SOVEREIGNT !== "Antarctica")
Insert cell
geojson = {
const geojson = raw_geojson
// For the mapping I'm doing of the world with distortion,
// it is preferable to remove exclaves like Alaska, the portions of Russia past -180 longitude,
// and French Guinea
geojson.features = geojson.features.filter(d => d.properties.SOVEREIGNT !== "Antarctica")
return geojson
for (let feature of geojson.features) {
if (false) {//feature.properties.GEOUNIT.match(/United States of America|United Kingdom|France|Russia/)) {
console.log(feature.properties.GEOUNIT)
reduce_to_largest_elements(feature)
}
}
return geojson
}
Insert cell
function reduce_to_largest_elements(feature, n = 1) {
const coords = feature.geometry.coordinates.sort((a, b) => +b[0].length - +a[0].length)
feature.geometry.coordinates = feature.geometry.coordinates.slice(0, n)
}
Insert cell
projection = d3.geoGingery().scale(300).lobes(6).rotate([98, -40, 0])//d3.geoInterruptedMollweide().scale(5000)//d3.geoMercator().scale(2).translate([-0, 0])//d3.geoInterruptedHomolosine().translate([0, 0]).scale(.2)
Insert cell
country_feather = feature_collection_to_feather_frame(geojson, projection, {clip_to_sphere: clip_to_sphere == "yes"})

Insert cell
arrow = require('apache-arrow@2.0.0')
Insert cell
function download_button() {return html`
${DOM.download(new Blob([country_feather.serialize().buffer], {type: "application/octet-stream"}), "countries.gleofeather", `Download ${country_feather.length} row gleofeather`)}
`}
Insert cell
Type JavaScript, then Shift-Enter. Ctrl-space for more options. Arrow ↑/↓ to switch modes.

Insert cell
ctx = DOM.context2d(width, width/2)
Insert cell
render = function(val) {
if (!val) {return}
const [[xmin, ymin], [xmax, ymax]] = secret.bounds;
const scale = (xmax-xmin)/width
ctx.save()
ctx.clearRect(0, 0, width, width)
ctx.scale(scale, scale)
// ctx.translate(width/2, ctx.canvas.height/2)
for (let feature of country_feather) {
ctx.fillStyle = "red"
const coords = new Float32Array(new Uint8Array(feature.coordinates).buffer)
const vertices = new window[`Uint${feature.coord_resolution}Array`](new Uint8Array(feature.vertices).buffer)
for (let i = 0; i < vertices.length; i += 3) {
if (i > vertices.length/2) {ctx.fillStyle="blue" }

ctx.beginPath();

ctx.moveTo(coords[2*vertices[i]], coords[2*vertices[i]+1]);
ctx.lineTo(coords[2*vertices[i+1]], coords[2*vertices[i+1]+1]);
ctx.lineTo(coords[2*vertices[i+2]], coords[2*vertices[i+2]+1]);
ctx.fill();
}
}
ctx.restore()
}

Insert cell
country_feather.get(1)
Insert cell
md`# Functions to turn geoJSON into feather frames.`
Insert cell
function polygon_to_triangles(polygon) {
// Actually perform the earcut work on a polygon.
const el_pos = []
const coords = polygon.flat(2)
const vertices = earcut(...Object.values(earcut.flatten(polygon)))
return { coords, vertices }
}
Insert cell
clip = require('polygon-clipping')
Insert cell
secret = new Object()
Insert cell
d3.geoPath().bounds(geojson)
Insert cell
yield secret.bounds
Insert cell
feature_collection_to_feather_frame = function(
feature_collection, projection, options = {dictionary_threshold: .75, clip_to_sphere: false}) {
if (projection === undefined) {throw "Must define a projection"}
// feature_collections: a (parsed) geoJSON object.
// projection: a d3.geoProjection instance; eg, d3.geoMollweide().transform([10, 20])
// options:
const properties = new Map()
properties.set("id", [])
const vertices = []
const coordinates = []
// Stores the number of bytes used for the coordinates.
const coord_resolutions = []
// centroids let you have fun with shapes. Store x and y separately.
const centroids = [[], []]
// Storing areas makes it possible to weight centroids.
const areas = []
let i = -1;
const path = d3.geoPath()
let clip_shape;
if (options.clip_to_sphere) {
clip_shape = d3.geoProject({"type": "Sphere"}, projection)
}
const bounds = [[Infinity, Infinity], [-Infinity, -Infinity]]
for (let feature of feature_collection.features) {

i++;
properties.get("id")[i] = feature.id;

for (let [k, v] of Object.entries(feature.properties)) {
if (!properties.get(k)) {properties.set(k, [])}
properties.get(k)[i] = v
}

/* Project and get new coords */
let projected = d3.geoProject(feature.geometry, projection);
if (options.clip_to_sphere) {
const new_coords = clip.intersection(projected.coordinates, clip_shape.coordinates)
if (projected.type == "Polygon" && typeof(new_coords[0][0][0] != "numeric")) {
projected.type = "MultiPolygon"
}
projected.coordinates = new_coords
}
[centroids[0][i], centroids[1][i]] = path.centroid(projected)
areas[i] = path.area(projected)
const loc_bounds = d3.geoPath().bounds(projected)
for (let dim of [0, 1]) {
if (loc_bounds[0][dim] < bounds[0][dim]) {bounds[0][dim] = loc_bounds[0][dim]}
if (loc_bounds[1][dim] > bounds[1][dim]) {bounds[1][dim] = loc_bounds[1][dim]}
}
let loc_coordinates;
if (projected === null) {
coord_resolutions[i] = null
coordinates[i] = null
vertices[i] = null
continue
} else
if (projected.type == "Polygon") {
loc_coordinates = [projected.coordinates]
} else if (projected.type == "MultiPolygon") {
loc_coordinates = projected.coordinates
} else {
throw "All elements must be polygons or multipolgyons."
}
let all_coords = []
let all_vertices = []
for (let polygon of loc_coordinates) {
const current_vertex = all_coords.length/2
const { coords, vertices } = polygon_to_triangles(polygon);
all_coords.push(...coords)
// If need to shift because we may be storing multiple triangle sets on a feature.
all_vertices.push(...vertices.map(d => d + current_vertex))
}
// Flatten to a silly degree just in case?
all_vertices = all_vertices.flat(10)
coordinates[i] = Float32Array.from(all_coords.flat(20))
// The type of the vertex array can be smaller if there
// are fewer coordinates to reference.
let MyArray
if (all_coords.length < 2**8) {
coord_resolutions[i] = 8
MyArray = Uint8Array
} else if (all_coords.length < 2**16) {
coord_resolutions[i] = 16
MyArray = Uint16Array
} else {
// Will not allow more than 4 billion points on a single feature,
// should be fine.
coord_resolutions[i] = 32
MyArray = Uint32Array
}
vertices[i] = MyArray.from(all_vertices.flat(10))
}
const cols = {
"coordinates": pack_binary(coordinates),
"vertices": pack_binary(vertices),
"coord_resolution": arrow.Uint8Vector.from(coord_resolutions),
"pixel_area": arrow.Float32Vector.from(areas),
"centroid_x": arrow.Float32Vector.from(centroids[0]),
"centroid_y": arrow.Float32Vector.from(centroids[1])
}
for (const [k, v] of properties.entries()) {
if (k in cols) {
// silently ignore.
//throw `Duplicate column names--rename ${k} `;
}
cols[k] = arrow.Vector.from({nullable: true, values: v, type: infer_type(v, options.dictionary_threshold)})
}
const named_columns = []
for (const [k, v] of Object.entries(cols)) {
// console.log(k, v)
named_columns.push(arrow.Column.new(k, v))
}
const tab = arrow.Table.new(...named_columns)
secret.bounds = bounds
return tab
}
Insert cell
//arrow_gl_map = feature_collection_to_feather_frame(k500_counties, projection)
Insert cell
function infer_type(array, dictionary_threshold = .75) {
// Certainly reinventing the wheel here--
// determine the most likely type of something based on a number of examples.
// Dictionary threshold: a number between 0 and one. Character strings will be cast
// as a dictionary if the unique values of the array are less than dictionary_threshold
// times as long as the length of all (not null) values.
const seen = new Set()
let strings = 0
let floats = 0
let max_int = 0

for (let el of array) {
if (Math.random() > 200/array.length) {continue} // Only check a subsample for speed. Try
// to get about 200 instances for each row.
if (el === undefined || el === null) {
continue
}
if (typeof(el) === "string") {
strings += 1
seen.add(el)
} else if (typeof(el) === "number") {
if (el % 1 > 0) {
floats += 1
} else if (isFinite(el)) {
max_int = Math.max(Math.abs(el), max_int)
} else {

}
} else {
throw `No behavior defined for type ${typeof(el)}`
}
}
if ( strings > 0 ) {
// moderate overlap
if (seen.length < strings.length * .75) {
return new arrow.Dictionary(new arrow.Utf8(), new arrow.Int32())
} else {
return new arrow.Utf8()
}
}
if (floats > 0) {
return new arrow.Float32()
}
if (Math.abs(max_int) < 2**8) {
return new arrow.Int8()
}
if (Math.abs(max_int) < 2**16) {
return new arrow.Int16()
}
if (Math.abs(max_int) < 2**32) {
return new arrow.Int32()
} else {
return new arrow.Int64()
}

}
Insert cell
function pack_binary(els) {
const { Builder, Binary } = arrow;
const binaryBuilder = Builder.new({
type: new Binary(),
nullValues: [null, undefined],
highWaterMark: 2**16
});
for (let el of els) { binaryBuilder.append(el) }
return binaryBuilder.finish().toVector()
}
Insert cell
earcut = require("earcut@2.2.4"); // https://unpkg.com/earcut@3.0.0/src/earcut.js
Insert cell
d3 = require("d3-geo-projection", "d3-fetch", "d3-geo")
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more