diff --git a/react/package-lock.json b/react/package-lock.json index 1fe0b81b8..2e4d82618 100644 --- a/react/package-lock.json +++ b/react/package-lock.json @@ -12,6 +12,7 @@ "@dnd-kit/core": "^6.0.6", "@dnd-kit/sortable": "^7.0.1", "@fontsource/jost": "^5.0.18", + "@observablehq/plot": "^0.6.16", "@types/google-protobuf": "^3.15.12", "dom-to-image-more": "^3.3.0", "expr-eval": "^2.0.2", @@ -2520,6 +2521,19 @@ "node": ">= 8" } }, + "node_modules/@observablehq/plot": { + "version": "0.6.16", + "resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.16.tgz", + "integrity": "sha512-LRi9Rn93yUx90MIo2Md7+vazxO3Wiat14but2ttCER0xVS+jnfoUjuCGoz6H7bz/lgI9CFcW0HWlvWjMFjAv8g==", + "dependencies": { + "d3": "^7.9.0", + "interval-tree-1d": "^1.0.0", + "isoformat": "^0.2.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -3835,6 +3849,11 @@ "node": ">=8" } }, + "node_modules/binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==" + }, "node_modules/bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -4796,6 +4815,384 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -5005,6 +5402,14 @@ "rimraf": "bin.js" } }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -6944,7 +7349,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -7166,6 +7570,14 @@ "dev": true, "optional": true }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", @@ -7175,6 +7587,14 @@ "node": ">=10.13.0" } }, + "node_modules/interval-tree-1d": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz", + "integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==", + "dependencies": { + "binary-search-bounds": "^2.0.0" + } + }, "node_modules/ip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", @@ -7497,6 +7917,11 @@ "node": ">=0.10.0" } }, + "node_modules/isoformat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", + "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==" + }, "node_modules/javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -9836,6 +10261,11 @@ "inherits": "^2.0.1" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -9859,6 +10289,11 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -14380,6 +14815,16 @@ "fastq": "^1.6.0" } }, + "@observablehq/plot": { + "version": "0.6.16", + "resolved": "https://registry.npmjs.org/@observablehq/plot/-/plot-0.6.16.tgz", + "integrity": "sha512-LRi9Rn93yUx90MIo2Md7+vazxO3Wiat14but2ttCER0xVS+jnfoUjuCGoz6H7bz/lgI9CFcW0HWlvWjMFjAv8g==", + "requires": { + "d3": "^7.9.0", + "interval-tree-1d": "^1.0.0", + "isoformat": "^0.2.0" + } + }, "@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -15460,6 +15905,11 @@ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", "dev": true }, + "binary-search-bounds": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", + "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==" + }, "bl": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", @@ -16206,6 +16656,276 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "dev": true }, + "d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "requires": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + } + }, + "d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "requires": { + "internmap": "1 - 2" + } + }, + "d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==" + }, + "d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + } + }, + "d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "requires": { + "d3-path": "1 - 3" + } + }, + "d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==" + }, + "d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "requires": { + "d3-array": "^3.2.0" + } + }, + "d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "requires": { + "delaunator": "5" + } + }, + "d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==" + }, + "d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + } + }, + "d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "requires": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "dependencies": { + "commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + } + } + }, + "d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==" + }, + "d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "requires": { + "d3-dsv": "1 - 3" + } + }, + "d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==" + }, + "d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "requires": { + "d3-array": "2.5.0 - 3" + } + }, + "d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==" + }, + "d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "requires": { + "d3-color": "1 - 3" + } + }, + "d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==" + }, + "d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==" + }, + "d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==" + }, + "d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==" + }, + "d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "requires": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + } + }, + "d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "requires": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + } + }, + "d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==" + }, + "d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "requires": { + "d3-path": "^3.1.0" + } + }, + "d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "requires": { + "d3-array": "2 - 3" + } + }, + "d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "requires": { + "d3-time": "1 - 3" + } + }, + "d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==" + }, + "d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "requires": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + } + }, + "d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "requires": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + } + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -16366,6 +17086,14 @@ } } }, + "delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "requires": { + "robust-predicates": "^3.0.2" + } + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -17879,7 +18607,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, "requires": { "safer-buffer": ">= 2.1.2 < 3.0.0" } @@ -18029,12 +18756,25 @@ "dev": true, "optional": true }, + "internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==" + }, "interpret": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", "dev": true }, + "interval-tree-1d": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/interval-tree-1d/-/interval-tree-1d-1.0.4.tgz", + "integrity": "sha512-wY8QJH+6wNI0uh4pDQzMvl+478Qh7Rl4qLmqiluxALlNvl+I+o5x38Pw3/z7mDPTPS1dQalZJXsmbvxx5gclhQ==", + "requires": { + "binary-search-bounds": "^2.0.0" + } + }, "ip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", @@ -18252,6 +18992,11 @@ "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", "dev": true }, + "isoformat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/isoformat/-/isoformat-0.2.1.tgz", + "integrity": "sha512-tFLRAygk9NqrRPhJSnNGh7g7oaVWDwR0wKh/GM2LgmPa50Eg4UfyaCO4I8k6EqJHl1/uh2RAD6g06n5ygEnrjQ==" + }, "javascript-natural-sort": { "version": "0.7.1", "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", @@ -20049,6 +20794,11 @@ "inherits": "^2.0.1" } }, + "robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==" + }, "run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -20058,6 +20808,11 @@ "queue-microtask": "^1.2.2" } }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==" + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", diff --git a/react/package.json b/react/package.json index e23344b14..d5ff074d4 100644 --- a/react/package.json +++ b/react/package.json @@ -50,6 +50,7 @@ "@dnd-kit/core": "^6.0.6", "@dnd-kit/sortable": "^7.0.1", "@fontsource/jost": "^5.0.18", + "@observablehq/plot": "^0.6.16", "@types/google-protobuf": "^3.15.12", "dom-to-image-more": "^3.3.0", "expr-eval": "^2.0.2", diff --git a/react/src/components/article-panel.tsx b/react/src/components/article-panel.tsx index d208ae00f..bd0350edd 100644 --- a/react/src/components/article-panel.tsx +++ b/react/src/components/article-panel.tsx @@ -31,7 +31,7 @@ export function ArticlePanel({ article } : { article: Article }) { elements_to_render: [headers_ref.current!, table_ref.current!, map_ref.current!], }) - return {() => + return {(template_info) =>
{article.shortname}
@@ -40,10 +40,12 @@ export function ArticlePanel({ article } : { article: Article }) {
- +
@@ -93,12 +95,12 @@ function ComparisonSearchBox({ longname }: { longname: string }) { /> } -function StatisticRowHeader() { +function StatisticRowHeader(props: { screenshot_mode : boolean}) { const [simple_ordinals] = useSetting("simple_ordinals"); - return + return } -function ArticlePanelRows(props: { article_row: Article, longname: string }) { +function ArticlePanelRows(props: { article_row: Article, longname: string, shortname: string, screenshot_mode: boolean }) { const curr_universe = useUniverse(); const settings = useTableCheckboxSettings(); const [simple_ordinals] = useSetting("simple_ordinals"); @@ -110,6 +112,8 @@ function ArticlePanelRows(props: { article_row: Article, longname: string }) { onReplace={x => { document.location = article_link(curr_universe, x) }} simple={simple_ordinals} longname={props.longname} + shortname={props.shortname} + screenshot_mode={props.screenshot_mode} />)} } \ No newline at end of file diff --git a/react/src/components/comparison-panel.tsx b/react/src/components/comparison-panel.tsx index 741f64736..fa0008589 100644 --- a/react/src/components/comparison-panel.tsx +++ b/react/src/components/comparison-panel.tsx @@ -11,7 +11,8 @@ import { SearchBox } from './search'; import { article_link, sanitize } from '../navigation/links'; import { lighten } from '../utils/color'; import { longname_is_exclusively_american, useUniverse } from '../universe'; -import { useTableCheckboxSettings } from '../page_template/settings'; +import { row_expanded_key, useSetting, useTableCheckboxSettings } from '../page_template/settings'; +import { WithPlot } from './plots'; import { Article } from "../utils/protos"; const main_columns = ["statval", "statval_unit", "statistic_ordinal", "statistic_percentile"]; @@ -141,6 +142,7 @@ export function ComparisonPanel(props: { joined_string: string, universes: strin
)} @@ -199,7 +201,7 @@ function all_data_types_same(datas: Article[]) { } -function ComparsionPageRows({ names, datas }: { names: string[], datas: Article[] }) { +function ComparsionPageRows({ names, datas, screenshot_mode }: { names: string[], datas: Article[], screenshot_mode: boolean }) { const curr_universe = useUniverse(); let rows: ArticleRow[][] = []; const idxs: number[][] = []; @@ -217,32 +219,58 @@ function ComparsionPageRows({ names, datas }: { names: string[], datas: Article[ params={() => { return { is_header: true } }} datas={datas} names={names} + screenshot_mode={screenshot_mode} />; - const render_rows = rows[0].map((_, row_idx) => - { - return { - key: row_idx, index: row_idx, is_header: false, ...rows[data_idx][row_idx] - } - }} - datas={datas} - names={names} - /> - ); return ( <> { - render_rows.map((row, i) => - + rows[0].map((_, row_idx) => + ) } ) } -function ComparisonRow({ names, params, datas }: { names: string[], params: (i: number) => { is_header: true } | ({ is_header: false, key: number, index: number } & ArticleRow), datas: Article[] }) { +function ComparisonRowBody({ rows, row_idx, datas, names, screenshot_mode }: { + rows: ArticleRow[][], + row_idx: number, + datas: Article[], + names: string[], + screenshot_mode: boolean +}) { + const [expanded] = useSetting(row_expanded_key(rows[0][row_idx].statname)); + const contents = { + return { + key: row_idx, index: row_idx, ...rows[data_idx][row_idx], is_header: false + } + }} + datas={datas} + names={names} + screenshot_mode={screenshot_mode} + />; + const plot_props = rows.map((row, data_idx) => ({...row[row_idx], color: color(data_idx), shortname: datas[data_idx].shortname})); + return + + +} + +function ComparisonRow({ names, params, datas, screenshot_mode }: { + names: string[], + params: (i: number) => { is_header: true } | ({ is_header: false, key: number, index: number } & ArticleRow), + datas: Article[], + screenshot_mode: boolean +}) { if (names == undefined) { throw new Error("ComparisonRow: names is undefined"); } @@ -276,7 +304,8 @@ function ComparisonRow({ names, params, datas }: { names: string[], params: (i: row_overall.push(...StatisticRowRawCellContents( { ...param_vals[0], only_columns: ["statname"], _idx: -1, simple: true, longname: datas[0].longname, - total_width: 100 * (left_margin_pct - left_bar_margin) + total_width: 100 * (left_margin_pct - left_bar_margin), + screenshot_mode: screenshot_mode } )); const only_columns = all_data_types_same(datas) ? main_columns : main_columns_across_types; @@ -287,7 +316,8 @@ function ComparisonRow({ names, params, datas }: { names: string[], params: (i: ...param_vals[i], only_columns: only_columns, _idx: i, simple: true, statistic_style: highlight_idx == i ? { backgroundColor: lighten(color(i), 0.7) } : {}, onReplace: x => on_change(names, i, x), - total_width: each(datas) + total_width: each(datas), + screenshot_mode: screenshot_mode } )); } @@ -356,6 +386,8 @@ function insert_missing(rows: ArticleRow[][], idxs: number[][]) { if (typeof empty_row_example[idx][key] === "number") { // @ts-expect-error Typescript is fucking up this assignment empty_row_example[idx][key] = NaN; + } else if (key == "extra_stat") { + empty_row_example[idx][key] = undefined; } } empty_row_example[idx].article_type = "none"; // doesn't matter since we are using simple mode diff --git a/react/src/components/load-article.ts b/react/src/components/load-article.ts index 67704134a..2d2b96fbd 100644 --- a/react/src/components/load-article.ts +++ b/react/src/components/load-article.ts @@ -1,6 +1,11 @@ import { TableCheckboxSettings } from "../page_template/settings"; import { universe_is_american } from "../universe"; -import { Article } from "../utils/protos"; +import { Article, IExtraStatistic } from "../utils/protos"; + +export interface ExtraStat { + stat: IExtraStatistic, + universe_total: number +} export interface ArticleRow { statval: number, @@ -16,7 +21,8 @@ export interface ArticleRow { total_count_in_class: number, total_count_overall: number, _index: number, - rendered_statname: string + rendered_statname: string, + extra_stat?: ExtraStat } const index_list_info = require("../data/index_lists.json") as { @@ -77,11 +83,23 @@ export function load_article(universe: string, data: Article, settings: TableChe const stats = require("../data/statistic_list.json") as string[]; const explanation_page = require("../data/explanation_page.json") as string[]; + const extra_stats = require("../data/extra_stats.json") as [number, number][]; + const extra_stat_idx_to_col = extra_stats.map((xy) => xy[0]); + const indices = compute_indices(data.longname, article_type) as number[]; const modified_rows: ArticleRow[] = data.rows.map((row_original, row_index) => { const i = indices[row_index]; // fresh row object + let extra_stat: ExtraStat | undefined = undefined; + if (extra_stat_idx_to_col.includes(i)) { + const extra_stat_idx = extra_stat_idx_to_col.indexOf(i); + const [, universe_total_idx] = extra_stats[extra_stat_idx]; + extra_stat = { + stat: data.extraStats[extra_stat_idx], + universe_total: data.rows.filter((row, row_index) => indices[row_index] === universe_total_idx)[0].statval! + }; + } return { statval: row_original.statval!, ordinal: row_original.ordinalByUniverse![universe_index], @@ -96,7 +114,8 @@ export function load_article(universe: string, data: Article, settings: TableChe total_count_in_class: for_type(universe, stats[i], article_type), total_count_overall: for_type(universe, stats[i], "overall"), _index: i, - rendered_statname: render_statname(i, names[i], exclusively_american) + rendered_statname: render_statname(i, names[i], exclusively_american), + extra_stat: extra_stat } satisfies ArticleRow }) const filtered_rows = modified_rows.filter((row) => { diff --git a/react/src/components/plots.tsx b/react/src/components/plots.tsx new file mode 100644 index 000000000..ae3e3f311 --- /dev/null +++ b/react/src/components/plots.tsx @@ -0,0 +1,392 @@ + +import React, { useEffect, useRef } from 'react'; +import { IHistogram } from '../utils/protos'; + +// imort Observable plot +import * as Plot from "@observablehq/plot"; +import { HistogramType, useSetting } from '../page_template/settings'; +import { ExtraStat } from './load-article'; +import { CheckboxSetting } from './sidebar'; +import { create_screenshot } from './screenshot'; +import { useUniverse } from '../universe'; + +interface PlotProps { + shortname?: string; + extra_stat?: ExtraStat; + color: string; +} + +const Y_PAD = 0.025; + +export function WithPlot(props: { children: React.ReactNode, plot_props: PlotProps[], expanded: boolean, screenshot_mode: boolean }) { + return ( +
+ {props.children} + {props.expanded ? : null} +
+ ) +} + +function RenderedPlot({ plot_props, screenshot_mode }: { plot_props: PlotProps[], screenshot_mode: boolean }) { + console.log("plot props", plot_props) + if (plot_props.some(p => p.extra_stat?.stat.histogram)) { + plot_props = plot_props.filter(p => p.extra_stat?.stat.histogram); + return ({ + shortname: props.shortname!, + histogram: props.extra_stat!.stat.histogram!, + color: props.color, + universe_total: props.extra_stat!.universe_total + }) + )} + screenshot_mode={screenshot_mode} + /> + } + throw new Error("plot not recognized: " + JSON.stringify(plot_props)); +} + +interface HistogramProps { + shortname: string; + histogram: IHistogram; + color: string; + universe_total: number; +} + +function Histogram(props: { histograms: HistogramProps[], screenshot_mode: boolean }) { + const [histogram_type] = useSetting("histogram_type"); + const [use_imperial] = useSetting("use_imperial"); + const [relative] = useSetting("histogram_relative"); + // series for each histogram. Each series is a list of [x, y] pairs + // x start at histogram.binMin and goes up by histogram.binSize + // y is histogram.counts + console.log("HISTOGRAMS", props.histograms); + // assert all the histograms have the same binMin and binSize + const binMin = props.histograms[0].histogram.binMin!; + const binSize = props.histograms[0].histogram.binSize!; + for (const histogram of props.histograms) { + if (histogram.histogram.binMin !== binMin || histogram.histogram.binSize !== binSize) { + throw new Error("histograms have different binMin or binSize"); + } + } + // get the length of the x values + // get the x values + + const plot_ref = useRef(null); + const title = props.histograms.length === 1 ? props.histograms[0].shortname : ""; + useEffect(() => { + if (plot_ref.current) { + const colors = props.histograms + // sort histograms by the shortname (since plots are sorted by the name for some reason) + .sort((a, b) => a.shortname.localeCompare(b.shortname)) + .map(h => h.color); + const render_y = relative ? (y: number) => y.toFixed(2) + "%" : (y: number) => render_number_highly_rounded(y, 2); + + const [x_idx_start, x_idx_end] = histogramBounds(props.histograms); + const xidxs = Array.from({ length: x_idx_end - x_idx_start }, (_, i) => i + x_idx_start); + const [x_axis_marks, render_x] = x_axis(xidxs, binSize, binMin, use_imperial); + const [marks, max_value] = createHistogramMarks(props.histograms, xidxs, histogram_type, relative, render_x, render_y); + marks.push( + ...x_axis_marks, + ...y_axis(max_value), + Plot.text([title], { frameAnchor: "top", dy: -40 }), + ); + // y grid marks + // marks.push(Plot.gridY([0, 25, 50, 75, 100])); + const plot_config = { + marks: marks, + x: { + label: `Density (/${use_imperial ? "mi" : "km"}²)`, + }, + y: { + label: relative ? "% of total" : "Population", + domain: [max_value * (-Y_PAD), max_value * (1 + Y_PAD)], + }, + grid: false, + width: 1000, + style: { + fontSize: "1em", + fontFamily: "Jost" + }, + marginTop: 80, + marginBottom: 40, + marginLeft: 50, + color: colors.length === 1 ? undefined : { legend: true, range: colors }, + }; + const plot = Plot.plot(plot_config); + plot_ref.current.innerHTML = ""; + plot_ref.current.appendChild(plot); + } + }, [histogram_type, use_imperial, relative]); + // put a button panel in the top right corner + return
+
+ {props.screenshot_mode + ? undefined + :
+ h.shortname)} /> +
+ } +
+} + +function HistogramSettings(props: { + shortnames: string[]; + plot_ref: React.RefObject +}) { + const universe = useUniverse(); + const [histogram_type, setHistogramType] = useSetting("histogram_type"); + // dropdown for histogram type + return
+ { + if (props.plot_ref.current) { + create_screenshot( + { + path: props.shortnames.join("_") + "_histogram", + overall_width: props.plot_ref.current.offsetWidth * 2, + elements_to_render: [props.plot_ref.current], + height_multiplier: 1.2, + }, + universe + ) + } + }} + width="20" height="20" + /> + + +
+} + +function histogramBounds(histograms: HistogramProps[]): [number, number] { + let x_idx_end = Math.max(...histograms.map(histogram => histogram.histogram.counts!.length)); + x_idx_end += 1; + const zeros_at_front = (arr: number[]) => { + let i = 0; + while (i < arr.length && arr[i] === 0) { + i++; + } + return i; + } + let x_idx_start = Math.min(...histograms.map(histogram => zeros_at_front(histogram.histogram.counts!))); + + if (x_idx_start > 0) { + x_idx_start--; + } + + // round x_idx_start down to the nearest number which, when divided by 10, has a remainder of 0, 3, or 7 + + while (x_idx_start % 10 !== 0 && x_idx_start % 10 !== 3 && x_idx_start % 10 !== 7) { + x_idx_start--; + } + + // same for x_idx_end + while (x_idx_end % 10 !== 0 && x_idx_end % 10 !== 3 && x_idx_end % 10 !== 7) { + x_idx_end++; + } + + return [x_idx_start, x_idx_end]; +} + +function mulitipleSeriesConsistentLength(histograms: HistogramProps[], xidxs: number[], relative: boolean, is_cumulative: boolean) { + // Create a list of series, each with the same length + const sum = (arr: number[]) => arr.reduce((a, b) => a + b, 0); + const sum_each = histograms.map(histogram => sum(histogram.histogram.counts!)); + const series = histograms.map((histogram, histogram_idx) => { + const counts = [...histogram.histogram.counts!]; + const after_val = 0; + if (is_cumulative) { + for (let i = counts.length - 2; i >= 0; i--) { + counts[i] += counts[i + 1]; + } + } + return { + values: xidxs.map(xidx => ({ + name: histogram.shortname, + xidx, + y: ( + xidx >= counts.length ? + after_val + : + counts[xidx] / sum_each[histogram_idx] + ) * (relative ? 100 : histogram.universe_total) + })), + color: histogram.color + }; + }); + return series; +} + +function dovetailSequences(series: { values: { xidx: number, y: number, name: string }[], color: string }[]) { + const series_single: { xidx_left: number, xidx_right: number, y: number, color: string }[] = []; + for (let i = 0; i < series.length; i++) { + const s = series[i]; + const width = 1 / (series.length) * 0.8; + const off = (i - (series.length - 1) / 2) * width; + series_single.push(... + s.values + .map(v => ({ + xidx_left: v.xidx + off, xidx_right: v.xidx + off + width, + y: v.y, color: s.color, name: v.name + })) + ) + } + return series_single; +} + +function maxSequences(series: { values: { xidx: number, y: number, name: string }[] }[]) { + const series_max: { xidx: number, y: number, names: string[], ys: number[] }[] = []; + for (let i = 0; i < series[0].values.length; i++) { + series_max.push({ + xidx: series[0].values[i].xidx, + y: Math.max(...series.map(s => s.values[i].y)), + names: series.map(s => s.values[i].name), + ys: series.map(s => s.values[i].y) + }); + } + return series_max; +} + +function x_axis(xidxs: number[], binSize: number, binMin: number, use_imperial: boolean): [Plot.Markish[], (x: number) => string] { + const x_keypoints: number[] = []; + for (const xidx of xidxs) { + let last_digit = xidx % 10; + if (use_imperial) { + last_digit = (last_digit + 4) % 10; + } + if (last_digit == 0 || last_digit == 3 || last_digit == 7) { + x_keypoints.push(xidx); + } + } + const adjustment = use_imperial ? Math.log10(1.60934) * 2 : 0; + return [ + [ + Plot.axisX(x_keypoints, { tickFormat: d => render_pow10(d * binSize + binMin + adjustment) }), + Plot.gridX(x_keypoints) + ], + x => render_number_highly_rounded(Math.pow(10, x * binSize + binMin + adjustment), 2) + "/" + (use_imperial ? "mi" : "km") + "²" + ] +} + +function y_axis(max_value: number) { + const MIN_N_Y_TICKS = 5; + const ideal_tick_gap = max_value / MIN_N_Y_TICKS; + const log10_tick_gap_times_3 = Math.floor(Math.log10(ideal_tick_gap) * 3); + const tick_gap_oom = Math.pow(10, Math.floor(log10_tick_gap_times_3 / 3)); + const tick_gap_mantissa = log10_tick_gap_times_3 % 3 == 0 ? 1 : log10_tick_gap_times_3 % 3 == 1 ? 2 : 5; + const tick_gap = tick_gap_mantissa * tick_gap_oom; + const max_value_rounded = Math.ceil(max_value / tick_gap) * tick_gap; + const y_keypoints = Array.from({ length: Math.floor(max_value_rounded / tick_gap) + 1 }, (_, i) => i * tick_gap); + + return [ + Plot.axisY(y_keypoints, { tickFormat: d => render_number_highly_rounded(d, 1) }), + Plot.gridY(y_keypoints), + ] +} + +function pow10_moral(x: number): number { + // 10 ** x, but "morally" so, i.e., 10 ** 0.3 = 2 + if (x < 0) { + return 1 / pow10_moral(-x); + } + if (x >= 1) { + return 10 ** Math.floor(x) * pow10_moral(x - Math.floor(x)); + } + const x10 = x * 10; + const error_round = Math.abs(x10 - Math.round(x10)); + if (error_round > 0.2) { + return 10 ** x; + } + if (Math.round(x10) == 0) { + return 1; + } + if (Math.round(x10) == 3) { + return 2; + } + if (Math.round(x10) == 7) { + return 5; + } + return 10 ** x; +} + +function render_pow10(x: number) { + const p10 = pow10_moral(x); + + return render_number_highly_rounded(p10); +} + +function render_number_highly_rounded(x: number, places = 0) { + if (x < 1000) { + return x.toFixed(0); + } + if (x < 1e6) { + return (x / 1e3).toFixed(places) + "k"; + } + if (x < 1e9) { + return (x / 1e6).toFixed(places) + "M"; + } + if (x < 1e12) { + return (x / 1e9).toFixed(places) + "B"; + } + return x.toExponential(1); +} + +function createHistogramMarks( + histograms: HistogramProps[], xidxs: number[], + histogram_type: HistogramType, relative: boolean, + render_x: (x: number) => string, + render_y: (y: number) => string +): [Plot.Markish[], number] { + const series = mulitipleSeriesConsistentLength(histograms, xidxs, relative, histogram_type === "Line (cumulative)"); + const series_single = dovetailSequences(series); + + const max_value = Math.max(...series.map(s => Math.max(...s.values.map(v => v.y)))); + const tip = Plot.tip(maxSequences(series), Plot.pointerX({ + x: "xidx", y: "y", + title: (d) => { + + let result = "Density: " + render_x(d.xidx) + "\n"; + if (d.names.length > 1) { + result += d.names.map((name: string, i: number) => `${name}: ${render_y(d.ys[i])}`).join("\n") + } else { + result += `Frequency: ${render_y(d.ys[0])}`; + } + return result; + }, + })) + const color = histograms.length === 1 ? histograms[0].color : "name"; + const marks: Plot.Markish[] = []; + if (histogram_type === "Line" || histogram_type === "Line (cumulative)") { + marks.push( + ...series.map(s => Plot.line(s.values, { + x: "xidx", y: "y", stroke: color, strokeWidth: 4 + })), + ); + } else if (histogram_type === "Bar") { + marks.push( + Plot.rectY(series_single, { + x1: "xidx_left", + x2: "xidx_right", + y: "y", + fill: color, + }) + ); + } else { + throw new Error("histogram_type not recognized: " + histogram_type); + } + marks.push(tip); + return [marks, max_value]; +} \ No newline at end of file diff --git a/react/src/components/screenshot.tsx b/react/src/components/screenshot.tsx index 0be66ae52..436ea10e2 100644 --- a/react/src/components/screenshot.tsx +++ b/react/src/components/screenshot.tsx @@ -51,24 +51,27 @@ export function ScreenshotButton(props: { screenshot_mode: boolean, onClick: () export interface ScreencapElements { path: string, overall_width: number, - elements_to_render: HTMLElement[] + elements_to_render: HTMLElement[], + height_multiplier?: number, } export async function create_screenshot(config: ScreencapElements, universe: string | undefined) { const overall_width = config.overall_width; + const height_multiplier = config.height_multiplier ?? 1; async function screencap_element(ref: HTMLElement): Promise<[string, number]> { + console.log("Processing element", ref); const scale_factor = overall_width / ref.offsetWidth; const link = await domtoimage.toPng(ref, { bgcolor: "#fff8f0", - height: ref.offsetHeight * scale_factor, + height: ref.offsetHeight * scale_factor * height_multiplier, width: ref.offsetWidth * scale_factor, style: { transform: "scale(" + scale_factor + ")", transformOrigin: "top left" } }); - return [link, scale_factor * ref.offsetHeight] + return [link, scale_factor * ref.offsetHeight * height_multiplier] } const png_links = []; diff --git a/react/src/components/sidebar.tsx b/react/src/components/sidebar.tsx index 6715a5e1f..f4623faea 100644 --- a/react/src/components/sidebar.tsx +++ b/react/src/components/sidebar.tsx @@ -97,7 +97,10 @@ export function Sidebar() { ); } -export function CheckboxSetting(props: { name: string, setting_key: K, classNameToUse?: string }) { +// type representing a key of SettingsDictionary that have boolean values +type BooleanSettingKey = keyof { [K in keyof SettingsDictionary as SettingsDictionary[K] extends boolean ? K : never]: boolean }; + +export function CheckboxSetting(props: { name: string, setting_key: K, classNameToUse?: string }) { const [checked, setChecked] = useSetting(props.setting_key); diff --git a/react/src/components/table.tsx b/react/src/components/table.tsx index 1804e6378..f6838e66e 100644 --- a/react/src/components/table.tsx +++ b/react/src/components/table.tsx @@ -7,8 +7,9 @@ import "./table.css"; import { is_historical_cd } from '../utils/is_historical'; import { display_type } from '../utils/text'; import { ArticleRow } from './load-article'; -import { useSetting } from '../page_template/settings'; +import { row_expanded_key, useSetting } from '../page_template/settings'; import { useUniverse } from '../universe'; +import { WithPlot } from './plots'; const table_row_style: React.CSSProperties = { display: "flex", @@ -31,14 +32,21 @@ export type StatisticRowRawProps = { } ) -export function StatisticRowRaw(props: StatisticRowRawProps & { index: number, longname?: string }) { +export function StatisticRowRaw(props: StatisticRowRawProps & { index: number, longname?: string, shortname?: string, screenshot_mode: boolean }) { - const cell_contents = StatisticRowRawCellContents({ ...props, total_width: 100 }); + const [expanded] = useSetting(row_expanded_key(props.is_header ? "header" : props.statname)); + + const cell_contents = StatisticRowRawCellContents({ ...props, total_width: 100, screenshot_mode: props.screenshot_mode }); + + return + + ; - return ; } -export function StatisticRowRawCellContents(props: StatisticRowRawProps & { total_width: number, longname?: string }) { +export function StatisticRowRawCellContents(props: StatisticRowRawProps & { + total_width: number, longname?: string, screenshot_mode: boolean +}) { const curr_universe = useUniverse(); const alignStyle: React.CSSProperties = { textAlign: props.is_header ? "center" : "right" }; let value_columns: [number, string, React.ReactNode][] = [ @@ -82,13 +90,16 @@ export function StatisticRowRawCellContents(props: StatisticRowRawProps & { tota "statname", { props.is_header ? "Statistic" : - {props.rendered_statname} + } ], @@ -177,6 +188,23 @@ export function StatisticRowRawCellContents(props: StatisticRowRawProps & { tota return contents; } +export function StatisticName(props: { + statname: string, article_type: string, ordinal: number, + longname: string, rendered_statname: string, + curr_universe: string, + use_toggle: boolean, + screenshot_mode: boolean +}) { + const link = {props.rendered_statname} + return link; +} + export function StatisticRow({ is_header, index, contents }: { is_header: boolean, index: number, contents: React.ReactNode }): React.ReactNode { return
; @@ -52,6 +61,8 @@ export function load_settings() { settings.show_historical_cds = settings.show_historical_cds ?? false settings.simple_ordinals = settings.simple_ordinals ?? false settings.use_imperial = settings.use_imperial ?? false + settings.histogram_type = settings.histogram_type ?? "Line" + settings.histogram_relative = settings.histogram_relative ?? true return [settings as SettingsDictionary, statistic_category_metadata_checkboxes] as const; } diff --git a/react/test/comparison_test.js b/react/test/comparison_test.js index fe241cbba..7438cb55d 100644 --- a/react/test/comparison_test.js +++ b/react/test/comparison_test.js @@ -3,11 +3,11 @@ import { Selector } from 'testcafe'; import { TARGET, getLocation, comparison_page, screencap, download_image } from './test_utils'; -const upper_sgv = "Upper San Gabriel Valley CCD [CCD], Los Angeles County, California, USA" -const pasadena = "Pasadena CCD [CCD], Los Angeles County, California, USA" -const sw_sgv = "Southwest San Gabriel Valley CCD [CCD], Los Angeles County, California, USA" -const east_sgv = "East San Gabriel Valley CCD [CCD], Los Angeles County, California, USA" -const chicago = "Chicago city [CCD], Cook County, Illinois, USA" +export const upper_sgv = "Upper San Gabriel Valley CCD [CCD], Los Angeles County, California, USA" +export const pasadena = "Pasadena CCD [CCD], Los Angeles County, California, USA" +export const sw_sgv = "Southwest San Gabriel Valley CCD [CCD], Los Angeles County, California, USA" +export const east_sgv = "East San Gabriel Valley CCD [CCD], Los Angeles County, California, USA" +export const chicago = "Chicago city [CCD], Cook County, Illinois, USA" fixture('comparison test heterogenous') .page(comparison_page(["San Marino city, California, USA", pasadena, sw_sgv]))