canvas-globe 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
- {
1
+ {
2
2
  "name": "canvas-globe",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Zero-dependency JavaScript and React library for interactive 3D globes and flat world maps rendered with Canvas 2D. No WebGL or API key.",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": {
@@ -14,7 +14,7 @@
14
14
  "bugs": {
15
15
  "url": "https://github.com/Shree-hari/canvas-globe/issues"
16
16
  },
17
- "keywords": [
17
+ "keywords": [
18
18
  "javascript-globe",
19
19
  "interactive-globe",
20
20
  "canvas-globe",
@@ -37,36 +37,36 @@
37
37
  "great-circle-arcs",
38
38
  "web-component",
39
39
  "vanilla-javascript"
40
- ],
41
- "type": "module",
42
- "main": "./src/index.js",
43
- "module": "./src/index.js",
44
- "types": "./types/index.d.ts",
40
+ ],
41
+ "type": "module",
42
+ "main": "./src/index.js",
43
+ "module": "./src/index.js",
44
+ "types": "./types/index.d.ts",
45
45
  "unpkg": "./dist/canvas-globe.umd.js",
46
46
  "jsdelivr": "./dist/canvas-globe.umd.js",
47
- "exports": {
48
- ".": {
49
- "types": "./types/index.d.ts",
50
- "import": "./src/index.js",
51
- "default": "./src/index.js"
52
- },
53
- "./element": {
54
- "types": "./types/element.d.ts",
55
- "import": "./src/element.js",
56
- "default": "./src/element.js"
57
- },
58
- "./react": {
59
- "types": "./types/react.d.ts",
60
- "import": "./src/react.js",
61
- "default": "./src/react.js"
62
- },
63
- "./data/world": "./src/data/world.js",
64
- "./package.json": "./package.json"
65
- },
66
- "files": [
67
- "src",
68
- "dist",
69
- "types",
47
+ "exports": {
48
+ ".": {
49
+ "types": "./types/index.d.ts",
50
+ "import": "./src/index.js",
51
+ "default": "./src/index.js"
52
+ },
53
+ "./element": {
54
+ "types": "./types/element.d.ts",
55
+ "import": "./src/element.js",
56
+ "default": "./src/element.js"
57
+ },
58
+ "./react": {
59
+ "types": "./types/react.d.ts",
60
+ "import": "./src/react.js",
61
+ "default": "./src/react.js"
62
+ },
63
+ "./data/world": "./src/data/world.js",
64
+ "./package.json": "./package.json"
65
+ },
66
+ "files": [
67
+ "src",
68
+ "dist",
69
+ "types",
70
70
  "README.md",
71
71
  "CHANGELOG.md",
72
72
  "COPYRIGHT",
@@ -74,37 +74,37 @@
74
74
  "LICENSING.md",
75
75
  "THIRD_PARTY_NOTICES.md",
76
76
  "codemeta.json"
77
- ],
77
+ ],
78
78
  "sideEffects": [
79
79
  "./src/element.js"
80
80
  ],
81
- "publishConfig": {
82
- "access": "public"
83
- },
84
- "scripts": {
85
- "build": "node scripts/build.mjs",
81
+ "publishConfig": {
82
+ "access": "public"
83
+ },
84
+ "scripts": {
85
+ "build": "node scripts/build.mjs",
86
86
  "data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs",
87
- "example": "node scripts/serve.mjs",
87
+ "example": "node scripts/serve.mjs",
88
88
  "test": "node --test",
89
89
  "typecheck": "tsc --noEmit -p tsconfig.json",
90
90
  "smoke:package": "node scripts/package-smoke.mjs",
91
91
  "release:check": "npm run test && npm run typecheck && npm run build && node scripts/release-check.mjs && npm run smoke:package",
92
92
  "prepublishOnly": "npm run test && npm run typecheck && npm run build && node scripts/release-check.mjs"
93
- },
94
- "peerDependencies": {
95
- "react": ">=17"
96
- },
97
- "peerDependenciesMeta": {
98
- "react": {
99
- "optional": true
100
- }
101
- },
102
- "devDependencies": {
103
- "polygon-clipping": "^0.15.7",
104
- "topojson-client": "^3.1.0",
105
- "typescript": "^5.5.0"
106
- },
107
- "engines": {
108
- "node": ">=18"
109
- }
110
- }
93
+ },
94
+ "peerDependencies": {
95
+ "react": ">=17"
96
+ },
97
+ "peerDependenciesMeta": {
98
+ "react": {
99
+ "optional": true
100
+ }
101
+ },
102
+ "devDependencies": {
103
+ "polygon-clipping": "^0.15.7",
104
+ "topojson-client": "^3.1.0",
105
+ "typescript": "^5.5.0"
106
+ },
107
+ "engines": {
108
+ "node": ">=18"
109
+ }
110
+ }
package/src/csv.js CHANGED
@@ -1,151 +1,151 @@
1
- /**
2
- * Turns spreadsheet-shaped data into markers.
3
- *
4
- * Marketing data arrives as CSV, not GeoJSON, so this resolves rows by
5
- * lat/lon columns first, then a city name, then a country code or name: using
6
- * only geometry that already ships in the package.
7
- */
8
- import { world as bundledWorld } from "./data/world.js";
9
- import { countryLocation, placeLocation } from "./viewer.js";
10
- import { geometryBounds } from "./geo.js";
11
-
12
- const LAT_KEYS = ["lat", "latitude", "y"];
13
- const LON_KEYS = ["lon", "lng", "long", "longitude", "x"];
14
- const CITY_KEYS = ["city", "place", "town", "location"];
15
- const COUNTRY_KEYS = ["country", "iso", "country_code", "countrycode", "nation"];
16
- const COUNT_KEYS = ["count", "value", "total", "users", "visits", "amount", "n"];
17
- const LABEL_KEYS = ["label", "name", "title"];
18
-
19
- let countryIndex = null;
20
-
21
- const buildCountryIndex = () => {
22
- if (countryIndex) return countryIndex;
23
- countryIndex = new Map();
24
- for (const shape of bundledWorld) {
25
- const [west, south, east, north] = geometryBounds(shape.geometry);
26
- const point = { lon: (west + east) / 2, lat: (south + north) / 2, country: shape.iso || null };
27
- for (const key of [shape.iso, shape.id, shape.name]) {
28
- if (key != null) countryIndex.set(String(key).toLowerCase(), point);
29
- }
30
- }
31
- return countryIndex;
32
- };
33
-
34
- /** Resolves a country code or name to a coordinate. */
35
- export function countryPoint(name) {
36
- if (!name) return null;
37
- const key = String(name).trim().toLowerCase();
38
- return buildCountryIndex().get(key) || countryLocation(key) || null;
39
- }
40
-
41
- /**
42
- * Best-effort coordinate for a free-text place. Tries the city table, then
43
- * countries. Pass `gazetteer` to plug in your own `{ "Ahmedabad": [72.58, 23.03] }`.
44
- */
45
- export function geocode(name, { gazetteer } = {}) {
46
- if (!name) return null;
47
- const key = String(name).trim();
48
- if (gazetteer) {
49
- const hit = gazetteer[key] ?? gazetteer[key.toLowerCase()];
50
- if (Array.isArray(hit)) return { lon: hit[0], lat: hit[1] };
51
- if (hit && typeof hit === "object") return { lon: hit.lon ?? hit.lng, lat: hit.lat };
52
- }
53
- return placeLocation(key) || countryPoint(key);
54
- }
55
-
56
- /** RFC 4180-ish CSV parser: handles quotes, embedded commas and CRLF. */
57
- export function parseCSV(text, { delimiter = "," } = {}) {
58
- const rows = [];
59
- let row = [], field = "", quoted = false;
60
- for (let i = 0; i < text.length; i++) {
61
- const c = text[i];
62
- if (quoted) {
63
- if (c === '"') {
64
- if (text[i + 1] === '"') {
65
- field += '"';
66
- i++;
67
- } else quoted = false;
68
- } else field += c;
69
- continue;
70
- }
71
- if (c === '"') quoted = true;
72
- else if (c === delimiter) {
73
- row.push(field);
74
- field = "";
75
- } else if (c === "\n" || c === "\r") {
76
- if (c === "\r" && text[i + 1] === "\n") i++;
77
- row.push(field);
78
- rows.push(row);
79
- row = [];
80
- field = "";
81
- } else field += c;
82
- }
83
- if (field !== "" || row.length) {
84
- row.push(field);
85
- rows.push(row);
86
- }
87
- if (!rows.length) return [];
88
- const header = rows[0].map((h) => h.trim().toLowerCase());
89
- return rows.slice(1)
90
- .filter((r) => r.some((v) => v !== ""))
91
- .map((r) => {
92
- const out = {};
93
- header.forEach((key, i) => (out[key] = (r[i] ?? "").trim()));
94
- return out;
95
- });
96
- }
97
-
98
- const pick = (row, keys) => {
99
- for (const key of keys) if (row[key] != null && row[key] !== "") return row[key];
100
- return null;
101
- };
102
-
103
- // Number(null) is 0, which would make every row look like it sits at (0, 0).
104
- const num = (value) => {
105
- if (value == null || value === "") return NaN;
106
- const n = Number(value);
107
- return Number.isFinite(n) ? n : NaN;
108
- };
109
-
110
- /**
111
- * Converts parsed rows to markers. Unresolvable rows are dropped and reported
112
- * on the returned array as `skipped`.
113
- */
114
- export function fromRows(rows, options = {}) {
115
- const { gazetteer, extra = true } = options;
116
- const markers = [];
117
- const skipped = [];
118
- for (const row of rows) {
119
- const lat = num(pick(row, LAT_KEYS));
120
- const lon = num(pick(row, LON_KEYS));
121
- let point = Number.isFinite(lat) && Number.isFinite(lon) ? { lat, lon } : null;
122
- if (!point) {
123
- const city = pick(row, CITY_KEYS);
124
- const country = pick(row, COUNTRY_KEYS);
125
- const found = (city && geocode(city, { gazetteer })) || (country && countryPoint(country));
126
- if (found) point = { lat: found.lat, lon: found.lon };
127
- }
128
- if (!point) {
129
- skipped.push(row);
130
- continue;
131
- }
132
- const marker = { lat: point.lat, lon: point.lon };
133
- const count = num(pick(row, COUNT_KEYS));
134
- if (Number.isFinite(count)) marker.count = count;
135
- const label = pick(row, LABEL_KEYS) || pick(row, CITY_KEYS) || pick(row, COUNTRY_KEYS);
136
- if (label) marker.label = label;
137
- if (row.emoji) marker.emoji = row.emoji;
138
- if (row.image) marker.image = row.image;
139
- if (row.color) marker.color = row.color;
140
- if (row.date) marker.date = row.date;
141
- if (extra) for (const [k, v] of Object.entries(row)) if (!(k in marker)) marker[k] = v;
142
- markers.push(marker);
143
- }
144
- markers.skipped = skipped;
145
- return markers;
146
- }
147
-
148
- /** `fromCSV("city,count\nAhmedabad,12")` → markers. */
149
- export function fromCSV(text, options = {}) {
150
- return fromRows(parseCSV(text, options), options);
151
- }
1
+ /**
2
+ * Turns spreadsheet-shaped data into markers.
3
+ *
4
+ * Marketing data arrives as CSV, not GeoJSON, so this resolves rows by
5
+ * lat/lon columns first, then a city name, then a country code or name: using
6
+ * only geometry that already ships in the package.
7
+ */
8
+ import { world as bundledWorld } from "./data/world.js";
9
+ import { countryLocation, placeLocation } from "./viewer.js";
10
+ import { geometryBounds } from "./geo.js";
11
+
12
+ const LAT_KEYS = ["lat", "latitude", "y"];
13
+ const LON_KEYS = ["lon", "lng", "long", "longitude", "x"];
14
+ const CITY_KEYS = ["city", "place", "town", "location"];
15
+ const COUNTRY_KEYS = ["country", "iso", "country_code", "countrycode", "nation"];
16
+ const COUNT_KEYS = ["count", "value", "total", "users", "visits", "amount", "n"];
17
+ const LABEL_KEYS = ["label", "name", "title"];
18
+
19
+ let countryIndex = null;
20
+
21
+ const buildCountryIndex = () => {
22
+ if (countryIndex) return countryIndex;
23
+ countryIndex = new Map();
24
+ for (const shape of bundledWorld) {
25
+ const [west, south, east, north] = geometryBounds(shape.geometry);
26
+ const point = { lon: (west + east) / 2, lat: (south + north) / 2, country: shape.iso || null };
27
+ for (const key of [shape.iso, shape.id, shape.name]) {
28
+ if (key != null) countryIndex.set(String(key).toLowerCase(), point);
29
+ }
30
+ }
31
+ return countryIndex;
32
+ };
33
+
34
+ /** Resolves a country code or name to a coordinate. */
35
+ export function countryPoint(name) {
36
+ if (!name) return null;
37
+ const key = String(name).trim().toLowerCase();
38
+ return buildCountryIndex().get(key) || countryLocation(key) || null;
39
+ }
40
+
41
+ /**
42
+ * Best-effort coordinate for a free-text place. Tries the city table, then
43
+ * countries. Pass `gazetteer` to plug in your own `{ "Ahmedabad": [72.58, 23.03] }`.
44
+ */
45
+ export function geocode(name, { gazetteer } = {}) {
46
+ if (!name) return null;
47
+ const key = String(name).trim();
48
+ if (gazetteer) {
49
+ const hit = gazetteer[key] ?? gazetteer[key.toLowerCase()];
50
+ if (Array.isArray(hit)) return { lon: hit[0], lat: hit[1] };
51
+ if (hit && typeof hit === "object") return { lon: hit.lon ?? hit.lng, lat: hit.lat };
52
+ }
53
+ return placeLocation(key) || countryPoint(key);
54
+ }
55
+
56
+ /** RFC 4180-ish CSV parser: handles quotes, embedded commas and CRLF. */
57
+ export function parseCSV(text, { delimiter = "," } = {}) {
58
+ const rows = [];
59
+ let row = [], field = "", quoted = false;
60
+ for (let i = 0; i < text.length; i++) {
61
+ const c = text[i];
62
+ if (quoted) {
63
+ if (c === '"') {
64
+ if (text[i + 1] === '"') {
65
+ field += '"';
66
+ i++;
67
+ } else quoted = false;
68
+ } else field += c;
69
+ continue;
70
+ }
71
+ if (c === '"') quoted = true;
72
+ else if (c === delimiter) {
73
+ row.push(field);
74
+ field = "";
75
+ } else if (c === "\n" || c === "\r") {
76
+ if (c === "\r" && text[i + 1] === "\n") i++;
77
+ row.push(field);
78
+ rows.push(row);
79
+ row = [];
80
+ field = "";
81
+ } else field += c;
82
+ }
83
+ if (field !== "" || row.length) {
84
+ row.push(field);
85
+ rows.push(row);
86
+ }
87
+ if (!rows.length) return [];
88
+ const header = rows[0].map((h) => h.trim().toLowerCase());
89
+ return rows.slice(1)
90
+ .filter((r) => r.some((v) => v !== ""))
91
+ .map((r) => {
92
+ const out = {};
93
+ header.forEach((key, i) => (out[key] = (r[i] ?? "").trim()));
94
+ return out;
95
+ });
96
+ }
97
+
98
+ const pick = (row, keys) => {
99
+ for (const key of keys) if (row[key] != null && row[key] !== "") return row[key];
100
+ return null;
101
+ };
102
+
103
+ // Number(null) is 0, which would make every row look like it sits at (0, 0).
104
+ const num = (value) => {
105
+ if (value == null || value === "") return NaN;
106
+ const n = Number(value);
107
+ return Number.isFinite(n) ? n : NaN;
108
+ };
109
+
110
+ /**
111
+ * Converts parsed rows to markers. Unresolvable rows are dropped and reported
112
+ * on the returned array as `skipped`.
113
+ */
114
+ export function fromRows(rows, options = {}) {
115
+ const { gazetteer, extra = true } = options;
116
+ const markers = [];
117
+ const skipped = [];
118
+ for (const row of rows) {
119
+ const lat = num(pick(row, LAT_KEYS));
120
+ const lon = num(pick(row, LON_KEYS));
121
+ let point = Number.isFinite(lat) && Number.isFinite(lon) ? { lat, lon } : null;
122
+ if (!point) {
123
+ const city = pick(row, CITY_KEYS);
124
+ const country = pick(row, COUNTRY_KEYS);
125
+ const found = (city && geocode(city, { gazetteer })) || (country && countryPoint(country));
126
+ if (found) point = { lat: found.lat, lon: found.lon };
127
+ }
128
+ if (!point) {
129
+ skipped.push(row);
130
+ continue;
131
+ }
132
+ const marker = { lat: point.lat, lon: point.lon };
133
+ const count = num(pick(row, COUNT_KEYS));
134
+ if (Number.isFinite(count)) marker.count = count;
135
+ const label = pick(row, LABEL_KEYS) || pick(row, CITY_KEYS) || pick(row, COUNTRY_KEYS);
136
+ if (label) marker.label = label;
137
+ if (row.emoji) marker.emoji = row.emoji;
138
+ if (row.image) marker.image = row.image;
139
+ if (row.color) marker.color = row.color;
140
+ if (row.date) marker.date = row.date;
141
+ if (extra) for (const [k, v] of Object.entries(row)) if (!(k in marker)) marker[k] = v;
142
+ markers.push(marker);
143
+ }
144
+ markers.skipped = skipped;
145
+ return markers;
146
+ }
147
+
148
+ /** `fromCSV("city,count\nAhmedabad,12")` → markers. */
149
+ export function fromCSV(text, options = {}) {
150
+ return fromRows(parseCSV(text, options), options);
151
+ }