├── .babelrc ├── .eslintrc.json ├── .gitignore ├── .gitmodules ├── .npmignore ├── LICENSE ├── README.md ├── changelog.md ├── justfile ├── logo-go-cart-wasm.png ├── package-lock.json ├── package.json ├── rollup.config.mjs └── src └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parserOptions": { 4 | "parser": "babel-eslint", 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true 9 | }, 10 | "plugins": [ 11 | ], 12 | "extends": [ 13 | "airbnb-base" 14 | ], 15 | "ignorePatterns": ["**", "!src", "!src/**"], 16 | "globals": { 17 | "window": false, 18 | "initGoCart": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Directories of built files 2 | dist/ 3 | build/ 4 | # Dependency directories 5 | fftw-3.3.3/ 6 | cJSON-1.7.15/ 7 | go_cart/cJSON.* 8 | 9 | # JS files 10 | node_modules/ 11 | 12 | # Stuff related to IDEs 13 | .vscode/ 14 | .idea/ 15 | 16 | # Unfinished examples 17 | example/ 18 | 19 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "go_cart"] 2 | path = go_cart 3 | url = https://github.com/mthh/go_cart 4 | branch = wasm-mod -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | cJSON-1.7.15/ 3 | example/ 4 | fftw-3.3.3/ 5 | go_cart/ 6 | .idea/ 7 | src/ 8 | .babelrc 9 | .eslintrc.json 10 | justfile 11 | rollup.config.mjs -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2022 Matthieu Viry 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Go-cart-wasm 2 | 3 | ![](./logo-go-cart-wasm.png) 4 | 5 | `Go-cart-wasm` is a JS/WASM library for making flow-based cartograms (as described in "*Gastner, Seguy, and More (2018). Fast flow-based algorithm for creating density-equalizing map projections. Proceedings of the National Academy of Sciences USA, 115:E2156-E2164*"), purely in the browser. 6 | 7 | This is a port of the [reference implementation](https://github.com/Flow-Based-Cartograms/go_cart) provided by the authors (however we needed to slightly modify some minor aspects of the code, and our modified version is located [here](https://github.com/mthh/go_cart/tree/wasm-mod)). 8 | 9 | ### Usage 10 | 11 | The library is available as a JS module, and can be used as follows: 12 | 13 | ```js 14 | import initGoCart from 'go-cart-wasm'; 15 | 16 | initGoCart() 17 | .then((GoCart) => { 18 | // The GeoJSON containing the data to be transformed 19 | const data = { 20 | type: 'FeatureCollection', 21 | features: ... 22 | }; 23 | 24 | // The name of the field that contains the data on which the cartogram will be based 25 | const fieldName = 'POP2021'; 26 | 27 | // Call the function that creates the cartogram 28 | const dataCartogram = GoCart.makeCartogram(data, fieldName); 29 | 30 | console.log(dataCartogram); // The resulting GeoJSON 31 | }); 32 | ``` 33 | 34 | Optionally, and depending on how you import the library, you may also need to pass a `config` object as argument to the `initGoCart` function, which can contain the `locateFile` property: if set, it will be used to locate the WASM file (which is needed by the library). 35 | 36 | For example with the [Vite](https://vite.dev/) build tool: 37 | 38 | ```js 39 | import initGoCart from 'go-cart-wasm'; 40 | import goCartWasm from 'go-cart-wasm/dist/cart.wasm'; 41 | 42 | initGoCart({ 43 | locateFile: () => goCartWasm, 44 | }).then((GoCart) => { 45 | // Use GoCart as in the previous example 46 | // ... 47 | }); 48 | ``` 49 | 50 | Or using [unpkg](https://unpkg.com/) and require: 51 | 52 | ```js 53 | const initGoCart = require("https://unpkg.com/go-cart-wasm@latest/dist/go-cart.cjs"); 54 | 55 | const GoCart = await initGoCart({ 56 | locateFile: (path) => 'https://unpkg.com/go-cart-wasm@latest/dist/cart.wasm', 57 | }); 58 | 59 | // Use GoCart as in the previous example 60 | // ... 61 | ``` 62 | 63 | Or in a HTML document: 64 | 65 | ```html 66 | 67 | 76 | ``` 77 | 78 | Note that the data that is passed to the `makeCartogram` function must be a valid GeoJSON FeatureCollection, its features must be of type Polygon or MultiPolygon and the field name must be a valid property of its features. 79 | Moreover, the data have to be in a projected reference coordinate system that maintains the areas of the polygons (e.g. Lambert-93 / EPSG:2154 for Metropolitan France, EPSG:3035 for the European Union, etc.). Performing the calculation in geographic coordinates (e.g. EPSG:4326) would give erroneous results. 80 | 81 | Note also that by default the calculation is done in the main thread. The freedom is left to the user to import go-cart.js from a webworker and use it that way (nevertheless you will have to implement a basic dialogue between the main thread and the webworker). 82 | 83 | ### Example 84 | 85 | See for example this [Observable Notebook](https://observablehq.com/@riate/flow-based-cartograms-gastner-seguy-more-2018-in-the-browse). 86 | 87 | ### Installation for development 88 | 89 | **Requirements:** 90 | 91 | - The **Just** command runner (https://github.com/casey/just) 92 | - Emscripten SDK (https://emscripten.org/docs/getting_started/downloads.html) 93 | - Node.js (https://nodejs.org/en/download/) 94 | - npm (https://www.npmjs.com/get-npm) 95 | 96 | **Install and compile dependencies:** 97 | 98 | 1) Install Just, install Emscripten SDK, install node.js / npm. 99 | 2) Install node dependencies with `npm install`. 100 | 3) Activate the various environment variables of Emscripten SDK in the current terminal (`source ./emsdk_env.sh` in emsdk directory). 101 | 4) Build the C dependencies with `npm run build-deps`. 102 | 103 | **Build the WASM/JS code:** 104 | 105 | 1) Run `npm run build` to build the WASM module and the JS wrapper. If you change stuff in the JS wrapper (in `src`) or in the C code of go_cart (in `go_cart`), you can resume from here. 106 | If you want to see debug information about the progress of the cartogram creation in the console (grid size used, number of iterations, max area error, correction factor, etc.) you can compile with the DEBUG flag with `npm run build-debug`. 107 | 2) Get the built files from the `dist` directory. 108 | 109 | Note that this has only been tested on GNU/Linux and that these instructions may need to be modified to work on Mac OS X and Windows. 110 | 111 | ### License 112 | 113 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 114 | Underlying code by Gastner, Seguy, and More (2018) is licensed under a modified MIT License - see their [LICENSE](https://github.com/Flow-Based-Cartograms/go_cart/blob/master/LICENSE) file for details. 115 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 0.5.0 (2024-12-11) 4 | 5 | - Improve the `exports` section of the package.json file to ease the use of the 6 | library with Vite since the changes made in v0.4.0. 7 | 8 | ### 0.4.0 (2024-12-11) 9 | 10 | - Fix output formats of rollup build (to expose the library as a ESM, CJS, and UMD module). 11 | 12 | ### 0.3.0 (2023-01-05) 13 | 14 | - Introduce a new stop condition if the maximal absolute area error is not decreasing anymore and starts to increase. 15 | 16 | ### 0.2.0 (2023-01-03) 17 | 18 | - Update README about library usage. 19 | 20 | - Update dependencies. 21 | 22 | ### 0.1.0 (2023-01-02) 23 | 24 | - Initial release 25 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set shell := ["bash", "-uc"] 2 | set dotenv-load 3 | 4 | FFTW_PACKAGE := "fftw-3.3.3" 5 | CJSON_VERSION := "1.7.15" 6 | 7 | default: build 8 | 9 | clean-all: clean 10 | rm -rf cJSON-{{CJSON_VERSION}} {{FFTW_PACKAGE}} 11 | 12 | clean: 13 | rm -f fftw-3.3.3/.libs/*.* 14 | rm -f fftw-3.3.3/api/*.o 15 | rm -f go_cart/cartogram_generator/cJSON.* 16 | 17 | fftw3: 18 | if [[ ! -d {{FFTW_PACKAGE}} ]]; then \ 19 | wget http://www.fftw.org/{{FFTW_PACKAGE}}.tar.gz; \ 20 | tar -xzf {{FFTW_PACKAGE}}.tar.gz; \ 21 | rm {{FFTW_PACKAGE}}.tar.gz; \ 22 | cd {{FFTW_PACKAGE}}; \ 23 | echo '{ "type" : "commonjs" }' > package.json; \ 24 | cd ..; \ 25 | fi 26 | cd {{FFTW_PACKAGE}} && \ 27 | CFLAGS='-O3' emconfigure ./configure --disable-fortran && \ 28 | emmake make -j4 29 | 30 | cjson: 31 | if [[ ! -d cJSON-{{CJSON_VERSION}} ]]; then \ 32 | wget https://github.com/DaveGamble/cJSON/archive/refs/tags/v{{CJSON_VERSION}}.zip; \ 33 | unzip v{{CJSON_VERSION}}.zip; \ 34 | rm v{{CJSON_VERSION}}.zip; \ 35 | fi 36 | cp cJSON-{{CJSON_VERSION}}/cJSON.h go_cart/cartogram_generator/ 37 | cp cJSON-{{CJSON_VERSION}}/cJSON.c go_cart/cartogram_generator/ 38 | 39 | build-deps: clean fftw3 cjson 40 | 41 | build DEBUG="": 42 | rm -rf build 43 | mkdir -p build 44 | rm -rf dist 45 | cd go_cart/cartogram_generator && \ 46 | emcc --bind -I../../{{FFTW_PACKAGE}}/api -L../../{{FFTW_PACKAGE}}/.libs -DUSE_FFTW -lfftw3 main.c cartogram.c ffb_integrate.c fill_with_density.c ps_figure.c read_map.c process_json.c cJSON.c \ 47 | {{ if DEBUG == "DEBUG" { "-DDEBUG" } else { "" } }} \ 48 | -o ../../build/cart.js \ 49 | -O3 \ 50 | -s FORCE_FILESYSTEM \ 51 | -s EXPORT_ES6=1 \ 52 | -s MODULARIZE=1 \ 53 | -s EXPORTED_FUNCTIONS="['_doCartogram']" \ 54 | -s EXPORTED_RUNTIME_METHODS="['ccall', 'FS']" \ 55 | -s EXPORT_NAME="GoCart" \ 56 | -s ALLOW_MEMORY_GROWTH=1 57 | -------------------------------------------------------------------------------- /logo-go-cart-wasm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/riatelab/go-cart-wasm/84bdd056f3b514378be506a118568eaa42abacf2/logo-go-cart-wasm.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "go-cart-wasm", 3 | "version": "0.5.0", 4 | "description": "Flow-Based Cartogram Generator in WASM", 5 | "main": "./dist/go-cart.cjs", 6 | "module": "./dist/go-cart.mjs", 7 | "browser": "./dist/go-cart.js", 8 | "exports": { 9 | ".": { 10 | "import": "./dist/go-cart.mjs", 11 | "require": "./dist/go-cart.cjs", 12 | "default": "./dist/go-cart.cjs" 13 | }, 14 | "./dist/*.wasm": { 15 | "import": "./dist/*.wasm", 16 | "require": "./dist/*.wasm" 17 | } 18 | }, 19 | "files": [ 20 | "dist/", 21 | "README.md", 22 | "LICENSE" 23 | ], 24 | "scripts": { 25 | "lint": "eslint .", 26 | "build-deps": "just build-deps", 27 | "build": "just build && rollup --config rollup.config.mjs", 28 | "build-debug": "just build DEBUG && rollup --config rollup.config.mjs", 29 | "test": "echo \"Error: no test specified\" && exit 1" 30 | }, 31 | "author": "Matthieu Viry ", 32 | "license": "MIT", 33 | "repository": { 34 | "type": "git", 35 | "url": "git+https://github.com/mthh/go-cart-wasm.git" 36 | }, 37 | "bugs": { 38 | "url": "https://github.com/mthh/go-cart-wasm/issues" 39 | }, 40 | "homepage": "https://github.com/mthh/go-cart-wasm", 41 | "dependencies": {}, 42 | "devDependencies": { 43 | "@babel/cli": "^7.26.4", 44 | "@babel/core": "^7.26.0", 45 | "@babel/preset-env": "^7.26.0", 46 | "@rollup/plugin-babel": "^6.0.4", 47 | "@rollup/plugin-commonjs": "^28.0.1", 48 | "@rollup/plugin-node-resolve": "^15.3.0", 49 | "@rollup/plugin-virtual": "^3.0.2", 50 | "babel-eslint": "^10.1.0", 51 | "eslint": "^8.57.1", 52 | "eslint-config-airbnb": "^19.0.4", 53 | "eslint-plugin-import": "^2.31.0", 54 | "rollup": "^4.28.1", 55 | "rollup-plugin-copy": "^3.5.0" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { babel } from '@rollup/plugin-babel'; 4 | import virtual from '@rollup/plugin-virtual'; 5 | import copy from 'rollup-plugin-copy'; 6 | 7 | const nodeLibs = { 8 | fs: `export default {};`, 9 | path: `export default {};`, 10 | string_decoder: `export default {};`, 11 | buffer: `export default {};`, 12 | crypto: `export default {};`, 13 | stream: `export default {};` 14 | }; 15 | 16 | export default [ 17 | { 18 | plugins: [ 19 | virtual(nodeLibs), 20 | nodeResolve(), 21 | commonjs({ transformMixedEsModules: true }), 22 | babel({ babelHelpers: 'bundled' }), 23 | copy({ 24 | targets: [ 25 | { src: 'build/cart.wasm', dest: 'dist/' }, 26 | ] 27 | }) 28 | ], 29 | input: 'src/index.js', 30 | output: [ 31 | { 32 | file: 'dist/go-cart.cjs', 33 | format: 'cjs', 34 | exports: 'auto', 35 | }, 36 | { 37 | file: 'dist/go-cart.mjs', 38 | format: 'esm', 39 | }, 40 | { 41 | file: 'dist/go-cart.js', 42 | format: 'umd', 43 | name: 'initGoCart', 44 | }, 45 | ] 46 | }, 47 | ]; 48 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-named-as-default, import/no-named-as-default-member 2 | import GoCartWasmModule from '../build/cart'; 3 | 4 | // eslint-disable-next-line no-restricted-globals 5 | const isNumber = (value) => value != null && value !== '' && isFinite(value); 6 | 7 | /** 8 | * Prepare GeoJSON and CSV data for GoCart. 9 | * 10 | * @param geojson 11 | * @param fieldName 12 | * @return {{rawGeoJSON: string, rawCsv: string}} 13 | */ 14 | const prepareGeoJSONandCSV = (geojson, fieldName) => { 15 | const newGeojson = JSON.parse(JSON.stringify(geojson)); 16 | const values = []; 17 | let xmin = Infinity; 18 | let xmax = -Infinity; 19 | let ymin = Infinity; 20 | let ymax = -Infinity; 21 | 22 | const bboxFn = (point) => { 23 | xmin = Math.min(xmin, point[0]); 24 | xmax = Math.max(xmax, point[0]); 25 | ymin = Math.min(ymin, point[1]); 26 | ymax = Math.max(ymax, point[1]); 27 | }; 28 | newGeojson.features.forEach((feature, i) => { 29 | const cartogramId = i + 1; 30 | // eslint-disable-next-line no-param-reassign 31 | feature.properties.cartogram_id = `${cartogramId}`; 32 | 33 | if (feature.geometry.type === 'Polygon') { 34 | feature.geometry.coordinates.forEach((ring) => { 35 | ring.forEach(bboxFn); 36 | }); 37 | } else if (feature.geometry.type === 'MultiPolygon') { 38 | feature.geometry.coordinates.forEach((polygon) => { 39 | polygon.forEach((ring) => { 40 | ring.forEach(bboxFn); 41 | }); 42 | }); 43 | } 44 | 45 | // Replace inexistent values and not number values by 0 46 | const valueIsNumber = isNumber(feature.properties[fieldName]); 47 | const value = valueIsNumber ? feature.properties[fieldName] : 0; 48 | values.push({ cartogramId, value }); 49 | }); 50 | 51 | newGeojson.bbox = [xmin, ymin, xmax, ymax]; 52 | const rawCsv = `Region Id, Region Data\n${values.map((v) => `${v.cartogramId}, ${v.value}`).join('\n')}`; 53 | const rawGeoJSON = JSON.stringify(newGeojson); 54 | return { rawGeoJSON, rawCsv }; 55 | }; 56 | 57 | const initGoCart = async (options = {}) => { 58 | const GoCartWasm = await GoCartWasmModule(options); 59 | 60 | /** 61 | * Make a cartogram according to Gastner, Seguy and More (2018) algorithm. 62 | * This function expects a GeoJSON object (FeatureCollection) and a field name. 63 | * 64 | * @param geojson {Object} - The FeatureCollection to be handled. 65 | * @param fieldName {String} - The name of the field to be used as data. 66 | * @return {Object} 67 | */ 68 | GoCartWasm.makeCartogram = function makeCartogram(geojson, fieldName) { 69 | // Check the arguments 70 | if ( 71 | !geojson 72 | || !fieldName 73 | || typeof geojson !== 'object' 74 | || !Object.prototype.hasOwnProperty.call(geojson, 'features') 75 | ) { 76 | throw new Error('Invalid arguments : first argument must be a GeoJSON FeatureCollection and second argument must be a field name'); 77 | } 78 | // Prepare the data 79 | const { rawGeoJSON, rawCsv } = prepareGeoJSONandCSV(geojson, fieldName); 80 | 81 | // Save the data in GoCart memory / file system 82 | const pathInputJsonFile = '/data/test.json'; 83 | const pathInputCsvFile = '/data/test.csv'; 84 | 85 | if (GoCartWasm.FS.findObject('/data') === null) { 86 | GoCartWasm.FS.mkdir('/data'); 87 | } 88 | GoCartWasm.FS.writeFile(pathInputJsonFile, rawGeoJSON); 89 | GoCartWasm.FS.writeFile(pathInputCsvFile, rawCsv); 90 | 91 | const cleanUp = () => { 92 | GoCartWasm.FS.unlink(pathInputJsonFile); 93 | GoCartWasm.FS.unlink(pathInputCsvFile); 94 | GoCartWasm.FS.unlink('cartogram.json'); 95 | GoCartWasm.FS.unlink('area_error.dat'); 96 | }; 97 | 98 | try { 99 | // Actually run the algorithm 100 | const retVal = GoCartWasm.ccall( 101 | 'doCartogram', 102 | 'number', 103 | ['string', 'string'], 104 | [pathInputJsonFile, pathInputCsvFile], 105 | ); 106 | if (retVal !== 0) { 107 | cleanUp(); 108 | throw new Error('Error while running the cartogram algorithm'); 109 | } 110 | } catch (e) { 111 | cleanUp(); 112 | throw e; 113 | } 114 | 115 | // Read the result 116 | const data = GoCartWasm.FS.readFile('cartogram.json', { encoding: 'utf8' }); 117 | 118 | // Read the log file about area errors 119 | const areaErrors = GoCartWasm.FS.readFile('area_error.dat', { encoding: 'utf8' }); 120 | const t = {}; 121 | areaErrors.split('\n').forEach((line) => { 122 | const id = line.substring(7, line.indexOf(': ')); 123 | const errorValue = line.substring(line.indexOf('relative error = ') + 'relative error = '.length); 124 | t[id] = +errorValue; 125 | }); 126 | 127 | // Store the area error in each feature properties 128 | const result = JSON.parse(data); 129 | result.features.forEach((feature) => { 130 | const id = feature.properties.cartogram_id; 131 | // eslint-disable-next-line no-param-reassign 132 | feature.properties.area_error = t[id]; 133 | }); 134 | 135 | // Clean the memory / file system 136 | cleanUp(); 137 | 138 | // Return the result 139 | return result; 140 | }; 141 | 142 | return GoCartWasm; 143 | }; 144 | 145 | export default initGoCart; 146 | --------------------------------------------------------------------------------