From bde5a98a3b0bf8c2c9cecbea00b9d0bc562b5af4 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 21:12:18 +1100 Subject: [PATCH 01/38] wip --- apps/registry/app/components/Menu.js | 10 ++++++++++ apps/registry/app/similarity/page.js | 12 ++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 apps/registry/app/similarity/page.js diff --git a/apps/registry/app/components/Menu.js b/apps/registry/app/components/Menu.js index 8b5f956..7524346 100644 --- a/apps/registry/app/components/Menu.js +++ b/apps/registry/app/components/Menu.js @@ -44,6 +44,16 @@ export default function Menu({ session }) { > Jobs + + Similarity + +

Similarity

+

Coming soon...

+ + ); +} From 7b40c4eecd949ec6302712412635720333424fd7 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 21:22:27 +1100 Subject: [PATCH 02/38] wip --- apps/registry/app/api/similarity/route.js | 58 + apps/registry/app/similarity/page.js | 137 +- apps/registry/package.json | 4 +- pnpm-lock.yaml | 1513 ++++++++++++++++++++- 4 files changed, 1707 insertions(+), 5 deletions(-) create mode 100644 apps/registry/app/api/similarity/route.js diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js new file mode 100644 index 0000000..2fa0270 --- /dev/null +++ b/apps/registry/app/api/similarity/route.js @@ -0,0 +1,58 @@ +import { createClient } from '@supabase/supabase-js'; +import { NextResponse } from 'next/server'; + +const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; + +// This ensures the route is always dynamic +export const dynamic = 'force-dynamic'; + +export async function GET(request) { + // During build time or when SUPABASE_KEY is not available + if (!process.env.SUPABASE_KEY) { + return NextResponse.json( + { message: 'API not available during build' }, + { status: 503 } + ); + } + + try { + const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); + const { searchParams } = new URL(request.url); + const limit = parseInt(searchParams.get('limit')) || 100; + + console.time('getSimilarityData'); + const { data, error } = await supabase + .from('resumes') + .select('username, embedding') + .not('embedding', 'is', null) + .limit(limit); + + if (error) { + console.error('Error fetching similarity data:', error); + return NextResponse.json( + { message: 'Error fetching similarity data' }, + { status: 500 } + ); + } + + console.timeEnd('getSimilarityData'); + + // Parse embeddings from strings to numerical arrays + const parsedData = data.map(item => ({ + ...item, + embedding: typeof item.embedding === 'string' + ? JSON.parse(item.embedding) + : Array.isArray(item.embedding) + ? item.embedding + : null + })).filter(item => item.embedding !== null); + + return NextResponse.json(parsedData); + } catch (error) { + console.error('Error in similarity endpoint:', error); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index ae182e8..8753aff 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -1,12 +1,143 @@ 'use client'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; +import { PrismaClient } from '@prisma/client'; + +// Import Plotly dynamically to avoid SSR issues +const Plot = dynamic(() => import('react-plotly.js'), { ssr: false }); + +// Helper function to compute cosine similarity +function cosineSimilarity(a, b) { + let dotProduct = 0; + let normA = 0; + let normB = 0; + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +// Helper function for dimensionality reduction using PCA +function pca(vectors, dimensions = 2) { + // Center the data + const mean = vectors[0].map((_, colIndex) => + vectors.reduce((sum, row) => sum + row[colIndex], 0) / vectors.length + ); + + const centered = vectors.map(vector => + vector.map((value, index) => value - mean[index]) + ); + + // Compute covariance matrix + const covMatrix = []; + for (let i = 0; i < centered[0].length; i++) { + covMatrix[i] = []; + for (let j = 0; j < centered[0].length; j++) { + let sum = 0; + for (let k = 0; k < centered.length; k++) { + sum += centered[k][i] * centered[k][j]; + } + covMatrix[i][j] = sum / (centered.length - 1); + } + } + + // For simplicity, we'll just take the first two dimensions + // In a production environment, you'd want to compute eigenvectors properly + const reduced = centered.map(vector => [ + vector.slice(0, dimensions).reduce((sum, val) => sum + val, 0), + vector.slice(dimensions, dimensions * 2).reduce((sum, val) => sum + val, 0) + ]); + + return reduced; +} export default function SimilarityPage() { + const [data, setData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + async function fetchData() { + try { + const response = await fetch('/api/similarity'); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const jsonData = await response.json(); + + // Reduce dimensions of embeddings using PCA + const reducedEmbeddings = pca(jsonData.map(item => item.embedding)); + + // Prepare data for plotting + const plotData = { + x: reducedEmbeddings.map(coords => coords[0]), + y: reducedEmbeddings.map(coords => coords[1]), + text: jsonData.map(item => item.username), + mode: 'markers+text', + type: 'scatter', + textposition: 'top', + marker: { + size: 10, + color: reducedEmbeddings.map((_, i) => i), + colorscale: 'Viridis', + }, + }; + + setData(plotData); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + if (loading) { + return ( +
+

Resume Similarity Map

+

Loading...

+
+ ); + } + + if (error) { + return ( +
+

Resume Similarity Map

+

Error: {error}

+
+ ); + } + return (
-

Similarity

-

Coming soon...

+

Resume Similarity Map

+

+ This visualization shows how similar resumes are to each other based on their content. + Resumes that are closer together are more similar. +

+
+ +
); } diff --git a/apps/registry/package.json b/apps/registry/package.json index 7371bd0..5831ee6 100644 --- a/apps/registry/package.json +++ b/apps/registry/package.json @@ -14,10 +14,10 @@ "dependencies": { "@ai-sdk/openai": "^0.0.14", "@faker-js/faker": "^8.0.2", + "@jsonresume/jsonresume-theme-professional": "workspace:*", "@jsonresume/schema": "^1.2.0", "@jsonresume/theme-papirus": "workspace:*", "@jsonresume/theme-stackoverflow": "^2", - "@jsonresume/jsonresume-theme-professional": "workspace:*", "@monaco-editor/react": "^4.6.0", "@pinecone-database/pinecone": "^0.1.6", "@prisma/client": "^4.15.0", @@ -93,6 +93,7 @@ "openai": "^4.0.0", "pg": "^8.11.0", "pinecone-client": "^1.1.0", + "plotly.js-dist": "^2.35.3", "postcss": "^8.4.39", "prisma": "^4.15.0", "prismjs": "^1.29.0", @@ -101,6 +102,7 @@ "react-autocomplete": "^1.8.1", "react-dom": "^18.3.1", "react-markdown": "^8.0.7", + "react-plotly.js": "^2.6.0", "react-speech-recognition": "^3.10.0", "recharts": "^2.12.7", "resume-schema": "^1.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d53d71..8258587 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -423,6 +423,9 @@ importers: pinecone-client: specifier: ^1.1.0 version: 1.1.0 + plotly.js-dist: + specifier: ^2.35.3 + version: 2.35.3 postcss: specifier: ^8.4.39 version: 8.4.39 @@ -447,6 +450,9 @@ importers: react-markdown: specifier: ^8.0.7 version: 8.0.7(@types/react@18.3.3)(react@18.3.1) + react-plotly.js: + specifier: ^2.6.0 + version: 2.6.0(plotly.js@2.35.3)(react@18.3.1) react-speech-recognition: specifier: ^3.10.0 version: 3.10.0(react@18.3.1) @@ -4573,6 +4579,13 @@ packages: prettier: 2.8.0 dev: false + /@choojs/findup@0.2.1: + resolution: {integrity: sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==} + hasBin: true + dependencies: + commander: 2.20.3 + dev: false + /@chronobserver/htmls@0.4.4: resolution: {integrity: sha512-50UaAsvJCP1Uocf5un8qrvjF23TL/mh72gfDQCPflUr4Bl6BIVzRS38hLjEvrliRkWA7435W52zvjlLi9c+FMg==} dependencies: @@ -5659,6 +5672,75 @@ packages: read-yaml-file: 1.1.0 dev: false + /@mapbox/geojson-rewind@0.5.2: + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} + hasBin: true + dependencies: + get-stream: 6.0.1 + minimist: 1.2.8 + dev: false + + /@mapbox/geojson-types@1.0.2: + resolution: {integrity: sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==} + dev: false + + /@mapbox/jsonlint-lines-primitives@2.0.2: + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + dev: false + + /@mapbox/mapbox-gl-supported@1.5.0(mapbox-gl@1.13.3): + resolution: {integrity: sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==} + peerDependencies: + mapbox-gl: '>=0.32.1 <2.0.0' + dependencies: + mapbox-gl: 1.13.3 + dev: false + + /@mapbox/point-geometry@0.1.0: + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + dev: false + + /@mapbox/tiny-sdf@1.2.5: + resolution: {integrity: sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==} + dev: false + + /@mapbox/tiny-sdf@2.0.6: + resolution: {integrity: sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==} + dev: false + + /@mapbox/unitbezier@0.0.0: + resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} + dev: false + + /@mapbox/unitbezier@0.0.1: + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + dev: false + + /@mapbox/vector-tile@1.3.1: + resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} + dependencies: + '@mapbox/point-geometry': 0.1.0 + dev: false + + /@mapbox/whoots-js@3.1.0: + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + dev: false + + /@maplibre/maplibre-gl-style-spec@20.4.0: + resolution: {integrity: sha512-AzBy3095fTFPjDjmWpR2w6HVRAZJ6hQZUCwk5Plz6EyfnfuQW1odeW5i2Ai47Y6TBA2hQnC+azscjBSALpaWgw==} + hasBin: true + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 4.0.0 + minimist: 1.2.8 + quickselect: 2.0.0 + rw: 1.3.3 + tinyqueue: 3.0.0 + dev: false + /@mdx-js/react@3.1.0(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ==} peerDependencies: @@ -6935,6 +7017,76 @@ packages: dependencies: playwright: 1.40.0 + /@plotly/d3-sankey-circular@0.33.1: + resolution: {integrity: sha512-FgBV1HEvCr3DV7RHhDsPXyryknucxtfnLwPtCKKxdolKyTFYoLX/ibEfX39iFYIL7DYbVeRtP43dbFcrHNE+KQ==} + dependencies: + d3-array: 1.2.4 + d3-collection: 1.0.7 + d3-shape: 1.3.7 + elementary-circuits-directed-graph: 1.3.1 + dev: false + + /@plotly/d3-sankey@0.7.2: + resolution: {integrity: sha512-2jdVos1N3mMp3QW0k2q1ph7Gd6j5PY1YihBrwpkFnKqO+cqtZq3AdEYUeSGXMeLsBDQYiqTVcihYfk8vr5tqhw==} + dependencies: + d3-array: 1.2.4 + d3-collection: 1.0.7 + d3-shape: 1.3.7 + dev: false + + /@plotly/d3@3.8.2: + resolution: {integrity: sha512-wvsNmh1GYjyJfyEBPKJLTMzgf2c2bEbSIL50lmqVUi+o1NHaLPi1Lb4v7VxXXJn043BhNyrxUrWI85Q+zmjOVA==} + dev: false + + /@plotly/mapbox-gl@1.13.4(mapbox-gl@1.13.3): + resolution: {integrity: sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==} + engines: {node: '>=6.4.0'} + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/geojson-types': 1.0.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 1.2.5 + '@mapbox/unitbezier': 0.0.0 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + csscolorparser: 1.0.3 + earcut: 2.2.4 + geojson-vt: 3.2.1 + gl-matrix: 3.4.3 + grid-index: 1.1.0 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 1.0.2 + quickselect: 2.0.0 + rw: 1.3.3 + supercluster: 7.1.5 + tinyqueue: 2.0.3 + vt-pbf: 3.1.3 + transitivePeerDependencies: + - mapbox-gl + dev: false + + /@plotly/point-cluster@3.1.9: + resolution: {integrity: sha512-MwaI6g9scKf68Orpr1pHZ597pYx9uP8UEFXLPbsCmuw3a84obwz6pnMXGc90VhgDNeNiLEdlmuK7CPo+5PIxXw==} + dependencies: + array-bounds: 1.0.1 + binary-search-bounds: 2.0.5 + clamp: 1.0.1 + defined: 1.0.1 + dtype: 2.0.0 + flatten-vertex-data: 1.0.2 + is-obj: 1.0.1 + math-log2: 1.0.1 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + dev: false + + /@plotly/regl@2.1.2: + resolution: {integrity: sha512-Mdk+vUACbQvjd0m/1JJjOOafmkp/EpmHjISsopEz5Av44CBq7rPC05HHNbYGKVyNUF2zmEoBS/TT0pd0SPFFyw==} + dev: false + /@pmmmwh/react-refresh-webpack-plugin@0.5.11(react-refresh@0.11.0)(webpack-dev-server@4.15.1)(webpack@5.89.0): resolution: {integrity: sha512-7j/6vdTym0+qZ6u4XbSAxrWBGYSdCfTzySkj7WAFgDLmSyWlOrWvpyzxlFh5jtw9dn0oL/jtW+06XfFiisN3JQ==} engines: {node: '>= 10.13'} @@ -8985,6 +9137,47 @@ packages: resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + /@turf/area@7.1.0: + resolution: {integrity: sha512-w91FEe02/mQfMPRX2pXua48scFuKJ2dSVMF2XmJ6+BJfFiCPxp95I3+Org8+ZsYv93CDNKbf0oLNEPnuQdgs2g==} + dependencies: + '@turf/helpers': 7.1.0 + '@turf/meta': 7.1.0 + '@types/geojson': 7946.0.15 + tslib: 2.6.3 + dev: false + + /@turf/bbox@7.1.0: + resolution: {integrity: sha512-PdWPz9tW86PD78vSZj2fiRaB8JhUHy6piSa/QXb83lucxPK+HTAdzlDQMTKj5okRCU8Ox/25IR2ep9T8NdopRA==} + dependencies: + '@turf/helpers': 7.1.0 + '@turf/meta': 7.1.0 + '@types/geojson': 7946.0.15 + tslib: 2.6.3 + dev: false + + /@turf/centroid@7.1.0: + resolution: {integrity: sha512-1Y1b2l+ZB1CZ+ITjUCsGqC4/tSjwm/R4OUfDztVqyyCq/VvezkLmTNqvXTGXgfP0GXkpv68iCfxF5M7QdM5pJQ==} + dependencies: + '@turf/helpers': 7.1.0 + '@turf/meta': 7.1.0 + '@types/geojson': 7946.0.15 + tslib: 2.6.3 + dev: false + + /@turf/helpers@7.1.0: + resolution: {integrity: sha512-dTeILEUVeNbaEeoZUOhxH5auv7WWlOShbx7QSd4s0T4Z0/iz90z9yaVCtZOLbU89umKotwKaJQltBNO9CzVgaQ==} + dependencies: + '@types/geojson': 7946.0.15 + tslib: 2.6.3 + dev: false + + /@turf/meta@7.1.0: + resolution: {integrity: sha512-ZgGpWWiKz797Fe8lfRj7HKCkGR+nSJ/5aKXMyofCvLSc2PuYJs/qyyifDPWjASQQCzseJ7AlF2Pc/XQ/3XkkuA==} + dependencies: + '@turf/helpers': 7.1.0 + '@types/geojson': 7946.0.15 + dev: false + /@types/aria-query@5.0.4: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: true @@ -9162,6 +9355,16 @@ packages: '@types/qs': 6.9.15 '@types/serve-static': 1.15.5 + /@types/geojson-vt@3.2.5: + resolution: {integrity: sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==} + dependencies: + '@types/geojson': 7946.0.15 + dev: false + + /@types/geojson@7946.0.15: + resolution: {integrity: sha512-9oSxFzDCT2Rj6DfcHF8G++jxBKS7mBqXl5xrRW+Kbvjry6Uduya2iiwqHPhVXpasAVMBYKkEPGgKhd3+/HZ6xA==} + dev: false + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -9226,6 +9429,18 @@ packages: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} dev: true + /@types/mapbox__point-geometry@0.1.4: + resolution: {integrity: sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==} + dev: false + + /@types/mapbox__vector-tile@1.3.4: + resolution: {integrity: sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==} + dependencies: + '@types/geojson': 7946.0.15 + '@types/mapbox__point-geometry': 0.1.4 + '@types/pbf': 3.0.5 + dev: false + /@types/mdast@3.0.11: resolution: {integrity: sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw==} dependencies: @@ -9289,6 +9504,10 @@ packages: /@types/parse-json@4.0.0: resolution: {integrity: sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==} + /@types/pbf@3.0.5: + resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==} + dev: false + /@types/phoenix@1.6.4: resolution: {integrity: sha512-B34A7uot1Cv0XtaHRYDATltAdKx0BvVKNgYNqE4WjtPUa4VQJM7kxeXcVKaH+KS+kCmZ+6w+QaUdcljiheiBJA==} dev: false @@ -9388,6 +9607,12 @@ packages: /@types/stylis@4.2.5: resolution: {integrity: sha512-1Xve+NMN7FWjY14vLoY5tL3BVEQ/n42YLwaqJIPYhotZ9uBHt87VceMwWQpzmdEt2TNXIorIFG+YeCUUW7RInw==} + /@types/supercluster@7.1.3: + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + dependencies: + '@types/geojson': 7946.0.15 + dev: false + /@types/testing-library__jest-dom@5.14.9: resolution: {integrity: sha512-FSYhIjFlfOpGSRyVoMBMuS3ws5ehFQODymf3vlI7U1K8c7PHwWwFY7VREfmsuzHSOnoKs/9/Y983ayOs7eRzqw==} dependencies: @@ -9989,6 +10214,10 @@ packages: resolution: {integrity: sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==} dev: true + /abs-svg-path@0.1.1: + resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} + dev: false + /accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -10204,6 +10433,10 @@ packages: repeat-string: 1.6.1 dev: false + /almost-equal@1.1.0: + resolution: {integrity: sha512-0V/PkoculFl5+0Lp47JoxUcO0xSxhIBvm+BxHdD/OgXNmdRpRHCFnKVuUoWyS9EzQP+otSGv0m9Lb4yVkQBn2A==} + dev: false + /amdefine@1.0.1: resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==} engines: {node: '>=0.4.2'} @@ -10545,6 +10778,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /array-bounds@1.0.1: + resolution: {integrity: sha512-8wdW3ZGk6UjMPJx/glyEt0sLzzwAE1bhToPsO1W2pbpR2gULyxe3BjSiuJFheP50T/GgODVPz2fuMUmIywt8cQ==} + dev: false + /array-buffer-byte-length@1.0.1: resolution: {integrity: sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==} engines: {node: '>= 0.4'} @@ -10552,6 +10789,11 @@ packages: call-bind: 1.0.7 is-array-buffer: 3.0.4 + /array-find-index@1.0.2: + resolution: {integrity: sha512-M1HQyIXcBGtVywBt8WVdim+lrNaK7VHp99Qt5pSNziXznKHViIBbXWtfRTpEFpF/c4FdfxNAsCCwPp5phBYJtw==} + engines: {node: '>=0.10.0'} + dev: false + /array-flatten@1.1.1: resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} @@ -10569,6 +10811,20 @@ packages: get-intrinsic: 1.2.4 is-string: 1.0.7 + /array-normalize@1.1.4: + resolution: {integrity: sha512-fCp0wKFLjvSPmCn4F5Tiw4M3lpMZoHlCjfcs7nNzuj3vqQQ1/a8cgB9DXcpDSn18c+coLnaW7rqfcYCvKbyJXg==} + dependencies: + array-bounds: 1.0.1 + dev: false + + /array-range@1.0.1: + resolution: {integrity: sha512-shdaI1zT3CVNL2hnx9c0JMc0ZogGaxDs5e85akgHWKYa0yVbIyp06Ind3dVkTj/uuFrzaHBOyqFzo+VV6aXgtA==} + dev: false + + /array-rearrange@2.2.2: + resolution: {integrity: sha512-UfobP5N12Qm4Qu4fwLDIi2v6+wZsSf6snYSxAMeKhrh37YGnNWZPRmVEKc/2wfms53TLQnzfpG8wCx2Y/6NG1w==} + dev: false + /array-sort@0.1.4: resolution: {integrity: sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ==} engines: {node: '>=0.10.0'} @@ -11141,6 +11397,11 @@ packages: safe-buffer: 5.2.1 dev: true + /base64-arraybuffer@1.0.2: + resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} + engines: {node: '>= 0.6.0'} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: true @@ -11196,6 +11457,25 @@ packages: engines: {node: '>=8'} requiresBuild: true + /binary-search-bounds@2.0.5: + resolution: {integrity: sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==} + dev: false + + /bit-twiddle@1.0.2: + resolution: {integrity: sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==} + dev: false + + /bitmap-sdf@1.0.4: + resolution: {integrity: sha512-1G3U4n5JE6RAiALMxu0p1XmeZkTeCwGKykzsLTCqVzfSDaN6S7fKnkIkfejogz+iwqBWc0UYAIKnKHNN7pSfDg==} + dev: false + + /bl@2.2.1: + resolution: {integrity: sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g==} + dependencies: + readable-stream: 2.3.8 + safe-buffer: 5.2.1 + dev: false + /bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} dependencies: @@ -11479,6 +11759,12 @@ packages: /caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + /canvas-fit@1.5.0: + resolution: {integrity: sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==} + dependencies: + element-size: 1.1.1 + dev: false + /case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} @@ -11640,6 +11926,10 @@ packages: /cjs-module-lexer@1.2.3: resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + /clamp@1.0.1: + resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==} + dev: false + /class-utils@0.3.6: resolution: {integrity: sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==} engines: {node: '>=0.10.0'} @@ -11803,6 +12093,12 @@ packages: object-visit: 1.0.1 dev: false + /color-alpha@1.0.4: + resolution: {integrity: sha512-lr8/t5NPozTSqli+duAN+x+no/2WaKTeWvxhHGN+aXT6AJ8vPlzLa7UriyjWak0pSC2jHol9JgjBYnnHsGha9A==} + dependencies: + color-parse: 1.4.3 + dev: false + /color-convert@1.9.3: resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} dependencies: @@ -11814,12 +12110,53 @@ packages: dependencies: color-name: 1.1.4 + /color-id@1.1.0: + resolution: {integrity: sha512-2iRtAn6dC/6/G7bBIo0uupVrIne1NsQJvJxZOBCzQOfk7jRq97feaDZ3RdzuHakRXXnHGNwglto3pqtRx1sX0g==} + dependencies: + clamp: 1.0.1 + dev: false + /color-name@1.1.3: resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} /color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + /color-normalize@1.5.0: + resolution: {integrity: sha512-rUT/HDXMr6RFffrR53oX3HGWkDOP9goSAQGBkUaAYKjOE2JxozccdGyufageWDlInRAjm/jYPrf/Y38oa+7obw==} + dependencies: + clamp: 1.0.1 + color-rgba: 2.1.1 + dtype: 2.0.0 + dev: false + + /color-parse@1.4.3: + resolution: {integrity: sha512-BADfVl/FHkQkyo8sRBwMYBqemqsgnu7JZAwUgvBvuwwuNUZAhSvLTbsEErS5bQXzOjDR0dWzJ4vXN2Q+QoPx0A==} + dependencies: + color-name: 1.1.4 + dev: false + + /color-parse@2.0.0: + resolution: {integrity: sha512-g2Z+QnWsdHLppAbrpcFWo629kLOnOPtpxYV69GCqm92gqSgyXbzlfyN3MXs0412fPBkFmiuS+rXposgBgBa6Kg==} + dependencies: + color-name: 1.1.4 + dev: false + + /color-rgba@2.1.1: + resolution: {integrity: sha512-VaX97wsqrMwLSOR6H7rU1Doa2zyVdmShabKrPEIFywLlHoibgD3QW9Dw6fSqM4+H/LfjprDNAUUW31qEQcGzNw==} + dependencies: + clamp: 1.0.1 + color-parse: 1.4.3 + color-space: 1.16.0 + dev: false + + /color-space@1.16.0: + resolution: {integrity: sha512-A6WMiFzunQ8KEPFmj02OnnoUnqhmSaHaZ/0LVFcPTdlvm8+3aMJ5x1HRHy3bDHPkovkf4sS0f4wsVvwk71fKkg==} + dependencies: + hsluv: 0.0.3 + mumath: 3.3.4 + dev: false + /colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} @@ -11916,6 +12253,16 @@ packages: /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /concat-stream@1.6.2: + resolution: {integrity: sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==} + engines: {'0': node >= 0.8} + dependencies: + buffer-from: 1.1.2 + inherits: 2.0.4 + readable-stream: 2.3.8 + typedarray: 0.0.6 + dev: false + /concat-with-sourcemaps@1.1.0: resolution: {integrity: sha512-4gEjHJFT9e+2W/77h/DS5SGUgwDaOwprX8L/gl5+3ixnzkVJJsZWDSelmN3Oilw3LNDZjZV0yqH1hLG3k6nghg==} dependencies: @@ -12072,6 +12419,10 @@ packages: resolution: {integrity: sha512-mcNlekdiVyiKO0lrjj3FrZ36vxlwjUwXTHpdRtVu9917SAYsHrlORmpHdPXeEXwTmw/yiKiTX4t6LC5uP5KVFg==} dev: false + /country-regex@1.1.0: + resolution: {integrity: sha512-iSPlClZP8vX7MC3/u6s3lrDuoQyhQukh5LyABJ3hvfzbQ3Yyayd4fp04zjLnfi267B/B2FkumcWWgrbban7sSA==} + dev: false + /create-frame@1.0.0: resolution: {integrity: sha512-SnJYqAwa5Jon3cP8e3LMFBoRG2m/hX20vtOnC3ynhyAa6jmy+BqrPoicBtmKUutnJuphXPj7C54yOXF58Tl71Q==} engines: {node: '>=0.10.0'} @@ -12156,6 +12507,40 @@ packages: dependencies: postcss: 8.4.39 + /css-font-size-keywords@1.0.0: + resolution: {integrity: sha512-Q+svMDbMlelgCfH/RVDKtTDaf5021O486ZThQPIpahnIjUkMUslC+WuOQSWTgGSrNCH08Y7tYNEmmy0hkfMI8Q==} + dev: false + + /css-font-stretch-keywords@1.0.1: + resolution: {integrity: sha512-KmugPO2BNqoyp9zmBIUGwt58UQSfyk1X5DbOlkb2pckDXFSAfjsD5wenb88fNrD6fvS+vu90a/tsPpb9vb0SLg==} + dev: false + + /css-font-style-keywords@1.0.1: + resolution: {integrity: sha512-0Fn0aTpcDktnR1RzaBYorIxQily85M2KXRpzmxQPgh8pxUN9Fcn00I8u9I3grNr1QXVgCl9T5Imx0ZwKU973Vg==} + dev: false + + /css-font-weight-keywords@1.0.0: + resolution: {integrity: sha512-5So8/NH+oDD+EzsnF4iaG4ZFHQ3vaViePkL1ZbZ5iC/KrsCY+WHq/lvOgrtmuOQ9pBBZ1ADGpaf+A4lj1Z9eYA==} + dev: false + + /css-font@1.2.0: + resolution: {integrity: sha512-V4U4Wps4dPDACJ4WpgofJ2RT5Yqwe1lEH6wlOOaIxMi0gTjdIijsc5FmxQlZ7ZZyKQkkutqqvULOp07l9c7ssA==} + dependencies: + css-font-size-keywords: 1.0.0 + css-font-stretch-keywords: 1.0.1 + css-font-style-keywords: 1.0.1 + css-font-weight-keywords: 1.0.0 + css-global-keywords: 1.0.1 + css-system-font-keywords: 1.0.0 + pick-by-alias: 1.2.0 + string-split-by: 1.0.0 + unquote: 1.1.1 + dev: false + + /css-global-keywords@1.0.1: + resolution: {integrity: sha512-X1xgQhkZ9n94WDwntqst5D/FKkmiU0GlJSFZSV3kLvyJ1WC5VeyoXDOuleUD+SIuH9C7W05is++0Woh0CGfKjQ==} + dev: false + /css-has-pseudo@3.0.4(postcss@8.4.39): resolution: {integrity: sha512-Vse0xpR1K9MNlp2j5w1pgWIJtm1a8qS0JwS9goFYcImjlHEmywP9VUF05aGBXzGpDJF86QXk4L0ypBmwPhGArw==} engines: {node: ^12 || ^14 || >=16} @@ -12182,6 +12567,29 @@ packages: semver: 7.6.2 webpack: 5.89.0(webpack-cli@5.1.4) + /css-loader@7.1.2(webpack@5.89.0): + resolution: {integrity: sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + '@rspack/core': 0.x || 1.x + webpack: ^5.27.0 + peerDependenciesMeta: + '@rspack/core': + optional: true + webpack: + optional: true + dependencies: + icss-utils: 5.1.0(postcss@8.4.39) + postcss: 8.4.39 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.39) + postcss-modules-local-by-default: 4.2.0(postcss@8.4.39) + postcss-modules-scope: 3.2.1(postcss@8.4.39) + postcss-modules-values: 4.0.0(postcss@8.4.39) + postcss-value-parser: 4.2.0 + semver: 7.6.2 + webpack: 5.89.0(webpack-cli@5.1.4) + dev: false + /css-minimizer-webpack-plugin@3.4.1(esbuild@0.23.0)(webpack@5.89.0): resolution: {integrity: sha512-1u6D71zeIfgngN2XNRJefc/hY7Ybsxd74Jm4qngIXyUEk7fss3VUzuHxLAq/R8NAba4QU9OUSaMZlbpRc7bM4Q==} engines: {node: '>= 12.13.0'} @@ -12249,6 +12657,10 @@ packages: nth-check: 2.1.1 dev: false + /css-system-font-keywords@1.0.0: + resolution: {integrity: sha512-1umTtVd/fXS25ftfjB71eASCrYhilmEsvDEI6wG/QplnmlfmVM5HkZ/ZX46DT5K3eblFPgLUHt5BRCb0YXkSFA==} + dev: false + /css-to-react-native@3.2.0: resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} dependencies: @@ -12307,6 +12719,10 @@ packages: source-map-resolve: 0.6.0 dev: true + /csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + dev: false + /cssdb@7.9.0: resolution: {integrity: sha512-WPMT9seTQq6fPAa1yN4zjgZZeoTriSN2LqW9C+otjar12DQIWA4LuSfFrvFJiKp4oD0xIk1vumDLw8K9ur4NBw==} @@ -12414,6 +12830,10 @@ packages: stream-transform: 2.1.3 dev: false + /d3-array@1.2.4: + resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} + dev: false + /d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} @@ -12421,21 +12841,62 @@ packages: internmap: 2.0.3 dev: false + /d3-collection@1.0.7: + resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} + dev: false + /d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} dev: false + /d3-dispatch@1.0.6: + resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} + dev: false + /d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} dev: false + /d3-force@1.2.1: + resolution: {integrity: sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==} + dependencies: + d3-collection: 1.0.7 + d3-dispatch: 1.0.6 + d3-quadtree: 1.0.7 + d3-timer: 1.0.10 + dev: false + + /d3-format@1.4.5: + resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} + dev: false + /d3-format@3.1.0: resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} engines: {node: '>=12'} dev: false + /d3-geo-projection@2.9.0: + resolution: {integrity: sha512-ZULvK/zBn87of5rWAfFMc9mJOipeSo57O+BBitsKIXmU4rTVAnX1kSsJkE0R+TxY8pGNoM1nbyRRE7GYHhdOEQ==} + hasBin: true + dependencies: + commander: 2.20.3 + d3-array: 1.2.4 + d3-geo: 1.12.1 + resolve: 1.22.8 + dev: false + + /d3-geo@1.12.1: + resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==} + dependencies: + d3-array: 1.2.4 + dev: false + + /d3-hierarchy@1.1.9: + resolution: {integrity: sha512-j8tPxlqh1srJHAtxfvOUwKNYJkQuBFdM1+JAUfq6xqH5eAqf93L7oG1NVqDa4CpFZNvnNKtCYEUC8KY9yEn9lQ==} + dev: false + /d3-interpolate@3.0.1: resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} engines: {node: '>=12'} @@ -12443,11 +12904,19 @@ packages: d3-color: 3.1.0 dev: false + /d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + dev: false + /d3-path@3.1.0: resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} engines: {node: '>=12'} dev: false + /d3-quadtree@1.0.7: + resolution: {integrity: sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==} + dev: false + /d3-scale@4.0.2: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} @@ -12459,6 +12928,12 @@ packages: d3-time-format: 4.1.0 dev: false + /d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + dependencies: + d3-path: 1.0.9 + dev: false + /d3-shape@3.2.0: resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} engines: {node: '>=12'} @@ -12466,6 +12941,12 @@ packages: d3-path: 3.1.0 dev: false + /d3-time-format@2.3.0: + resolution: {integrity: sha512-guv6b2H37s2Uq/GefleCDtbe0XZAuy7Wa49VGkPVPMfLL9qObgBST3lEHJBMUp8S7NdLQAGIvr2KXk8Hc98iKQ==} + dependencies: + d3-time: 1.1.0 + dev: false + /d3-time-format@4.1.0: resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} engines: {node: '>=12'} @@ -12473,6 +12954,10 @@ packages: d3-time: 3.1.0 dev: false + /d3-time@1.1.0: + resolution: {integrity: sha512-Xh0isrZ5rPYYdqhAVk8VLnMEidhz5aP7htAADH6MfzgmmicPkTo8LhkLxci61/lCB7n7UmE3bN0leRt+qvkLxA==} + dev: false + /d3-time@3.1.0: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} @@ -12480,11 +12965,23 @@ packages: d3-array: 3.2.4 dev: false + /d3-timer@1.0.10: + resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==} + dev: false + /d3-timer@3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} dev: false + /d@1.0.2: + resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} + engines: {node: '>=0.12'} + dependencies: + es5-ext: 0.10.64 + type: 2.7.3 + dev: false + /damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -12741,6 +13238,10 @@ packages: isobject: 3.0.1 dev: false + /defined@1.0.1: + resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==} + dev: false + /defu@6.1.3: resolution: {integrity: sha512-Vy2wmG3NTkmHNg/kzpuvHhkqeIx3ODWqasgCRbKtbXEN0G+HpEEv9BtJLp7ZG1CZloFaC41Ah3ZFbq7aqCqMeQ==} dev: true @@ -12769,6 +13270,10 @@ packages: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} engines: {node: '>=8'} + /detect-kerning@2.1.2: + resolution: {integrity: sha512-I3JIbrnKPAntNLl1I6TpSQQdQ4AutYzv/sKMFKbepawV/hlH0GmYKhUoOEMd4xqaUHT+Bm0f4127lh5qs1m1tw==} + dev: false + /detect-libc@1.0.3: resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} engines: {node: '>=0.10'} @@ -13016,9 +13521,42 @@ packages: engines: {node: '>=6'} dev: true + /draw-svg-path@1.0.0: + resolution: {integrity: sha512-P8j3IHxcgRMcY6sDzr0QvJDLzBnJJqpTG33UZ2Pvp8rw0apCHhJCWqYprqrXjrgHnJ6tuhP1iTJSAodPDHxwkg==} + dependencies: + abs-svg-path: 0.1.1 + normalize-svg-path: 0.1.0 + dev: false + + /dtype@2.0.0: + resolution: {integrity: sha512-s2YVcLKdFGS0hpFqJaTwscsyt0E8nNFdmo73Ocd81xNPj4URI4rj6D60A+vFMIw7BXWlb4yRkEwfBqcZzPGiZg==} + engines: {node: '>= 0.8.0'} + dev: false + + /dup@1.0.0: + resolution: {integrity: sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==} + dev: false + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} + /duplexify@3.7.1: + resolution: {integrity: sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==} + dependencies: + end-of-stream: 1.4.4 + inherits: 2.0.4 + readable-stream: 2.3.8 + stream-shift: 1.0.3 + dev: false + + /earcut@2.2.4: + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + dev: false + + /earcut@3.0.1: + resolution: {integrity: sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==} + dev: false + /eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} dev: true @@ -13051,6 +13589,16 @@ packages: /electron-to-chromium@1.5.63: resolution: {integrity: sha512-ddeXKuY9BHo/mw145axlyWjlJ1UBt4WK3AlvkT7W2AbqfRQoacVoRUCF6wL3uIx/8wT9oLKXzI+rFqHHscByaA==} + /element-size@1.1.1: + resolution: {integrity: sha512-eaN+GMOq/Q+BIWy0ybsgpcYImjGIdNLyjLFJU4XsLHXYQao5jCNb36GyN6C2qwmDDYSfIBmKpPpr4VnBdLCsPQ==} + dev: false + + /elementary-circuits-directed-graph@1.3.1: + resolution: {integrity: sha512-ZEiB5qkn2adYmpXGnJKkxT8uJHlW/mxmBpmeqawEHzPxh9HkLD4/1mFYX5l0On+f6rcPIt8/EWlRU2Vo3fX6dQ==} + dependencies: + strongly-connected-components: 1.0.1 + dev: false + /email-scramble@2.0.1: resolution: {integrity: sha512-AYTUVD/Ac2BsCoOqCWaW01s/RPzNfwruLyjKYr01Q02SGLAWHDu9bsgm23FwJhS599jBSf9do8buc06o7DyP6A==} dev: false @@ -13091,6 +13639,12 @@ packages: engines: {node: '>= 0.8'} dev: true + /end-of-stream@1.4.4: + resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + dependencies: + once: 1.4.0 + dev: false + /endent@2.1.0: resolution: {integrity: sha512-r8VyPX7XL8U01Xgnb1CjZ3XV+z90cXIJ9JPE/R9SEC9vpw2P6CfsRPJmp20DppC5N7ZAMCmjYkJIa744Iyg96w==} dependencies: @@ -13287,6 +13841,42 @@ packages: is-date-object: 1.0.5 is-symbol: 1.0.4 + /es5-ext@0.10.64: + resolution: {integrity: sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==} + engines: {node: '>=0.10'} + requiresBuild: true + dependencies: + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + esniff: 2.0.1 + next-tick: 1.1.0 + dev: false + + /es6-iterator@2.0.3: + resolution: {integrity: sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-symbol: 3.1.4 + dev: false + + /es6-symbol@3.1.4: + resolution: {integrity: sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==} + engines: {node: '>=0.12'} + dependencies: + d: 1.0.2 + ext: 1.7.0 + dev: false + + /es6-weak-map@2.0.3: + resolution: {integrity: sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + es6-iterator: 2.0.3 + es6-symbol: 3.1.4 + dev: false + /esbuild-register@3.5.0(esbuild@0.19.8): resolution: {integrity: sha512-+4G/XmakeBAsvJuDugJvtyF1x+XJT4FMocynNpxrvEBViirpfUn2PgNpCHedfWhF4WokNsO/OvMKrmJOIJsI5A==} peerDependencies: @@ -13884,6 +14474,16 @@ packages: - supports-color dev: true + /esniff@2.0.1: + resolution: {integrity: sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==} + engines: {node: '>=0.10'} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + event-emitter: 0.3.5 + type: 2.7.3 + dev: false + /espree@10.0.1: resolution: {integrity: sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -13955,6 +14555,13 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + /event-emitter@0.3.5: + resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + dependencies: + d: 1.0.2 + es5-ext: 0.10.64 + dev: false + /event-stream@3.3.4: resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} dependencies: @@ -14127,6 +14734,12 @@ packages: - supports-color dev: true + /ext@1.7.0: + resolution: {integrity: sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==} + dependencies: + type: 2.7.3 + dev: false + /extend-shallow@2.0.1: resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} engines: {node: '>=0.10.0'} @@ -14179,6 +14792,14 @@ packages: resolution: {integrity: sha512-9tCqYEDHI5RYFQigXFwF1hnCwcWCOJl/hmll0lr5D2Ljjb0o4wphb69wikeJDz5qCEzXCoPvG6ss5SDP6IfOdg==} dev: false + /falafel@2.2.5: + resolution: {integrity: sha512-HuC1qF9iTnHDnML9YZAdCDQwT0yKl/U55K4XSUXqGAA2GLoafFgWRqdAbhWJxXaYD4pyoVxAJ8wH670jMpI9DQ==} + engines: {node: '>=0.4.0'} + dependencies: + acorn: 7.4.1 + isarray: 2.0.5 + dev: false + /falsey@0.3.2: resolution: {integrity: sha512-lxEuefF5MBIVDmE6XeqCdM4BWk1+vYmGZtkbKZ/VFcg6uBBw6fXNEbWmxCjDdQlFc9hy450nkiWwM3VAW6G1qg==} engines: {node: '>=0.10.0'} @@ -14204,6 +14825,12 @@ packages: merge2: 1.4.1 micromatch: 4.0.7 + /fast-isnumeric@1.1.4: + resolution: {integrity: sha512-1mM8qOr2LYz8zGaUdmiqRDiuue00Dxjgcb1NQR7TnhLVh6sQyngP9xvLo7Sl7LZpP/sk5eb+bcyWXw530NTBZw==} + dependencies: + is-string-blank: 1.0.1 + dev: false + /fast-json-parse@1.0.3: resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==} dev: true @@ -14427,6 +15054,12 @@ packages: /flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + /flatten-vertex-data@1.0.2: + resolution: {integrity: sha512-BvCBFK2NZqerFTdMDgqfHBwxYWnxeCkwONsw6PvBMcUXqo8U/KDWwmXhqx1x2kLIg7DqIsJfOaJFOmlua3Lxuw==} + dependencies: + dtype: 2.0.0 + dev: false + /flow-parser@0.222.0: resolution: {integrity: sha512-Fq5OkFlFRSMV2EOZW+4qUYMTE0uj8pcLsYJMxXYriSBDpHAF7Ofx3PibCTy3cs5P6vbsry7eYj7Z7xFD49GIOQ==} engines: {node: '>=0.4.0'} @@ -14443,6 +15076,18 @@ packages: dependencies: debug: 4.3.4 + /font-atlas@2.1.0: + resolution: {integrity: sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==} + dependencies: + css-font: 1.2.0 + dev: false + + /font-measure@1.2.2: + resolution: {integrity: sha512-mRLEpdrWzKe9hbfaF3Qpr06TAjquuBVP5cHy4b3hyeNdjc9i0PO6HniGsX5vjL5OWv7+Bd++NiooNpT/s8BvIA==} + dependencies: + css-font: 1.2.0 + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -14592,6 +15237,13 @@ packages: resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} engines: {node: '>= 0.6'} + /from2@2.3.0: + resolution: {integrity: sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==} + dependencies: + inherits: 2.0.4 + readable-stream: 2.3.8 + dev: false + /from@0.1.7: resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} dev: true @@ -14714,10 +15366,22 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + /geojson-vt@3.2.1: + resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} + dev: false + + /geojson-vt@4.0.2: + resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==} + dev: false + /get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + /get-canvas-context@1.0.2: + resolution: {integrity: sha512-LnpfLf/TNzr9zVOGiIY6aKCz8EKuXmlYNV7CM2pUjBa/B+c2I15tS7KLySep75+FuerJdmArvJLcsAXWEy2H0A==} + dev: false + /get-contrast@2.0.0: resolution: {integrity: sha512-7OPD09d+X1fM/HCxTwXsZecMFD1vUHJ2pbXHhvUvnIxOo6CxErrMoxcVAVC6ka0vlujjMyYO75RDLBBW8ZFqNQ==} hasBin: true @@ -14825,6 +15489,48 @@ packages: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} dev: true + /gl-mat4@1.2.0: + resolution: {integrity: sha512-sT5C0pwB1/e9G9AvAoLsoaJtbMGjfd/jfxo8jMCKqYYEnjZuFvqV5rehqar0538EmssjdDeiEWnKyBSTw7quoA==} + dev: false + + /gl-matrix@3.4.3: + resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} + dev: false + + /gl-text@1.4.0: + resolution: {integrity: sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==} + dependencies: + bit-twiddle: 1.0.2 + color-normalize: 1.5.0 + css-font: 1.2.0 + detect-kerning: 2.1.2 + es6-weak-map: 2.0.3 + flatten-vertex-data: 1.0.2 + font-atlas: 2.1.0 + font-measure: 1.2.2 + gl-util: 3.1.3 + is-plain-obj: 1.1.0 + object-assign: 4.1.1 + parse-rect: 1.2.0 + parse-unit: 1.0.1 + pick-by-alias: 1.2.0 + regl: 2.1.1 + to-px: 1.0.1 + typedarray-pool: 1.2.0 + dev: false + + /gl-util@3.1.3: + resolution: {integrity: sha512-dvRTggw5MSkJnCbh74jZzSoTOGnVYK+Bt+Ckqm39CVcl6+zSsxqWk4lr5NKhkqXHL6qvZAU9h17ZF8mIskY9mA==} + dependencies: + is-browser: 2.1.0 + is-firefox: 1.0.3 + is-plain-obj: 1.1.0 + number-is-integer: 1.0.1 + object-assign: 4.1.1 + pick-by-alias: 1.2.0 + weak-map: 1.0.8 + dev: false + /glob-parent@3.1.0: resolution: {integrity: sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==} dependencies: @@ -14895,6 +15601,15 @@ packages: kind-of: 6.0.3 which: 1.3.1 + /global-prefix@4.0.0: + resolution: {integrity: sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==} + engines: {node: '>=16'} + dependencies: + ini: 4.1.3 + kind-of: 6.0.3 + which: 4.0.0 + dev: false + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -14951,6 +15666,119 @@ packages: pinkie-promise: 2.0.1 dev: false + /glsl-inject-defines@1.0.3: + resolution: {integrity: sha512-W49jIhuDtF6w+7wCMcClk27a2hq8znvHtlGnrYkSWEr8tHe9eA2dcnohlcAmxLYBSpSSdzOkRdyPTrx9fw49+A==} + dependencies: + glsl-token-inject-block: 1.1.0 + glsl-token-string: 1.0.1 + glsl-tokenizer: 2.1.5 + dev: false + + /glsl-resolve@0.0.1: + resolution: {integrity: sha512-xxFNsfnhZTK9NBhzJjSBGX6IOqYpvBHxxmo+4vapiljyGNCY0Bekzn0firQkQrazK59c1hYxMDxYS8MDlhw4gA==} + dependencies: + resolve: 0.6.3 + xtend: 2.2.0 + dev: false + + /glsl-token-assignments@2.0.2: + resolution: {integrity: sha512-OwXrxixCyHzzA0U2g4btSNAyB2Dx8XrztY5aVUCjRSh4/D0WoJn8Qdps7Xub3sz6zE73W3szLrmWtQ7QMpeHEQ==} + dev: false + + /glsl-token-defines@1.0.0: + resolution: {integrity: sha512-Vb5QMVeLjmOwvvOJuPNg3vnRlffscq2/qvIuTpMzuO/7s5kT+63iL6Dfo2FYLWbzuiycWpbC0/KV0biqFwHxaQ==} + dependencies: + glsl-tokenizer: 2.1.5 + dev: false + + /glsl-token-depth@1.1.2: + resolution: {integrity: sha512-eQnIBLc7vFf8axF9aoi/xW37LSWd2hCQr/3sZui8aBJnksq9C7zMeUYHVJWMhFzXrBU7fgIqni4EhXVW4/krpg==} + dev: false + + /glsl-token-descope@1.0.2: + resolution: {integrity: sha512-kS2PTWkvi/YOeicVjXGgX5j7+8N7e56srNDEHDTVZ1dcESmbmpmgrnpjPcjxJjMxh56mSXYoFdZqb90gXkGjQw==} + dependencies: + glsl-token-assignments: 2.0.2 + glsl-token-depth: 1.1.2 + glsl-token-properties: 1.0.1 + glsl-token-scope: 1.1.2 + dev: false + + /glsl-token-inject-block@1.1.0: + resolution: {integrity: sha512-q/m+ukdUBuHCOtLhSr0uFb/qYQr4/oKrPSdIK2C4TD+qLaJvqM9wfXIF/OOBjuSA3pUoYHurVRNao6LTVVUPWA==} + dev: false + + /glsl-token-properties@1.0.1: + resolution: {integrity: sha512-dSeW1cOIzbuUoYH0y+nxzwK9S9O3wsjttkq5ij9ZGw0OS41BirKJzzH48VLm8qLg+au6b0sINxGC0IrGwtQUcA==} + dev: false + + /glsl-token-scope@1.1.2: + resolution: {integrity: sha512-YKyOMk1B/tz9BwYUdfDoHvMIYTGtVv2vbDSLh94PT4+f87z21FVdou1KNKgF+nECBTo0fJ20dpm0B1vZB1Q03A==} + dev: false + + /glsl-token-string@1.0.1: + resolution: {integrity: sha512-1mtQ47Uxd47wrovl+T6RshKGkRRCYWhnELmkEcUAPALWGTFe2XZpH3r45XAwL2B6v+l0KNsCnoaZCSnhzKEksg==} + dev: false + + /glsl-token-whitespace-trim@1.0.0: + resolution: {integrity: sha512-ZJtsPut/aDaUdLUNtmBYhaCmhIjpKNg7IgZSfX5wFReMc2vnj8zok+gB/3Quqs0TsBSX/fGnqUUYZDqyuc2xLQ==} + dev: false + + /glsl-tokenizer@2.1.5: + resolution: {integrity: sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==} + dependencies: + through2: 0.6.5 + dev: false + + /glslify-bundle@5.1.1: + resolution: {integrity: sha512-plaAOQPv62M1r3OsWf2UbjN0hUYAB7Aph5bfH58VxJZJhloRNbxOL9tl/7H71K7OLJoSJ2ZqWOKk3ttQ6wy24A==} + dependencies: + glsl-inject-defines: 1.0.3 + glsl-token-defines: 1.0.0 + glsl-token-depth: 1.1.2 + glsl-token-descope: 1.0.2 + glsl-token-scope: 1.1.2 + glsl-token-string: 1.0.1 + glsl-token-whitespace-trim: 1.0.0 + glsl-tokenizer: 2.1.5 + murmurhash-js: 1.0.0 + shallow-copy: 0.0.1 + dev: false + + /glslify-deps@1.3.2: + resolution: {integrity: sha512-7S7IkHWygJRjcawveXQjRXLO2FTjijPDYC7QfZyAQanY+yGLCFHYnPtsGT9bdyHiwPTw/5a1m1M9hamT2aBpag==} + dependencies: + '@choojs/findup': 0.2.1 + events: 3.3.0 + glsl-resolve: 0.0.1 + glsl-tokenizer: 2.1.5 + graceful-fs: 4.2.11 + inherits: 2.0.4 + map-limit: 0.0.1 + resolve: 1.22.8 + dev: false + + /glslify@7.1.1: + resolution: {integrity: sha512-bud98CJ6kGZcP9Yxcsi7Iz647wuDz3oN+IZsjCRi5X1PI7t/xPKeL0mOwXJjo+CRZMqvq0CkSJiywCcY7kVYog==} + hasBin: true + dependencies: + bl: 2.2.1 + concat-stream: 1.6.2 + duplexify: 3.7.1 + falafel: 2.2.5 + from2: 2.3.0 + glsl-resolve: 0.0.1 + glsl-token-whitespace-trim: 1.0.0 + glslify-bundle: 5.1.1 + glslify-deps: 1.3.2 + minimist: 1.2.8 + resolve: 1.22.8 + stack-trace: 0.0.9 + static-eval: 2.1.1 + through2: 2.0.5 + xtend: 4.0.2 + dev: false + /gopd@1.0.1: resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} dependencies: @@ -14992,6 +15820,10 @@ packages: strip-bom-string: 1.0.0 dev: false + /grid-index@1.1.0: + resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + dev: false + /gulp-header@1.8.12: resolution: {integrity: sha512-lh9HLdb53sC7XIZOYzTXM4lFuXElv3EVkSDhsd7DoJBj7hm+Ni7D3qYbb+Rr8DuM8nRanBvkVO9d7askreXGnQ==} deprecated: Removed event-stream from gulp-header @@ -15144,6 +15976,18 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + /has-hover@1.0.1: + resolution: {integrity: sha512-0G6w7LnlcpyDzpeGUTuT0CEw05+QlMuGVk1IHNAlHrGJITGodjZu3x8BNDUMfKJSZXNB2ZAclqc1bvrd+uUpfg==} + dependencies: + is-browser: 2.1.0 + dev: false + + /has-passive-events@1.0.0: + resolution: {integrity: sha512-2vSj6IeIsgvsRMyeQ0JaCX5Q3lX4zMn5HpoVc7MEhQ6pv8Iq9rsXjsp+E5ZwaT7T0xhMT0KmU8gtt1EFVdbJiw==} + dependencies: + is-browser: 2.1.0 + dev: false + /has-property-descriptors@1.0.2: resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} dependencies: @@ -15407,6 +16251,10 @@ packages: readable-stream: 2.3.8 wbuf: 1.7.3 + /hsluv@0.0.3: + resolution: {integrity: sha512-08iL2VyCRbkQKBySkSh6m8zMUa3sADAxGVWs3Z1aPcUkTJeK0ETG4Fc27tEmQBGUAXZjIsXOZqBvacuVNSC/fQ==} + dev: false + /html-encoding-sniffer@2.0.1: resolution: {integrity: sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==} engines: {node: '>=10'} @@ -15695,7 +16543,6 @@ packages: /ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: true /ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} @@ -15764,6 +16611,11 @@ packages: /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + /ini@4.1.3: + resolution: {integrity: sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + dev: false + /inline-style-parser@0.1.1: resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} dev: false @@ -15880,6 +16732,10 @@ packages: call-bind: 1.0.7 has-tostringtag: 1.0.2 + /is-browser@2.1.0: + resolution: {integrity: sha512-F5rTJxDQ2sW81fcfOR1GnCXT6sVJC104fCyfj+mjpwNEwaPYSn5fte5jiHmBg3DHsIoL/l8Kvw5VN5SsTRcRFQ==} + dev: false + /is-buffer@1.1.6: resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==} dev: false @@ -16000,6 +16856,16 @@ packages: dependencies: call-bind: 1.0.7 + /is-finite@1.1.0: + resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} + engines: {node: '>=0.10.0'} + dev: false + + /is-firefox@1.0.3: + resolution: {integrity: sha512-6Q9ITjvWIm0Xdqv+5U12wgOKEM2KoBw4Y926m0OFkvlCxnbG94HKAsVz8w3fWcfAS5YA2fJORXX1dLrkprCCxA==} + engines: {node: '>=0.10.0'} + dev: false + /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -16043,6 +16909,11 @@ packages: resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} dev: false + /is-iexplorer@1.0.0: + resolution: {integrity: sha512-YeLzceuwg3K6O0MLM3UyUUjKAlyULetwryFp1mHy1I5PfArK0AEqlfa+MR4gkJjcbuJXoDJCvXbyqZVf5CR2Sg==} + engines: {node: '>=0.10.0'} + dev: false + /is-interactive@1.0.0: resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} engines: {node: '>=8'} @@ -16056,6 +16927,10 @@ packages: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} + /is-mobile@4.0.0: + resolution: {integrity: sha512-mlcHZA84t1qLSuWkt2v0I2l61PYdyQDt4aG1mLIXF5FDMm4+haBCxCPYSr/uwqQNRk1MiTizn0ypEuRAOLRAew==} + dev: false + /is-module@1.0.0: resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} @@ -16200,6 +17075,10 @@ packages: engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} dev: true + /is-string-blank@1.0.1: + resolution: {integrity: sha512-9H+ZBCVs3L9OYqv8nuUAzpcT9OTgMD1yAWrG7ihlnibdkbtB850heAmYWxHuXc4CHy4lKeK69tN+ny1K7gBIrw==} + dev: false + /is-string@1.0.7: resolution: {integrity: sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==} engines: {node: '>= 0.4'} @@ -16213,6 +17092,10 @@ packages: better-path-resolve: 1.0.0 dev: false + /is-svg-path@1.0.2: + resolution: {integrity: sha512-Lj4vePmqpPR1ZnRctHv8ltSh1OrSxHkhUkd7wi+VQdcdP15/KvQFyk7LhNuM7ZW0EVbJz8kZLVmL9quLrfq4Kg==} + dev: false + /is-symbol@1.0.4: resolution: {integrity: sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==} engines: {node: '>= 0.4'} @@ -16271,6 +17154,10 @@ packages: dependencies: is-docker: 2.2.1 + /isarray@0.0.1: + resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} + dev: false + /isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -16280,6 +17167,11 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + /isexe@3.1.1: + resolution: {integrity: sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==} + engines: {node: '>=16'} + dev: false + /iso-3166-1@2.1.1: resolution: {integrity: sha512-RZxXf8cw5Y8LyHZIwIRvKw8sWTIHh2/txBT+ehO0QroesVfnz3JNFFX4i/OC/Yuv2bDIVYrHna5PMvjtpefq5w==} dev: false @@ -17034,6 +17926,10 @@ packages: /json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + /json-stringify-pretty-compact@4.0.0: + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + dev: false + /json-to-pretty-yaml@1.2.2: resolution: {integrity: sha512-rvm6hunfCcqegwYaG5T4yKJWxc9FXFgBVrcTZ4XfSVRwa5HA/Xs+vB/Eo9treYYHCeNM0nrSUr82V/M31Urc7A==} engines: {node: '>= 0.2.0'} @@ -17422,6 +18318,14 @@ packages: object.assign: 4.1.5 object.values: 1.2.0 + /kdbush@3.0.0: + resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} + dev: false + + /kdbush@4.0.2: + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -17989,6 +18893,12 @@ packages: engines: {node: '>=0.10.0'} dev: false + /map-limit@0.0.1: + resolution: {integrity: sha512-pJpcfLPnIF/Sk3taPW21G/RQsEEirGaFpCW3oXRwH9dnFHPHNGjNyvh++rdmC2fNqEaTw2MhYJraoJWAHx8kEg==} + dependencies: + once: 1.3.3 + dev: false + /map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -18014,6 +18924,66 @@ packages: object-visit: 1.0.1 dev: false + /mapbox-gl@1.13.3: + resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} + engines: {node: '>=6.4.0'} + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/geojson-types': 1.0.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 1.2.5 + '@mapbox/unitbezier': 0.0.0 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + csscolorparser: 1.0.3 + earcut: 2.2.4 + geojson-vt: 3.2.1 + gl-matrix: 3.4.3 + grid-index: 1.1.0 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 1.0.2 + quickselect: 2.0.0 + rw: 1.3.3 + supercluster: 7.1.5 + tinyqueue: 2.0.3 + vt-pbf: 3.1.3 + dev: false + + /maplibre-gl@4.7.1: + resolution: {integrity: sha512-lgL7XpIwsgICiL82ITplfS7IGwrB1OJIw/pCvprDp2dhmSSEBgmPzYRvwYYYvJGJD7fxUv1Tvpih4nZ6VrLuaA==} + engines: {node: '>=16.14.0', npm: '>=8.1.0'} + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 2.0.6 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + '@maplibre/maplibre-gl-style-spec': 20.4.0 + '@types/geojson': 7946.0.15 + '@types/geojson-vt': 3.2.5 + '@types/mapbox__point-geometry': 0.1.4 + '@types/mapbox__vector-tile': 1.3.4 + '@types/pbf': 3.0.5 + '@types/supercluster': 7.1.3 + earcut: 3.0.1 + geojson-vt: 4.0.2 + gl-matrix: 3.4.3 + global-prefix: 4.0.0 + kdbush: 4.0.2 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 2.0.0 + quickselect: 3.0.0 + supercluster: 8.0.1 + tinyqueue: 3.0.0 + vt-pbf: 3.1.3 + dev: false + /markdown-it-abbr@1.0.4: resolution: {integrity: sha512-ZeA4Z4SaBbYysZap5iZcxKmlPL6bYA8grqhzJIHB1ikn7njnzaP8uwbtuXc4YXD5LicI4/2Xmc0VwmSiFV04gg==} dev: false @@ -18067,6 +19037,11 @@ packages: hasBin: true dev: false + /math-log2@1.0.1: + resolution: {integrity: sha512-9W0yGtkaMAkf74XGYVy4Dqw3YUMnTNB2eeiw9aQbUl4A3KmuCEHTt2DgAB07ENzOYAjsYSAYufkAq0Zd+jU7zA==} + engines: {node: '>=0.10.0'} + dev: false + /md5@2.3.0: resolution: {integrity: sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==} dependencies: @@ -18770,6 +19745,28 @@ packages: resolution: {integrity: sha512-8CclLCmrRRh+sul7C08BmPBP3P8wVWfBHomsTcndxg5NRCEPfu/mc2AGU8k37ajjDVXcXFc12ORAMUkmk+lkFA==} dev: false + /mouse-change@1.4.0: + resolution: {integrity: sha512-vpN0s+zLL2ykyyUDh+fayu9Xkor5v/zRD9jhSqjRS1cJTGS0+oakVZzNm5n19JvvEj0you+MXlYTpNxUDQUjkQ==} + dependencies: + mouse-event: 1.0.5 + dev: false + + /mouse-event-offset@3.0.2: + resolution: {integrity: sha512-s9sqOs5B1Ykox3Xo8b3Ss2IQju4UwlW6LSR+Q5FXWpprJ5fzMLefIIItr3PH8RwzfGy6gxs/4GAmiNuZScE25w==} + dev: false + + /mouse-event@1.0.5: + resolution: {integrity: sha512-ItUxtL2IkeSKSp9cyaX2JLUuKk2uMoxBg4bbOWVd29+CskYJR9BGsUqtXenNzKbnDshvupjUewDIYVrOB6NmGw==} + dev: false + + /mouse-wheel@1.2.0: + resolution: {integrity: sha512-+OfYBiUOCTWcTECES49neZwL5AoGkXE+lFjIvzwNCnYRlso+EnfvovcBxGoyQ0yQt806eSPjS675K0EwWknXmw==} + dependencies: + right-now: 1.0.0 + signum: 1.0.0 + to-px: 1.0.1 + dev: false + /mri@1.2.0: resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} engines: {node: '>=4'} @@ -18812,6 +19809,17 @@ packages: dns-packet: 5.6.1 thunky: 1.1.0 + /mumath@3.3.4: + resolution: {integrity: sha512-VAFIOG6rsxoc7q/IaY3jdjmrsuX9f15KlRLYTHmixASBZkZEKC1IFqE2BC5CdhXmK6WLM1Re33z//AGmeRI6FA==} + deprecated: Redundant dependency in your project. + dependencies: + almost-equal: 1.1.0 + dev: false + + /murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + dev: false + /mustache@2.3.2: resolution: {integrity: sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==} engines: {npm: '>=1.4.0'} @@ -18855,12 +19863,28 @@ packages: - supports-color dev: false + /native-promise-only@0.8.1: + resolution: {integrity: sha512-zkVhZUA3y8mbz652WrL5x0fB0ehrBkulWT3TomAQ9iDtyXZvzKeEA6GPxAItBYeNYl5yngKRX612qHOhvMkDeg==} + dev: false + /natural-compare-lite@1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} /natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + /needle@2.9.1: + resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==} + engines: {node: '>= 4.4.x'} + hasBin: true + dependencies: + debug: 3.2.7 + iconv-lite: 0.4.24 + sax: 1.2.4 + transitivePeerDependencies: + - supports-color + dev: false + /negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -18899,6 +19923,10 @@ packages: react-dom: 18.3.1(react@18.3.1) dev: false + /next-tick@1.1.0: + resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} + dev: false + /next@14.2.5(@playwright/test@1.40.0)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-0f8aRfBVL+mpzfBjYfQuLWh2WyAwtJXCRfkPF4UJ5qd2YwrHczsrSzXU4tRMV0OAxR8ZJZWPFn6uhSC56UTsLA==} engines: {node: '>=18.17.0'} @@ -19075,6 +20103,16 @@ packages: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} + /normalize-svg-path@0.1.0: + resolution: {integrity: sha512-1/kmYej2iedi5+ROxkRESL/pI02pkg0OBnaR4hJkSIX6+ORzepwbuUXfrdZaPjysTsJInj0Rj5NuX027+dMBvA==} + dev: false + + /normalize-svg-path@1.1.0: + resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} + dependencies: + svg-arc-to-cubic-bezier: 3.2.0 + dev: false + /normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} @@ -19236,6 +20274,13 @@ packages: resolution: {integrity: sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==} dev: true + /number-is-integer@1.0.1: + resolution: {integrity: sha512-Dq3iuiFBkrbmuQjGFFF3zckXNCQoSD37/SdSbgcBailUx6knDvDwb5CympBgcoWHy36sfS12u74MHYkXyHq6bg==} + engines: {node: '>=0.10.0'} + dependencies: + is-finite: 1.1.0 + dev: false + /nwsapi@2.2.7: resolution: {integrity: sha512-ub5E4+FBPKwAZx0UwIQOjYWGHTEq5sPqHQNRN8Z9e4A7u3Tj1weLJsL59yH9vmvqEtBHaOmT6cYQKIZOxp35FQ==} @@ -19382,6 +20427,12 @@ packages: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} engines: {node: '>= 0.8'} + /once@1.3.3: + resolution: {integrity: sha512-6vaNInhu+CHxtONf3zw3vq4SP2DOQhjBvIa3rNcG0+P7eKWlYH6Peu7rHizSloRU2EwMz6GraLieis9Ac9+p1w==} + dependencies: + wrappy: 1.0.2 + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: @@ -19634,6 +20685,10 @@ packages: dependencies: callsites: 3.1.0 + /parenthesis@3.1.8: + resolution: {integrity: sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==} + dev: false + /parse-entities@2.0.0: resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} dependencies: @@ -19654,6 +20709,20 @@ packages: json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 + /parse-rect@1.2.0: + resolution: {integrity: sha512-4QZ6KYbnE6RTwg9E0HpLchUM9EZt6DnDxajFZZDSV4p/12ZJEvPO702DZpGvRYEPo00yKDys7jASi+/w7aO8LA==} + dependencies: + pick-by-alias: 1.2.0 + dev: false + + /parse-svg-path@0.1.2: + resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} + dev: false + + /parse-unit@1.0.1: + resolution: {integrity: sha512-hrqldJHokR3Qj88EIlV/kAyAi/G5R2+R56TBANxNMy0uPlYcttx0jnMW6Yx5KsKPSbC3KddM/7qQm3+0wEXKxg==} + dev: false + /parse5@6.0.1: resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} @@ -19752,6 +20821,14 @@ packages: through: 2.3.8 dev: true + /pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + dev: false + /performance-now@2.1.0: resolution: {integrity: sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==} requiresBuild: true @@ -19828,6 +20905,10 @@ packages: split2: 4.2.0 dev: false + /pick-by-alias@1.2.0: + resolution: {integrity: sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==} + dev: false + /picocolors@0.2.1: resolution: {integrity: sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==} @@ -19918,6 +20999,72 @@ packages: optionalDependencies: fsevents: 2.3.2 + /plotly.js-dist@2.35.3: + resolution: {integrity: sha512-dqB9+FUyBFZN04xWnZoYwaeeF4Jj9T/m0CHYmoozmPC3R4Dy0TRJsHgbRVLPxgYQqodzniVUj17+2wmJuGaZAg==} + dev: false + + /plotly.js@2.35.3(mapbox-gl@1.13.3)(webpack@5.89.0): + resolution: {integrity: sha512-7RaC6FxmCUhpD6H4MpD+QLUu3hCn76I11rotRefrh3m1iDvWqGnVqVk9dSaKmRAhFD3vsNsYea0OxnR1rc2IzQ==} + dependencies: + '@plotly/d3': 3.8.2 + '@plotly/d3-sankey': 0.7.2 + '@plotly/d3-sankey-circular': 0.33.1 + '@plotly/mapbox-gl': 1.13.4(mapbox-gl@1.13.3) + '@turf/area': 7.1.0 + '@turf/bbox': 7.1.0 + '@turf/centroid': 7.1.0 + base64-arraybuffer: 1.0.2 + canvas-fit: 1.5.0 + color-alpha: 1.0.4 + color-normalize: 1.5.0 + color-parse: 2.0.0 + color-rgba: 2.1.1 + country-regex: 1.1.0 + css-loader: 7.1.2(webpack@5.89.0) + d3-force: 1.2.1 + d3-format: 1.4.5 + d3-geo: 1.12.1 + d3-geo-projection: 2.9.0 + d3-hierarchy: 1.1.9 + d3-interpolate: 3.0.1 + d3-time: 1.1.0 + d3-time-format: 2.3.0 + fast-isnumeric: 1.1.4 + gl-mat4: 1.2.0 + gl-text: 1.4.0 + has-hover: 1.0.1 + has-passive-events: 1.0.0 + is-mobile: 4.0.0 + maplibre-gl: 4.7.1 + mouse-change: 1.4.0 + mouse-event-offset: 3.0.2 + mouse-wheel: 1.2.0 + native-promise-only: 0.8.1 + parse-svg-path: 0.1.2 + point-in-polygon: 1.1.0 + polybooljs: 1.2.2 + probe-image-size: 7.2.3 + regl: /@plotly/regl@2.1.2 + regl-error2d: 2.0.12 + regl-line2d: 3.1.3 + regl-scatter2d: 3.3.1 + regl-splom: 1.0.14 + strongly-connected-components: 1.0.1 + style-loader: 4.0.0(webpack@5.89.0) + superscript-text: 1.0.0 + svg-path-sdf: 1.1.3 + tinycolor2: 1.6.0 + to-px: 1.0.1 + topojson-client: 3.1.0 + webgl-context: 2.2.0 + world-calendars: 1.0.3 + transitivePeerDependencies: + - '@rspack/core' + - mapbox-gl + - supports-color + - webpack + dev: false + /pluralize@7.0.0: resolution: {integrity: sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow==} engines: {node: '>=4'} @@ -19932,6 +21079,10 @@ packages: - typescript dev: true + /point-in-polygon@1.1.0: + resolution: {integrity: sha512-3ojrFwjnnw8Q9242TzgXuTD+eKiutbzyslcq1ydfu82Db2y+Ogbmyrkpv0Hgj31qwT3lbS9+QAAO/pIQM35XRw==} + dev: false + /polished@4.2.2: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'} @@ -19939,6 +21090,10 @@ packages: '@babel/runtime': 7.24.7 dev: true + /polybooljs@1.2.2: + resolution: {integrity: sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==} + dev: false + /posix-character-classes@0.1.1: resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==} engines: {node: '>=0.10.0'} @@ -20344,6 +21499,15 @@ packages: dependencies: postcss: 8.4.39 + /postcss-modules-extract-imports@3.1.0(postcss@8.4.39): + resolution: {integrity: sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.39 + dev: false + /postcss-modules-local-by-default@4.0.3(postcss@8.4.39): resolution: {integrity: sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==} engines: {node: ^10 || ^12 || >= 14} @@ -20355,6 +21519,18 @@ packages: postcss-selector-parser: 6.0.13 postcss-value-parser: 4.2.0 + /postcss-modules-local-by-default@4.2.0(postcss@8.4.39): + resolution: {integrity: sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + icss-utils: 5.1.0(postcss@8.4.39) + postcss: 8.4.39 + postcss-selector-parser: 7.0.0 + postcss-value-parser: 4.2.0 + dev: false + /postcss-modules-scope@3.0.0(postcss@8.4.39): resolution: {integrity: sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==} engines: {node: ^10 || ^12 || >= 14} @@ -20364,6 +21540,16 @@ packages: postcss: 8.4.39 postcss-selector-parser: 6.0.13 + /postcss-modules-scope@3.2.1(postcss@8.4.39): + resolution: {integrity: sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==} + engines: {node: ^10 || ^12 || >= 14} + peerDependencies: + postcss: ^8.1.0 + dependencies: + postcss: 8.4.39 + postcss-selector-parser: 7.0.0 + dev: false + /postcss-modules-values@4.0.0(postcss@8.4.39): resolution: {integrity: sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==} engines: {node: ^10 || ^12 || >= 14} @@ -20646,6 +21832,14 @@ packages: cssesc: 3.0.0 util-deprecate: 1.0.2 + /postcss-selector-parser@7.0.0: + resolution: {integrity: sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==} + engines: {node: '>=4'} + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + dev: false + /postcss-svgo@5.1.0(postcss@8.4.39): resolution: {integrity: sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA==} engines: {node: ^10 || ^12 || >=14.0} @@ -20751,6 +21945,14 @@ packages: posthtml-render: 3.0.0 dev: true + /potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + dev: false + + /potpack@2.0.0: + resolution: {integrity: sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==} + dev: false + /preact-render-to-string@5.2.3(preact@10.11.3): resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} peerDependencies: @@ -20852,6 +22054,16 @@ packages: engines: {node: '>=6'} dev: false + /probe-image-size@7.2.3: + resolution: {integrity: sha512-HubhG4Rb2UH8YtV4ba0Vp5bQ7L78RTONYu/ujmCu5nBI8wGv24s4E9xSKBi0N1MowRpxk76pFCpJtW0KPzOK0w==} + dependencies: + lodash.merge: 4.6.2 + needle: 2.9.1 + stream-parser: 0.3.1 + transitivePeerDependencies: + - supports-color + dev: false + /process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -20904,6 +22116,10 @@ packages: resolution: {integrity: sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==} dev: false + /protocol-buffers-schema@3.6.0: + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + dev: false + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -21177,6 +22393,14 @@ packages: engines: {node: '>=12'} dev: false + /quickselect@2.0.0: + resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + dev: false + + /quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + dev: false + /raf@3.4.1: resolution: {integrity: sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==} dependencies: @@ -21420,6 +22644,17 @@ packages: - supports-color dev: false + /react-plotly.js@2.6.0(plotly.js@2.35.3)(react@18.3.1): + resolution: {integrity: sha512-g93xcyhAVCSt9kV1svqG1clAEdL6k3U+jjuSzfTV7owaSU9Go6Ph8bl25J+jKfKvIGAEYpe4qj++WHJuc9IaeA==} + peerDependencies: + plotly.js: '>1.34.0' + react: '>0.13.0' + dependencies: + plotly.js: 2.35.3(mapbox-gl@1.13.3)(webpack@5.89.0) + prop-types: 15.8.1 + react: 18.3.1 + dev: false + /react-refresh@0.11.0: resolution: {integrity: sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==} engines: {node: '>=0.10.0'} @@ -21689,6 +22924,15 @@ packages: strip-bom: 3.0.0 dev: false + /readable-stream@1.0.34: + resolution: {integrity: sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==} + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 0.0.1 + string_decoder: 0.10.31 + dev: false + /readable-stream@2.3.8: resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} dependencies: @@ -21888,6 +23132,71 @@ packages: dependencies: jsesc: 0.5.0 + /regl-error2d@2.0.12: + resolution: {integrity: sha512-r7BUprZoPO9AbyqM5qlJesrSRkl+hZnVKWKsVp7YhOl/3RIpi4UDGASGJY0puQ96u5fBYw/OlqV24IGcgJ0McA==} + dependencies: + array-bounds: 1.0.1 + color-normalize: 1.5.0 + flatten-vertex-data: 1.0.2 + object-assign: 4.1.1 + pick-by-alias: 1.2.0 + to-float32: 1.1.0 + update-diff: 1.1.0 + dev: false + + /regl-line2d@3.1.3: + resolution: {integrity: sha512-fkgzW+tTn4QUQLpFKsUIE0sgWdCmXAM3ctXcCgoGBZTSX5FE2A0M7aynz7nrZT5baaftLrk9te54B+MEq4QcSA==} + dependencies: + array-bounds: 1.0.1 + array-find-index: 1.0.2 + array-normalize: 1.1.4 + color-normalize: 1.5.0 + earcut: 2.2.4 + es6-weak-map: 2.0.3 + flatten-vertex-data: 1.0.2 + object-assign: 4.1.1 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + to-float32: 1.1.0 + dev: false + + /regl-scatter2d@3.3.1: + resolution: {integrity: sha512-seOmMIVwaCwemSYz/y4WE0dbSO9svNFSqtTh5RE57I7PjGo3tcUYKtH0MTSoshcAsreoqN8HoCtnn8wfHXXfKQ==} + dependencies: + '@plotly/point-cluster': 3.1.9 + array-range: 1.0.1 + array-rearrange: 2.2.2 + clamp: 1.0.1 + color-id: 1.1.0 + color-normalize: 1.5.0 + color-rgba: 2.1.1 + flatten-vertex-data: 1.0.2 + glslify: 7.1.1 + is-iexplorer: 1.0.0 + object-assign: 4.1.1 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + to-float32: 1.1.0 + update-diff: 1.1.0 + dev: false + + /regl-splom@1.0.14: + resolution: {integrity: sha512-OiLqjmPRYbd7kDlHC6/zDf6L8lxgDC65BhC8JirhP4ykrK4x22ZyS+BnY8EUinXKDeMgmpRwCvUmk7BK4Nweuw==} + dependencies: + array-bounds: 1.0.1 + array-range: 1.0.1 + color-alpha: 1.0.4 + flatten-vertex-data: 1.0.2 + parse-rect: 1.2.0 + pick-by-alias: 1.2.0 + raf: 3.4.1 + regl-scatter2d: 3.3.1 + dev: false + + /regl@2.1.1: + resolution: {integrity: sha512-+IOGrxl3FZ8ZM9ixCWQZzFRiRn7Rzn9bu3iFHwg/yz4tlOUQgbO4PHLgG+1ZT60zcIV8tief6Qrmyl8qcoJP0g==} + dev: false + /rehype-external-links@3.0.0: resolution: {integrity: sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==} dependencies: @@ -22061,6 +23370,12 @@ packages: /resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + /resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + dependencies: + protocol-buffers-schema: 3.6.0 + dev: false + /resolve-url-loader@4.0.0: resolution: {integrity: sha512-05VEMczVREcbtT7Bz+C+96eUO5HDNvdthIiMB34t7FcF8ehcu4wC0sSgPUubs3XW2Q3CNLJk/BJrCU9wVRymiA==} engines: {node: '>=8.9'} @@ -22088,6 +23403,10 @@ packages: resolution: {integrity: sha512-/NtpHNDN7jWhAaQ9BvBUYZ6YTXsRBgfqWFWP7BZBaoMJO/I3G5OFzvTuWNlZC3aPjins1F+TNrLKsGbH4rfsRQ==} engines: {node: '>=10'} + /resolve@0.6.3: + resolution: {integrity: sha512-UHBY3viPlJKf85YijDUcikKX6tmF4SokIDp518ZDVT92JNDcG5uKIthaT/owt3Sar0lwtOafsQuwrg22/v2Dwg==} + dev: false + /resolve@1.22.8: resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} hasBin: true @@ -22162,6 +23481,10 @@ packages: align-text: 0.1.4 dev: false + /right-now@1.0.0: + resolution: {integrity: sha512-DA8+YS+sMIVpbsuKgy+Z67L9Lxb1p05mNxRpDPNksPDEFir4vmBlUtuN9jkTGn9YMMdlBuK7XQgFiz6ws+yhSg==} + dev: false + /rimraf@2.6.3: resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} deprecated: Rimraf versions prior to v4 are no longer supported @@ -22250,6 +23573,10 @@ packages: dependencies: queue-microtask: 1.2.3 + /rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + dev: false + /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: @@ -22576,6 +23903,10 @@ packages: dependencies: kind-of: 6.0.3 + /shallow-copy@0.0.1: + resolution: {integrity: sha512-b6i4ZpVuUxB9h5gfCxPiusKYkqTMOjEbBs4wMaFbkfia4yFv92UKZ6Df8WXcKbn08JNL/abvg3FnMAOfakDvUw==} + dev: false + /shallowequal@1.1.0: resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} @@ -22621,6 +23952,10 @@ packages: engines: {node: '>=14'} dev: true + /signum@1.0.0: + resolution: {integrity: sha512-yodFGwcyt59XRh7w5W3jPcIQb3Bwi21suEfT7MAWnBX3iCdklJpgDgvGT9o04UonglZN5SNMfJFkHIR/jO8GHw==} + dev: false + /sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} @@ -22929,6 +24264,10 @@ packages: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + /stack-trace@0.0.9: + resolution: {integrity: sha512-vjUc6sfgtgY0dxCdnc40mK6Oftjo9+2K8H/NG81TMhgL392FtiPA9tn9RLyTxXmTLPJPjF3VyzFp6bsWFLisMQ==} + dev: false + /stack-utils@2.0.6: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} @@ -22964,6 +24303,12 @@ packages: dependencies: escodegen: 1.14.3 + /static-eval@2.1.1: + resolution: {integrity: sha512-MgWpQ/ZjGieSVB3eOJVs4OA2LT/q1vx98KPCTTQPzq/aLr0YUXTsgryTXr4SLfR0ZfUUCiedM9n/ABeDIyy4mA==} + dependencies: + escodegen: 2.1.0 + dev: false + /static-extend@0.1.2: resolution: {integrity: sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==} engines: {node: '>=0.10.0'} @@ -23050,6 +24395,18 @@ packages: duplexer: 0.1.2 dev: true + /stream-parser@0.3.1: + resolution: {integrity: sha512-bJ/HgKq41nlKvlhccD5kaCr/P+Hu0wPNKPJOH7en+YrJu/9EgqUF+88w5Jb6KNcjOFMhfX4B2asfeAtIGuHObQ==} + dependencies: + debug: 2.6.9 + transitivePeerDependencies: + - supports-color + dev: false + + /stream-shift@1.0.3: + resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==} + dev: false + /stream-transform@2.1.3: resolution: {integrity: sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ==} dependencies: @@ -23083,6 +24440,12 @@ packages: /string-natural-compare@3.0.1: resolution: {integrity: sha512-n3sPwynL1nwKi3WJ6AIsClwBMa0zTi54fn2oLU6ndfTSIO05xaznjSf15PcBZU6FNWbmN5Q6cxT4V5hGvB4taw==} + /string-split-by@1.0.0: + resolution: {integrity: sha512-KaJKY+hfpzNyet/emP81PJA9hTVSfxNLS9SFTWxdCnnW1/zOOwiV248+EfoX7IQFcBaOp4G5YE6xTJMF+pLg6A==} + dependencies: + parenthesis: 3.1.8 + dev: false + /string-width@4.2.3: resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} engines: {node: '>=8'} @@ -23156,6 +24519,10 @@ packages: define-properties: 1.2.1 es-object-atoms: 1.0.0 + /string_decoder@0.10.31: + resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} + dev: false + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -23235,6 +24602,10 @@ packages: resolution: {integrity: sha512-g45ZOGzHDMe2bdYMdIvdAfCQkCTDMGBazSw1ypMowwGIee7ZQ5dU0rBJ8Jqgl+jAKIv4dbeE1jscZq9wid1Tkw==} dev: false + /strongly-connected-components@1.0.1: + resolution: {integrity: sha512-i0TFx4wPcO0FwX+4RkLJi1MxmcTv90jNZgxMu9XRnMXMeFUY1VJlIoXpZunPUvUUqbCT1pg5PEkFqqpcaElNaA==} + dev: false + /stubborn-fs@1.2.4: resolution: {integrity: sha512-KRa4nIRJ8q6uApQbPwYZVhOof8979fw4xbajBWa5kPJFa4nyY3aFaMWVyIVCDnkNCCG/3HLipUZ4QaNlYsmX1w==} dev: false @@ -23247,6 +24618,15 @@ packages: dependencies: webpack: 5.89.0(webpack-cli@5.1.4) + /style-loader@4.0.0(webpack@5.89.0): + resolution: {integrity: sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==} + engines: {node: '>= 18.12.0'} + peerDependencies: + webpack: ^5.27.0 + dependencies: + webpack: 5.89.0(webpack-cli@5.1.4) + dev: false + /style-to-object@0.4.1: resolution: {integrity: sha512-HFpbb5gr2ypci7Qw+IOhnP2zOU7e77b+rzM+wTzXzfi1PrtBCX0E7Pk4wL4iTLnhzZ+JgEGAhX81ebTg/aYjQw==} dependencies: @@ -23353,6 +24733,22 @@ packages: resolution: {integrity: sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA==} dev: false + /supercluster@7.1.5: + resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} + dependencies: + kdbush: 3.0.0 + dev: false + + /supercluster@8.0.1: + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} + dependencies: + kdbush: 4.0.2 + dev: false + + /superscript-text@1.0.0: + resolution: {integrity: sha512-gwu8l5MtRZ6koO0icVTlmN5pm7Dhh1+Xpe9O4x6ObMAsW+3jPbW14d1DsBq1F4wiI+WOFjXF35pslgec/G8yCQ==} + dev: false + /supports-color@5.5.0: resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} engines: {node: '>=4'} @@ -23402,9 +24798,32 @@ packages: periscopic: 3.1.0 dev: false + /svg-arc-to-cubic-bezier@3.2.0: + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + dev: false + /svg-parser@2.0.4: resolution: {integrity: sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==} + /svg-path-bounds@1.0.2: + resolution: {integrity: sha512-H4/uAgLWrppIC0kHsb2/dWUYSmb4GE5UqH06uqWBcg6LBjX2fu0A8+JrO2/FJPZiSsNOKZAhyFFgsLTdYUvSqQ==} + dependencies: + abs-svg-path: 0.1.1 + is-svg-path: 1.0.2 + normalize-svg-path: 1.1.0 + parse-svg-path: 0.1.2 + dev: false + + /svg-path-sdf@1.1.3: + resolution: {integrity: sha512-vJJjVq/R5lSr2KLfVXVAStktfcfa1pNFjFOgyJnzZFXlO/fDZ5DmM8FpnSKKzLPfEYTVeXuVBTHF296TpxuJVg==} + dependencies: + bitmap-sdf: 1.0.4 + draw-svg-path: 1.0.0 + is-svg-path: 1.0.2 + parse-svg-path: 0.1.2 + svg-path-bounds: 1.0.2 + dev: false + /svgo@1.3.2: resolution: {integrity: sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==} engines: {node: '>=4.0.0'} @@ -23702,6 +25121,13 @@ packages: /throat@6.0.2: resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} + /through2@0.6.5: + resolution: {integrity: sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==} + dependencies: + readable-stream: 1.0.34 + xtend: 4.0.2 + dev: false + /through2@2.0.5: resolution: {integrity: sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==} dependencies: @@ -23728,6 +25154,18 @@ packages: /tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + /tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + dev: false + + /tinyqueue@2.0.3: + resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + dev: false + + /tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + dev: false + /tinyspy@2.2.1: resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} engines: {node: '>=14.0.0'} @@ -23752,6 +25190,10 @@ packages: resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} engines: {node: '>=4'} + /to-float32@1.1.0: + resolution: {integrity: sha512-keDnAusn/vc+R3iEiSDw8TOF7gPiTLdK1ArvWtYbJQiVfmRg6i/CAvbKq3uIS0vWroAC7ZecN3DjQKw3aSklUg==} + dev: false + /to-gfm-code-block@0.1.1: resolution: {integrity: sha512-LQRZWyn8d5amUKnfR9A9Uu7x9ss7Re8peuWR2gkh1E+ildOfv2aF26JpuDg8JtvCduu5+hOrMIH+XstZtnagqg==} engines: {node: '>=0.10.0'} @@ -23764,6 +25206,12 @@ packages: kind-of: 3.2.2 dev: false + /to-px@1.0.1: + resolution: {integrity: sha512-2y3LjBeIZYL19e5gczp14/uRWFDtDUErJPVN3VU9a7SJO+RjGRtYR47aMN2bZgGlxvW4ZcEz2ddUPVHXcMfuXw==} + dependencies: + parse-unit: 1.0.1 + dev: false + /to-regex-range@2.1.1: resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} engines: {node: '>=0.10.0'} @@ -23800,6 +25248,13 @@ packages: resolution: {integrity: sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==} dev: false + /topojson-client@3.1.0: + resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} + hasBin: true + dependencies: + commander: 2.20.3 + dev: false + /tough-cookie@4.1.2: resolution: {integrity: sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==} engines: {node: '>=6'} @@ -24073,6 +25528,10 @@ packages: media-typer: 0.3.0 mime-types: 2.1.35 + /type@2.7.3: + resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} + dev: false + /typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -24113,11 +25572,22 @@ packages: is-typed-array: 1.1.13 possible-typed-array-names: 1.0.0 + /typedarray-pool@1.2.0: + resolution: {integrity: sha512-YTSQbzX43yvtpfRtIDAYygoYtgT+Rpjuxy9iOpczrjpXLgGoyG7aS5USJXV2d3nn8uHTeb9rXDvzS27zUg5KYQ==} + dependencies: + bit-twiddle: 1.0.2 + dup: 1.0.0 + dev: false + /typedarray-to-buffer@3.1.5: resolution: {integrity: sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==} dependencies: is-typedarray: 1.0.0 + /typedarray@0.0.6: + resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} + dev: false + /typeof-article@0.1.1: resolution: {integrity: sha512-Vn42zdX3FhmUrzEmitX3iYyLb+Umwpmv8fkZRIknYh84lmdrwqZA5xYaoKiIj2Rc5i/5wcDrpUmZcbk1U51vTw==} engines: {node: '>=4'} @@ -24410,6 +25880,10 @@ packages: escalade: 3.2.0 picocolors: 1.1.1 + /update-diff@1.1.0: + resolution: {integrity: sha512-rCiBPiHxZwT4+sBhEbChzpO5hYHjm91kScWgdHf4Qeafs6Ba7MBl+d9GlGv72bcTZQO0sLmtQS1pHSWoCLtN/A==} + dev: false + /upper-case@1.1.3: resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} dev: false @@ -24665,6 +26139,14 @@ packages: resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} engines: {node: '>=0.10.0'} + /vt-pbf@3.1.3: + resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + dependencies: + '@mapbox/point-geometry': 0.1.0 + '@mapbox/vector-tile': 1.3.1 + pbf: 3.3.0 + dev: false + /vue@3.4.31(typescript@5.3.2): resolution: {integrity: sha512-njqRrOy7W3YLAlVqSKpBebtZpDVg21FPoaq1I7f/+qqBThK9ChAIjkRWgeP6Eat+8C+iia4P3OYqpATP21BCoQ==} peerDependencies: @@ -24749,6 +26231,10 @@ packages: resolution: {integrity: sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==} dev: true + /weak-map@1.0.8: + resolution: {integrity: sha512-lNR9aAefbGPpHO7AEnY0hCFjz1eTkWCXYvkTRrTHs9qv8zJp+SkVYpzfLIFXQQiG3tVvbNFQgVg2bQS8YGgxyw==} + dev: false + /web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} dev: false @@ -24762,6 +26248,12 @@ packages: resolution: {integrity: sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg==} dev: false + /webgl-context@2.2.0: + resolution: {integrity: sha512-q/fGIivtqTT7PEoF07axFIlHNk/XCPaYpq64btnepopSWvKNFkoORlQYgqDigBIuGA1ExnFd/GnSUnBNEPQY7Q==} + dependencies: + get-canvas-context: 1.0.2 + dev: false + /webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} @@ -25145,6 +26637,14 @@ packages: dependencies: isexe: 2.0.0 + /which@4.0.0: + resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} + engines: {node: ^16.13.0 || >=18.0.0} + hasBin: true + dependencies: + isexe: 3.1.1 + dev: false + /wildcard@2.0.1: resolution: {integrity: sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==} @@ -25335,6 +26835,12 @@ packages: '@types/trusted-types': 2.0.7 workbox-core: 6.6.0 + /world-calendars@1.0.3: + resolution: {integrity: sha512-sAjLZkBnsbHkHWVhrsCU5Sa/EVuf9QqgvrN8zyJ2L/F9FR9Oc6CvVK0674+PGAtmmmYQMH98tCUSO4QLQv3/TQ==} + dependencies: + object-assign: 4.1.1 + dev: false + /wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -25419,6 +26925,11 @@ packages: /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + /xtend@2.2.0: + resolution: {integrity: sha512-SLt5uylT+4aoXxXuwtQp5ZnMMzhDb1Xkg4pEqc00WUJCQifPfV9Ub1VrNhp9kXkrjZD2I2Hl8WnjP37jzZLPZw==} + engines: {node: '>=0.4'} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} From 400b528dca2ac7c9a35ecfa36ed97dc098128f27 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 21:26:06 +1100 Subject: [PATCH 03/38] wip --- apps/registry/app/similarity/page.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index 8753aff..b509195 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -84,6 +84,8 @@ export default function SimilarityPage() { color: reducedEmbeddings.map((_, i) => i), colorscale: 'Viridis', }, + hoverinfo: 'text', + username: jsonData.map(item => item.username), // Store usernames for click handling }; setData(plotData); @@ -136,6 +138,13 @@ export default function SimilarityPage() { }} useResizeHandler className="w-full h-full" + onClick={(event) => { + if (event?.points?.[0]) { + const pointIndex = event.points[0].pointIndex; + const username = data.username[pointIndex]; + window.open(`/${username}`, '_blank'); + } + }} /> From 0d7fb3d41cc688997998153f0107088b33cf6229 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 21:30:44 +1100 Subject: [PATCH 04/38] wip --- apps/registry/app/api/similarity/route.js | 5 ++-- apps/registry/app/similarity/page.js | 31 ++++++++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 2fa0270..00bb8fa 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -18,14 +18,15 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 100; + const limit = parseInt(searchParams.get('limit')) || 500; console.time('getSimilarityData'); const { data, error } = await supabase .from('resumes') .select('username, embedding') .not('embedding', 'is', null) - .limit(limit); + .limit(limit) + .order('created_at', { ascending: false }); if (error) { console.error('Error fetching similarity data:', error); diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index b509195..a645a4e 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -77,14 +77,16 @@ export default function SimilarityPage() { y: reducedEmbeddings.map(coords => coords[1]), text: jsonData.map(item => item.username), mode: 'markers+text', - type: 'scatter', + type: 'scattergl', // Use WebGL renderer for better performance textposition: 'top', marker: { - size: 10, + size: 8, // Slightly smaller markers for less overlap color: reducedEmbeddings.map((_, i) => i), colorscale: 'Viridis', + opacity: 0.7, // Add some transparency }, hoverinfo: 'text', + hovertemplate: '%{text}', // Clean hover label username: jsonData.map(item => item.username), // Store usernames for click handling }; @@ -129,12 +131,33 @@ export default function SimilarityPage() { data={[data]} layout={{ title: 'Resume Similarity Map', - xaxis: { title: 'Component 1' }, - yaxis: { title: 'Component 2' }, + xaxis: { + title: 'Component 1', + showgrid: true, + zeroline: false, + }, + yaxis: { + title: 'Component 2', + showgrid: true, + zeroline: false, + }, hovermode: 'closest', width: null, height: null, autosize: true, + showlegend: false, + dragmode: 'zoom', // Enable box zoom by default + modebar: { + remove: ['lasso', 'select'], + add: ['drawopenpath', 'eraseshape'], + }, + }} + config={{ + responsive: true, + scrollZoom: true, // Enable scroll to zoom + displayModeBar: true, // Always show the mode bar + modeBarButtonsToAdd: ['select2d', 'lasso2d'], // Add selection tools + displaylogo: false, }} useResizeHandler className="w-full h-full" From ed1c0f5c9fe8b4084a7ad07e527087fb1d4a29ba Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 21:43:00 +1100 Subject: [PATCH 05/38] wip --- apps/registry/app/api/similarity/route.js | 31 +++++++++++++++-------- apps/registry/app/similarity/page.js | 12 +++++---- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 00bb8fa..ce83b58 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -23,7 +23,7 @@ export async function GET(request) { console.time('getSimilarityData'); const { data, error } = await supabase .from('resumes') - .select('username, embedding') + .select('username, embedding, resume') .not('embedding', 'is', null) .limit(limit) .order('created_at', { ascending: false }); @@ -38,15 +38,26 @@ export async function GET(request) { console.timeEnd('getSimilarityData'); - // Parse embeddings from strings to numerical arrays - const parsedData = data.map(item => ({ - ...item, - embedding: typeof item.embedding === 'string' - ? JSON.parse(item.embedding) - : Array.isArray(item.embedding) - ? item.embedding - : null - })).filter(item => item.embedding !== null); + // Parse embeddings from strings to numerical arrays and extract position + const parsedData = data.map(item => { + let resumeData; + try { + resumeData = JSON.parse(item.resume); + } catch (e) { + console.warn('Failed to parse resume for user:', item.username); + resumeData = {}; + } + + return { + username: item.username, + embedding: typeof item.embedding === 'string' + ? JSON.parse(item.embedding) + : Array.isArray(item.embedding) + ? item.embedding + : null, + position: resumeData?.basics?.label || 'Unknown Position' + }; + }).filter(item => item.embedding !== null); return NextResponse.json(parsedData); } catch (error) { diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index a645a4e..9cc28ac 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -75,10 +75,10 @@ export default function SimilarityPage() { const plotData = { x: reducedEmbeddings.map(coords => coords[0]), y: reducedEmbeddings.map(coords => coords[1]), - text: jsonData.map(item => item.username), + text: jsonData.map(item => item.position), mode: 'markers+text', type: 'scattergl', // Use WebGL renderer for better performance - textposition: 'top', + textposition: 'top center', marker: { size: 8, // Slightly smaller markers for less overlap color: reducedEmbeddings.map((_, i) => i), @@ -86,7 +86,9 @@ export default function SimilarityPage() { opacity: 0.7, // Add some transparency }, hoverinfo: 'text', - hovertemplate: '%{text}', // Clean hover label + hovertemplate: + '%{text}
' + + 'Click to view resume', username: jsonData.map(item => item.username), // Store usernames for click handling }; @@ -121,7 +123,7 @@ export default function SimilarityPage() { return (
-

Resume Similarity Map

+

Resume Similarity Map by Position

This visualization shows how similar resumes are to each other based on their content. Resumes that are closer together are more similar. @@ -130,7 +132,7 @@ export default function SimilarityPage() { Date: Thu, 19 Dec 2024 22:07:43 +1100 Subject: [PATCH 06/38] wip --- apps/registry/app/api/similarity/route.js | 2 +- apps/registry/app/similarity/page.js | 255 ++++---- apps/registry/package.json | 2 + pnpm-lock.yaml | 730 +++++++++++++++++++++- 4 files changed, 868 insertions(+), 121 deletions(-) diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index ce83b58..5d3aa01 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 500; + const limit = parseInt(searchParams.get('limit')) || 1500; console.time('getSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index 9cc28ac..8b2684c 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -1,11 +1,10 @@ 'use client'; -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import dynamic from 'next/dynamic'; -import { PrismaClient } from '@prisma/client'; -// Import Plotly dynamically to avoid SSR issues -const Plot = dynamic(() => import('react-plotly.js'), { ssr: false }); +// Import ForceGraph dynamically to avoid SSR issues +const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false }); // Helper function to compute cosine similarity function cosineSimilarity(a, b) { @@ -20,44 +19,13 @@ function cosineSimilarity(a, b) { return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } -// Helper function for dimensionality reduction using PCA -function pca(vectors, dimensions = 2) { - // Center the data - const mean = vectors[0].map((_, colIndex) => - vectors.reduce((sum, row) => sum + row[colIndex], 0) / vectors.length - ); - - const centered = vectors.map(vector => - vector.map((value, index) => value - mean[index]) - ); - - // Compute covariance matrix - const covMatrix = []; - for (let i = 0; i < centered[0].length; i++) { - covMatrix[i] = []; - for (let j = 0; j < centered[0].length; j++) { - let sum = 0; - for (let k = 0; k < centered.length; k++) { - sum += centered[k][i] * centered[k][j]; - } - covMatrix[i][j] = sum / (centered.length - 1); - } - } - - // For simplicity, we'll just take the first two dimensions - // In a production environment, you'd want to compute eigenvectors properly - const reduced = centered.map(vector => [ - vector.slice(0, dimensions).reduce((sum, val) => sum + val, 0), - vector.slice(dimensions, dimensions * 2).reduce((sum, val) => sum + val, 0) - ]); - - return reduced; -} - export default function SimilarityPage() { - const [data, setData] = useState(null); + const [graphData, setGraphData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [highlightNodes, setHighlightNodes] = useState(new Set()); + const [highlightLinks, setHighlightLinks] = useState(new Set()); + const [hoverNode, setHoverNode] = useState(null); useEffect(() => { async function fetchData() { @@ -68,31 +36,61 @@ export default function SimilarityPage() { } const jsonData = await response.json(); - // Reduce dimensions of embeddings using PCA - const reducedEmbeddings = pca(jsonData.map(item => item.embedding)); - - // Prepare data for plotting - const plotData = { - x: reducedEmbeddings.map(coords => coords[0]), - y: reducedEmbeddings.map(coords => coords[1]), - text: jsonData.map(item => item.position), - mode: 'markers+text', - type: 'scattergl', // Use WebGL renderer for better performance - textposition: 'top center', - marker: { - size: 8, // Slightly smaller markers for less overlap - color: reducedEmbeddings.map((_, i) => i), - colorscale: 'Viridis', - opacity: 0.7, // Add some transparency - }, - hoverinfo: 'text', - hovertemplate: - '%{text}
' + - 'Click to view resume', - username: jsonData.map(item => item.username), // Store usernames for click handling - }; - - setData(plotData); + // Group similar positions + const positionGroups = {}; + jsonData.forEach(item => { + const position = item.position; + if (!positionGroups[position]) { + positionGroups[position] = []; + } + positionGroups[position].push(item); + }); + + // Create nodes and links + const nodes = []; + const links = []; + const similarityThreshold = 0.7; + + // Create nodes for each unique position + Object.entries(positionGroups).forEach(([position, items], index) => { + nodes.push({ + id: position, + group: index, + size: Math.log(items.length + 1) * 3, + count: items.length, + usernames: items.map(item => item.username), + embeddings: items.map(item => item.embedding), + color: `hsl(${Math.random() * 360}, 70%, 50%)` + }); + }); + + // Create links between similar positions + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + // Calculate average similarity between groups + let totalSimilarity = 0; + let comparisons = 0; + + nodes[i].embeddings.forEach(emb1 => { + nodes[j].embeddings.forEach(emb2 => { + totalSimilarity += cosineSimilarity(emb1, emb2); + comparisons++; + }); + }); + + const avgSimilarity = totalSimilarity / comparisons; + + if (avgSimilarity > similarityThreshold) { + links.push({ + source: nodes[i].id, + target: nodes[j].id, + value: avgSimilarity + }); + } + } + } + + setGraphData({ nodes, links }); } catch (err) { setError(err.message); } finally { @@ -103,10 +101,18 @@ export default function SimilarityPage() { fetchData(); }, []); + const handleNodeHover = useCallback(node => { + setHighlightNodes(new Set(node ? [node] : [])); + setHighlightLinks(new Set(graphData?.links.filter(link => + link.source.id === node?.id || link.target.id === node?.id + ) || [])); + setHoverNode(node || null); + }, [graphData]); + if (loading) { return (

-

Resume Similarity Map

+

Resume Position Network

Loading...

); @@ -115,7 +121,7 @@ export default function SimilarityPage() { if (error) { return (
-

Resume Similarity Map

+

Resume Position Network

Error: {error}

); @@ -123,54 +129,79 @@ export default function SimilarityPage() { return (
-

Resume Similarity Map by Position

+

Resume Position Network

- This visualization shows how similar resumes are to each other based on their content. - Resumes that are closer together are more similar. + Explore similar positions in an interactive network. Each node represents a position, with size indicating the number of resumes. + Connected positions are similar based on resume content. Hover to highlight connections, click to view resumes.

-
- { - if (event?.points?.[0]) { - const pointIndex = event.points[0].pointIndex; - const username = data.username[pointIndex]; - window.open(`/${username}`, '_blank'); - } - }} - /> +
+ {graphData && ( + highlightNodes.has(node) ? '#ff0000' : node.color} + nodeCanvasObject={(node, ctx, globalScale) => { + // Draw node + ctx.beginPath(); + ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); + ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; + ctx.fill(); + + // Only draw label if node is highlighted + if (highlightNodes.has(node)) { + const label = node.id; + const fontSize = Math.max(14, node.size * 1.5); + ctx.font = `${fontSize}px Sans-Serif`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); + + // Draw background for label + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] * 2, + bckgDimensions[0], + bckgDimensions[1] + ); + + // Draw label + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; + ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); + + // Draw count + const countLabel = `(${node.count})`; + const smallerFont = fontSize * 0.7; + ctx.font = `${smallerFont}px Sans-Serif`; + ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); + } + }} + nodeRelSize={6} + linkWidth={link => highlightLinks.has(link) ? 2 : 1} + linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} + linkOpacity={0.3} + linkDirectionalParticles={4} + linkDirectionalParticleWidth={2} + onNodeHover={handleNodeHover} + onNodeClick={(node) => { + if (node.usernames && node.usernames.length > 0) { + window.open(`/${node.usernames[0]}`, '_blank'); + } + }} + enableNodeDrag={true} + cooldownTicks={100} + d3AlphaDecay={0.02} + d3VelocityDecay={0.3} + warmupTicks={100} + /> + )} + {hoverNode && ( +
+

{hoverNode.id}

+

{hoverNode.count} resumes

+

Click to view a sample resume

+
+ )}
); diff --git a/apps/registry/package.json b/apps/registry/package.json index 5831ee6..a4bb021 100644 --- a/apps/registry/package.json +++ b/apps/registry/package.json @@ -101,6 +101,8 @@ "react": "^18.3.1", "react-autocomplete": "^1.8.1", "react-dom": "^18.3.1", + "react-force-graph": "^1.45.4", + "react-force-graph-2d": "^1.26.1", "react-markdown": "^8.0.7", "react-plotly.js": "^2.6.0", "react-speech-recognition": "^3.10.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8258587..2ba465e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -447,6 +447,12 @@ importers: react-dom: specifier: ^18.3.1 version: 18.3.1(react@18.3.1) + react-force-graph: + specifier: ^1.45.4 + version: 1.45.4(react@18.3.1)(three@0.171.0) + react-force-graph-2d: + specifier: ^1.26.1 + version: 1.26.1(react@18.3.1) react-markdown: specifier: ^8.0.7 version: 8.0.7(@types/react@18.3.3)(react@18.3.1) @@ -942,6 +948,42 @@ importers: packages: + /3d-force-graph-ar@1.9.3(three@0.171.0): + resolution: {integrity: sha512-KHcwKVF8394ioKhc4h3y5H9jPBvw+lUmD1BJd1AEV/SO+FM324CXVYTvbGg2IuW0nOPR/ChXvlWhvIZaOtyeTg==} + engines: {node: '>=12'} + dependencies: + aframe-forcegraph-component: 3.1.0(three@0.171.0) + kapsule: 1.16.0 + transitivePeerDependencies: + - three + dev: false + + /3d-force-graph-vr@2.4.3(three@0.171.0): + resolution: {integrity: sha512-os/IPpkWUqNnqWFQISaa5Snts1uUFf485SMPpPi2BIJLHbW9Jxt+1PQWc2rqT3tDFTh9J72BnxyuqegyMrqxdQ==} + engines: {node: '>=12'} + dependencies: + accessor-fn: 1.5.1 + aframe: 1.6.0 + aframe-extras: 7.5.2 + aframe-forcegraph-component: 3.1.0(three@0.171.0) + kapsule: 1.16.0 + polished: 4.2.2 + transitivePeerDependencies: + - supports-color + - three + dev: false + + /3d-force-graph@1.74.5: + resolution: {integrity: sha512-CyneQqxoFwTGOqBVe8DSED0uQrU3q4+xpgvl0kbNHctv/kdBRgOKSSJgyB7j+08mUKaHchelMmwNVmQf5XaJrA==} + engines: {node: '>=12'} + dependencies: + accessor-fn: 1.5.1 + kapsule: 1.16.0 + three: 0.171.0 + three-forcegraph: 1.42.10(three@0.171.0) + three-render-objects: 1.32.1(three@0.171.0) + dev: false + /@adobe/css-tools@4.4.1: resolution: {integrity: sha512-12WGKBQzjUAI4ayyF4IAtfw2QR/IDoqk6jTddXDhtYTJF9ASmoE1zst7cVtP0aL/F1jUJL5r+JxKXKEgHNbEUQ==} dev: true @@ -8063,6 +8105,11 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@sindresorhus/is@0.14.0: + resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} + engines: {node: '>=6'} + dev: false + /@sindresorhus/merge-streams@2.3.0: resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -8984,6 +9031,13 @@ packages: resolution: {integrity: sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==} dev: true + /@szmarczak/http-timer@1.1.2: + resolution: {integrity: sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==} + engines: {node: '>=6'} + dependencies: + defer-to-connect: 1.1.3 + dev: false + /@tailwindcss/typography@0.5.13(tailwindcss@3.4.3): resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} peerDependencies: @@ -9178,6 +9232,10 @@ packages: '@types/geojson': 7946.0.15 dev: false + /@tweenjs/tween.js@25.0.0: + resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==} + dev: false + /@types/aria-query@5.0.4: resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} dev: true @@ -9425,6 +9483,12 @@ packages: /@types/json5@0.0.29: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + /@types/keyv@3.1.4: + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + dependencies: + '@types/node': 20.10.0 + dev: false + /@types/lodash@4.14.202: resolution: {integrity: sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==} dev: true @@ -9561,6 +9625,12 @@ packages: resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} dev: true + /@types/responselike@1.0.3: + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + dependencies: + '@types/node': 20.10.0 + dev: false + /@types/retry@0.12.0: resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} @@ -10225,6 +10295,11 @@ packages: mime-types: 2.1.35 negotiator: 0.6.3 + /accessor-fn@1.5.1: + resolution: {integrity: sha512-zZpFYBqIL1Aqg+f2qmYHJ8+yIZF7/tP6PUGx2/QM0uGPSO5UegpinmkNwDohxWtOj586BpMPVRUjce2HI6xB3A==} + engines: {node: '>=12'} + dev: false + /acorn-globals@3.1.0: resolution: {integrity: sha512-uWttZCk96+7itPxK8xCzY86PnxKTMrReKDqrHzv42VQY0K30PUO8WY13WMOuI+cOdX4EIdzdvQ8k6jkuGRFMYw==} dependencies: @@ -10305,6 +10380,39 @@ packages: loader-utils: 2.0.4 regex-parser: 2.2.11 + /aframe-extras@7.5.2: + resolution: {integrity: sha512-rLjKZTMDVt+ial6/S8IH1lnKAQQgPclMNjuImh80nC67OTUT9yOEOZh79IqSOQD2YGs1DEwkl9hAkUSiPkUKCQ==} + dependencies: + nipplejs: 0.10.2 + three: 0.164.1 + three-pathfinding: 1.3.0(three@0.164.1) + dev: false + + /aframe-forcegraph-component@3.1.0(three@0.171.0): + resolution: {integrity: sha512-WJH++Au5LnIjISqkSkkQMN0PJdVzk5n7DSQe1iBy1juQm/FM0mIkHhW13BvmKwr1xgeA8NCQoLvMJFkhb6qX2g==} + dependencies: + accessor-fn: 1.5.1 + three-forcegraph: 1.42.10(three@0.171.0) + transitivePeerDependencies: + - three + dev: false + + /aframe@1.6.0: + resolution: {integrity: sha512-+P1n2xKGZQbCNW4lTwfue9in2KmfAwYD/BZOU5uXKrJCTegPyUZZX/haJRR9Rb33ij+KPj3vFdwT5ALaucXTNA==} + engines: {node: '>= 4.6.0', npm: '>= 2.15.9'} + dependencies: + buffer: 6.0.3 + debug: 4.3.5(supports-color@5.5.0) + deep-assign: 2.0.0 + load-bmfont: 1.4.2(debug@4.3.5) + super-animejs: 3.1.0 + three: /super-three@0.164.0 + three-bmfont-text: github.com/dmarcos/three-bmfont-text/eed4878795be9b3e38cf6aec6b903f56acd1f695 + webvr-polyfill: 0.10.12 + transitivePeerDependencies: + - supports-color + dev: false + /agent-base@6.0.2: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} @@ -10444,6 +10552,10 @@ packages: dev: false optional: true + /an-array@1.0.0: + resolution: {integrity: sha512-M175GYI7RmsYu24Ok383yZQa3eveDfNnmhTe3OQ3bm70bEovz2gWenH+ST/n32M8lrwLWk74hcPds5CDRPe2wg==} + dev: false + /ansi-bgblack@0.1.1: resolution: {integrity: sha512-tp8M/NCmSr6/skdteeo9UgJ2G1rG88X3ZVNZWXUxFw4Wh0PAGaAAWQS61sfBt/1QNcwMTY3EBKOMPujwioJLaw==} engines: {node: '>=0.10.0'} @@ -10825,6 +10937,11 @@ packages: resolution: {integrity: sha512-UfobP5N12Qm4Qu4fwLDIi2v6+wZsSf6snYSxAMeKhrh37YGnNWZPRmVEKc/2wfms53TLQnzfpG8wCx2Y/6NG1w==} dev: false + /array-shuffle@1.0.1: + resolution: {integrity: sha512-PBqgo1Y2XWSksBzq3GFPEb798ZrW2snAcmr4drbVeF/6MT/5aBlkGJEvu5A/CzXHf4EjbHOj/ZowatjlIiVidA==} + engines: {node: '>=0.10.0'} + dev: false + /array-sort@0.1.4: resolution: {integrity: sha512-BNcM+RXxndPxiZ2rd76k6nyQLRZr2/B/sdi8pQ+Joafr5AH279L40dfokSUTp8O+AaqYjXWhblBWa2st2nc4fQ==} engines: {node: '>=0.10.0'} @@ -10941,6 +11058,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /as-number@1.0.0: + resolution: {integrity: sha512-HkI/zLo2AbSRO4fqVkmyf3hms0bJDs3iboHqTrNuwTiCRvdYXM7HFhfhB6Dk51anV2LM/IMB83mtK9mHw4FlAg==} + dev: false + /asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} @@ -11050,7 +11171,7 @@ packages: /axios@0.26.1: resolution: {integrity: sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 transitivePeerDependencies: - debug dev: false @@ -11058,7 +11179,7 @@ packages: /axios@1.3.6: resolution: {integrity: sha512-PEcdkk7JcdPiMDkvM4K6ZBRYq9keuVJsToxm2zQIM70Qqo2WHTdJZMXcG9X+RmRp2VPNUQC8W1RAGbgt6b1yMg==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.2 form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -11068,7 +11189,7 @@ packages: /axios@1.6.2(debug@4.3.4): resolution: {integrity: sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==} dependencies: - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.9(debug@4.3.4) form-data: 4.0.0 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -11404,7 +11525,6 @@ packages: /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - dev: true /base@0.11.2: resolution: {integrity: sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==} @@ -11439,6 +11559,10 @@ packages: is-windows: 1.0.2 dev: false + /bezier-js@6.1.4: + resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==} + dev: false + /bfj@7.1.0: resolution: {integrity: sha512-I6MMLkn+anzNdCUp9hMRyui1HaNEUCco50lxbvNS4+EyXg8lN3nJ48PjPWtbH8UVS9CuMoaKE9U2V3l29DaRQw==} engines: {node: '>= 8.0.0'} @@ -11625,6 +11749,11 @@ packages: dependencies: node-int64: 0.4.0 + /buffer-equal@0.0.1: + resolution: {integrity: sha512-RgSV6InVQ9ODPdLWJ5UAqBqJBOg370Nz6ZQtRzpt6nUjc8v0St97uJ4PYC6NztqIScrAXafKM3mZPMygSe1ggA==} + engines: {node: '>=0.4.0'} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} requiresBuild: true @@ -11641,6 +11770,13 @@ packages: ieee754: 1.2.1 dev: true + /buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + dev: false + /builtin-modules@3.3.0: resolution: {integrity: sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==} engines: {node: '>=6'} @@ -11680,6 +11816,19 @@ packages: unset-value: 1.0.0 dev: false + /cacheable-request@6.1.0: + resolution: {integrity: sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==} + engines: {node: '>=8'} + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.1.1 + keyv: 3.1.0 + lowercase-keys: 2.0.0 + normalize-url: 4.5.1 + responselike: 1.0.2 + dev: false + /call-bind@1.0.7: resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==} engines: {node: '>= 0.4'} @@ -11759,12 +11908,27 @@ packages: /caniuse-lite@1.0.30001680: resolution: {integrity: sha512-rPQy70G6AGUMnbwS1z6Xg+RkHYPAi18ihs47GH0jcxIG7wArmPgY3XbS2sRdBbxJljp3thdT8BIqv9ccCypiPA==} + /canvas-color-tracker@1.3.1: + resolution: {integrity: sha512-eNycxGS7oQ3IS/9QQY41f/aQjiO9Y/MtedhCgSdsbLSxC9EyUD8L3ehl/Q3Kfmvt8um79S45PBV+5Rxm5ztdSw==} + engines: {node: '>=12'} + dependencies: + tinycolor2: 1.6.0 + dev: false + /canvas-fit@1.5.0: resolution: {integrity: sha512-onIcjRpz69/Hx5bB5HGbYKUF2uC6QT6Gp+pfpGm3A7mPfcluSLV5v4Zu+oflDUwLdUw0rLIBhUbi0v8hM4FJQQ==} dependencies: element-size: 1.1.1 dev: false + /cardboard-vr-display@1.0.19: + resolution: {integrity: sha512-+MjcnWKAkb95p68elqZLDPzoiF/dGncQilLGvPBM5ZorABp/ao3lCs7nnRcYBckmuNkg1V/5rdGDKoUaCVsHzQ==} + dependencies: + gl-preserve-state: 1.0.0 + nosleep.js: 0.7.0 + webvr-polyfill-dpdb: 1.0.18 + dev: false + /case-sensitive-paths-webpack-plugin@2.4.0: resolution: {integrity: sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==} engines: {node: '>=4'} @@ -11781,6 +11945,14 @@ packages: lazy-cache: 1.0.4 dev: false + /centra@2.7.0(debug@4.3.5): + resolution: {integrity: sha512-PbFMgMSrmgx6uxCdm57RUos9Tc3fclMvhLSATYN39XsDV29B89zZ3KA89jmY0vwSGazyU+uerqwa6t+KaodPcg==} + dependencies: + follow-redirects: 1.15.9(debug@4.3.5) + transitivePeerDependencies: + - debug + dev: false + /chai@4.5.0: resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} engines: {node: '>=4'} @@ -12041,6 +12213,12 @@ packages: kind-of: 6.0.3 shallow-clone: 3.0.1 + /clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + dependencies: + mimic-response: 1.0.1 + dev: false + /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -12841,6 +13019,10 @@ packages: internmap: 2.0.3 dev: false + /d3-binarytree@1.0.2: + resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + dev: false + /d3-collection@1.0.7: resolution: {integrity: sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A==} dev: false @@ -12854,11 +13036,30 @@ packages: resolution: {integrity: sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==} dev: false + /d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 1.0.6 + d3-selection: 3.0.0 + dev: false + /d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} dev: false + /d3-force-3d@3.0.5: + resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==} + engines: {node: '>=12'} + dependencies: + d3-binarytree: 1.0.2 + d3-dispatch: 1.0.6 + d3-octree: 1.0.2 + d3-quadtree: 1.0.7 + d3-timer: 3.0.1 + dev: false + /d3-force@1.2.1: resolution: {integrity: sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==} dependencies: @@ -12904,6 +13105,10 @@ packages: d3-color: 3.1.0 dev: false + /d3-octree@1.0.2: + resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==} + dev: false + /d3-path@1.0.9: resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} dev: false @@ -12917,6 +13122,14 @@ packages: resolution: {integrity: sha512-RKPAeXnkC59IDGD0Wu5mANy0Q2V28L+fNe65pOCXVdVuTJS3WPKaJlFHer32Rbh9gIo9qMuJXio8ra4+YmIymA==} dev: false + /d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + dev: false + /d3-scale@4.0.2: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} @@ -12928,6 +13141,11 @@ packages: d3-time-format: 4.1.0 dev: false + /d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + dev: false + /d3-shape@1.3.7: resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} dependencies: @@ -12974,6 +13192,31 @@ packages: engines: {node: '>=12'} dev: false + /d3-transition@3.0.1(d3-selection@3.0.0): + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + dependencies: + d3-color: 3.1.0 + d3-dispatch: 1.0.6 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + dev: false + + /d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 1.0.6 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + dev: false + /d@1.0.2: resolution: {integrity: sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==} engines: {node: '>=0.12'} @@ -12992,6 +13235,13 @@ packages: assert-plus: 1.0.0 dev: false + /data-bind-mapper@1.0.1: + resolution: {integrity: sha512-xWkgLj/mSDs/Y2flAMXwLKxnCh+rFScf4N8hSOtpsMxXYXui7CbtIUYP52VXQze9HhRND2Ua/AiEHZ8j/vtB0w==} + engines: {node: '>=12'} + dependencies: + accessor-fn: 1.5.1 + dev: false + /data-urls@2.0.0: resolution: {integrity: sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==} engines: {node: '>=10'} @@ -13132,6 +13382,13 @@ packages: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + /decompress-response@3.3.0: + resolution: {integrity: sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==} + engines: {node: '>=4'} + dependencies: + mimic-response: 1.0.1 + dev: false + /dedent@0.7.0: resolution: {integrity: sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==} @@ -13141,6 +13398,13 @@ packages: lodash.isplainobject: 4.0.6 dev: false + /deep-assign@2.0.0: + resolution: {integrity: sha512-2QhG3Kxulu4XIF3WL5C5x0sc/S17JLgm1SfvDfIRsR/5m7ZGmcejII7fZ2RyWhN0UWIJm0TNM/eKow6LAn3evQ==} + engines: {node: '>=0.10.0'} + dependencies: + is-obj: 1.0.1 + dev: false + /deep-eql@4.1.4: resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} engines: {node: '>=6'} @@ -13196,6 +13460,10 @@ packages: dependencies: clone: 1.0.4 + /defer-to-connect@1.1.3: + resolution: {integrity: sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==} + dev: false + /define-data-property@1.1.4: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} @@ -13429,6 +13697,10 @@ packages: entities: 4.5.0 dev: false + /dom-walk@0.1.2: + resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} + dev: false + /domelementtype@1.3.1: resolution: {integrity: sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==} @@ -13537,6 +13809,10 @@ packages: resolution: {integrity: sha512-Bz5jxMMC0wgp23Zm15ip1x8IhYRqJvF3nFC0UInJUDkN1z4uNPk9jTnfCUJXbOGiQ1JbXLQsiV41Fb+HXcj5BA==} dev: false + /duplexer3@0.1.5: + resolution: {integrity: sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==} + dev: false + /duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -15065,9 +15341,19 @@ packages: engines: {node: '>=0.4.0'} dev: true - /follow-redirects@1.15.2(debug@4.3.4): + /follow-redirects@1.15.2: resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==} engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + + /follow-redirects@1.15.9(debug@4.3.4): + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} peerDependencies: debug: '*' peerDependenciesMeta: @@ -15076,6 +15362,18 @@ packages: dependencies: debug: 4.3.4 + /follow-redirects@1.15.9(debug@4.3.5): + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dependencies: + debug: 4.3.5(supports-color@5.5.0) + dev: false + /font-atlas@2.1.0: resolution: {integrity: sha512-kP3AmvX+HJpW4w3d+PiPR2X6E1yvsBXt2yhuCw+yReO9F1WYhvZwx3c95DGZGwg9xYzDGrgJYa885xmVA+28Cg==} dependencies: @@ -15105,6 +15403,26 @@ packages: for-in: 1.0.2 dev: false + /force-graph@1.47.1: + resolution: {integrity: sha512-NF0prpR8tNGq7oCE/aFImT/6/3wSk585bcp39UAj6SNSPjvKbX6ktCH6cZnjfsnPNx/DYj1rn+cvvjH814HCFA==} + engines: {node: '>=12'} + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.1 + bezier-js: 6.1.4 + canvas-color-tracker: 1.3.1 + d3-array: 3.2.4 + d3-drag: 3.0.0 + d3-force-3d: 3.0.5 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + index-array-by: 1.4.2 + kapsule: 1.16.0 + lodash-es: 4.17.21 + dev: false + /foreground-child@3.2.1: resolution: {integrity: sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==} engines: {node: '>=14'} @@ -15437,6 +15755,20 @@ packages: engines: {node: '>=6'} dev: true + /get-stream@4.1.0: + resolution: {integrity: sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==} + engines: {node: '>=6'} + dependencies: + pump: 3.0.2 + dev: false + + /get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + dependencies: + pump: 3.0.2 + dev: false + /get-stream@6.0.1: resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} engines: {node: '>=10'} @@ -15497,6 +15829,10 @@ packages: resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==} dev: false + /gl-preserve-state@1.0.0: + resolution: {integrity: sha512-zQZ25l3haD4hvgJZ6C9+s0ebdkW9y+7U2qxvGu1uWOJh8a4RU+jURIKEQhf8elIlFpMH6CrAY2tH0mYrRjet3Q==} + dev: false + /gl-text@1.4.0: resolution: {integrity: sha512-o47+XBqLCj1efmuNyCHt7/UEJmB9l66ql7pnobD6p+sgmBUdzfMZXIF0zD2+KRfpd99DJN+QXdvTFAGCKCVSmQ==} dependencies: @@ -15610,6 +15946,13 @@ packages: which: 4.0.0 dev: false + /global@4.4.0: + resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} + dependencies: + min-document: 2.19.0 + process: 0.11.10 + dev: false + /globals@11.12.0: resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} engines: {node: '>=4'} @@ -15784,6 +16127,25 @@ packages: dependencies: get-intrinsic: 1.2.4 + /got@9.6.0: + resolution: {integrity: sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==} + engines: {node: '>=8.6'} + dependencies: + '@sindresorhus/is': 0.14.0 + '@szmarczak/http-timer': 1.1.2 + '@types/keyv': 3.1.4 + '@types/responselike': 1.0.3 + cacheable-request: 6.1.0 + decompress-response: 3.3.0 + duplexer3: 0.1.5 + get-stream: 4.1.0 + lowercase-keys: 1.0.1 + mimic-response: 1.0.1 + p-cancelable: 1.1.0 + to-readable-stream: 1.0.0 + url-parse-lax: 3.0.0 + dev: false + /graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -16384,6 +16746,10 @@ packages: entities: 3.0.1 dev: true + /http-cache-semantics@4.1.1: + resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==} + dev: false + /http-deceiver@1.2.7: resolution: {integrity: sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==} @@ -16442,7 +16808,7 @@ packages: engines: {node: '>=8.0.0'} dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.2(debug@4.3.4) + follow-redirects: 1.15.9(debug@4.3.4) requires-port: 1.0.0 transitivePeerDependencies: - debug @@ -16590,6 +16956,11 @@ packages: engines: {node: '>=12'} dev: false + /index-array-by@1.4.2: + resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==} + engines: {node: '>=12'} + dev: false + /inflight@1.0.6: resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. @@ -16882,6 +17253,10 @@ packages: get-east-asian-width: 1.2.0 dev: true + /is-function@1.0.2: + resolution: {integrity: sha512-lw7DUp0aWXYg+CBCN+JKkcE0Q2RayZnSvnZBlwgxHBQhqt5pZNVy4Ri7H9GmmXkdu7LUthszM+Tor1u/2iBcpQ==} + dev: false + /is-generator-fn@2.1.0: resolution: {integrity: sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==} engines: {node: '>=6'} @@ -17261,6 +17636,11 @@ packages: filelist: 1.0.4 minimatch: 3.1.2 + /jerrypick@1.1.1: + resolution: {integrity: sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==} + engines: {node: '>=12'} + dev: false + /jest-changed-files@27.5.1: resolution: {integrity: sha512-buBLMiByfWGCoMsLLzGUUSpAmIAGnbR2KJoMN10ziLhOLvP4e0SlypHnAel8iqQXTrcbmfEY9sSqae5sgUsTvw==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} @@ -17902,6 +18282,10 @@ packages: engines: {node: '>=6'} hasBin: true + /json-buffer@3.0.0: + resolution: {integrity: sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==} + dev: false + /json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} @@ -18318,6 +18702,13 @@ packages: object.assign: 4.1.5 object.values: 1.2.0 + /kapsule@1.16.0: + resolution: {integrity: sha512-4f/z/Luu0cEXmagCwaFyzvfZai2HKgB4CQLwmsMUA+jlUbW94HfFSX+TWZxzWoMSO6b6aR+FD2Xd5z88VYZJTw==} + engines: {node: '>=12'} + dependencies: + lodash-es: 4.17.21 + dev: false + /kdbush@3.0.0: resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} dev: false @@ -18326,6 +18717,12 @@ packages: resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} dev: false + /keyv@3.1.0: + resolution: {integrity: sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==} + dependencies: + json-buffer: 3.0.0 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -18387,6 +18784,14 @@ packages: picocolors: 1.0.1 shell-quote: 1.8.1 + /layout-bmfont-text@1.3.4: + resolution: {integrity: sha512-mceomHZ8W7pSKQhTdLvOe1Im4n37u8xa5Gr0J3KPCHRMO/9o7+goWIOzZcUUd+Xgzy3+22bvoIQ0OaN3LRtgaw==} + dependencies: + as-number: 1.0.0 + word-wrapper: 1.0.7 + xtend: 4.0.2 + dev: false + /lazy-ass@1.6.0: resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==} engines: {node: '> 0.8'} @@ -18606,6 +19011,21 @@ packages: '@lmdb/lmdb-win32-x64': 2.8.5 dev: true + /load-bmfont@1.4.2(debug@4.3.5): + resolution: {integrity: sha512-qElWkmjW9Oq1F9EI5Gt7aD9zcdHb9spJCW1L/dmPf7KzCCEJxq8nhHz5eCgI9aMf7vrG/wyaCqdsI+Iy9ZTlog==} + dependencies: + buffer-equal: 0.0.1 + mime: 1.6.0 + parse-bmfont-ascii: 1.0.6 + parse-bmfont-binary: 1.0.6 + parse-bmfont-xml: 1.1.6 + phin: 3.7.1(debug@4.3.5) + xhr: 2.6.0 + xtend: 4.0.2 + transitivePeerDependencies: + - debug + dev: false + /load-yaml-file@0.2.0: resolution: {integrity: sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==} engines: {node: '>=6'} @@ -18670,6 +19090,10 @@ packages: dependencies: p-locate: 6.0.0 + /lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + dev: false + /lodash._reinterpolate@3.0.0: resolution: {integrity: sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==} dev: false @@ -18803,6 +19227,16 @@ packages: dependencies: tslib: 2.6.3 + /lowercase-keys@1.0.1: + resolution: {integrity: sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==} + engines: {node: '>=0.10.0'} + dev: false + + /lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + dev: false + /lowlight@1.20.0: resolution: {integrity: sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==} dependencies: @@ -19645,6 +20079,17 @@ packages: resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} engines: {node: '>=12'} + /mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + dev: false + + /min-document@2.19.0: + resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + dependencies: + dom-walk: 0.1.2 + dev: false + /min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -19892,6 +20337,10 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + /new-array@1.0.0: + resolution: {integrity: sha512-K5AyFYbuHZ4e/ti52y7k18q8UHsS78FlRd85w2Fmsd6AkuLipDihPflKC0p3PN5i8II7+uHxo+CtkLiJDfmS5A==} + dev: false + /next-auth@5.0.0-beta.18(next@14.2.5)(react@18.3.1): resolution: {integrity: sha512-x55L8wZb8PcPGCYA3e/l9tdpd7YL3FDuhas4W8pxq3PjrWJ9OoDxNN0otK9axJamJBbBgjfzTJjVQB6hXoe0ZQ==} peerDependencies: @@ -19970,6 +20419,46 @@ packages: - babel-plugin-macros dev: false + /ngraph.events@1.2.2: + resolution: {integrity: sha512-JsUbEOzANskax+WSYiAPETemLWYXmixuPAlmZmhIbIj6FH/WDgEGCGnRwUQBK0GjOnVm8Ui+e5IJ+5VZ4e32eQ==} + dev: false + + /ngraph.forcelayout@3.3.1: + resolution: {integrity: sha512-MKBuEh1wujyQHFTW57y5vd/uuEOK0XfXYxm3lC7kktjJLRdt/KEKEknyOlc6tjXflqBKEuYBBcu7Ax5VY+S6aw==} + dependencies: + ngraph.events: 1.2.2 + ngraph.merge: 1.0.0 + ngraph.random: 1.1.0 + dev: false + + /ngraph.graph@20.0.1: + resolution: {integrity: sha512-VFsQ+EMkT+7lcJO1QP8Ik3w64WbHJl27Q53EO9hiFU9CRyxJ8HfcXtfWz/U8okuoYKDctbciL6pX3vG5dt1rYA==} + dependencies: + ngraph.events: 1.2.2 + dev: false + + /ngraph.merge@1.0.0: + resolution: {integrity: sha512-5J8YjGITUJeapsomtTALYsw7rFveYkM+lBj3QiYZ79EymQcuri65Nw3knQtFxQBU1r5iOaVRXrSwMENUPK62Vg==} + dev: false + + /ngraph.random@1.1.0: + resolution: {integrity: sha512-h25UdUN/g8U7y29TzQtRm/GvGr70lK37yQPvPKXXuVfs7gCm82WipYFZcksQfeKumtOemAzBIcT7lzzyK/edLw==} + dev: false + + /nice-color-palettes@3.0.0: + resolution: {integrity: sha512-lL4AjabAAFi313tjrtmgm/bxCRzp4l3vCshojfV/ij3IPdtnRqv6Chcw+SqJUhbe7g3o3BecaqCJYUNLswGBhQ==} + hasBin: true + dependencies: + got: 9.6.0 + map-limit: 0.0.1 + minimist: 1.2.8 + new-array: 1.0.0 + dev: false + + /nipplejs@0.10.2: + resolution: {integrity: sha512-XGxFY8C2DOtobf1fK+MXINTzkkXJLjZDDpfQhOUZf4TSytbc9s4bmA0lB9eKKM8iDivdr9NQkO7DpIQfsST+9g==} + dev: false + /no-case@2.3.2: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} dependencies: @@ -20113,10 +20602,19 @@ packages: svg-arc-to-cubic-bezier: 3.2.0 dev: false + /normalize-url@4.5.1: + resolution: {integrity: sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==} + engines: {node: '>=8'} + dev: false + /normalize-url@6.1.0: resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} engines: {node: '>=10'} + /nosleep.js@0.7.0: + resolution: {integrity: sha512-Z4B1HgvzR+en62ghwZf6BwAR6x4/pjezsiMcbF9KMLh7xoscpoYhaSXfY3lLkqC68AtW+/qLJ1lzvBIj0FGaTA==} + dev: false + /npm-run-path@4.0.1: resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} engines: {node: '>=8'} @@ -20552,6 +21050,11 @@ packages: resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} dev: false + /p-cancelable@1.1.0: + resolution: {integrity: sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==} + engines: {node: '>=6'} + dev: false + /p-filter@2.1.0: resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} engines: {node: '>=8'} @@ -20689,6 +21192,21 @@ packages: resolution: {integrity: sha512-KF/U8tk54BgQewkJPvB4s/US3VQY68BRDpH638+7O/n58TpnwiwnOtGIOsT2/i+M78s61BBpeC83STB88d8sqw==} dev: false + /parse-bmfont-ascii@1.0.6: + resolution: {integrity: sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA==} + dev: false + + /parse-bmfont-binary@1.0.6: + resolution: {integrity: sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA==} + dev: false + + /parse-bmfont-xml@1.1.6: + resolution: {integrity: sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA==} + dependencies: + xml-parse-from-string: 1.0.1 + xml2js: 0.5.0 + dev: false + /parse-entities@2.0.0: resolution: {integrity: sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==} dependencies: @@ -20700,6 +21218,10 @@ packages: is-hexadecimal: 1.0.4 dev: false + /parse-headers@2.0.5: + resolution: {integrity: sha512-ft3iAoLOB/MlwbNXgzy43SWGP6sQki2jQvAyBg/zDFAgr9bfNWZIUj42Kw2eJIl8kEi4PbgE6U1Zau/HwI75HA==} + dev: false + /parse-json@5.2.0: resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} engines: {node: '>=8'} @@ -20905,6 +21427,15 @@ packages: split2: 4.2.0 dev: false + /phin@3.7.1(debug@4.3.5): + resolution: {integrity: sha512-GEazpTWwTZaEQ9RhL7Nyz0WwqilbqgLahDM3D0hxWwmVDI52nXEybHqiN6/elwpkJBhcuj+WbBu+QfT0uhPGfQ==} + engines: {node: '>= 8'} + dependencies: + centra: 2.7.0(debug@4.3.5) + transitivePeerDependencies: + - debug + dev: false + /pick-by-alias@1.2.0: resolution: {integrity: sha512-ESj2+eBxhGrcA1azgHs7lARG5+5iLakc/6nlfbpjcLl00HuuUOIuORhYXN4D1HfvMSKuVtFQjAlnwi1JHEeDIw==} dev: false @@ -21088,7 +21619,6 @@ packages: engines: {node: '>=10'} dependencies: '@babel/runtime': 7.24.7 - dev: true /polybooljs@1.2.2: resolution: {integrity: sha512-ziHW/02J0XuNuUtmidBc6GXE8YohYydp3DWPWXYsd7O721TjcmN+k6ezjdwkDqep+gnWnFY+yqZHvzElra2oCg==} @@ -21984,6 +22514,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + /prepend-http@2.0.0: + resolution: {integrity: sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==} + engines: {node: '>=4'} + dev: false + /prettier@2.8.0: resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==} engines: {node: '>=10.13.0'} @@ -22070,7 +22605,6 @@ packages: /process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - dev: true /promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} @@ -22330,6 +22864,13 @@ packages: pug-strip-comments: 2.0.0 dev: false + /pump@3.0.2: + resolution: {integrity: sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==} + dependencies: + end-of-stream: 1.4.4 + once: 1.4.0 + dev: false + /punycode@1.4.1: resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} dev: true @@ -22370,6 +22911,14 @@ packages: side-channel: 1.0.6 dev: true + /quad-indices@2.0.1: + resolution: {integrity: sha512-6jtmCsEbGAh5npThXrBaubbTjPcF0rMbn57XCJVI7LkW8PUT56V+uIrRCCWCn85PSgJC9v8Pm5tnJDwmOBewvA==} + dependencies: + an-array: 1.0.0 + dtype: 2.0.0 + is-buffer: 1.1.6 + dev: false + /querystring@0.2.0: resolution: {integrity: sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==} engines: {node: '>=0.4.x'} @@ -22577,6 +23126,36 @@ packages: resolution: {integrity: sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==} dev: true + /react-force-graph-2d@1.26.1(react@18.3.1): + resolution: {integrity: sha512-7dRD0zNjMpeNghc6dwqzKrdWz45kM1/RNQ7OfR/Y4t9cK02NvHjtmA5JeKePAmzZajqmQQFCbTtwxEfhKgcsww==} + engines: {node: '>=12'} + peerDependencies: + react: '*' + dependencies: + force-graph: 1.47.1 + prop-types: 15.8.1 + react: 18.3.1 + react-kapsule: 2.5.6(react@18.3.1) + dev: false + + /react-force-graph@1.45.4(react@18.3.1)(three@0.171.0): + resolution: {integrity: sha512-Mppx8pU2TjJm+NToLDpcBR/8zk/aXbhFvP2r8Mw1pdQXnScpVVVb/7rfvZObSvnE9R2RoEjBpr4Ily4thO/Pfw==} + engines: {node: '>=12'} + peerDependencies: + react: '*' + dependencies: + 3d-force-graph: 1.74.5 + 3d-force-graph-ar: 1.9.3(three@0.171.0) + 3d-force-graph-vr: 2.4.3(three@0.171.0) + force-graph: 1.47.1 + prop-types: 15.8.1 + react: 18.3.1 + react-kapsule: 2.5.6(react@18.3.1) + transitivePeerDependencies: + - supports-color + - three + dev: false + /react-i18next@12.2.2(i18next@22.4.15)(react-dom@18.3.1)(react@18.3.1): resolution: {integrity: sha512-KBB6buBmVKXUWNxXHdnthp+38gPyBT46hJCAIQ8rX19NFL/m2ahte2KARfIDf2tMnSAL7wwck6eDOd/9zn6aFg==} peerDependencies: @@ -22617,6 +23196,16 @@ packages: /react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + /react-kapsule@2.5.6(react@18.3.1): + resolution: {integrity: sha512-aE4Nq7dDG8R/LdNmvOL6Azjr97I2E7ycFDJRkoHJSp9OQgTJDT3MHTJtJDrOTwzCl6sllYSqrtcndaCzizyAjQ==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.13.1' + dependencies: + jerrypick: 1.1.1 + react: 18.3.1 + dev: false + /react-markdown@8.0.7(@types/react@18.3.3)(react@18.3.1): resolution: {integrity: sha512-bvWbzG4MtOU62XqBx3Xx+zB2raaFFsq4mYiAzfjXJMEz2sixgeAfraA3tvzULF02ZdOMUOKTBFFaZJDDrq+BJQ==} peerDependencies: @@ -23423,6 +24012,12 @@ packages: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 + /responselike@1.0.2: + resolution: {integrity: sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==} + dependencies: + lowercase-keys: 1.0.1 + dev: false + /restore-cursor@3.1.0: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} @@ -24733,6 +25328,14 @@ packages: resolution: {integrity: sha512-I5PWXAFKx3FYnI9a+dQMWNqTxoRt6vdBdb0O+BJ1sxXCWtSoQCusc13E58f+9p4MYx/qCnEMkD5jac6K2j3dgA==} dev: false + /super-animejs@3.1.0: + resolution: {integrity: sha512-6MFAFJDRuvwkovxQZPruuyHinTa4rgj4hNLOndjcYYhZLckoXtVRY9rJPuq8p6c/tgZJrFYEAYAfJ2/hhNtUCA==} + dev: false + + /super-three@0.164.0: + resolution: {integrity: sha512-yMtOkw2hSXfIvGlwcghCbhHGsKRAmh8ksDeOo/0HI7KlEVoIYKHiYLYe9GF6QBViNwzKGpMIz77XUDRveZ4XJg==} + dev: false + /supercluster@7.1.5: resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} dependencies: @@ -25118,6 +25721,54 @@ packages: dependencies: any-promise: 1.3.0 + /three-forcegraph@1.42.10(three@0.171.0): + resolution: {integrity: sha512-lKwVWY+/7e9smxK0iQ13WdzYfn9fB2hmQUeuegXgqpn5ARLlG2m0x/nkeLdv9gLlfXZ6nOKuT0m8RgZ2XqOrrQ==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.118.3' + dependencies: + accessor-fn: 1.5.1 + d3-array: 3.2.4 + d3-force-3d: 3.0.5 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + data-bind-mapper: 1.0.1 + kapsule: 1.16.0 + ngraph.forcelayout: 3.3.1 + ngraph.graph: 20.0.1 + three: 0.171.0 + tinycolor2: 1.6.0 + dev: false + + /three-pathfinding@1.3.0(three@0.164.1): + resolution: {integrity: sha512-LKxMI3/YqdMYvt6AdE2vB6s5ueDFczt/DWoxhtPNgRsH6E0D8LMYQxz+eIrmKo0MQpDvMVzXYUMBk+b86+k97w==} + peerDependencies: + three: 0.x.x + dependencies: + three: 0.164.1 + dev: false + + /three-render-objects@1.32.1(three@0.171.0): + resolution: {integrity: sha512-HcbVhMFwPxtxrrQYe+pD8HFZmx22lYuYZeHXcZlDdxqWyr5wAZgjD+vX23oALrmP3i1LW8udheXxbntwYmA9sw==} + engines: {node: '>=12'} + peerDependencies: + three: '>=0.168' + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.1 + kapsule: 1.16.0 + polished: 4.2.2 + three: 0.171.0 + dev: false + + /three@0.164.1: + resolution: {integrity: sha512-iC/hUBbl1vzFny7f5GtqzVXYjMJKaTPxiCxXfrvVdBi1Sf+jhd1CAkitiFwC7mIBFCo3MrDLJG97yisoaWig0w==} + dev: false + + /three@0.171.0: + resolution: {integrity: sha512-Y/lAXPaKZPcEdkKjh0JOAHVv8OOnv/NDJqm0wjfCzyQmfKxV7zvkwsnBgPBKTzJHToSOhRGQAGbPJObT59B/PQ==} + dev: false + /throat@6.0.2: resolution: {integrity: sha512-WKexMoJj3vEuK0yFEapj8y64V0A6xcuPuK9Gt1d0R+dzCSJc0lHqQytAbSB4cDAK0dWh4T0E2ETkoLE2WZ41OQ==} @@ -25212,6 +25863,11 @@ packages: parse-unit: 1.0.1 dev: false + /to-readable-stream@1.0.0: + resolution: {integrity: sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==} + engines: {node: '>=6'} + dev: false + /to-regex-range@2.1.1: resolution: {integrity: sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==} engines: {node: '>=0.10.0'} @@ -25898,6 +26554,13 @@ packages: deprecated: Please see https://github.com/lydell/urix#deprecated dev: false + /url-parse-lax@3.0.0: + resolution: {integrity: sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==} + engines: {node: '>=4'} + dependencies: + prepend-http: 2.0.0 + dev: false + /url-parse@1.5.10: resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} requiresBuild: true @@ -26523,6 +27186,16 @@ packages: resolution: {integrity: sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==} engines: {node: '>=0.8.0'} + /webvr-polyfill-dpdb@1.0.18: + resolution: {integrity: sha512-O0S1ZGEWyPvyZEkS2VbyV7mtir/NM9MNK3EuhbHPoJ8EHTky2pTXehjIl+IiDPr+Lldgx129QGt3NGly7rwRPw==} + dev: false + + /webvr-polyfill@0.10.12: + resolution: {integrity: sha512-trDJEVUQnRIVAnmImjEQ0BlL1NfuWl8+eaEdu+bs4g59c7OtETi/5tFkgEFDRaWEYwHntXs/uFF3OXZuutNGGA==} + dependencies: + cardboard-vr-display: 1.0.19 + dev: false + /wget@0.0.1: resolution: {integrity: sha512-iKDSrvontU6lAQq89bNn7me3HU/+Cau7NedEYz607TOS4n0AgksloCt2UwU+ZH5Kn0Lq+XbJ6STOpnhpjicvOQ==} engines: {node: '>= 0.6.18'} @@ -26674,6 +27347,10 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + /word-wrapper@1.0.7: + resolution: {integrity: sha512-VOPBFCm9b6FyYKQYfn9AVn2dQvdR/YOVFV6IBRA1TBMJWKffvhEX1af6FMGrttILs2Q9ikCRhLqkbY2weW6dOQ==} + dev: false + /wordwrap@0.0.2: resolution: {integrity: sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==} engines: {node: '>=0.4.0'} @@ -26919,9 +27596,35 @@ packages: utf-8-validate: optional: true + /xhr@2.6.0: + resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + dependencies: + global: 4.4.0 + is-function: 1.0.2 + parse-headers: 2.0.5 + xtend: 4.0.2 + dev: false + /xml-name-validator@3.0.0: resolution: {integrity: sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==} + /xml-parse-from-string@1.0.1: + resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} + dev: false + + /xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + dependencies: + sax: 1.2.4 + xmlbuilder: 11.0.1 + dev: false + + /xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + dev: false + /xmlchars@2.2.0: resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} @@ -27081,6 +27784,17 @@ packages: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: false + github.com/dmarcos/three-bmfont-text/eed4878795be9b3e38cf6aec6b903f56acd1f695: + resolution: {tarball: https://codeload.github.com/dmarcos/three-bmfont-text/tar.gz/eed4878795be9b3e38cf6aec6b903f56acd1f695} + name: three-bmfont-text + version: 3.0.0 + dependencies: + array-shuffle: 1.0.1 + layout-bmfont-text: 1.3.4 + nice-color-palettes: 3.0.0 + quad-indices: 2.0.1 + dev: false + settings: autoInstallPeers: true excludeLinksFromLockfile: false From fdb8264c31a9afa3fe492168f282a60b0d59ff5e Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:14:33 +1100 Subject: [PATCH 07/38] wip --- apps/registry/app/api/job-similarity/route.js | 70 ++++++ apps/registry/app/job-similarity/page.js | 208 ++++++++++++++++++ 2 files changed, 278 insertions(+) create mode 100644 apps/registry/app/api/job-similarity/route.js create mode 100644 apps/registry/app/job-similarity/page.js diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js new file mode 100644 index 0000000..aed2617 --- /dev/null +++ b/apps/registry/app/api/job-similarity/route.js @@ -0,0 +1,70 @@ +import { createClient } from '@supabase/supabase-js'; +import { NextResponse } from 'next/server'; + +const supabaseUrl = 'https://itxuhvvwryeuzuyihpkp.supabase.co'; + +// This ensures the route is always dynamic +export const dynamic = 'force-dynamic'; + +export async function GET(request) { + // During build time or when SUPABASE_KEY is not available + if (!process.env.SUPABASE_KEY) { + return NextResponse.json( + { message: 'API not available during build' }, + { status: 503 } + ); + } + + try { + const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); + const { searchParams } = new URL(request.url); + const limit = parseInt(searchParams.get('limit')) || 500; + + console.time('getJobSimilarityData'); + const { data, error } = await supabase + .from('jobs') + .select('uuid, embedding_v5, gpt_content') + .not('embedding_v5', 'is', null) + .limit(limit) + .order('created_at', { ascending: false }); + + if (error) { + console.error('Error fetching job similarity data:', error); + return NextResponse.json( + { message: 'Error fetching job similarity data' }, + { status: 500 } + ); + } + + console.timeEnd('getJobSimilarityData'); + + // Parse embeddings and job titles + const parsedData = data.map(item => { + let jobTitle = 'Unknown Position'; + try { + const gptContent = JSON.parse(item.gpt_content); + jobTitle = gptContent.title || 'Unknown Position'; + } catch (e) { + console.warn('Failed to parse gpt_content for job:', item.uuid); + } + + return { + uuid: item.uuid, + title: jobTitle, + embedding: typeof item.embedding_v5 === 'string' + ? JSON.parse(item.embedding_v5) + : Array.isArray(item.embedding_v5) + ? item.embedding_v5 + : null + }; + }).filter(item => item.embedding !== null); + + return NextResponse.json(parsedData); + } catch (error) { + console.error('Error in job similarity endpoint:', error); + return NextResponse.json( + { message: 'Internal server error' }, + { status: 500 } + ); + } +} diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js new file mode 100644 index 0000000..83e097f --- /dev/null +++ b/apps/registry/app/job-similarity/page.js @@ -0,0 +1,208 @@ +'use client'; + +import React, { useEffect, useState, useCallback } from 'react'; +import dynamic from 'next/dynamic'; + +// Import ForceGraph dynamically to avoid SSR issues +const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false }); + +// Helper function to compute cosine similarity +function cosineSimilarity(a, b) { + let dotProduct = 0; + let normA = 0; + let normB = 0; + for (let i = 0; i < a.length; i++) { + dotProduct += a[i] * b[i]; + normA += a[i] * a[i]; + normB += b[i] * b[i]; + } + return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); +} + +export default function JobSimilarityPage() { + const [graphData, setGraphData] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [highlightNodes, setHighlightNodes] = useState(new Set()); + const [highlightLinks, setHighlightLinks] = useState(new Set()); + const [hoverNode, setHoverNode] = useState(null); + + useEffect(() => { + async function fetchData() { + try { + const response = await fetch('/api/job-similarity'); + if (!response.ok) { + throw new Error('Failed to fetch data'); + } + const jsonData = await response.json(); + + // Group similar job titles + const jobGroups = {}; + jsonData.forEach(item => { + const title = item.title; + if (!jobGroups[title]) { + jobGroups[title] = []; + } + jobGroups[title].push(item); + }); + + // Create nodes and links + const nodes = []; + const links = []; + const similarityThreshold = 0.7; + + // Create nodes for each unique job title + Object.entries(jobGroups).forEach(([title, items], index) => { + nodes.push({ + id: title, + group: index, + size: Math.log(items.length + 1) * 3, + count: items.length, + uuids: items.map(item => item.uuid), + embeddings: items.map(item => item.embedding), + color: `hsl(${Math.random() * 360}, 70%, 50%)` + }); + }); + + // Create links between similar jobs + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + // Calculate average similarity between groups + let totalSimilarity = 0; + let comparisons = 0; + + nodes[i].embeddings.forEach(emb1 => { + nodes[j].embeddings.forEach(emb2 => { + totalSimilarity += cosineSimilarity(emb1, emb2); + comparisons++; + }); + }); + + const avgSimilarity = totalSimilarity / comparisons; + + if (avgSimilarity > similarityThreshold) { + links.push({ + source: nodes[i].id, + target: nodes[j].id, + value: avgSimilarity + }); + } + } + } + + setGraphData({ nodes, links }); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + } + + fetchData(); + }, []); + + const handleNodeHover = useCallback(node => { + setHighlightNodes(new Set(node ? [node] : [])); + setHighlightLinks(new Set(graphData?.links.filter(link => + link.source.id === node?.id || link.target.id === node?.id + ) || [])); + setHoverNode(node || null); + }, [graphData]); + + if (loading) { + return ( +
+

Job Similarity Network

+

Loading...

+
+ ); + } + + if (error) { + return ( +
+

Job Similarity Network

+

Error: {error}

+
+ ); + } + + return ( +
+

Job Similarity Network

+

+ Explore similar job positions in an interactive network. Each node represents a job title, with size indicating the number of listings. + Connected positions are similar based on job content. Hover to highlight connections, click to view job details. +

+
+ {graphData && ( + highlightNodes.has(node) ? '#ff0000' : node.color} + nodeCanvasObject={(node, ctx, globalScale) => { + // Draw node + ctx.beginPath(); + ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); + ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; + ctx.fill(); + + // Only draw label if node is highlighted + if (highlightNodes.has(node)) { + const label = node.id; + const fontSize = Math.max(14, node.size * 1.5); + ctx.font = `${fontSize}px Sans-Serif`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); + + // Draw background for label + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] * 2, + bckgDimensions[0], + bckgDimensions[1] + ); + + // Draw label + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; + ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); + + // Draw count + const countLabel = `(${node.count})`; + const smallerFont = fontSize * 0.7; + ctx.font = `${smallerFont}px Sans-Serif`; + ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); + } + }} + nodeRelSize={6} + linkWidth={link => highlightLinks.has(link) ? 2 : 1} + linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} + linkOpacity={0.3} + linkDirectionalParticles={4} + linkDirectionalParticleWidth={2} + onNodeHover={handleNodeHover} + onNodeClick={(node) => { + if (node.uuids && node.uuids.length > 0) { + window.open(`/jobs/${node.uuids[0]}`, '_blank'); + } + }} + enableNodeDrag={true} + cooldownTicks={100} + d3AlphaDecay={0.02} + d3VelocityDecay={0.3} + warmupTicks={100} + /> + )} + {hoverNode && ( +
+

{hoverNode.id}

+

{hoverNode.count} job listings

+

Click to view a sample job

+
+ )} +
+
+ ); +} From d51318951d6721c982f83efd37c7e43e276e50da Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:17:42 +1100 Subject: [PATCH 08/38] wip --- apps/registry/app/job-similarity/page.js | 80 +++++++++++++++--------- 1 file changed, 52 insertions(+), 28 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 83e097f..7fa0b5d 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -19,6 +19,25 @@ function cosineSimilarity(a, b) { return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); } +// Helper function to normalize vector +function normalizeVector(vector) { + const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)); + return vector.map(val => val / magnitude); +} + +// Helper function to get average embedding +function getAverageEmbedding(embeddings) { + const dim = embeddings[0].length; + const sum = new Array(dim).fill(0); + embeddings.forEach(emb => { + emb.forEach((val, i) => { + sum[i] += val; + }); + }); + const avg = sum.map(val => val / embeddings.length); + return normalizeVector(avg); +} + export default function JobSimilarityPage() { const [graphData, setGraphData] = useState(null); const [loading, setLoading] = useState(true); @@ -48,49 +67,54 @@ export default function JobSimilarityPage() { // Create nodes and links const nodes = []; - const links = []; + const links = new Set(); // Use Set to avoid duplicate links + const K_NEIGHBORS = 3; // Number of connections per node const similarityThreshold = 0.7; - // Create nodes for each unique job title + // Create nodes for each unique job title with normalized average embeddings Object.entries(jobGroups).forEach(([title, items], index) => { + const normalizedEmbeddings = items.map(item => normalizeVector(item.embedding)); + const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); + nodes.push({ id: title, group: index, size: Math.log(items.length + 1) * 3, count: items.length, uuids: items.map(item => item.uuid), - embeddings: items.map(item => item.embedding), + avgEmbedding, color: `hsl(${Math.random() * 360}, 70%, 50%)` }); }); - // Create links between similar jobs - for (let i = 0; i < nodes.length; i++) { - for (let j = i + 1; j < nodes.length; j++) { - // Calculate average similarity between groups - let totalSimilarity = 0; - let comparisons = 0; - - nodes[i].embeddings.forEach(emb1 => { - nodes[j].embeddings.forEach(emb2 => { - totalSimilarity += cosineSimilarity(emb1, emb2); - comparisons++; - }); + // For each node, find its K most similar neighbors + nodes.forEach((node, i) => { + // Calculate similarities with all other nodes + const similarities = nodes.map((otherNode, j) => ({ + index: j, + similarity: i === j ? -1 : cosineSimilarity(node.avgEmbedding, otherNode.avgEmbedding) + })); + + // Sort by similarity and get top K + similarities + .sort((a, b) => b.similarity - a.similarity) + .slice(0, K_NEIGHBORS) + .forEach(({ index, similarity }) => { + if (similarity > similarityThreshold) { + const linkId = [i, index].sort().join('-'); + links.add({ + source: nodes[i].id, + target: nodes[index].id, + value: similarity + }); + } }); + }); - const avgSimilarity = totalSimilarity / comparisons; - - if (avgSimilarity > similarityThreshold) { - links.push({ - source: nodes[i].id, - target: nodes[j].id, - value: avgSimilarity - }); - } - } - } - - setGraphData({ nodes, links }); + setGraphData({ + nodes, + links: Array.from(links) + }); } catch (err) { setError(err.message); } finally { From aba383c5ef6d818889c6268d287ece7598d56f7a Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:20:02 +1100 Subject: [PATCH 09/38] wip --- apps/registry/app/job-similarity/page.js | 165 +++++++++++++++++------ 1 file changed, 126 insertions(+), 39 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 7fa0b5d..1e23c04 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -38,6 +38,100 @@ function getAverageEmbedding(embeddings) { return normalizeVector(avg); } +// Similarity algorithms +const algorithms = { + knn: { + name: 'K-Nearest Neighbors', + compute: (nodes, K = 3, minSimilarity = 0.5) => { + const links = new Set(); + nodes.forEach((node, i) => { + const similarities = nodes.map((otherNode, j) => ({ + index: j, + similarity: i === j ? -1 : cosineSimilarity(node.avgEmbedding, otherNode.avgEmbedding) + })); + + similarities + .sort((a, b) => b.similarity - a.similarity) + .slice(0, K) + .forEach(({ index, similarity }) => { + if (similarity > minSimilarity) { + links.add({ + source: nodes[i].id, + target: nodes[index].id, + value: similarity + }); + } + }); + }); + return Array.from(links); + } + }, + threshold: { + name: 'Similarity Threshold', + compute: (nodes, threshold = 0.7) => { + const links = new Set(); + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity > threshold) { + links.add({ + source: nodes[i].id, + target: nodes[j].id, + value: similarity + }); + } + } + } + return Array.from(links); + } + }, + mst: { + name: 'Minimum Spanning Tree', + compute: (nodes, minSimilarity = 0.3) => { + // Kruskal's algorithm for MST + const links = []; + const parent = new Array(nodes.length).fill(0).map((_, i) => i); + + function find(x) { + if (parent[x] !== x) parent[x] = find(parent[x]); + return parent[x]; + } + + function union(x, y) { + parent[find(x)] = find(y); + } + + // Create all possible edges with weights + const edges = []; + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity > minSimilarity) { + edges.push({ i, j, similarity }); + } + } + } + + // Sort edges by similarity (descending) + edges.sort((a, b) => b.similarity - a.similarity); + + // Build MST + edges.forEach(({ i, j, similarity }) => { + if (find(i) !== find(j)) { + union(i, j); + links.push({ + source: nodes[i].id, + target: nodes[j].id, + value: similarity + }); + } + }); + + return links; + } + } +}; + export default function JobSimilarityPage() { const [graphData, setGraphData] = useState(null); const [loading, setLoading] = useState(true); @@ -45,6 +139,8 @@ export default function JobSimilarityPage() { const [highlightNodes, setHighlightNodes] = useState(new Set()); const [highlightLinks, setHighlightLinks] = useState(new Set()); const [hoverNode, setHoverNode] = useState(null); + const [algorithm, setAlgorithm] = useState('knn'); + const [rawNodes, setRawNodes] = useState(null); useEffect(() => { async function fetchData() { @@ -65,18 +161,12 @@ export default function JobSimilarityPage() { jobGroups[title].push(item); }); - // Create nodes and links - const nodes = []; - const links = new Set(); // Use Set to avoid duplicate links - const K_NEIGHBORS = 3; // Number of connections per node - const similarityThreshold = 0.7; - - // Create nodes for each unique job title with normalized average embeddings - Object.entries(jobGroups).forEach(([title, items], index) => { + // Create nodes with normalized embeddings + const nodes = Object.entries(jobGroups).map(([title, items], index) => { const normalizedEmbeddings = items.map(item => normalizeVector(item.embedding)); const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); - nodes.push({ + return { id: title, group: index, size: Math.log(items.length + 1) * 3, @@ -84,37 +174,12 @@ export default function JobSimilarityPage() { uuids: items.map(item => item.uuid), avgEmbedding, color: `hsl(${Math.random() * 360}, 70%, 50%)` - }); - }); - - // For each node, find its K most similar neighbors - nodes.forEach((node, i) => { - // Calculate similarities with all other nodes - const similarities = nodes.map((otherNode, j) => ({ - index: j, - similarity: i === j ? -1 : cosineSimilarity(node.avgEmbedding, otherNode.avgEmbedding) - })); - - // Sort by similarity and get top K - similarities - .sort((a, b) => b.similarity - a.similarity) - .slice(0, K_NEIGHBORS) - .forEach(({ index, similarity }) => { - if (similarity > similarityThreshold) { - const linkId = [i, index].sort().join('-'); - links.add({ - source: nodes[i].id, - target: nodes[index].id, - value: similarity - }); - } - }); + }; }); - setGraphData({ - nodes, - links: Array.from(links) - }); + setRawNodes(nodes); + const links = algorithms[algorithm].compute(nodes); + setGraphData({ nodes, links }); } catch (err) { setError(err.message); } finally { @@ -125,6 +190,14 @@ export default function JobSimilarityPage() { fetchData(); }, []); + // Update graph when algorithm changes + useEffect(() => { + if (rawNodes) { + const links = algorithms[algorithm].compute(rawNodes); + setGraphData({ nodes: rawNodes, links }); + } + }, [algorithm, rawNodes]); + const handleNodeHover = useCallback(node => { setHighlightNodes(new Set(node ? [node] : [])); setHighlightLinks(new Set(graphData?.links.filter(link => @@ -153,7 +226,21 @@ export default function JobSimilarityPage() { return (
-

Job Similarity Network

+
+

Job Similarity Network

+
+ + +
+

Explore similar job positions in an interactive network. Each node represents a job title, with size indicating the number of listings. Connected positions are similar based on job content. Hover to highlight connections, click to view job details. From b3ccc983c371db6d72a22b7bd2bf4fab1ed7cbfe Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:24:21 +1100 Subject: [PATCH 10/38] wip --- apps/registry/app/job-similarity/page.js | 190 +++++++++++++++++++---- 1 file changed, 159 insertions(+), 31 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 1e23c04..9a77eca 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -40,6 +40,51 @@ function getAverageEmbedding(embeddings) { // Similarity algorithms const algorithms = { + mst: { + name: 'Minimum Spanning Tree', + compute: (nodes, minSimilarity = 0.3) => { + // Kruskal's algorithm for MST + const links = []; + const parent = new Array(nodes.length).fill(0).map((_, i) => i); + + function find(x) { + if (parent[x] !== x) parent[x] = find(parent[x]); + return parent[x]; + } + + function union(x, y) { + parent[find(x)] = find(y); + } + + // Create all possible edges with weights + const edges = []; + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity > minSimilarity) { + edges.push({ i, j, similarity }); + } + } + } + + // Sort edges by similarity (descending) + edges.sort((a, b) => b.similarity - a.similarity); + + // Build MST + edges.forEach(({ i, j, similarity }) => { + if (find(i) !== find(j)) { + union(i, j); + links.push({ + source: nodes[i].id, + target: nodes[j].id, + value: similarity + }); + } + }); + + return links; + } + }, knn: { name: 'K-Nearest Neighbors', compute: (nodes, K = 3, minSimilarity = 0.5) => { @@ -85,49 +130,132 @@ const algorithms = { return Array.from(links); } }, - mst: { - name: 'Minimum Spanning Tree', - compute: (nodes, minSimilarity = 0.3) => { - // Kruskal's algorithm for MST - const links = []; - const parent = new Array(nodes.length).fill(0).map((_, i) => i); - - function find(x) { - if (parent[x] !== x) parent[x] = find(parent[x]); - return parent[x]; + hierarchical: { + name: 'Hierarchical Clustering', + compute: (nodes, threshold = 0.5) => { + const links = new Set(); + let clusters = new Array(nodes.length).fill(0).map((_, i) => [i]); + const similarities = []; + + // Calculate all pairwise similarities + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + similarities.push({ i, j, similarity }); + } } - - function union(x, y) { - parent[find(x)] = find(y); + + // Sort by similarity descending + similarities.sort((a, b) => b.similarity - a.similarity); + + // Merge clusters and add links + similarities.forEach(({ i, j, similarity }) => { + if (similarity > threshold) { + const cluster1 = clusters.find(c => c.includes(i)); + const cluster2 = clusters.find(c => c.includes(j)); + + if (cluster1 !== cluster2) { + // Add links between closest points in clusters + links.add({ + source: nodes[i].id, + target: nodes[j].id, + value: similarity + }); + + // Merge clusters + const merged = [...cluster1, ...cluster2]; + clusters = clusters.filter(c => c !== cluster1 && c !== cluster2); + clusters.push(merged); + } + } + }); + + return Array.from(links); + } + }, + community: { + name: 'Community Detection', + compute: (nodes, threshold = 0.5, communityThreshold = 0.6) => { + const links = new Set(); + const communities = new Map(); + let communityId = 0; + + // First pass: create initial communities based on strong similarities + for (let i = 0; i < nodes.length; i++) { + if (!communities.has(i)) { + const community = new Set([i]); + communities.set(i, communityId); + + // Find strongly connected nodes + for (let j = 0; j < nodes.length; j++) { + if (i !== j && !communities.has(j)) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity > communityThreshold) { + community.add(j); + communities.set(j, communityId); + } + } + } + communityId++; + } } - // Create all possible edges with weights - const edges = []; + // Second pass: connect communities for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); - if (similarity > minSimilarity) { - edges.push({ i, j, similarity }); + const sameCommunity = communities.get(i) === communities.get(j); + + // Add links within communities and strong links between communities + if (similarity > (sameCommunity ? threshold : communityThreshold)) { + links.add({ + source: nodes[i].id, + target: nodes[j].id, + value: similarity + }); } } } - // Sort edges by similarity (descending) - edges.sort((a, b) => b.similarity - a.similarity); + return Array.from(links); + } + }, + adaptive: { + name: 'Adaptive Threshold', + compute: (nodes) => { + const links = new Set(); + const similarities = []; + + // Calculate all pairwise similarities + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + similarities.push(similarity); + } + } - // Build MST - edges.forEach(({ i, j, similarity }) => { - if (find(i) !== find(j)) { - union(i, j); - links.push({ - source: nodes[i].id, - target: nodes[j].id, - value: similarity - }); + // Calculate adaptive threshold using mean and standard deviation + const mean = similarities.reduce((a, b) => a + b, 0) / similarities.length; + const std = Math.sqrt( + similarities.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / similarities.length + ); + const adaptiveThreshold = mean + 0.5 * std; + + // Create links based on adaptive threshold + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity > adaptiveThreshold) { + links.add({ + source: nodes[i].id, + target: nodes[j].id, + value: similarity + }); + } } - }); + } - return links; + return Array.from(links); } } }; @@ -139,7 +267,7 @@ export default function JobSimilarityPage() { const [highlightNodes, setHighlightNodes] = useState(new Set()); const [highlightLinks, setHighlightLinks] = useState(new Set()); const [hoverNode, setHoverNode] = useState(null); - const [algorithm, setAlgorithm] = useState('knn'); + const [algorithm, setAlgorithm] = useState('mst'); const [rawNodes, setRawNodes] = useState(null); useEffect(() => { From 03f0f0283907300fbbbd6eb7c34cad9dc5f2bacb Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:33:54 +1100 Subject: [PATCH 11/38] wip --- apps/registry/app/job-similarity/page.js | 136 +++++++++++++++-------- 1 file changed, 90 insertions(+), 46 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 9a77eca..56172b7 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -21,20 +21,26 @@ function cosineSimilarity(a, b) { // Helper function to normalize vector function normalizeVector(vector) { + if (!Array.isArray(vector) || vector.length === 0) return null; const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)); + if (magnitude === 0) return null; return vector.map(val => val / magnitude); } // Helper function to get average embedding function getAverageEmbedding(embeddings) { - const dim = embeddings[0].length; + // Filter out null or invalid embeddings + const validEmbeddings = embeddings.filter(emb => Array.isArray(emb) && emb.length > 0); + if (validEmbeddings.length === 0) return null; + + const dim = validEmbeddings[0].length; const sum = new Array(dim).fill(0); - embeddings.forEach(emb => { + validEmbeddings.forEach(emb => { emb.forEach((val, i) => { sum[i] += val; }); }); - const avg = sum.map(val => val / embeddings.length); + const avg = sum.map(val => val / validEmbeddings.length); return normalizeVector(avg); } @@ -268,47 +274,80 @@ export default function JobSimilarityPage() { const [highlightLinks, setHighlightLinks] = useState(new Set()); const [hoverNode, setHoverNode] = useState(null); const [algorithm, setAlgorithm] = useState('mst'); + const [dataSource, setDataSource] = useState('jobs'); const [rawNodes, setRawNodes] = useState(null); useEffect(() => { async function fetchData() { + setLoading(true); try { - const response = await fetch('/api/job-similarity'); + const endpoint = dataSource === 'jobs' ? '/api/job-similarity' : '/api/similarity'; + const response = await fetch(endpoint); if (!response.ok) { throw new Error('Failed to fetch data'); } const jsonData = await response.json(); - // Group similar job titles - const jobGroups = {}; - jsonData.forEach(item => { - const title = item.title; - if (!jobGroups[title]) { - jobGroups[title] = []; + // Filter out items without valid embeddings + const validData = jsonData.filter(item => { + const embedding = dataSource === 'jobs' ? + item.embedding : + (typeof item.embedding === 'string' ? JSON.parse(item.embedding) : item.embedding); + return Array.isArray(embedding) && embedding.length > 0; + }); + + // Group similar items + const groups = {}; + validData.forEach(item => { + const label = dataSource === 'jobs' + ? item.title + : (item.position || 'Unknown Position'); + + if (!groups[label]) { + groups[label] = []; } - jobGroups[title].push(item); + groups[label].push(item); }); // Create nodes with normalized embeddings - const nodes = Object.entries(jobGroups).map(([title, items], index) => { - const normalizedEmbeddings = items.map(item => normalizeVector(item.embedding)); - const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); - - return { - id: title, - group: index, - size: Math.log(items.length + 1) * 3, - count: items.length, - uuids: items.map(item => item.uuid), - avgEmbedding, - color: `hsl(${Math.random() * 360}, 70%, 50%)` - }; - }); + const nodes = Object.entries(groups) + .map(([label, items], index) => { + const embeddings = items.map(item => { + if (dataSource === 'jobs') return item.embedding; + return typeof item.embedding === 'string' ? + JSON.parse(item.embedding) : item.embedding; + }); + + const normalizedEmbeddings = embeddings + .map(emb => normalizeVector(emb)) + .filter(emb => emb !== null); + + if (normalizedEmbeddings.length === 0) return null; + + const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); + if (!avgEmbedding) return null; + + return { + id: label, + group: index, + size: Math.log(items.length + 1) * 3, + count: items.length, + uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), + avgEmbedding, + color: `hsl(${Math.random() * 360}, 70%, 50%)` + }; + }) + .filter(node => node !== null); + + if (nodes.length === 0) { + throw new Error('No valid data found with embeddings'); + } setRawNodes(nodes); const links = algorithms[algorithm].compute(nodes); setGraphData({ nodes, links }); } catch (err) { + console.error('Error in fetchData:', err); setError(err.message); } finally { setLoading(false); @@ -316,15 +355,7 @@ export default function JobSimilarityPage() { } fetchData(); - }, []); - - // Update graph when algorithm changes - useEffect(() => { - if (rawNodes) { - const links = algorithms[algorithm].compute(rawNodes); - setGraphData({ nodes: rawNodes, links }); - } - }, [algorithm, rawNodes]); + }, [algorithm, dataSource]); const handleNodeHover = useCallback(node => { setHighlightNodes(new Set(node ? [node] : [])); @@ -334,10 +365,17 @@ export default function JobSimilarityPage() { setHoverNode(node || null); }, [graphData]); + const handleNodeClick = useCallback(node => { + if (node.uuids && node.uuids.length > 0) { + const baseUrl = dataSource === 'jobs' ? '/jobs/' : '/'; + window.open(`${baseUrl}${node.uuids[0]}`, '_blank'); + } + }, [dataSource]); + if (loading) { return (

-

Job Similarity Network

+

Similarity Network

Loading...

); @@ -346,7 +384,7 @@ export default function JobSimilarityPage() { if (error) { return (
-

Job Similarity Network

+

Similarity Network

Error: {error}

); @@ -355,8 +393,17 @@ export default function JobSimilarityPage() { return (
-

Job Similarity Network

+

Similarity Network

+ + +

@@ -463,19 +494,23 @@ export default function JobSimilarityPage() { ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); } }} - nodeRelSize={6} + nodeRelSize={performanceMode ? 4 : 6} linkWidth={link => highlightLinks.has(link) ? 2 : 1} linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} linkOpacity={0.3} - linkDirectionalParticles={4} + linkDirectionalParticles={performanceMode ? 0 : 4} linkDirectionalParticleWidth={2} onNodeHover={handleNodeHover} onNodeClick={handleNodeClick} - enableNodeDrag={true} - cooldownTicks={100} - d3AlphaDecay={0.02} - d3VelocityDecay={0.3} - warmupTicks={100} + enableNodeDrag={!performanceMode} + cooldownTicks={performanceMode ? 50 : 100} + d3AlphaDecay={performanceMode ? 0.05 : 0.02} + d3VelocityDecay={performanceMode ? 0.4 : 0.3} + warmupTicks={performanceMode ? 50 : 100} + d3Force={performanceMode ? { + collision: 1, + charge: -30 + } : undefined} /> )} {hoverNode && ( From 1a363b5e90ccf205558a8ec048c86548e80645a5 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:49:51 +1100 Subject: [PATCH 13/38] wip --- apps/registry/app/api/job-similarity/route.js | 7 +++++-- apps/registry/app/job-similarity/page.js | 20 ++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index aed2617..5cade68 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -41,9 +41,10 @@ export async function GET(request) { // Parse embeddings and job titles const parsedData = data.map(item => { let jobTitle = 'Unknown Position'; + let gptContent = null; try { - const gptContent = JSON.parse(item.gpt_content); - jobTitle = gptContent.title || 'Unknown Position'; + gptContent = JSON.parse(item.gpt_content); + jobTitle = gptContent?.title || 'Unknown Position'; } catch (e) { console.warn('Failed to parse gpt_content for job:', item.uuid); } @@ -51,6 +52,8 @@ export async function GET(request) { return { uuid: item.uuid, title: jobTitle, + company: gptContent?.company, + countryCode: gptContent?.countryCode, embedding: typeof item.embedding_v5 === 'string' ? JSON.parse(item.embedding_v5) : Array.isArray(item.embedding_v5) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 64fbbff..35d8ba3 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -338,7 +338,9 @@ export default function JobSimilarityPage() { count: items.length, uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), avgEmbedding, - color: `hsl(${Math.random() * 360}, 70%, 50%)` + color: `hsl(${Math.random() * 360}, 70%, 50%)`, + companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, + countryCodes: dataSource === 'jobs' ? [...new Set(items.map(item => item.countryCode || 'Unknown Location'))] : null }; }) .filter(node => node !== null); @@ -514,10 +516,22 @@ export default function JobSimilarityPage() { /> )} {hoverNode && ( -

+

{hoverNode.id}

{hoverNode.count} {dataSource === 'jobs' ? 'job listings' : 'resumes'}

-

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

+ {dataSource === 'jobs' && hoverNode.companies && ( +
+

Companies:

+

{hoverNode.companies.slice(0, 5).join(', ')}{hoverNode.companies.length > 5 ? `, +${hoverNode.companies.length - 5} more` : ''}

+
+ )} + {dataSource === 'jobs' && hoverNode.countryCodes && ( +
+

Locations:

+

{hoverNode.countryCodes.slice(0, 5).join(', ')}{hoverNode.countryCodes.length > 5 ? `, +${hoverNode.countryCodes.length - 5} more` : ''}

+
+ )} +

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

)}
From 5c6863b3e1c2ff979a69538e58bb9f7051b76535 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:52:59 +1100 Subject: [PATCH 14/38] wip --- apps/registry/app/job-similarity/page.js | 182 +++++++++++++++++++++++ 1 file changed, 182 insertions(+) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 35d8ba3..2370534 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -263,6 +263,188 @@ const algorithms = { return Array.from(links); } + }, + maxSpanningTree: { + name: 'Maximum Spanning Tree', + compute: (nodes, minSimilarity = 0.3) => { + const edges = []; + // Calculate all pairwise similarities + for (let i = 0; i < nodes.length; i++) { + for (let j = i + 1; j < nodes.length; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity >= minSimilarity) { + edges.push({ + source: nodes[i], + target: nodes[j], + similarity: similarity + }); + } + } + } + + // Sort edges by similarity in descending order + edges.sort((a, b) => b.similarity - a.similarity); + + // Kruskal's algorithm for maximum spanning tree + const disjointSet = new Map(); + const find = (node) => { + if (!disjointSet.has(node.id)) { + disjointSet.set(node.id, node.id); + } + if (disjointSet.get(node.id) !== node.id) { + disjointSet.set(node.id, find({ id: disjointSet.get(node.id) })); + } + return disjointSet.get(node.id); + }; + + const union = (node1, node2) => { + const root1 = find(node1); + const root2 = find(node2); + if (root1 !== root2) { + disjointSet.set(root1, root2); + } + }; + + const mstEdges = edges.filter(edge => { + const sourceRoot = find(edge.source); + const targetRoot = find(edge.target); + if (sourceRoot !== targetRoot) { + union(edge.source, edge.target); + return true; + } + return false; + }); + + return mstEdges; + } + }, + clique: { + name: 'Maximum Cliques', + compute: (nodes, minSimilarity = 0.6) => { + // Build adjacency matrix + const n = nodes.length; + const adj = Array(n).fill().map(() => Array(n).fill(false)); + + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + if (similarity >= minSimilarity) { + adj[i][j] = adj[j][i] = true; + } + } + } + + // Find maximal cliques using Bron-Kerbosch algorithm + const cliques = []; + const bronKerbosch = (r, p, x) => { + if (p.length === 0 && x.length === 0) { + if (r.length >= 3) { // Only consider cliques of size 3 or larger + cliques.push([...r]); + } + return; + } + + const pivot = [...p, ...x][0]; + const candidates = p.filter(v => !adj[pivot][v]); + + for (const v of candidates) { + const newR = [...r, v]; + const newP = p.filter(u => adj[v][u]); + const newX = x.filter(u => adj[v][u]); + bronKerbosch(newR, newP, newX); + p = p.filter(u => u !== v); + x.push(v); + } + }; + + const vertices = Array.from({length: n}, (_, i) => i); + bronKerbosch([], vertices, []); + + // Convert cliques to edges + const edges = []; + for (const clique of cliques) { + for (let i = 0; i < clique.length; i++) { + for (let j = i + 1; j < clique.length; j++) { + const similarity = cosineSimilarity( + nodes[clique[i]].avgEmbedding, + nodes[clique[j]].avgEmbedding + ); + edges.push({ + source: nodes[clique[i]], + target: nodes[clique[j]], + similarity + }); + } + } + } + + return edges; + } + }, + pathfinder: { + name: 'Pathfinder Network', + compute: (nodes, r = 2, q = 2) => { + const n = nodes.length; + const distances = Array(n).fill().map(() => Array(n).fill(Infinity)); + + // Calculate initial distances + for (let i = 0; i < n; i++) { + distances[i][i] = 0; + for (let j = i + 1; j < n; j++) { + const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + // Convert similarity to distance (1 - similarity) + const distance = 1 - similarity; + distances[i][j] = distances[j][i] = distance; + } + } + + // Pathfinder network scaling + for (let k = 0; k < n; k++) { + for (let i = 0; i < n; i++) { + for (let j = 0; j < n; j++) { + const sum = Math.pow( + Math.pow(distances[i][k], r) + Math.pow(distances[k][j], r), + 1/r + ); + if (sum < distances[i][j]) { + distances[i][j] = sum; + } + } + } + } + + // Generate edges for the minimal network + const edges = []; + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + let isMinimal = true; + for (let k = 0; k < n; k++) { + if (k !== i && k !== j) { + const sum = Math.pow( + Math.pow(distances[i][k], r) + Math.pow(distances[k][j], r), + 1/r + ); + if (Math.abs(sum - distances[i][j]) < 1e-10) { + isMinimal = false; + break; + } + } + } + if (isMinimal) { + const similarity = 1 - distances[i][j]; + if (similarity > 0.3) { // Only include edges with reasonable similarity + edges.push({ + source: nodes[i], + target: nodes[j], + similarity + }); + } + } + } + } + + return edges; + } } }; From 0149c6fd32017d1fa0576dcb941e72d2fa187e00 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 22:55:27 +1100 Subject: [PATCH 15/38] wip --- apps/registry/app/job-similarity/page.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 2370534..0ebab22 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -519,6 +519,7 @@ export default function JobSimilarityPage() { size: Math.log(items.length + 1) * 3, count: items.length, uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), + usernames: dataSource === 'jobs' ? null : items.map(item => item.username), avgEmbedding, color: `hsl(${Math.random() * 360}, 70%, 50%)`, companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, @@ -713,6 +714,12 @@ export default function JobSimilarityPage() {

{hoverNode.countryCodes.slice(0, 5).join(', ')}{hoverNode.countryCodes.length > 5 ? `, +${hoverNode.countryCodes.length - 5} more` : ''}

)} + {dataSource !== 'jobs' && hoverNode.usernames && ( +
+

Usernames:

+

{hoverNode.usernames.slice(0, 5).join(', ')}{hoverNode.usernames.length > 5 ? `, +${hoverNode.usernames.length - 5} more` : ''}

+
+ )}

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

)} From 50ffde4953c52ff345ac663459cb528ffeabc7ae Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:02:57 +1100 Subject: [PATCH 16/38] wip --- apps/registry/app/api/job-similarity/route.js | 2 +- apps/registry/app/api/similarity/route.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 5cade68..4aceb37 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 500; + const limit = parseInt(searchParams.get('limit')) || 1500; console.time('getJobSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 5d3aa01..5785f34 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -18,9 +18,9 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 1500; + const limit = parseInt(searchParams.get('limit')) || 2000; - console.time('getSimilarityData'); + console.time('getResumeSimilarityData'); const { data, error } = await supabase .from('resumes') .select('username, embedding, resume') @@ -29,14 +29,14 @@ export async function GET(request) { .order('created_at', { ascending: false }); if (error) { - console.error('Error fetching similarity data:', error); + console.error('Error fetching resume similarity data:', error); return NextResponse.json( - { message: 'Error fetching similarity data' }, + { message: 'Error fetching resume similarity data' }, { status: 500 } ); } - console.timeEnd('getSimilarityData'); + console.timeEnd('getResumeSimilarityData'); // Parse embeddings from strings to numerical arrays and extract position const parsedData = data.map(item => { From ae287fd66d266b02ffdf63edf3747f16d40b424f Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:05:04 +1100 Subject: [PATCH 17/38] wip --- apps/registry/app/job-similarity/page.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 0ebab22..15f64bb 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -519,7 +519,7 @@ export default function JobSimilarityPage() { size: Math.log(items.length + 1) * 3, count: items.length, uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), - usernames: dataSource === 'jobs' ? null : items.map(item => item.username), + usernames: dataSource === 'jobs' ? null : [...new Set(items.map(item => item.username))], avgEmbedding, color: `hsl(${Math.random() * 360}, 70%, 50%)`, companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, @@ -717,7 +717,13 @@ export default function JobSimilarityPage() { {dataSource !== 'jobs' && hoverNode.usernames && (

Usernames:

-

{hoverNode.usernames.slice(0, 5).join(', ')}{hoverNode.usernames.length > 5 ? `, +${hoverNode.usernames.length - 5} more` : ''}

+
+ {hoverNode.usernames.map((username, i) => ( +
window.open(`/${username}`, '_blank')}> + {username} +
+ ))} +
)}

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

From 5958cd24660deb316a11e6a72c7b9fa0cea62ff1 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:07:43 +1100 Subject: [PATCH 18/38] wip --- apps/registry/app/job-similarity/page.js | 45 +++++++++++++++++++----- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 15f64bb..040e0de 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -598,9 +598,40 @@ export default function JobSimilarityPage() { } return ( -
+
+
+

Job Market Neural Network

+
+

+ This visualization represents a neural network of the current job market, created by analyzing thousands of job postings from "Who's Hiring" threads + and comparing them against JSON Resume profiles. The data is processed using OpenAI's GPT-4 and embedding models to create a semantic understanding + of job roles and skills. +

+

+ Data Processing Pipeline: +

+
    +
  • + Job Data: Scraped from HN "Who's Hiring" threads → Processed through GPT-4 to generate standardized job descriptions → + Converted to vectors using OpenAI's text-embedding-ada-002 model +
  • +
  • + Resume Data: Sourced from the JSON Resume Registry → Position and skills extracted → + Vectorized using the same embedding model for direct comparison +
  • +
+

+ Visualization Algorithms: Choose from multiple graph algorithms to explore different aspects of the job market. The MST (Minimum Spanning Tree) + shows core relationships, while algorithms like Community Detection and Maximum Cliques reveal clusters of similar roles. +

+

+ Toggle between Jobs and Resumes to compare market demand against available talent. Performance Mode optimizes rendering for large datasets + by reducing animations and physics calculations. +

+
+
+
-

Similarity Network

Date: Thu, 19 Dec 2024 23:13:32 +1100 Subject: [PATCH 20/38] wip --- apps/registry/app/api/job-similarity/route.js | 11 ++++++++++- apps/registry/app/api/similarity/route.js | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 4aceb37..4d998a9 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -62,7 +62,16 @@ export async function GET(request) { }; }).filter(item => item.embedding !== null); - return NextResponse.json(parsedData); + // Add strong cache headers + const headers = new Headers(); + headers.set('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400'); + headers.set('CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); + headers.set('Vercel-CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); + + return NextResponse.json(parsedData, { + headers, + status: 200 + }); } catch (error) { console.error('Error in job similarity endpoint:', error); return NextResponse.json( diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 5785f34..daf38a0 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -59,7 +59,16 @@ export async function GET(request) { }; }).filter(item => item.embedding !== null); - return NextResponse.json(parsedData); + // Add strong cache headers + const headers = new Headers(); + headers.set('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400'); + headers.set('CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); + headers.set('Vercel-CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); + + return NextResponse.json(parsedData, { + headers, + status: 200 + }); } catch (error) { console.error('Error in similarity endpoint:', error); return NextResponse.json( From 34ebaf4040b318e52e99de6eb23bea2a8e31eec9 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:25:42 +1100 Subject: [PATCH 21/38] wip --- apps/registry/app/api/job-similarity/route.js | 11 +- apps/registry/app/api/similarity/route.js | 11 +- apps/registry/app/job-similarity/page.js | 492 ++++++++++-------- 3 files changed, 274 insertions(+), 240 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 4d998a9..4aceb37 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -62,16 +62,7 @@ export async function GET(request) { }; }).filter(item => item.embedding !== null); - // Add strong cache headers - const headers = new Headers(); - headers.set('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400'); - headers.set('CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); - headers.set('Vercel-CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); - - return NextResponse.json(parsedData, { - headers, - status: 200 - }); + return NextResponse.json(parsedData); } catch (error) { console.error('Error in job similarity endpoint:', error); return NextResponse.json( diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index daf38a0..5785f34 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -59,16 +59,7 @@ export async function GET(request) { }; }).filter(item => item.embedding !== null); - // Add strong cache headers - const headers = new Headers(); - headers.set('Cache-Control', 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400'); - headers.set('CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); - headers.set('Vercel-CDN-Cache-Control', 'public, max-age=86400, s-maxage=86400'); - - return NextResponse.json(parsedData, { - headers, - status: 200 - }); + return NextResponse.json(parsedData); } catch (error) { console.error('Error in similarity endpoint:', error); return NextResponse.json( diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index b8cbe91..ac18961 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -1,48 +1,41 @@ 'use client'; -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useState, useCallback, memo, useEffect } from 'react'; import dynamic from 'next/dynamic'; -// Import ForceGraph dynamically to avoid SSR issues const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false }); // Helper function to compute cosine similarity -function cosineSimilarity(a, b) { - let dotProduct = 0; - let normA = 0; - let normB = 0; - for (let i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); -} +const cosineSimilarity = (a, b) => { + if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return 0; + + const dotProduct = a.reduce((sum, _, i) => sum + a[i] * b[i], 0); + const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); + const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); + + return dotProduct / (magnitudeA * magnitudeB); +}; -// Helper function to normalize vector -function normalizeVector(vector) { +// Helper function to normalize a vector +const normalizeVector = (vector) => { if (!Array.isArray(vector) || vector.length === 0) return null; + const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)); if (magnitude === 0) return null; + return vector.map(val => val / magnitude); -} +}; // Helper function to get average embedding -function getAverageEmbedding(embeddings) { - // Filter out null or invalid embeddings - const validEmbeddings = embeddings.filter(emb => Array.isArray(emb) && emb.length > 0); - if (validEmbeddings.length === 0) return null; - - const dim = validEmbeddings[0].length; - const sum = new Array(dim).fill(0); - validEmbeddings.forEach(emb => { - emb.forEach((val, i) => { - sum[i] += val; - }); - }); - const avg = sum.map(val => val / validEmbeddings.length); - return normalizeVector(avg); -} +const getAverageEmbedding = (embeddings) => { + if (!Array.isArray(embeddings) || embeddings.length === 0) return null; + + const sum = embeddings.reduce((acc, curr) => { + return acc.map((val, i) => val + curr[i]); + }, new Array(embeddings[0].length).fill(0)); + + return sum.map(val => val / embeddings.length); +}; // Similarity algorithms const algorithms = { @@ -448,21 +441,76 @@ const algorithms = { } }; -export default function JobSimilarityPage() { +const Header = memo(() => ( +
+

Job Market Neural Network

+
+

+ An interactive visualization of the tech job market, powered by data from HN "Who's Hiring" threads and the JSON Resume Registry. + The network reveals patterns and clusters in job roles and resume profiles through semantic analysis. +

+
    +
  • + Jobs View: Job posts from "Who's Hiring" → GPT-4 standardization → OpenAI embeddings +
  • +
  • + Resumes View: JSON Resume profiles → OpenAI embeddings +
  • +
+

+ Multiple graph algorithms available to explore different relationships. Performance Mode recommended for larger datasets. +

+
+
+)); + +const Controls = memo(({ dataSource, algorithm, performanceMode, onDataSourceChange, onAlgorithmChange, onPerformanceModeChange, algorithms }) => ( +
+
+ + + + + +
+
+)); + +const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { const [graphData, setGraphData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [highlightNodes, setHighlightNodes] = useState(new Set()); - const [highlightLinks, setHighlightLinks] = useState(new Set()); const [hoverNode, setHoverNode] = useState(null); - const [algorithm, setAlgorithm] = useState('mst'); - const [dataSource, setDataSource] = useState('jobs'); const [rawNodes, setRawNodes] = useState(null); - const [performanceMode, setPerformanceMode] = useState(true); + const [highlightNodes, setHighlightNodes] = useState(new Set()); + const [highlightLinks, setHighlightLinks] = useState(new Set()); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - // Fetch data only when dataSource changes + // Fetch data when dataSource changes useEffect(() => { - async function fetchData() { + const fetchData = async () => { setLoading(true); setError(null); try { @@ -539,30 +587,75 @@ export default function JobSimilarityPage() { } finally { setLoading(false); } - } + }; fetchData(); - }, [dataSource]); // Only depend on dataSource + }, [dataSource]); // Compute links when algorithm changes or when we have new nodes useEffect(() => { if (!rawNodes) return; - - try { - const links = algorithms[algorithm].compute(rawNodes); - setGraphData({ nodes: rawNodes, links }); - } catch (err) { - console.error('Error computing links:', err); - setError(err.message); - } - }, [algorithm, rawNodes]); - useEffect(() => { - // Enable performance mode automatically for large datasets - if (graphData?.nodes?.length > 1000) { - setPerformanceMode(true); + const links = []; + const threshold = 0.7; // Similarity threshold + + // Different algorithms for computing links + if (algorithm === 'mst') { + // Kruskal's algorithm for MST + const parent = new Array(rawNodes.length).fill(0).map((_, i) => i); + + function find(x) { + if (parent[x] !== x) parent[x] = find(parent[x]); + return parent[x]; + } + + function union(x, y) { + parent[find(x)] = find(y); + } + + // Create all possible edges with weights + const edges = []; + for (let i = 0; i < rawNodes.length; i++) { + for (let j = i + 1; j < rawNodes.length; j++) { + const similarity = cosineSimilarity(rawNodes[i].avgEmbedding, rawNodes[j].avgEmbedding); + if (similarity > threshold) { + edges.push({ i, j, similarity }); + } + } + } + + // Sort edges by similarity (descending) + edges.sort((a, b) => b.similarity - a.similarity); + + // Build MST + edges.forEach(({ i, j, similarity }) => { + if (find(i) !== find(j)) { + union(i, j); + links.push({ + source: rawNodes[i].id, + target: rawNodes[j].id, + value: similarity + }); + } + }); + } else if (algorithm === 'threshold') { + // Simple threshold algorithm + for (let i = 0; i < rawNodes.length; i++) { + for (let j = i + 1; j < rawNodes.length; j++) { + const similarity = cosineSimilarity(rawNodes[i].avgEmbedding, rawNodes[j].avgEmbedding); + if (similarity > threshold) { + links.push({ + source: rawNodes[i].id, + target: rawNodes[j].id, + value: similarity + }); + } + } + } } - }, [graphData]); + + setGraphData({ nodes: rawNodes, links }); + }, [rawNodes, algorithm]); const handleNodeHover = useCallback(node => { setHighlightNodes(new Set(node ? [node] : [])); @@ -579,173 +672,132 @@ export default function JobSimilarityPage() { } }, [dataSource]); - if (loading) { - return ( -
-

Similarity Network

-

Loading...

-
- ); - } + if (loading) return
Loading...
; + if (error) return
Error: {error}
; - if (error) { - return ( -
-

Similarity Network

-

Error: {error}

-
- ); - } + return ( +
+ {graphData && ( + highlightNodes.has(node) ? '#ff0000' : node.color} + nodeCanvasObject={(node, ctx, globalScale) => { + // Draw node + ctx.beginPath(); + ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); + ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; + ctx.fill(); + + // Only draw label if node is highlighted + if (highlightNodes.has(node)) { + const label = node.id; + const fontSize = Math.max(14, node.size * 1.5); + ctx.font = `${fontSize}px Sans-Serif`; + const textWidth = ctx.measureText(label).width; + const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); + + // Draw background for label + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; + ctx.fillRect( + node.x - bckgDimensions[0] / 2, + node.y - bckgDimensions[1] * 2, + bckgDimensions[0], + bckgDimensions[1] + ); + + // Draw label + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; + ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); + + // Draw count + const countLabel = `(${node.count})`; + const smallerFont = fontSize * 0.7; + ctx.font = `${smallerFont}px Sans-Serif`; + ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); + } + }} + nodeRelSize={performanceMode ? 4 : 6} + linkWidth={link => highlightLinks.has(link) ? 2 : 1} + linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} + linkOpacity={0.3} + linkDirectionalParticles={performanceMode ? 0 : 4} + linkDirectionalParticleWidth={2} + onNodeHover={handleNodeHover} + onNodeClick={handleNodeClick} + enableNodeDrag={!performanceMode} + cooldownTicks={performanceMode ? 50 : 100} + d3AlphaDecay={performanceMode ? 0.05 : 0.02} + d3VelocityDecay={performanceMode ? 0.4 : 0.3} + warmupTicks={performanceMode ? 50 : 100} + d3Force={performanceMode ? { + collision: 1, + charge: -30 + } : undefined} + /> + )} + {hoverNode && ( +
+

{hoverNode.id}

+

{hoverNode.count} {dataSource === 'jobs' ? 'job listings' : 'resumes'}

+ {dataSource === 'jobs' && hoverNode.companies && ( +
+

Companies:

+

{hoverNode.companies.slice(0, 5).join(', ')}{hoverNode.companies.length > 5 ? `, +${hoverNode.companies.length - 5} more` : ''}

+
+ )} + {dataSource === 'jobs' && hoverNode.countryCodes && ( +
+

Locations:

+

{hoverNode.countryCodes.slice(0, 5).join(', ')}{hoverNode.countryCodes.length > 5 ? `, +${hoverNode.countryCodes.length - 5} more` : ''}

+
+ )} + {dataSource !== 'jobs' && hoverNode.usernames && ( +
+

Usernames:

+
+ {hoverNode.usernames.map((username, i) => ( +
window.open(`/${username}`, '_blank')}> + {username} +
+ ))} +
+
+ )} +

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

+
+ )} +
+ ); +}; + +export default function Page() { + const [dataSource, setDataSource] = useState('jobs'); + const [algorithm, setAlgorithm] = useState('mst'); + const [performanceMode, setPerformanceMode] = useState(false); + + const handleDataSourceChange = useCallback((e) => setDataSource(e.target.value), []); + const handleAlgorithmChange = useCallback((e) => setAlgorithm(e.target.value), []); + const handlePerformanceModeChange = useCallback((e) => setPerformanceMode(e.checked), []); return (
-
-

Job Market Neural Network

-
-

- An interactive visualization of the tech job market, powered by data from HN "Who's Hiring" threads and the JSON Resume Registry. - The network reveals patterns and clusters in job roles and resume profiles through semantic analysis. -

-
    -
  • - Jobs View: Job posts from "Who's Hiring" → GPT-4 standardization → OpenAI embeddings -
  • -
  • - Resumes View: JSON Resume profiles → OpenAI embeddings -
  • -
-

- Multiple graph algorithms available to explore different relationships. Performance Mode recommended for larger datasets. -

-
-
- -
-
- - - - - -
-
- -
- {graphData && ( - highlightNodes.has(node) ? '#ff0000' : node.color} - nodeCanvasObject={(node, ctx, globalScale) => { - // Draw node - ctx.beginPath(); - ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); - ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; - ctx.fill(); - - // Only draw label if node is highlighted - if (highlightNodes.has(node)) { - const label = node.id; - const fontSize = Math.max(14, node.size * 1.5); - ctx.font = `${fontSize}px Sans-Serif`; - const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); - - // Draw background for label - ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; - ctx.fillRect( - node.x - bckgDimensions[0] / 2, - node.y - bckgDimensions[1] * 2, - bckgDimensions[0], - bckgDimensions[1] - ); - - // Draw label - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = '#000'; - ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); - - // Draw count - const countLabel = `(${node.count})`; - const smallerFont = fontSize * 0.7; - ctx.font = `${smallerFont}px Sans-Serif`; - ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); - } - }} - nodeRelSize={performanceMode ? 4 : 6} - linkWidth={link => highlightLinks.has(link) ? 2 : 1} - linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} - linkOpacity={0.3} - linkDirectionalParticles={performanceMode ? 0 : 4} - linkDirectionalParticleWidth={2} - onNodeHover={handleNodeHover} - onNodeClick={handleNodeClick} - enableNodeDrag={!performanceMode} - cooldownTicks={performanceMode ? 50 : 100} - d3AlphaDecay={performanceMode ? 0.05 : 0.02} - d3VelocityDecay={performanceMode ? 0.4 : 0.3} - warmupTicks={performanceMode ? 50 : 100} - d3Force={performanceMode ? { - collision: 1, - charge: -30 - } : undefined} - /> - )} - {hoverNode && ( -
-

{hoverNode.id}

-

{hoverNode.count} {dataSource === 'jobs' ? 'job listings' : 'resumes'}

- {dataSource === 'jobs' && hoverNode.companies && ( -
-

Companies:

-

{hoverNode.companies.slice(0, 5).join(', ')}{hoverNode.companies.length > 5 ? `, +${hoverNode.companies.length - 5} more` : ''}

-
- )} - {dataSource === 'jobs' && hoverNode.countryCodes && ( -
-

Locations:

-

{hoverNode.countryCodes.slice(0, 5).join(', ')}{hoverNode.countryCodes.length > 5 ? `, +${hoverNode.countryCodes.length - 5} more` : ''}

-
- )} - {dataSource !== 'jobs' && hoverNode.usernames && ( -
-

Usernames:

-
- {hoverNode.usernames.map((username, i) => ( -
window.open(`/${username}`, '_blank')}> - {username} -
- ))} -
-
- )} -

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

-
- )} -
+
+ +
); } From f0c798b1647b878b4fa8b935b0e31c4c07419e4e Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:29:03 +1100 Subject: [PATCH 22/38] wip --- apps/registry/app/api/job-similarity/route.js | 2 +- apps/registry/app/api/similarity/route.js | 2 +- apps/registry/app/job-similarity/page.js | 38 ++++++++++--------- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 4aceb37..a97f889 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 1500; + const limit = parseInt(searchParams.get('limit')) || 50; console.time('getJobSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 5785f34..ea98d01 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 2000; + const limit = parseInt(searchParams.get('limit')) || 100; console.time('getResumeSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index ac18961..422c00f 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -676,7 +676,7 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { if (error) return
Error: {error}
; return ( -
+
{graphData && ( setPerformanceMode(e.checked), []); return ( -
-
- - +
+
+
+ +
+
+ +
); } From 57ba00c91ee694ff6acef8ddac3a952b447c8855 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:36:38 +1100 Subject: [PATCH 23/38] wip --- apps/registry/app/api/job-similarity/route.js | 2 +- apps/registry/app/components/Menu.js | 4 +-- apps/registry/app/job-similarity/page.js | 25 ++++++++++++++++--- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index a97f889..029afb8 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 50; + const limit = parseInt(searchParams.get('limit')) || 250; console.time('getJobSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/components/Menu.js b/apps/registry/app/components/Menu.js index 7524346..73b7219 100644 --- a/apps/registry/app/components/Menu.js +++ b/apps/registry/app/components/Menu.js @@ -45,9 +45,9 @@ export default function Menu({ session }) { Jobs (
-

Job Market Neural Network

+

Job Market Simlarity

An interactive visualization of the tech job market, powered by data from HN "Who's Hiring" threads and the JSON Resume Registry. @@ -672,8 +672,27 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { } }, [dataSource]); - if (loading) return

Loading...
; - if (error) return
Error: {error}
; + if (loading) return ( +
+
+
+
Loading graph data...
+
+
+ ); + if (error) return ( +
+
+
+ + + +
+
Error loading graph data
+
{error}
+
+
+ ); return (
From 644dfaae9240734dd6f7f733b9a04a62568b73e8 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:40:10 +1100 Subject: [PATCH 24/38] wip --- apps/registry/app/job-similarity/page.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 479b737..bdc7482 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -673,7 +673,7 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { }, [dataSource]); if (loading) return ( -
+
Loading graph data...
@@ -681,7 +681,7 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => {
); if (error) return ( -
+
@@ -695,7 +695,7 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { ); return ( -
+
{graphData && ( Date: Thu, 19 Dec 2024 23:41:39 +1100 Subject: [PATCH 25/38] wip --- apps/registry/app/job-similarity/page.js | 144 +++++++++++------------ 1 file changed, 66 insertions(+), 78 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index bdc7482..728cdf4 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -464,42 +464,43 @@ const Header = memo(() => (
)); -const Controls = memo(({ dataSource, algorithm, performanceMode, onDataSourceChange, onAlgorithmChange, onPerformanceModeChange, algorithms }) => ( -
-
- - - - - +const Controls = memo(({ dataSource, setDataSource, algorithm, setAlgorithm }) => ( +
+
+
+ + +
+
+ + +
)); -const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { +const GraphContainer = ({ dataSource, algorithm }) => { const [graphData, setGraphData] = useState(null); const [hoverNode, setHoverNode] = useState(null); const [rawNodes, setRawNodes] = useState(null); @@ -712,48 +713,48 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { const label = node.id; const fontSize = Math.max(14, node.size * 1.5); ctx.font = `${fontSize}px Sans-Serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; + + // Add background to text const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); - - // Draw background for label - ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; + const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.4); + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.fillRect( node.x - bckgDimensions[0] / 2, - node.y - bckgDimensions[1] * 2, + node.y - bckgDimensions[1] / 2, bckgDimensions[0], bckgDimensions[1] ); - - // Draw label - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; + ctx.fillStyle = '#000'; - ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); + ctx.fillText(label, node.x, node.y); - // Draw count - const countLabel = `(${node.count})`; + // Add count below label + const countLabel = `${node.count} ${dataSource === 'jobs' ? 'jobs' : 'resumes'}`; const smallerFont = fontSize * 0.7; ctx.font = `${smallerFont}px Sans-Serif`; ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); } }} - nodeRelSize={performanceMode ? 4 : 6} + nodeRelSize={4} linkWidth={link => highlightLinks.has(link) ? 2 : 1} linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} linkOpacity={0.3} - linkDirectionalParticles={performanceMode ? 0 : 4} + linkDirectionalParticles={0} linkDirectionalParticleWidth={2} onNodeHover={handleNodeHover} onNodeClick={handleNodeClick} - enableNodeDrag={!performanceMode} - cooldownTicks={performanceMode ? 50 : 100} - d3AlphaDecay={performanceMode ? 0.05 : 0.02} - d3VelocityDecay={performanceMode ? 0.4 : 0.3} - warmupTicks={performanceMode ? 50 : 100} - d3Force={performanceMode ? { + enableNodeDrag={false} + cooldownTicks={50} + d3AlphaDecay={0.05} + d3VelocityDecay={0.4} + warmupTicks={50} + d3Force={{ collision: 1, charge: -30 - } : undefined} + }} /> )} {hoverNode && ( @@ -794,33 +795,20 @@ const GraphContainer = ({ dataSource, algorithm, performanceMode }) => { export default function Page() { const [dataSource, setDataSource] = useState('jobs'); const [algorithm, setAlgorithm] = useState('mst'); - const [performanceMode, setPerformanceMode] = useState(false); - - const handleDataSourceChange = useCallback((e) => setDataSource(e.target.value), []); - const handleAlgorithmChange = useCallback((e) => setAlgorithm(e.target.value), []); - const handlePerformanceModeChange = useCallback((e) => setPerformanceMode(e.checked), []); return ( -
-
-
- -
-
- -
+
+
+ +
); } From 4f4aacbfbb4a89f4c4fe152cf031187d8bf59cae Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:44:54 +1100 Subject: [PATCH 26/38] wip --- apps/registry/app/job-similarity/page.js | 30 ++++++++++++++---------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 728cdf4..5a6536a 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -696,7 +696,7 @@ const GraphContainer = ({ dataSource, algorithm }) => { ); return ( -
+
{graphData && ( { collision: 1, charge: -30 }} + width={window.innerWidth} + height={window.innerHeight - 32 * 16} /> )} {hoverNode && ( @@ -798,17 +800,21 @@ export default function Page() { return (
-
- - +
+
+ +
+
+ +
); } From 595a268c6f8a95171e918f95ac5327758c8b2d38 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:46:31 +1100 Subject: [PATCH 27/38] wip --- apps/registry/app/job-similarity/page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 5a6536a..8dcd672 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -800,7 +800,7 @@ export default function Page() { return (
-
+
Date: Thu, 19 Dec 2024 23:47:48 +1100 Subject: [PATCH 28/38] wip --- apps/registry/app/api/job-similarity/route.js | 7 ++++++- apps/registry/app/api/similarity/route.js | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 029afb8..0c4170c 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -62,7 +62,12 @@ export async function GET(request) { }; }).filter(item => item.embedding !== null); - return NextResponse.json(parsedData); + return NextResponse.json(parsedData, { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400', + }, + }); } catch (error) { console.error('Error in job similarity endpoint:', error); return NextResponse.json( diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index ea98d01..7cb0f36 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -59,7 +59,12 @@ export async function GET(request) { }; }).filter(item => item.embedding !== null); - return NextResponse.json(parsedData); + return NextResponse.json(parsedData, { + headers: { + 'Content-Type': 'application/json', + 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400', + }, + }); } catch (error) { console.error('Error in similarity endpoint:', error); return NextResponse.json( From 8c25d9bdba01b86c62585b106db6480d5f685024 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:55:31 +1100 Subject: [PATCH 29/38] wip --- apps/registry/app/job-similarity/page.js | 256 ++++++++++------------- apps/registry/app/similarity/page.js | 5 +- 2 files changed, 108 insertions(+), 153 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 8dcd672..4b64acc 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -376,7 +376,7 @@ const algorithms = { }, pathfinder: { name: 'Pathfinder Network', - compute: (nodes, r = 2, q = 2) => { + compute: (nodes, r = 2) => { const n = nodes.length; const distances = Array(n).fill().map(() => Array(n).fill(Infinity)); @@ -463,6 +463,7 @@ const Header = memo(() => (
)); +Header.displayName = 'Header'; const Controls = memo(({ dataSource, setDataSource, algorithm, setAlgorithm }) => (
@@ -499,6 +500,7 @@ const Controls = memo(({ dataSource, setDataSource, algorithm, setAlgorithm }) =
)); +Controls.displayName = 'Controls'; const GraphContainer = ({ dataSource, algorithm }) => { const [graphData, setGraphData] = useState(null); @@ -508,170 +510,121 @@ const GraphContainer = ({ dataSource, algorithm }) => { const [highlightLinks, setHighlightLinks] = useState(new Set()); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [edges, setEdges] = useState([]); - // Fetch data when dataSource changes - useEffect(() => { - const fetchData = async () => { - setLoading(true); - setError(null); - try { - const endpoint = dataSource === 'jobs' ? '/api/job-similarity' : '/api/similarity'; - const response = await fetch(endpoint); - if (!response.ok) { - throw new Error('Failed to fetch data'); - } - const jsonData = await response.json(); - - // Filter out items without valid embeddings - const validData = jsonData.filter(item => { - const embedding = dataSource === 'jobs' ? - item.embedding : - (typeof item.embedding === 'string' ? JSON.parse(item.embedding) : item.embedding); - return Array.isArray(embedding) && embedding.length > 0; - }); - - // Group similar items - const groups = {}; + const handleNodeHover = useCallback((node) => { + setHighlightNodes(new Set(node ? [node] : [])); + setHighlightLinks(new Set(node ? edges.filter(link => link.source === node || link.target === node) : [])); + setHoverNode(node || null); + }, [edges]); - validData.forEach(item => { - const label = dataSource === 'jobs' - ? item.title - : (item.position || 'Unknown Position'); - - if (!groups[label]) { - groups[label] = []; - } - groups[label].push(item); + const handleNodeClick = useCallback((node) => { + if (!node) return; + const nodeData = rawNodes.find(n => n.title === node.id); + if (nodeData) { + window.open(nodeData.url, '_blank'); + } + }, [rawNodes]); + + const processData = useCallback((data) => { + // Filter out items without valid embeddings + const validData = data.filter(item => { + const embedding = dataSource === 'jobs' ? + item.embedding : + (typeof item.embedding === 'string' ? JSON.parse(item.embedding) : item.embedding); + return Array.isArray(embedding) && embedding.length > 0; + }); + + // Group similar items + const groups = {}; + + validData.forEach(item => { + const label = dataSource === 'jobs' + ? item.title + : (item.position || 'Unknown Position'); + + if (!groups[label]) { + groups[label] = []; + } + groups[label].push(item); + }); + + // Create nodes with normalized embeddings + const nodes = Object.entries(groups) + .map(([label, items], index) => { + const embeddings = items.map(item => { + if (dataSource === 'jobs') return item.embedding; + return typeof item.embedding === 'string' ? + JSON.parse(item.embedding) : item.embedding; }); - // Create nodes with normalized embeddings - const nodes = Object.entries(groups) - .map(([label, items], index) => { - const embeddings = items.map(item => { - if (dataSource === 'jobs') return item.embedding; - return typeof item.embedding === 'string' ? - JSON.parse(item.embedding) : item.embedding; - }); - - const normalizedEmbeddings = embeddings - .map(emb => normalizeVector(emb)) - .filter(emb => emb !== null); - - if (normalizedEmbeddings.length === 0) return null; - - const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); - if (!avgEmbedding) return null; - - return { - id: label, - group: index, - size: Math.log(items.length + 1) * 3, - count: items.length, - uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), - usernames: dataSource === 'jobs' ? null : [...new Set(items.map(item => item.username))], - avgEmbedding, - color: `hsl(${Math.random() * 360}, 70%, 50%)`, - companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, - countryCodes: dataSource === 'jobs' ? [...new Set(items.map(item => item.countryCode || 'Unknown Location'))] : null - }; - }) - .filter(node => node !== null); - - if (nodes.length === 0) { - throw new Error('No valid data found with embeddings'); - } - - setRawNodes(nodes); - } catch (err) { - console.error('Error in fetchData:', err); - setError(err.message); - } finally { - setLoading(false); - } - }; + const normalizedEmbeddings = embeddings + .map(emb => normalizeVector(emb)) + .filter(emb => emb !== null); + + if (normalizedEmbeddings.length === 0) return null; + + const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); + if (!avgEmbedding) return null; + + return { + id: label, + group: index, + size: Math.log(items.length + 1) * 3, + count: items.length, + uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), + usernames: dataSource === 'jobs' ? null : [...new Set(items.map(item => item.username))], + avgEmbedding, + color: `hsl(${Math.random() * 360}, 70%, 50%)`, + companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, + countryCodes: dataSource === 'jobs' ? [...new Set(items.map(item => item.countryCode || 'Unknown Location'))] : null + }; + }) + .filter(node => node !== null); + + if (nodes.length === 0) { + throw new Error('No valid data found with embeddings'); + } - fetchData(); + return nodes; }, [dataSource]); - // Compute links when algorithm changes or when we have new nodes - useEffect(() => { - if (!rawNodes) return; - - const links = []; - const threshold = 0.7; // Similarity threshold - - // Different algorithms for computing links - if (algorithm === 'mst') { - // Kruskal's algorithm for MST - const parent = new Array(rawNodes.length).fill(0).map((_, i) => i); - - function find(x) { - if (parent[x] !== x) parent[x] = find(parent[x]); - return parent[x]; - } - - function union(x, y) { - parent[find(x)] = find(y); - } - - // Create all possible edges with weights - const edges = []; - for (let i = 0; i < rawNodes.length; i++) { - for (let j = i + 1; j < rawNodes.length; j++) { - const similarity = cosineSimilarity(rawNodes[i].avgEmbedding, rawNodes[j].avgEmbedding); - if (similarity > threshold) { - edges.push({ i, j, similarity }); - } - } + const fetchData = useCallback(async () => { + setLoading(true); + setError(null); + try { + const response = await fetch(`/api/${dataSource === 'jobs' ? 'job-' : ''}similarity?limit=250&algorithm=${algorithm}`); + if (!response.ok) { + throw new Error('Failed to fetch data'); } + const data = await response.json(); + const processedData = processData(data); + setRawNodes(processedData); + } catch (err) { + console.error('Error fetching data:', err); + setError(err.message); + } finally { + setLoading(false); + } + }, [dataSource, algorithm, processData]); - // Sort edges by similarity (descending) - edges.sort((a, b) => b.similarity - a.similarity); + const processLinks = useCallback(() => { + if (!rawNodes) return; - // Build MST - edges.forEach(({ i, j, similarity }) => { - if (find(i) !== find(j)) { - union(i, j); - links.push({ - source: rawNodes[i].id, - target: rawNodes[j].id, - value: similarity - }); - } - }); - } else if (algorithm === 'threshold') { - // Simple threshold algorithm - for (let i = 0; i < rawNodes.length; i++) { - for (let j = i + 1; j < rawNodes.length; j++) { - const similarity = cosineSimilarity(rawNodes[i].avgEmbedding, rawNodes[j].avgEmbedding); - if (similarity > threshold) { - links.push({ - source: rawNodes[i].id, - target: rawNodes[j].id, - value: similarity - }); - } - } - } - } + const { compute } = algorithms[algorithm]; + const links = compute(rawNodes); setGraphData({ nodes: rawNodes, links }); + setEdges(links); }, [rawNodes, algorithm]); - const handleNodeHover = useCallback(node => { - setHighlightNodes(new Set(node ? [node] : [])); - setHighlightLinks(new Set(graphData?.links.filter(link => - link.source.id === node?.id || link.target.id === node?.id - ) || [])); - setHoverNode(node || null); - }, [graphData]); + useEffect(() => { + fetchData(); + }, [fetchData]); - const handleNodeClick = useCallback(node => { - if (node.uuids && node.uuids.length > 0) { - const baseUrl = dataSource === 'jobs' ? '/jobs/' : '/'; - window.open(`${baseUrl}${node.uuids[0]}`, '_blank'); - } - }, [dataSource]); + useEffect(() => { + processLinks(); + }, [processLinks]); if (loading) return (
@@ -703,8 +656,9 @@ const GraphContainer = ({ dataSource, algorithm }) => { nodeColor={node => highlightNodes.has(node) ? '#ff0000' : node.color} nodeCanvasObject={(node, ctx, globalScale) => { // Draw node + const size = node.size * (4 / Math.max(1, globalScale)); ctx.beginPath(); - ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; ctx.fill(); diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index 8b2684c..24c12f2 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -141,15 +141,16 @@ export default function SimilarityPage() { nodeColor={node => highlightNodes.has(node) ? '#ff0000' : node.color} nodeCanvasObject={(node, ctx, globalScale) => { // Draw node + const size = node.size * (4 / Math.max(1, globalScale)); ctx.beginPath(); - ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); + ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; ctx.fill(); // Only draw label if node is highlighted if (highlightNodes.has(node)) { const label = node.id; - const fontSize = Math.max(14, node.size * 1.5); + const fontSize = Math.max(14, size * 1.5); ctx.font = `${fontSize}px Sans-Serif`; const textWidth = ctx.measureText(label).width; const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); From e522b0582b6cfbe286d8afe998119ea082395b50 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Thu, 19 Dec 2024 23:59:52 +1100 Subject: [PATCH 30/38] wip --- apps/registry/app/job-similarity/page.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 4b64acc..ec78c1f 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -447,7 +447,10 @@ const Header = memo(() => (

An interactive visualization of the tech job market, powered by data from HN "Who's Hiring" threads and the JSON Resume Registry. - The network reveals patterns and clusters in job roles and resume profiles through semantic analysis. + Each node represents a job category, with edges connecting similar roles. The size of each node indicates the number of job listings in that category. +

+

+ Hover over a node to see details about the companies and locations hiring for that role. Click a node to view the original job listing or resume profile.

  • @@ -458,7 +461,7 @@ const Header = memo(() => (

- Multiple graph algorithms available to explore different relationships. Performance Mode recommended for larger datasets. + Multiple graph algorithms available to explore different relationships.

@@ -520,11 +523,11 @@ const GraphContainer = ({ dataSource, algorithm }) => { const handleNodeClick = useCallback((node) => { if (!node) return; - const nodeData = rawNodes.find(n => n.title === node.id); - if (nodeData) { - window.open(nodeData.url, '_blank'); + if (node.uuids && node.uuids.length > 0) { + const baseUrl = dataSource === 'jobs' ? '/jobs/' : '/'; + window.open(`${baseUrl}${node.uuids[0]}`, '_blank'); } - }, [rawNodes]); + }, [dataSource]); const processData = useCallback((data) => { // Filter out items without valid embeddings From 0bbdfee6ddf68869ff14fd9c0ab8286eba884846 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:02:55 +1100 Subject: [PATCH 31/38] wip --- apps/registry/app/api/job-similarity/route.js | 2 +- apps/registry/app/api/similarity/route.js | 2 +- apps/registry/app/job-similarity/page.js | 42 ++++++++++++++++--- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 0c4170c..9b7a787 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 250; + const limit = parseInt(searchParams.get('limit')) || 750; console.time('getJobSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 7cb0f36..2e45a14 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 100; + const limit = parseInt(searchParams.get('limit')) || 500; console.time('getResumeSimilarityData'); const { data, error } = await supabase diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index ec78c1f..a605a52 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -441,6 +441,29 @@ const algorithms = { } }; +const colors = [ + '#FF6B6B', // coral red + '#4ECDC4', // turquoise + '#45B7D1', // sky blue + '#96CEB4', // sage green + '#F7D794', // mellow yellow + '#9B59B6', // amethyst purple + '#FF8C94', // rose pink + '#4A90E2', // ocean blue + '#50E3C2', // mint + '#F39C12', // amber + '#D35400', // pumpkin + '#FF5E3A', // sunset orange + '#2ECC71', // emerald + '#F64747', // pomegranate + '#786FA6', // lavender + '#00B894', // mountain meadow + '#FD79A8', // pink glamour + '#6C5CE7', // blue violet + '#FDA7DF', // light pink + '#A8E6CF', // light green +]; + const Header = memo(() => (

Job Market Simlarity

@@ -578,7 +601,7 @@ const GraphContainer = ({ dataSource, algorithm }) => { uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), usernames: dataSource === 'jobs' ? null : [...new Set(items.map(item => item.username))], avgEmbedding, - color: `hsl(${Math.random() * 360}, 70%, 50%)`, + color: colors[index % colors.length], companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, countryCodes: dataSource === 'jobs' ? [...new Set(items.map(item => item.countryCode || 'Unknown Location'))] : null }; @@ -656,15 +679,24 @@ const GraphContainer = ({ dataSource, algorithm }) => { {graphData && ( highlightNodes.has(node) ? '#ff0000' : node.color} + nodeColor={node => highlightNodes.has(node) ? '#FF4757' : node.color} nodeCanvasObject={(node, ctx, globalScale) => { // Draw node const size = node.size * (4 / Math.max(1, globalScale)); ctx.beginPath(); ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; + ctx.fillStyle = highlightNodes.has(node) ? '#FF4757' : node.color; ctx.fill(); + // Add a subtle glow effect + if (highlightNodes.has(node)) { + ctx.shadowColor = '#FF4757'; + ctx.shadowBlur = 15; + ctx.strokeStyle = '#FF4757'; + ctx.lineWidth = 2; + ctx.stroke(); + ctx.shadowBlur = 0; + } // Only draw label if node is highlighted if (highlightNodes.has(node)) { const label = node.id; @@ -697,8 +729,8 @@ const GraphContainer = ({ dataSource, algorithm }) => { }} nodeRelSize={4} linkWidth={link => highlightLinks.has(link) ? 2 : 1} - linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} - linkOpacity={0.3} + linkColor={link => highlightLinks.has(link) ? '#FF4757' : '#E5E9F2'} + linkOpacity={0.5} linkDirectionalParticles={0} linkDirectionalParticleWidth={2} onNodeHover={handleNodeHover} From 18e56776a8a9248bae1d9f94785418e4219c628b Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:04:11 +1100 Subject: [PATCH 32/38] wip --- apps/registry/app/api/resumes/route.js | 2 +- apps/registry/app/api/similarity/route.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/registry/app/api/resumes/route.js b/apps/registry/app/api/resumes/route.js index 2bbc874..dab9a3d 100644 --- a/apps/registry/app/api/resumes/route.js +++ b/apps/registry/app/api/resumes/route.js @@ -19,7 +19,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 3000; + const limit = parseInt(searchParams.get('limit')) || 2000; const page = parseInt(searchParams.get('page')) || 1; const search = searchParams.get('search') || ''; diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 2e45a14..8f13eb9 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -18,7 +18,7 @@ export async function GET(request) { try { const supabase = createClient(supabaseUrl, process.env.SUPABASE_KEY); const { searchParams } = new URL(request.url); - const limit = parseInt(searchParams.get('limit')) || 500; + const limit = parseInt(searchParams.get('limit')) || 1000; console.time('getResumeSimilarityData'); const { data, error } = await supabase From 34f9b772c0805eee7909d5e3b9c96feb26c43a7e Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:09:07 +1100 Subject: [PATCH 33/38] wip --- apps/registry/app/api/job-similarity/route.js | 48 +- apps/registry/app/api/similarity/route.js | 42 +- apps/registry/app/job-similarity/page.js | 562 +++++++++++------- apps/registry/app/similarity/page.js | 69 ++- 4 files changed, 443 insertions(+), 278 deletions(-) diff --git a/apps/registry/app/api/job-similarity/route.js b/apps/registry/app/api/job-similarity/route.js index 9b7a787..9afa5df 100644 --- a/apps/registry/app/api/job-similarity/route.js +++ b/apps/registry/app/api/job-similarity/route.js @@ -39,33 +39,37 @@ export async function GET(request) { console.timeEnd('getJobSimilarityData'); // Parse embeddings and job titles - const parsedData = data.map(item => { - let jobTitle = 'Unknown Position'; - let gptContent = null; - try { - gptContent = JSON.parse(item.gpt_content); - jobTitle = gptContent?.title || 'Unknown Position'; - } catch (e) { - console.warn('Failed to parse gpt_content for job:', item.uuid); - } + const parsedData = data + .map((item) => { + let jobTitle = 'Unknown Position'; + let gptContent = null; + try { + gptContent = JSON.parse(item.gpt_content); + jobTitle = gptContent?.title || 'Unknown Position'; + } catch (e) { + console.warn('Failed to parse gpt_content for job:', item.uuid); + } - return { - uuid: item.uuid, - title: jobTitle, - company: gptContent?.company, - countryCode: gptContent?.countryCode, - embedding: typeof item.embedding_v5 === 'string' - ? JSON.parse(item.embedding_v5) - : Array.isArray(item.embedding_v5) - ? item.embedding_v5 - : null - }; - }).filter(item => item.embedding !== null); + return { + uuid: item.uuid, + title: jobTitle, + company: gptContent?.company, + countryCode: gptContent?.countryCode, + embedding: + typeof item.embedding_v5 === 'string' + ? JSON.parse(item.embedding_v5) + : Array.isArray(item.embedding_v5) + ? item.embedding_v5 + : null, + }; + }) + .filter((item) => item.embedding !== null); return NextResponse.json(parsedData, { headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400', + 'Cache-Control': + 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400', }, }); } catch (error) { diff --git a/apps/registry/app/api/similarity/route.js b/apps/registry/app/api/similarity/route.js index 8f13eb9..395c723 100644 --- a/apps/registry/app/api/similarity/route.js +++ b/apps/registry/app/api/similarity/route.js @@ -39,30 +39,34 @@ export async function GET(request) { console.timeEnd('getResumeSimilarityData'); // Parse embeddings from strings to numerical arrays and extract position - const parsedData = data.map(item => { - let resumeData; - try { - resumeData = JSON.parse(item.resume); - } catch (e) { - console.warn('Failed to parse resume for user:', item.username); - resumeData = {}; - } + const parsedData = data + .map((item) => { + let resumeData; + try { + resumeData = JSON.parse(item.resume); + } catch (e) { + console.warn('Failed to parse resume for user:', item.username); + resumeData = {}; + } - return { - username: item.username, - embedding: typeof item.embedding === 'string' - ? JSON.parse(item.embedding) - : Array.isArray(item.embedding) - ? item.embedding - : null, - position: resumeData?.basics?.label || 'Unknown Position' - }; - }).filter(item => item.embedding !== null); + return { + username: item.username, + embedding: + typeof item.embedding === 'string' + ? JSON.parse(item.embedding) + : Array.isArray(item.embedding) + ? item.embedding + : null, + position: resumeData?.basics?.label || 'Unknown Position', + }; + }) + .filter((item) => item.embedding !== null); return NextResponse.json(parsedData, { headers: { 'Content-Type': 'application/json', - 'Cache-Control': 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400', + 'Cache-Control': + 'public, max-age=86400, s-maxage=86400, stale-while-revalidate=86400', }, }); } catch (error) { diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index a605a52..a2fc103 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -3,38 +3,40 @@ import React, { useState, useCallback, memo, useEffect } from 'react'; import dynamic from 'next/dynamic'; -const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false }); +const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { + ssr: false, +}); // Helper function to compute cosine similarity const cosineSimilarity = (a, b) => { if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return 0; - + const dotProduct = a.reduce((sum, _, i) => sum + a[i] * b[i], 0); const magnitudeA = Math.sqrt(a.reduce((sum, val) => sum + val * val, 0)); const magnitudeB = Math.sqrt(b.reduce((sum, val) => sum + val * val, 0)); - + return dotProduct / (magnitudeA * magnitudeB); }; // Helper function to normalize a vector const normalizeVector = (vector) => { if (!Array.isArray(vector) || vector.length === 0) return null; - + const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)); if (magnitude === 0) return null; - - return vector.map(val => val / magnitude); + + return vector.map((val) => val / magnitude); }; // Helper function to get average embedding const getAverageEmbedding = (embeddings) => { if (!Array.isArray(embeddings) || embeddings.length === 0) return null; - + const sum = embeddings.reduce((acc, curr) => { return acc.map((val, i) => val + curr[i]); }, new Array(embeddings[0].length).fill(0)); - - return sum.map(val => val / embeddings.length); + + return sum.map((val) => val / embeddings.length); }; // Similarity algorithms @@ -45,12 +47,12 @@ const algorithms = { // Kruskal's algorithm for MST const links = []; const parent = new Array(nodes.length).fill(0).map((_, i) => i); - + function find(x) { if (parent[x] !== x) parent[x] = find(parent[x]); return parent[x]; } - + function union(x, y) { parent[find(x)] = find(y); } @@ -59,7 +61,10 @@ const algorithms = { const edges = []; for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); if (similarity > minSimilarity) { edges.push({ i, j, similarity }); } @@ -76,13 +81,13 @@ const algorithms = { links.push({ source: nodes[i].id, target: nodes[j].id, - value: similarity + value: similarity, }); } }); return links; - } + }, }, knn: { name: 'K-Nearest Neighbors', @@ -91,7 +96,10 @@ const algorithms = { nodes.forEach((node, i) => { const similarities = nodes.map((otherNode, j) => ({ index: j, - similarity: i === j ? -1 : cosineSimilarity(node.avgEmbedding, otherNode.avgEmbedding) + similarity: + i === j + ? -1 + : cosineSimilarity(node.avgEmbedding, otherNode.avgEmbedding), })); similarities @@ -102,13 +110,13 @@ const algorithms = { links.add({ source: nodes[i].id, target: nodes[index].id, - value: similarity + value: similarity, }); } }); }); return Array.from(links); - } + }, }, threshold: { name: 'Similarity Threshold', @@ -116,18 +124,21 @@ const algorithms = { const links = new Set(); for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); if (similarity > threshold) { links.add({ source: nodes[i].id, target: nodes[j].id, - value: similarity + value: similarity, }); } } } return Array.from(links); - } + }, }, hierarchical: { name: 'Hierarchical Clustering', @@ -139,7 +150,10 @@ const algorithms = { // Calculate all pairwise similarities for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); similarities.push({ i, j, similarity }); } } @@ -150,27 +164,27 @@ const algorithms = { // Merge clusters and add links similarities.forEach(({ i, j, similarity }) => { if (similarity > threshold) { - const cluster1 = clusters.find(c => c.includes(i)); - const cluster2 = clusters.find(c => c.includes(j)); - + const cluster1 = clusters.find((c) => c.includes(i)); + const cluster2 = clusters.find((c) => c.includes(j)); + if (cluster1 !== cluster2) { // Add links between closest points in clusters links.add({ source: nodes[i].id, target: nodes[j].id, - value: similarity + value: similarity, }); - + // Merge clusters const merged = [...cluster1, ...cluster2]; - clusters = clusters.filter(c => c !== cluster1 && c !== cluster2); + clusters = clusters.filter((c) => c !== cluster1 && c !== cluster2); clusters.push(merged); } } }); return Array.from(links); - } + }, }, community: { name: 'Community Detection', @@ -188,7 +202,10 @@ const algorithms = { // Find strongly connected nodes for (let j = 0; j < nodes.length; j++) { if (i !== j && !communities.has(j)) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); if (similarity > communityThreshold) { community.add(j); communities.set(j, communityId); @@ -202,7 +219,10 @@ const algorithms = { // Second pass: connect communities for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); const sameCommunity = communities.get(i) === communities.get(j); // Add links within communities and strong links between communities @@ -210,52 +230,60 @@ const algorithms = { links.add({ source: nodes[i].id, target: nodes[j].id, - value: similarity + value: similarity, }); } } } return Array.from(links); - } + }, }, adaptive: { name: 'Adaptive Threshold', compute: (nodes) => { const links = new Set(); const similarities = []; - + // Calculate all pairwise similarities for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); similarities.push(similarity); } } // Calculate adaptive threshold using mean and standard deviation - const mean = similarities.reduce((a, b) => a + b, 0) / similarities.length; + const mean = + similarities.reduce((a, b) => a + b, 0) / similarities.length; const std = Math.sqrt( - similarities.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / similarities.length + similarities.reduce((a, b) => a + Math.pow(b - mean, 2), 0) / + similarities.length ); const adaptiveThreshold = mean + 0.5 * std; // Create links based on adaptive threshold for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); if (similarity > adaptiveThreshold) { links.add({ source: nodes[i].id, target: nodes[j].id, - value: similarity + value: similarity, }); } } } return Array.from(links); - } + }, }, maxSpanningTree: { name: 'Maximum Spanning Tree', @@ -264,12 +292,15 @@ const algorithms = { // Calculate all pairwise similarities for (let i = 0; i < nodes.length; i++) { for (let j = i + 1; j < nodes.length; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); if (similarity >= minSimilarity) { edges.push({ source: nodes[i], target: nodes[j], - similarity: similarity + similarity: similarity, }); } } @@ -289,7 +320,7 @@ const algorithms = { } return disjointSet.get(node.id); }; - + const union = (node1, node2) => { const root1 = find(node1); const root2 = find(node2); @@ -298,7 +329,7 @@ const algorithms = { } }; - const mstEdges = edges.filter(edge => { + const mstEdges = edges.filter((edge) => { const sourceRoot = find(edge.source); const targetRoot = find(edge.target); if (sourceRoot !== targetRoot) { @@ -309,18 +340,23 @@ const algorithms = { }); return mstEdges; - } + }, }, clique: { name: 'Maximum Cliques', compute: (nodes, minSimilarity = 0.6) => { // Build adjacency matrix const n = nodes.length; - const adj = Array(n).fill().map(() => Array(n).fill(false)); - + const adj = Array(n) + .fill() + .map(() => Array(n).fill(false)); + for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); if (similarity >= minSimilarity) { adj[i][j] = adj[j][i] = true; } @@ -331,26 +367,27 @@ const algorithms = { const cliques = []; const bronKerbosch = (r, p, x) => { if (p.length === 0 && x.length === 0) { - if (r.length >= 3) { // Only consider cliques of size 3 or larger + if (r.length >= 3) { + // Only consider cliques of size 3 or larger cliques.push([...r]); } return; } const pivot = [...p, ...x][0]; - const candidates = p.filter(v => !adj[pivot][v]); + const candidates = p.filter((v) => !adj[pivot][v]); for (const v of candidates) { const newR = [...r, v]; - const newP = p.filter(u => adj[v][u]); - const newX = x.filter(u => adj[v][u]); + const newP = p.filter((u) => adj[v][u]); + const newX = x.filter((u) => adj[v][u]); bronKerbosch(newR, newP, newX); - p = p.filter(u => u !== v); + p = p.filter((u) => u !== v); x.push(v); } }; - const vertices = Array.from({length: n}, (_, i) => i); + const vertices = Array.from({ length: n }, (_, i) => i); bronKerbosch([], vertices, []); // Convert cliques to edges @@ -365,26 +402,31 @@ const algorithms = { edges.push({ source: nodes[clique[i]], target: nodes[clique[j]], - similarity + similarity, }); } } } return edges; - } + }, }, pathfinder: { name: 'Pathfinder Network', compute: (nodes, r = 2) => { const n = nodes.length; - const distances = Array(n).fill().map(() => Array(n).fill(Infinity)); - + const distances = Array(n) + .fill() + .map(() => Array(n).fill(Infinity)); + // Calculate initial distances for (let i = 0; i < n; i++) { distances[i][i] = 0; for (let j = i + 1; j < n; j++) { - const similarity = cosineSimilarity(nodes[i].avgEmbedding, nodes[j].avgEmbedding); + const similarity = cosineSimilarity( + nodes[i].avgEmbedding, + nodes[j].avgEmbedding + ); // Convert similarity to distance (1 - similarity) const distance = 1 - similarity; distances[i][j] = distances[j][i] = distance; @@ -397,7 +439,7 @@ const algorithms = { for (let j = 0; j < n; j++) { const sum = Math.pow( Math.pow(distances[i][k], r) + Math.pow(distances[k][j], r), - 1/r + 1 / r ); if (sum < distances[i][j]) { distances[i][j] = sum; @@ -415,7 +457,7 @@ const algorithms = { if (k !== i && k !== j) { const sum = Math.pow( Math.pow(distances[i][k], r) + Math.pow(distances[k][j], r), - 1/r + 1 / r ); if (Math.abs(sum - distances[i][j]) < 1e-10) { isMinimal = false; @@ -425,11 +467,12 @@ const algorithms = { } if (isMinimal) { const similarity = 1 - distances[i][j]; - if (similarity > 0.3) { // Only include edges with reasonable similarity + if (similarity > 0.3) { + // Only include edges with reasonable similarity edges.push({ source: nodes[i], target: nodes[j], - similarity + similarity, }); } } @@ -437,8 +480,8 @@ const algorithms = { } return edges; - } - } + }, + }, }; const colors = [ @@ -469,18 +512,24 @@ const Header = memo(() => (

Job Market Simlarity

- An interactive visualization of the tech job market, powered by data from HN "Who's Hiring" threads and the JSON Resume Registry. - Each node represents a job category, with edges connecting similar roles. The size of each node indicates the number of job listings in that category. + An interactive visualization of the tech job market, powered by data + from HN "Who's Hiring" threads and the JSON Resume Registry. Each node + represents a job category, with edges connecting similar roles. The size + of each node indicates the number of job listings in that category.

- Hover over a node to see details about the companies and locations hiring for that role. Click a node to view the original job listing or resume profile. + Hover over a node to see details about the companies and locations + hiring for that role. Click a node to view the original job listing or + resume profile.

  • - Jobs View: Job posts from "Who's Hiring" → GPT-4 standardization → OpenAI embeddings + Jobs View: Job posts from "Who's Hiring" → GPT-4 + standardization → OpenAI embeddings
  • - Resumes View: JSON Resume profiles → OpenAI embeddings + Resumes View: JSON Resume profiles → OpenAI + embeddings

@@ -491,41 +540,43 @@ const Header = memo(() => ( )); Header.displayName = 'Header'; -const Controls = memo(({ dataSource, setDataSource, algorithm, setAlgorithm }) => ( -

-
-
- - -
-
- - +const Controls = memo( + ({ dataSource, setDataSource, algorithm, setAlgorithm }) => ( +
+
+
+ + +
+
+ + +
-
-)); + ) +); Controls.displayName = 'Controls'; const GraphContainer = ({ dataSource, algorithm }) => { @@ -538,88 +589,135 @@ const GraphContainer = ({ dataSource, algorithm }) => { const [error, setError] = useState(null); const [edges, setEdges] = useState([]); - const handleNodeHover = useCallback((node) => { - setHighlightNodes(new Set(node ? [node] : [])); - setHighlightLinks(new Set(node ? edges.filter(link => link.source === node || link.target === node) : [])); - setHoverNode(node || null); - }, [edges]); - - const handleNodeClick = useCallback((node) => { - if (!node) return; - if (node.uuids && node.uuids.length > 0) { - const baseUrl = dataSource === 'jobs' ? '/jobs/' : '/'; - window.open(`${baseUrl}${node.uuids[0]}`, '_blank'); - } - }, [dataSource]); - - const processData = useCallback((data) => { - // Filter out items without valid embeddings - const validData = data.filter(item => { - const embedding = dataSource === 'jobs' ? - item.embedding : - (typeof item.embedding === 'string' ? JSON.parse(item.embedding) : item.embedding); - return Array.isArray(embedding) && embedding.length > 0; - }); - - // Group similar items - const groups = {}; - - validData.forEach(item => { - const label = dataSource === 'jobs' - ? item.title - : (item.position || 'Unknown Position'); - - if (!groups[label]) { - groups[label] = []; + const handleNodeHover = useCallback( + (node) => { + setHighlightNodes(new Set(node ? [node] : [])); + setHighlightLinks( + new Set( + node + ? edges.filter( + (link) => link.source === node || link.target === node + ) + : [] + ) + ); + setHoverNode(node || null); + }, + [edges] + ); + + const handleNodeClick = useCallback( + (node) => { + if (!node) return; + if (node.uuids && node.uuids.length > 0) { + const baseUrl = dataSource === 'jobs' ? '/jobs/' : '/'; + window.open(`${baseUrl}${node.uuids[0]}`, '_blank'); } - groups[label].push(item); - }); - - // Create nodes with normalized embeddings - const nodes = Object.entries(groups) - .map(([label, items], index) => { - const embeddings = items.map(item => { - if (dataSource === 'jobs') return item.embedding; - return typeof item.embedding === 'string' ? - JSON.parse(item.embedding) : item.embedding; - }); - - const normalizedEmbeddings = embeddings - .map(emb => normalizeVector(emb)) - .filter(emb => emb !== null); - - if (normalizedEmbeddings.length === 0) return null; - - const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); - if (!avgEmbedding) return null; - - return { - id: label, - group: index, - size: Math.log(items.length + 1) * 3, - count: items.length, - uuids: items.map(item => dataSource === 'jobs' ? item.uuid : item.username), - usernames: dataSource === 'jobs' ? null : [...new Set(items.map(item => item.username))], - avgEmbedding, - color: colors[index % colors.length], - companies: dataSource === 'jobs' ? [...new Set(items.map(item => item.company || 'Unknown Company'))] : null, - countryCodes: dataSource === 'jobs' ? [...new Set(items.map(item => item.countryCode || 'Unknown Location'))] : null - }; - }) - .filter(node => node !== null); - - if (nodes.length === 0) { - throw new Error('No valid data found with embeddings'); - } + }, + [dataSource] + ); + + const processData = useCallback( + (data) => { + // Filter out items without valid embeddings + const validData = data.filter((item) => { + const embedding = + dataSource === 'jobs' + ? item.embedding + : typeof item.embedding === 'string' + ? JSON.parse(item.embedding) + : item.embedding; + return Array.isArray(embedding) && embedding.length > 0; + }); + + // Group similar items + const groups = {}; - return nodes; - }, [dataSource]); + validData.forEach((item) => { + const label = + dataSource === 'jobs' + ? item.title + : item.position || 'Unknown Position'; + + if (!groups[label]) { + groups[label] = []; + } + groups[label].push(item); + }); + + // Create nodes with normalized embeddings + const nodes = Object.entries(groups) + .map(([label, items], index) => { + const embeddings = items.map((item) => { + if (dataSource === 'jobs') return item.embedding; + return typeof item.embedding === 'string' + ? JSON.parse(item.embedding) + : item.embedding; + }); + + const normalizedEmbeddings = embeddings + .map((emb) => normalizeVector(emb)) + .filter((emb) => emb !== null); + + if (normalizedEmbeddings.length === 0) return null; + + const avgEmbedding = getAverageEmbedding(normalizedEmbeddings); + if (!avgEmbedding) return null; + + return { + id: label, + group: index, + size: Math.log(items.length + 1) * 3, + count: items.length, + uuids: items.map((item) => + dataSource === 'jobs' ? item.uuid : item.username + ), + usernames: + dataSource === 'jobs' + ? null + : [...new Set(items.map((item) => item.username))], + avgEmbedding, + color: colors[index % colors.length], + companies: + dataSource === 'jobs' + ? [ + ...new Set( + items.map((item) => item.company || 'Unknown Company') + ), + ] + : null, + countryCodes: + dataSource === 'jobs' + ? [ + ...new Set( + items.map( + (item) => item.countryCode || 'Unknown Location' + ) + ), + ] + : null, + }; + }) + .filter((node) => node !== null); + + if (nodes.length === 0) { + throw new Error('No valid data found with embeddings'); + } + + return nodes; + }, + [dataSource] + ); const fetchData = useCallback(async () => { setLoading(true); setError(null); try { - const response = await fetch(`/api/${dataSource === 'jobs' ? 'job-' : ''}similarity?limit=250&algorithm=${algorithm}`); + const response = await fetch( + `/api/${ + dataSource === 'jobs' ? 'job-' : '' + }similarity?limit=250&algorithm=${algorithm}` + ); if (!response.ok) { throw new Error('Failed to fetch data'); } @@ -652,34 +750,52 @@ const GraphContainer = ({ dataSource, algorithm }) => { processLinks(); }, [processLinks]); - if (loading) return ( -
-
-
-
Loading graph data...
+ if (loading) + return ( +
+
+
+
+ Loading graph data... +
+
-
- ); - if (error) return ( -
-
-
- - - + ); + if (error) + return ( +
+
+
+ + + +
+
+ Error loading graph data +
+
{error}
-
Error loading graph data
-
{error}
-
- ); + ); return (
{graphData && ( highlightNodes.has(node) ? '#FF4757' : node.color} + nodeColor={(node) => + highlightNodes.has(node) ? '#FF4757' : node.color + } nodeCanvasObject={(node, ctx, globalScale) => { // Draw node const size = node.size * (4 / Math.max(1, globalScale)); @@ -705,10 +821,12 @@ const GraphContainer = ({ dataSource, algorithm }) => { ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillStyle = '#000'; - + // Add background to text const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.4); + const bckgDimensions = [textWidth, fontSize].map( + (n) => n + fontSize * 0.4 + ); ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; ctx.fillRect( node.x - bckgDimensions[0] / 2, @@ -716,20 +834,24 @@ const GraphContainer = ({ dataSource, algorithm }) => { bckgDimensions[0], bckgDimensions[1] ); - + ctx.fillStyle = '#000'; ctx.fillText(label, node.x, node.y); // Add count below label - const countLabel = `${node.count} ${dataSource === 'jobs' ? 'jobs' : 'resumes'}`; + const countLabel = `${node.count} ${ + dataSource === 'jobs' ? 'jobs' : 'resumes' + }`; const smallerFont = fontSize * 0.7; ctx.font = `${smallerFont}px Sans-Serif`; ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); } }} nodeRelSize={4} - linkWidth={link => highlightLinks.has(link) ? 2 : 1} - linkColor={link => highlightLinks.has(link) ? '#FF4757' : '#E5E9F2'} + linkWidth={(link) => (highlightLinks.has(link) ? 2 : 1)} + linkColor={(link) => + highlightLinks.has(link) ? '#FF4757' : '#E5E9F2' + } linkOpacity={0.5} linkDirectionalParticles={0} linkDirectionalParticleWidth={2} @@ -742,7 +864,7 @@ const GraphContainer = ({ dataSource, algorithm }) => { warmupTicks={50} d3Force={{ collision: 1, - charge: -30 + charge: -30, }} width={window.innerWidth} height={window.innerHeight - 32 * 16} @@ -751,17 +873,30 @@ const GraphContainer = ({ dataSource, algorithm }) => { {hoverNode && (

{hoverNode.id}

-

{hoverNode.count} {dataSource === 'jobs' ? 'job listings' : 'resumes'}

+

+ {hoverNode.count}{' '} + {dataSource === 'jobs' ? 'job listings' : 'resumes'} +

{dataSource === 'jobs' && hoverNode.companies && (

Companies:

-

{hoverNode.companies.slice(0, 5).join(', ')}{hoverNode.companies.length > 5 ? `, +${hoverNode.companies.length - 5} more` : ''}

+

+ {hoverNode.companies.slice(0, 5).join(', ')} + {hoverNode.companies.length > 5 + ? `, +${hoverNode.companies.length - 5} more` + : ''} +

)} {dataSource === 'jobs' && hoverNode.countryCodes && (

Locations:

-

{hoverNode.countryCodes.slice(0, 5).join(', ')}{hoverNode.countryCodes.length > 5 ? `, +${hoverNode.countryCodes.length - 5} more` : ''}

+

+ {hoverNode.countryCodes.slice(0, 5).join(', ')} + {hoverNode.countryCodes.length > 5 + ? `, +${hoverNode.countryCodes.length - 5} more` + : ''} +

)} {dataSource !== 'jobs' && hoverNode.usernames && ( @@ -769,14 +904,20 @@ const GraphContainer = ({ dataSource, algorithm }) => {

Usernames:

{hoverNode.usernames.map((username, i) => ( -
window.open(`/${username}`, '_blank')}> +
window.open(`/${username}`, '_blank')} + > {username}
))}
)} -

Click to view {dataSource === 'jobs' ? 'job' : 'resume'}

+

+ Click to view {dataSource === 'jobs' ? 'job' : 'resume'} +

)}
@@ -799,10 +940,7 @@ export default function Page() { />
- +
); diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index 24c12f2..590c9c2 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -4,7 +4,9 @@ import React, { useEffect, useState, useCallback } from 'react'; import dynamic from 'next/dynamic'; // Import ForceGraph dynamically to avoid SSR issues -const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false }); +const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { + ssr: false, +}); // Helper function to compute cosine similarity function cosineSimilarity(a, b) { @@ -35,10 +37,10 @@ export default function SimilarityPage() { throw new Error('Failed to fetch data'); } const jsonData = await response.json(); - + // Group similar positions const positionGroups = {}; - jsonData.forEach(item => { + jsonData.forEach((item) => { const position = item.position; if (!positionGroups[position]) { positionGroups[position] = []; @@ -58,9 +60,9 @@ export default function SimilarityPage() { group: index, size: Math.log(items.length + 1) * 3, count: items.length, - usernames: items.map(item => item.username), - embeddings: items.map(item => item.embedding), - color: `hsl(${Math.random() * 360}, 70%, 50%)` + usernames: items.map((item) => item.username), + embeddings: items.map((item) => item.embedding), + color: `hsl(${Math.random() * 360}, 70%, 50%)`, }); }); @@ -70,21 +72,21 @@ export default function SimilarityPage() { // Calculate average similarity between groups let totalSimilarity = 0; let comparisons = 0; - - nodes[i].embeddings.forEach(emb1 => { - nodes[j].embeddings.forEach(emb2 => { + + nodes[i].embeddings.forEach((emb1) => { + nodes[j].embeddings.forEach((emb2) => { totalSimilarity += cosineSimilarity(emb1, emb2); comparisons++; }); }); const avgSimilarity = totalSimilarity / comparisons; - + if (avgSimilarity > similarityThreshold) { links.push({ source: nodes[i].id, target: nodes[j].id, - value: avgSimilarity + value: avgSimilarity, }); } } @@ -101,13 +103,20 @@ export default function SimilarityPage() { fetchData(); }, []); - const handleNodeHover = useCallback(node => { - setHighlightNodes(new Set(node ? [node] : [])); - setHighlightLinks(new Set(graphData?.links.filter(link => - link.source.id === node?.id || link.target.id === node?.id - ) || [])); - setHoverNode(node || null); - }, [graphData]); + const handleNodeHover = useCallback( + (node) => { + setHighlightNodes(new Set(node ? [node] : [])); + setHighlightLinks( + new Set( + graphData?.links.filter( + (link) => link.source.id === node?.id || link.target.id === node?.id + ) || [] + ) + ); + setHoverNode(node || null); + }, + [graphData] + ); if (loading) { return ( @@ -131,14 +140,18 @@ export default function SimilarityPage() {

Resume Position Network

- Explore similar positions in an interactive network. Each node represents a position, with size indicating the number of resumes. - Connected positions are similar based on resume content. Hover to highlight connections, click to view resumes. + Explore similar positions in an interactive network. Each node + represents a position, with size indicating the number of resumes. + Connected positions are similar based on resume content. Hover to + highlight connections, click to view resumes.

{graphData && ( highlightNodes.has(node) ? '#ff0000' : node.color} + nodeColor={(node) => + highlightNodes.has(node) ? '#ff0000' : node.color + } nodeCanvasObject={(node, ctx, globalScale) => { // Draw node const size = node.size * (4 / Math.max(1, globalScale)); @@ -153,7 +166,9 @@ export default function SimilarityPage() { const fontSize = Math.max(14, size * 1.5); ctx.font = `${fontSize}px Sans-Serif`; const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map(n => n + fontSize * 0.2); + const bckgDimensions = [textWidth, fontSize].map( + (n) => n + fontSize * 0.2 + ); // Draw background for label ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; @@ -178,8 +193,10 @@ export default function SimilarityPage() { } }} nodeRelSize={6} - linkWidth={link => highlightLinks.has(link) ? 2 : 1} - linkColor={link => highlightLinks.has(link) ? '#ff0000' : '#cccccc'} + linkWidth={(link) => (highlightLinks.has(link) ? 2 : 1)} + linkColor={(link) => + highlightLinks.has(link) ? '#ff0000' : '#cccccc' + } linkOpacity={0.3} linkDirectionalParticles={4} linkDirectionalParticleWidth={2} @@ -200,7 +217,9 @@ export default function SimilarityPage() {

{hoverNode.id}

{hoverNode.count} resumes

-

Click to view a sample resume

+

+ Click to view a sample resume +

)}
From fca12055bb83e76ddb95df8abef0a42a848dfc5c Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:19:57 +1100 Subject: [PATCH 34/38] wip --- apps/registry/app/job-similarity/page.js | 2 +- apps/registry/app/similarity/page.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index a2fc103..2a28e3c 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -716,7 +716,7 @@ const GraphContainer = ({ dataSource, algorithm }) => { const response = await fetch( `/api/${ dataSource === 'jobs' ? 'job-' : '' - }similarity?limit=250&algorithm=${algorithm}` + }similarity?limit=1500&algorithm=${algorithm}` ); if (!response.ok) { throw new Error('Failed to fetch data'); diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js index 590c9c2..6212ed0 100644 --- a/apps/registry/app/similarity/page.js +++ b/apps/registry/app/similarity/page.js @@ -30,9 +30,9 @@ export default function SimilarityPage() { const [hoverNode, setHoverNode] = useState(null); useEffect(() => { - async function fetchData() { + const fetchData = async () => { try { - const response = await fetch('/api/similarity'); + const response = await fetch('/api/similarity?limit=1500'); if (!response.ok) { throw new Error('Failed to fetch data'); } @@ -98,7 +98,7 @@ export default function SimilarityPage() { } finally { setLoading(false); } - } + }; fetchData(); }, []); From be024697cbf6f587968bb29b454546a48bf80d0b Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:23:09 +1100 Subject: [PATCH 35/38] wip --- apps/registry/app/job-similarity/page.js | 8 +- apps/registry/app/similarity/page.js | 228 ----------------------- 2 files changed, 5 insertions(+), 231 deletions(-) delete mode 100644 apps/registry/app/similarity/page.js diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 2a28e3c..ce672ea 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -858,13 +858,15 @@ const GraphContainer = ({ dataSource, algorithm }) => { onNodeHover={handleNodeHover} onNodeClick={handleNodeClick} enableNodeDrag={false} + warmupTicks={100} cooldownTicks={50} d3AlphaDecay={0.05} d3VelocityDecay={0.4} - warmupTicks={50} d3Force={{ - collision: 1, - charge: -30, + collision: d3.forceCollide().radius((node) => Math.max(20, node.size * 2)), + charge: d3.forceManyBody().strength(-150), + x: d3.forceX().strength(0.05), + y: d3.forceY().strength(0.05) }} width={window.innerWidth} height={window.innerHeight - 32 * 16} diff --git a/apps/registry/app/similarity/page.js b/apps/registry/app/similarity/page.js deleted file mode 100644 index 6212ed0..0000000 --- a/apps/registry/app/similarity/page.js +++ /dev/null @@ -1,228 +0,0 @@ -'use client'; - -import React, { useEffect, useState, useCallback } from 'react'; -import dynamic from 'next/dynamic'; - -// Import ForceGraph dynamically to avoid SSR issues -const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { - ssr: false, -}); - -// Helper function to compute cosine similarity -function cosineSimilarity(a, b) { - let dotProduct = 0; - let normA = 0; - let normB = 0; - for (let i = 0; i < a.length; i++) { - dotProduct += a[i] * b[i]; - normA += a[i] * a[i]; - normB += b[i] * b[i]; - } - return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); -} - -export default function SimilarityPage() { - const [graphData, setGraphData] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [highlightNodes, setHighlightNodes] = useState(new Set()); - const [highlightLinks, setHighlightLinks] = useState(new Set()); - const [hoverNode, setHoverNode] = useState(null); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await fetch('/api/similarity?limit=1500'); - if (!response.ok) { - throw new Error('Failed to fetch data'); - } - const jsonData = await response.json(); - - // Group similar positions - const positionGroups = {}; - jsonData.forEach((item) => { - const position = item.position; - if (!positionGroups[position]) { - positionGroups[position] = []; - } - positionGroups[position].push(item); - }); - - // Create nodes and links - const nodes = []; - const links = []; - const similarityThreshold = 0.7; - - // Create nodes for each unique position - Object.entries(positionGroups).forEach(([position, items], index) => { - nodes.push({ - id: position, - group: index, - size: Math.log(items.length + 1) * 3, - count: items.length, - usernames: items.map((item) => item.username), - embeddings: items.map((item) => item.embedding), - color: `hsl(${Math.random() * 360}, 70%, 50%)`, - }); - }); - - // Create links between similar positions - for (let i = 0; i < nodes.length; i++) { - for (let j = i + 1; j < nodes.length; j++) { - // Calculate average similarity between groups - let totalSimilarity = 0; - let comparisons = 0; - - nodes[i].embeddings.forEach((emb1) => { - nodes[j].embeddings.forEach((emb2) => { - totalSimilarity += cosineSimilarity(emb1, emb2); - comparisons++; - }); - }); - - const avgSimilarity = totalSimilarity / comparisons; - - if (avgSimilarity > similarityThreshold) { - links.push({ - source: nodes[i].id, - target: nodes[j].id, - value: avgSimilarity, - }); - } - } - } - - setGraphData({ nodes, links }); - } catch (err) { - setError(err.message); - } finally { - setLoading(false); - } - }; - - fetchData(); - }, []); - - const handleNodeHover = useCallback( - (node) => { - setHighlightNodes(new Set(node ? [node] : [])); - setHighlightLinks( - new Set( - graphData?.links.filter( - (link) => link.source.id === node?.id || link.target.id === node?.id - ) || [] - ) - ); - setHoverNode(node || null); - }, - [graphData] - ); - - if (loading) { - return ( -
-

Resume Position Network

-

Loading...

-
- ); - } - - if (error) { - return ( -
-

Resume Position Network

-

Error: {error}

-
- ); - } - - return ( -
-

Resume Position Network

-

- Explore similar positions in an interactive network. Each node - represents a position, with size indicating the number of resumes. - Connected positions are similar based on resume content. Hover to - highlight connections, click to view resumes. -

-
- {graphData && ( - - highlightNodes.has(node) ? '#ff0000' : node.color - } - nodeCanvasObject={(node, ctx, globalScale) => { - // Draw node - const size = node.size * (4 / Math.max(1, globalScale)); - ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; - ctx.fill(); - - // Only draw label if node is highlighted - if (highlightNodes.has(node)) { - const label = node.id; - const fontSize = Math.max(14, size * 1.5); - ctx.font = `${fontSize}px Sans-Serif`; - const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map( - (n) => n + fontSize * 0.2 - ); - - // Draw background for label - ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; - ctx.fillRect( - node.x - bckgDimensions[0] / 2, - node.y - bckgDimensions[1] * 2, - bckgDimensions[0], - bckgDimensions[1] - ); - - // Draw label - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = '#000'; - ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); - - // Draw count - const countLabel = `(${node.count})`; - const smallerFont = fontSize * 0.7; - ctx.font = `${smallerFont}px Sans-Serif`; - ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); - } - }} - nodeRelSize={6} - linkWidth={(link) => (highlightLinks.has(link) ? 2 : 1)} - linkColor={(link) => - highlightLinks.has(link) ? '#ff0000' : '#cccccc' - } - linkOpacity={0.3} - linkDirectionalParticles={4} - linkDirectionalParticleWidth={2} - onNodeHover={handleNodeHover} - onNodeClick={(node) => { - if (node.usernames && node.usernames.length > 0) { - window.open(`/${node.usernames[0]}`, '_blank'); - } - }} - enableNodeDrag={true} - cooldownTicks={100} - d3AlphaDecay={0.02} - d3VelocityDecay={0.3} - warmupTicks={100} - /> - )} - {hoverNode && ( -
-

{hoverNode.id}

-

{hoverNode.count} resumes

-

- Click to view a sample resume -

-
- )} -
-
- ); -} From 6177bdb403b5a624bced78aa6efaee85f5db98b4 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:28:12 +1100 Subject: [PATCH 36/38] wip --- apps/registry/app/job-similarity/page.js | 17 +++++++++++------ apps/registry/package.json | 1 + pnpm-lock.yaml | 12 ++++++++++++ 3 files changed, 24 insertions(+), 6 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index ce672ea..4fd2825 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -2,6 +2,7 @@ import React, { useState, useCallback, memo, useEffect } from 'react'; import dynamic from 'next/dynamic'; +import { forceCollide, forceManyBody, forceX, forceY } from 'd3-force'; const ForceGraph2D = dynamic(() => import('react-force-graph-2d'), { ssr: false, @@ -713,10 +714,14 @@ const GraphContainer = ({ dataSource, algorithm }) => { setLoading(true); setError(null); try { + // Check if we're in development environment + const isLocal = process.env.NODE_ENV === 'development'; + const limit = isLocal ? 300 : 1500; + const response = await fetch( `/api/${ dataSource === 'jobs' ? 'job-' : '' - }similarity?limit=1500&algorithm=${algorithm}` + }similarity?limit=${limit}` ); if (!response.ok) { throw new Error('Failed to fetch data'); @@ -730,7 +735,7 @@ const GraphContainer = ({ dataSource, algorithm }) => { } finally { setLoading(false); } - }, [dataSource, algorithm, processData]); + }, [dataSource, processData]); const processLinks = useCallback(() => { if (!rawNodes) return; @@ -863,10 +868,10 @@ const GraphContainer = ({ dataSource, algorithm }) => { d3AlphaDecay={0.05} d3VelocityDecay={0.4} d3Force={{ - collision: d3.forceCollide().radius((node) => Math.max(20, node.size * 2)), - charge: d3.forceManyBody().strength(-150), - x: d3.forceX().strength(0.05), - y: d3.forceY().strength(0.05) + collision: forceCollide().radius((node) => Math.max(20, node.size * 2)), + charge: forceManyBody().strength(-150), + x: forceX().strength(0.05), + y: forceY().strength(0.05) }} width={window.innerWidth} height={window.innerHeight - 32 * 16} diff --git a/apps/registry/package.json b/apps/registry/package.json index a4bb021..1d60fc2 100644 --- a/apps/registry/package.json +++ b/apps/registry/package.json @@ -31,6 +31,7 @@ "axios": "^1.3.6", "chatgpt": "^5.2.4", "compromise": "^14.13.0", + "d3-force": "^3.0.0", "dotenv-cli": "^7.2.1", "eventsource-parser": "^1.0.0", "express": "^4.18.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2ba465e..79798b0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,6 +237,9 @@ importers: compromise: specifier: ^14.13.0 version: 14.13.0 + d3-force: + specifier: ^3.0.0 + version: 3.0.0 dotenv-cli: specifier: ^7.2.1 version: 7.2.1 @@ -13069,6 +13072,15 @@ packages: d3-timer: 1.0.10 dev: false + /d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + dependencies: + d3-dispatch: 1.0.6 + d3-quadtree: 1.0.7 + d3-timer: 3.0.1 + dev: false + /d3-format@1.4.5: resolution: {integrity: sha512-J0piedu6Z8iB6TbIGfZgDzfXxUFN3qQRMofy2oPdXzQibYGqPB/9iMcxr/TGalU+2RsyDO+U4f33id8tbnSRMQ==} dev: false From 2471caeb5e57b6f8e2ea9cc0f0c12ed4fc2f1739 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:37:39 +1100 Subject: [PATCH 37/38] wip --- apps/registry/app/job-similarity/page.js | 62 +++++++++--------------- 1 file changed, 22 insertions(+), 40 deletions(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 4fd2825..60f4236 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -799,80 +799,62 @@ const GraphContainer = ({ dataSource, algorithm }) => { - highlightNodes.has(node) ? '#FF4757' : node.color + highlightNodes.has(node) ? '#ff0000' : node.color } nodeCanvasObject={(node, ctx, globalScale) => { // Draw node - const size = node.size * (4 / Math.max(1, globalScale)); ctx.beginPath(); - ctx.arc(node.x, node.y, size, 0, 2 * Math.PI); - ctx.fillStyle = highlightNodes.has(node) ? '#FF4757' : node.color; + ctx.arc(node.x, node.y, node.size * 2, 0, 2 * Math.PI); + ctx.fillStyle = highlightNodes.has(node) ? '#ff0000' : node.color; ctx.fill(); - // Add a subtle glow effect - if (highlightNodes.has(node)) { - ctx.shadowColor = '#FF4757'; - ctx.shadowBlur = 15; - ctx.strokeStyle = '#FF4757'; - ctx.lineWidth = 2; - ctx.stroke(); - ctx.shadowBlur = 0; - } // Only draw label if node is highlighted if (highlightNodes.has(node)) { const label = node.id; const fontSize = Math.max(14, node.size * 1.5); ctx.font = `${fontSize}px Sans-Serif`; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = '#000'; - - // Add background to text const textWidth = ctx.measureText(label).width; - const bckgDimensions = [textWidth, fontSize].map( - (n) => n + fontSize * 0.4 + const bckgDimensions = [textWidth, fontSize].map((n) => + n + fontSize * 0.2 ); - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + + // Draw background for label + ctx.fillStyle = 'rgba(255, 255, 255, 0.9)'; ctx.fillRect( node.x - bckgDimensions[0] / 2, - node.y - bckgDimensions[1] / 2, + node.y - bckgDimensions[1] * 2, bckgDimensions[0], bckgDimensions[1] ); + // Draw label + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; ctx.fillStyle = '#000'; - ctx.fillText(label, node.x, node.y); + ctx.fillText(label, node.x, node.y - bckgDimensions[1] * 1.5); - // Add count below label - const countLabel = `${node.count} ${ - dataSource === 'jobs' ? 'jobs' : 'resumes' - }`; + // Draw count + const countLabel = `(${node.count})`; const smallerFont = fontSize * 0.7; ctx.font = `${smallerFont}px Sans-Serif`; ctx.fillText(countLabel, node.x, node.y - bckgDimensions[1]); } }} - nodeRelSize={4} + nodeRelSize={6} linkWidth={(link) => (highlightLinks.has(link) ? 2 : 1)} linkColor={(link) => - highlightLinks.has(link) ? '#FF4757' : '#E5E9F2' + highlightLinks.has(link) ? '#ff0000' : '#cccccc' } - linkOpacity={0.5} - linkDirectionalParticles={0} + linkOpacity={0.3} + linkDirectionalParticles={4} linkDirectionalParticleWidth={2} onNodeHover={handleNodeHover} onNodeClick={handleNodeClick} enableNodeDrag={false} + cooldownTicks={100} + d3AlphaDecay={0.02} + d3VelocityDecay={0.3} warmupTicks={100} - cooldownTicks={50} - d3AlphaDecay={0.05} - d3VelocityDecay={0.4} - d3Force={{ - collision: forceCollide().radius((node) => Math.max(20, node.size * 2)), - charge: forceManyBody().strength(-150), - x: forceX().strength(0.05), - y: forceY().strength(0.05) - }} width={window.innerWidth} height={window.innerHeight - 32 * 16} /> From 47b6d73cb9baddbe132a844dd43b523dc8034659 Mon Sep 17 00:00:00 2001 From: Thomas Davis Date: Fri, 20 Dec 2024 00:40:22 +1100 Subject: [PATCH 38/38] wip --- apps/registry/app/job-similarity/page.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/registry/app/job-similarity/page.js b/apps/registry/app/job-similarity/page.js index 60f4236..e36bc76 100644 --- a/apps/registry/app/job-similarity/page.js +++ b/apps/registry/app/job-similarity/page.js @@ -846,7 +846,7 @@ const GraphContainer = ({ dataSource, algorithm }) => { highlightLinks.has(link) ? '#ff0000' : '#cccccc' } linkOpacity={0.3} - linkDirectionalParticles={4} + linkDirectionalParticles={0} linkDirectionalParticleWidth={2} onNodeHover={handleNodeHover} onNodeClick={handleNodeClick}