├── .editorconfig ├── .github └── workflows │ ├── build.yml │ ├── deploy-app.yml │ └── deploy-docker.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── Dockerfile ├── LICENSE.md ├── README.md ├── babel.config.js ├── config.ts ├── docker-compose.yml ├── entrypoint.sh ├── eslint.config.mjs ├── i18next-scanner.config.ts ├── lerna.json ├── nginx.conf ├── package-lock.json ├── package.json ├── packages ├── broadcast │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ │ ├── driver.ts │ │ ├── index.ts │ │ └── types.ts │ └── tsconfig.json ├── gephi-lite │ ├── .gitignore │ ├── e2e │ │ ├── load-graph.spec.ts │ │ └── load-graph.spec.ts-snapshots │ │ │ ├── Java-gexf-chromium-linux.png │ │ │ ├── Les-Miserables-gexf-chromium-linux.png │ │ │ ├── Power-Grid-gexf-chromium-linux.png │ │ │ └── airlines-graphml-chromium-linux.png │ ├── index.html │ ├── package.json │ ├── playwright.config.ts │ ├── public │ │ ├── favicon.ico │ │ ├── logo192.png │ │ ├── logo512.png │ │ ├── manifest.json │ │ ├── robots.txt │ │ └── samples │ │ │ ├── Java.gexf │ │ │ ├── Les Miserables.gexf │ │ │ ├── Power Grid.gexf │ │ │ └── airlines.graphml │ ├── src │ │ ├── assets │ │ │ ├── font │ │ │ │ ├── gibson-light-italic-webfont.eot │ │ │ │ ├── gibson-light-italic-webfont.svg │ │ │ │ ├── gibson-light-italic-webfont.ttf │ │ │ │ ├── gibson-light-italic-webfont.woff │ │ │ │ ├── gibson-semibold-webfont.eot │ │ │ │ ├── gibson-semibold-webfont.svg │ │ │ │ ├── gibson-semibold-webfont.ttf │ │ │ │ ├── gibson-semibold-webfont.woff │ │ │ │ ├── gibson-webfont.eot │ │ │ │ ├── gibson-webfont.svg │ │ │ │ ├── gibson-webfont.ttf │ │ │ │ ├── gibson-webfont.woff │ │ │ │ ├── licenses.txt │ │ │ │ ├── mfglabsiconset-webfont.eot │ │ │ │ ├── mfglabsiconset-webfont.svg │ │ │ │ ├── mfglabsiconset-webfont.ttf │ │ │ │ └── mfglabsiconset-webfont.woff │ │ │ └── gephi-logo.svg │ │ ├── components │ │ │ ├── ColorPicker.tsx │ │ │ ├── DropInput.tsx │ │ │ ├── Dropdown.tsx │ │ │ ├── Edge.tsx │ │ │ ├── Error.tsx │ │ │ ├── GraphAppearance │ │ │ │ ├── TransformationMethodPreview.tsx │ │ │ │ ├── TransformationMethodSelect.tsx │ │ │ │ ├── color │ │ │ │ │ ├── ColorFixedEditor.tsx │ │ │ │ │ ├── ColorItem.tsx │ │ │ │ │ ├── ColorPartitionEditor.tsx │ │ │ │ │ ├── ColorPickerTooltip.tsx │ │ │ │ │ ├── ColorRankingEditor.tsx │ │ │ │ │ ├── ShadingColorEditor.tsx │ │ │ │ │ └── utils.ts │ │ │ │ ├── index.tsx │ │ │ │ ├── label │ │ │ │ │ ├── LabelEllipsis.tsx │ │ │ │ │ ├── LabelSizeItem.tsx │ │ │ │ │ └── StringAttrItem.tsx │ │ │ │ ├── size │ │ │ │ │ ├── SizeFixedEditor.tsx │ │ │ │ │ ├── SizeItem.tsx │ │ │ │ │ ├── SizeRankingEditor.tsx │ │ │ │ │ └── utils.ts │ │ │ │ └── zIndex │ │ │ │ │ └── EdgesZIndexItem.tsx │ │ │ ├── GraphCaption │ │ │ │ ├── CaptionItemTitle.tsx │ │ │ │ ├── ColorSlider.tsx │ │ │ │ ├── ItemColorCaption.tsx │ │ │ │ ├── ItemSizeCaption.tsx │ │ │ │ ├── LayoutQualityCaption.tsx │ │ │ │ └── index.tsx │ │ │ ├── GraphFilters │ │ │ │ ├── FilterCreator.tsx │ │ │ │ ├── FilteredGraphSummary.tsx │ │ │ │ ├── RangeFilter.tsx │ │ │ │ ├── ScriptFilter.tsx │ │ │ │ ├── TermsFilter.tsx │ │ │ │ ├── TopologicalFilter.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── utils.ts │ │ │ ├── GraphPartitioning │ │ │ │ ├── GraphPartitioningForm.tsx │ │ │ │ ├── GraphPartitioningStatus.tsx │ │ │ │ └── index.tsx │ │ │ ├── GraphSearch.tsx │ │ │ ├── InfiniteScroll.tsx │ │ │ ├── InformationTooltip.tsx │ │ │ ├── Loader.tsx │ │ │ ├── LocalSwitcher.tsx │ │ │ ├── Matomo.tsx │ │ │ ├── MessageTooltip.tsx │ │ │ ├── Node.tsx │ │ │ ├── Tabs.tsx │ │ │ ├── ThemeSwitcher.tsx │ │ │ ├── Toggle.tsx │ │ │ ├── Tooltip.tsx │ │ │ ├── Transition.tsx │ │ │ ├── common-icons.tsx │ │ │ ├── consts.ts │ │ │ ├── forms │ │ │ │ ├── AttributeSelect.tsx │ │ │ │ ├── GraphMetadataForm.tsx │ │ │ │ ├── GraphModelForm.tsx │ │ │ │ ├── LayoutQualityForm.tsx │ │ │ │ └── TypedInputs.tsx │ │ │ ├── modals.tsx │ │ │ ├── notifications.tsx │ │ │ └── user │ │ │ │ ├── SignInModal.tsx │ │ │ │ └── UserAvatar.tsx │ │ ├── config.ts │ │ ├── core │ │ │ ├── Initialize.tsx │ │ │ ├── Root.tsx │ │ │ ├── appearance │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── broadcast │ │ │ │ ├── actions.ts │ │ │ │ ├── client.ts │ │ │ │ ├── useBroadcast.tsx │ │ │ │ └── utils.ts │ │ │ ├── cloud │ │ │ │ ├── github │ │ │ │ │ ├── GithubLoginModal.tsx │ │ │ │ │ ├── provider.tsx │ │ │ │ │ └── useGithubAuth.tsx │ │ │ │ ├── types.ts │ │ │ │ └── useCloudProvider.ts │ │ │ ├── context │ │ │ │ ├── dataContexts.tsx │ │ │ │ └── uiContext.ts │ │ │ ├── file │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── filters │ │ │ │ ├── index.spec.ts │ │ │ │ ├── index.ts │ │ │ │ ├── topological │ │ │ │ │ ├── ego.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── kCore.ts │ │ │ │ │ └── largestConnectedComponentFilter.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── graph │ │ │ │ ├── dynamicAttributes.ts │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ ├── utils.spec.ts │ │ │ │ └── utils.ts │ │ │ ├── layouts │ │ │ │ ├── collection.ts │ │ │ │ ├── collection │ │ │ │ │ ├── circlePack.ts │ │ │ │ │ ├── circular.ts │ │ │ │ │ ├── force.ts │ │ │ │ │ ├── forceAtlas2.ts │ │ │ │ │ ├── noverlap.ts │ │ │ │ │ ├── random.ts │ │ │ │ │ └── script.ts │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── metrics │ │ │ │ ├── collections.ts │ │ │ │ ├── edges │ │ │ │ │ ├── disparityMetric.ts │ │ │ │ │ ├── edgeScript.ts │ │ │ │ │ └── simmelianStrength.ts │ │ │ │ ├── index.ts │ │ │ │ ├── mixed │ │ │ │ │ └── louvainEdgeAmbiguity.tsx │ │ │ │ ├── nodes │ │ │ │ │ ├── betweennessCentralityMetric.ts │ │ │ │ │ ├── degreeMetric.ts │ │ │ │ │ ├── hitsMetric.ts │ │ │ │ │ ├── louvainMetric.ts │ │ │ │ │ ├── nodeScript.ts │ │ │ │ │ └── pagerankMetric.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── modals │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── notifications │ │ │ │ ├── index.ts │ │ │ │ └── types.ts │ │ │ ├── preferences │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── search │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── selection │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── session │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── sigma │ │ │ │ ├── index.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── types.ts │ │ │ └── user │ │ │ │ ├── AuthInit.tsx │ │ │ │ ├── index.tsx │ │ │ │ └── types.ts │ │ ├── hooks │ │ │ ├── useKeyboardShortcuts.ts │ │ │ └── useTimeout.tsx │ │ ├── index.tsx │ │ ├── locales │ │ │ ├── LOCALES.ts │ │ │ ├── dev.json │ │ │ ├── en.json │ │ │ ├── fa.json │ │ │ ├── fr.json │ │ │ ├── hu.json │ │ │ ├── ko.json │ │ │ ├── provider.tsx │ │ │ ├── pt.json │ │ │ ├── uk.json │ │ │ └── zh-Hans.json │ │ ├── styles │ │ │ ├── _base.scss │ │ │ ├── _dark.scss │ │ │ ├── _filters.scss │ │ │ ├── _forms.scss │ │ │ ├── _graph-caption.scss │ │ │ ├── _graph-page.scss │ │ │ ├── _highlightjs.scss │ │ │ ├── _layout.scss │ │ │ ├── _loader.scss │ │ │ ├── _slider.scss │ │ │ ├── _transition.scss │ │ │ ├── _user.scss │ │ │ ├── _variables-override.scss │ │ │ ├── _variables.scss │ │ │ ├── index.scss │ │ │ ├── mfglabs_iconset.css │ │ │ ├── rc-slider.scss │ │ │ └── react-select.scss │ │ ├── utils │ │ │ ├── bordered-node-program │ │ │ │ ├── index.ts │ │ │ │ ├── program.frag.ts │ │ │ │ └── program.vert.ts │ │ │ ├── check.ts │ │ │ ├── colors.ts │ │ │ ├── date.ts │ │ │ ├── labels.ts │ │ │ ├── promises.ts │ │ │ ├── sigma.ts │ │ │ └── url.tsx │ │ └── views │ │ │ ├── ErrorPage.tsx │ │ │ ├── NotFoundPage.tsx │ │ │ ├── graphPage │ │ │ ├── AppearancePanel.tsx │ │ │ ├── ContextPanel.tsx │ │ │ ├── FilePanel.tsx │ │ │ ├── FiltersPanel.tsx │ │ │ ├── GraphDataPanel.tsx │ │ │ ├── GraphRendering.tsx │ │ │ ├── LayoutsPanel.tsx │ │ │ ├── Selection.tsx │ │ │ ├── StatisticsPanel.tsx │ │ │ ├── UserSettingsPanel.tsx │ │ │ ├── controllers │ │ │ │ ├── AppearanceController.tsx │ │ │ │ ├── EventsController.tsx │ │ │ │ ├── GridController.tsx │ │ │ │ ├── MarqueeController.tsx │ │ │ │ └── SettingsController.tsx │ │ │ ├── index.tsx │ │ │ └── modals │ │ │ │ ├── ConfirmModal.tsx │ │ │ │ ├── FunctionEditorModal.tsx │ │ │ │ ├── WelcomeModal.tsx │ │ │ │ ├── edition │ │ │ │ ├── UpdateEdgeModal.tsx │ │ │ │ └── UpdateNodeModal.tsx │ │ │ │ ├── open │ │ │ │ ├── CloudFileModal.tsx │ │ │ │ ├── LocalFileModal.tsx │ │ │ │ └── RemoteFileModal.tsx │ │ │ │ └── save │ │ │ │ ├── ExportPNGModal.tsx │ │ │ │ └── SaveCloudFileModal.tsx │ │ │ └── layout │ │ │ └── index.tsx │ ├── tsconfig.json │ ├── types │ │ ├── locale-emoji.d.ts │ │ ├── react-tether.d.ts │ │ └── svg.d.ts │ ├── vite-env.d.ts │ ├── vite.config.mts │ └── vitest.config.mts └── sdk │ ├── .gitignore │ ├── .npmignore │ ├── README.md │ ├── package.json │ ├── src │ ├── appearance │ │ ├── index.ts │ │ └── types.ts │ ├── filters │ │ ├── index.ts │ │ └── types.ts │ ├── graph │ │ ├── index.ts │ │ └── types.ts │ ├── index.ts │ └── utils │ │ ├── casting.spec.ts │ │ ├── casting.ts │ │ ├── index.ts │ │ ├── json.spec.ts │ │ └── json.ts │ ├── tsconfig.json │ └── vitest.config.mts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | timeout-minutes: 60 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 22 15 | 16 | - name: Cache node_modules 17 | uses: actions/cache@v4 18 | with: 19 | path: "node_modules" 20 | key: ${{ runner.os }}-modules-${{ hashFiles('package-lock.json') }} 21 | 22 | - name: Check Playwright version 23 | id: playwright-version 24 | run: echo "PLAYWRIGHT_VERSION=$(node -e "console.log(require('./package-lock.json').packages['node_modules/playwright'].version)")" >> $GITHUB_ENV 25 | 26 | - name: Cache Playwright binaries 27 | uses: actions/cache@v4 28 | id: playwright-cache 29 | with: 30 | path: "~/.cache/ms-playwright" 31 | key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} 32 | 33 | - name: Install dependencies 34 | run: npm ci 35 | 36 | - name: Install Playwright Browsers 37 | run: npx playwright install --with-deps 38 | if: steps.playwright-cache.outputs.cache-hit != 'true' 39 | 40 | - name: Build 41 | run: npm run build 42 | env: 43 | NODE_OPTIONS: --max_old_space_size=4096 44 | 45 | - name: Run unit tests 46 | run: npm run test 47 | 48 | - name: Run end-to-end tests 49 | run: npm run test:e2e 50 | 51 | - name: Upload Playwright report 52 | uses: actions/upload-artifact@v4 53 | if: always() 54 | with: 55 | name: playwright-report 56 | path: packages/gephi-lite/playwright-report/ 57 | retention-days: 30 58 | -------------------------------------------------------------------------------- /.github/workflows/deploy-app.yml: -------------------------------------------------------------------------------- 1 | name: deploy-app 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | env: 10 | CI: false 11 | VITE_GITHUB_PROXY: "https://githubapi.gephi.org" 12 | VITE_MATOMO_URL: "https://matomo.ouestware.com" 13 | VITE_MATOMO_SITEID: 32 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Cache node_modules 19 | uses: actions/cache@v4 20 | with: 21 | path: "**/node_modules" 22 | key: ${{ runner.os }}-modules-${{ hashFiles('**/package-lock.json') }} 23 | 24 | - name: Install 25 | run: npm install 26 | 27 | - name: Build 28 | run: npm run build 29 | 30 | - name: Deploy 🚀 31 | uses: JamesIves/github-pages-deploy-action@v4 32 | with: 33 | branch: gh-pages # The branch the action should deploy to. 34 | folder: packages/gephi-lite/build # The folder the action should deploy. 35 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: 7 | 8 | env: 9 | REGISTRY: docker.io 10 | IMAGE_NAME: ouestware/gephi-lite 11 | 12 | jobs: 13 | push_to_registry: 14 | name: Push Docker image to Docker Hub 15 | runs-on: ubuntu-latest 16 | permissions: 17 | packages: write 18 | contents: read 19 | attestations: write 20 | id-token: write 21 | steps: 22 | - name: Check out the repo 23 | uses: actions/checkout@v4 24 | 25 | - name: Install 26 | run: npm install 27 | 28 | - name: Build 29 | run: npm run build 30 | env: 31 | BASE_URL: "./" 32 | 33 | - name: Log in to Docker Hub 34 | uses: docker/login-action@v3 35 | with: 36 | username: ${{ secrets.DOCKER_USERNAME }} 37 | password: ${{ secrets.DOCKER_PASSWORD }} 38 | 39 | - name: Extract metadata (tags, labels) for Docker 40 | id: meta 41 | uses: docker/metadata-action@v5 42 | with: 43 | images: ${{ env.IMAGE_NAME }} 44 | 45 | - name: Build and push Docker image 46 | id: push 47 | uses: docker/build-push-action@v6 48 | with: 49 | context: . 50 | file: ./Dockerfile 51 | push: true 52 | tags: ${{ steps.meta.outputs.tags }} 53 | labels: ${{ steps.meta.outputs.labels }} 54 | 55 | - name: Generate artifact attestation 56 | uses: actions/attest-build-provenance@v2 57 | with: 58 | subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}} 59 | subject-digest: ${{ steps.push.outputs.digest }} 60 | push-to-registry: true 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # IDE's specific 4 | .idea 5 | .vscode 6 | 7 | # dependencies 8 | node_modules 9 | /.pnp 10 | .pnp.js 11 | 12 | # testing 13 | /coverage 14 | 15 | # production 16 | build 17 | 18 | # misc 19 | .DS_Store 20 | .env.local 21 | .env.development.local 22 | .env.test.local 23 | .env.production.local 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | test-results/ 29 | playwright-report/ 30 | playwright/.cache/ 31 | tsconfig.tsbuildinfo 32 | **/__screenshots__ 33 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "all", 3 | "printWidth": 120, 4 | "importOrder": ["", "^[./]"], 5 | "importOrderSeparation": true, 6 | "importOrderSortSpecifiers": true, 7 | "importOrderGroupNamespaceSpecifiers": true, 8 | "plugins": ["@trivago/prettier-plugin-sort-imports"] 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:latest 2 | 3 | COPY ./packages/gephi-lite/build/ /var/www/html/ 4 | COPY ./nginx.conf /etc/nginx/conf.d/default.conf 5 | 6 | EXPOSE 80 -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: ["@babel/preset-env", ["@babel/preset-react", { runtime: "automatic" }], "@babel/preset-typescript"], 3 | }; 4 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | // export BASE_PATH to reuse it in e2e test 2 | export const BASE_URL = process.env.BASE_URL || "/gephi-lite/"; 3 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | gephi-lite: 3 | image: node:lts 4 | user: node 5 | hostname: gephi-lite 6 | volumes: 7 | - .:/gephi-lite 8 | entrypoint: /gephi-lite/entrypoint.sh 9 | ports: 10 | - 5173:5173 11 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | cd /gephi-lite 3 | npm i 4 | npm run start -- -- --host -------------------------------------------------------------------------------- /i18next-scanner.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | debug: true, 4 | // read strings from functions: IllegalMoveError('KEY') or t('KEY') 5 | func: { 6 | list: ["t"], 7 | extensions: [".ts", ".tsx"], 8 | }, 9 | 10 | // Create and update files `en.json`, `fr.json`, `es.json` 11 | lngs: ["dev"], 12 | defaultLng: "dev", 13 | 14 | // Put a blank string as initial translation 15 | // (useful for Weblate be marked as 'not yet translated', see later) 16 | defaultValue: (_lng, _ns, _key) => "", 17 | 18 | // Location of translation files 19 | resource: { 20 | loadPath: "src/locales/{{lng}}.json", 21 | savePath: "src/locales/{{lng}}.json", 22 | jsonIndent: 2, 23 | }, 24 | 25 | nsSeparator: ":", 26 | keySeparator: ".", 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "version": "independent", 4 | "command": { 5 | "version": { 6 | "ignoreChanges": ["*.md"] 7 | }, 8 | "publish": { 9 | "ignorePrivate": true 10 | } 11 | }, 12 | "push": false, 13 | "includeMergedTags": true 14 | } 15 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default_server; 3 | listen [::]:80 default_server; 4 | root /var/www/html; 5 | server_name _; 6 | 7 | location /_github/login { 8 | add_header Access-Control-Allow-Origin "*"; 9 | add_header Access-Control-Allow-Methods "GET, POST, OPTIONS"; 10 | add_header Access-Control-Allow-Headers "Origin, X-Requested-With, Content-Type, Accept, user-agent"; 11 | if ($request_method = OPTIONS) { 12 | return 204; 13 | } 14 | proxy_pass https://github.com/login; 15 | } 16 | 17 | location / { 18 | try_files $uri $uri/ =404; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gephi/gephi-lite-root", 3 | "homepage": "https://github.com/gephi/gephi-lite", 4 | "license": "gpl-3.0", 5 | "bugs": "http://github.com/jacomyal/sigma.js/issues", 6 | "workspaces": [ 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "clean": "npm exec --workspaces -- npx rimraf node_modules && npx rimraf node_modules", 11 | "start": "preconstruct dev && npm run start --workspace=@gephi/gephi-lite", 12 | "test": "npm run test --workspaces --if-present", 13 | "test:e2e": "npm run test:e2e --workspaces --if-present", 14 | "lint": "eslint .", 15 | "build": "preconstruct build && npm run build --workspace=@gephi/gephi-lite", 16 | "postinstall": "preconstruct dev", 17 | "postpublish": "preconstruct dev", 18 | "prepublishOnly": "npm run test && npm run lint && npm run build" 19 | }, 20 | "devDependencies": { 21 | "@playwright/test": "^1.50.1", 22 | "@babel/core": "^7.25.2", 23 | "@babel/preset-env": "^7.25.4", 24 | "@babel/preset-react": "^7.24.7", 25 | "@babel/preset-typescript": "^7.24.7", 26 | "@eslint/compat": "^1.2.5", 27 | "@eslint/eslintrc": "^3.2.0", 28 | "@eslint/js": "^9.18.0", 29 | "@preconstruct/cli": "^2.8.10", 30 | "@trivago/prettier-plugin-sort-imports": "^5.2.2", 31 | "@vitest/browser": "^3.0.5", 32 | "eslint": "^9.19.0", 33 | "eslint-config-prettier": "^10.0.1", 34 | "eslint-plugin-import": "^2.31.0", 35 | "eslint-plugin-prettier": "^5.2.3", 36 | "eslint-plugin-react": "^7.37.4", 37 | "eslint-plugin-react-hooks": "^5.1.0", 38 | "globals": "^15.14.0", 39 | "lerna": "^8.2.0", 40 | "playwright": "^1.50.1", 41 | "prettier": "^3.4.2", 42 | "rimraf": "^6.0.1", 43 | "typescript": "^5.7.3", 44 | "typescript-eslint": "^8.22.0", 45 | "vitest": "^3.0.5" 46 | }, 47 | "preconstruct": { 48 | "packages": [ 49 | "packages/sdk", 50 | "packages/broadcast" 51 | ], 52 | "exports": { 53 | "importConditionDefaultExport": "default" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /packages/broadcast/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /packages/broadcast/.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules 3 | src 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /packages/broadcast/README.md: -------------------------------------------------------------------------------- 1 | # @gephi/gephi-lite-broadcast 2 | 3 | The package [@gephi/gephi-lite-broadcast](https://www.npmjs.com/package/@gephi/gephi-lite-broadcast) is a browser TypeScript library to control a Gephi Lite instance using the [Broadcast Channel API](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API). 4 | 5 | It exports a `Driver` class, that exposes various asynchronous methods to help feed Gephi Lite data, appearance settings, filters... 6 | 7 | Here is a simple example to import a [Graphology](https://graphology.github.io/) graph in Gephi Lite in a new tab: 8 | 9 | ```typescript 10 | import { GephiLiteDriver } from "@gephi/gephi-lite-broadcast"; 11 | import Graph from "graphology"; 12 | 13 | async function openGraphInGephiLite(graph: Graph) { 14 | const driver = new GephiLiteDriver(); 15 | 16 | await new Promise((resolve) => { 17 | // Wait for new instance to be fully working: 18 | driver.on("newInstance", () => { 19 | resolve(); 20 | }); 21 | driver.openGephiLite(); 22 | }); 23 | 24 | await driver.importGraph(graph.toJSON()); 25 | 26 | driver.destroy(); 27 | } 28 | ``` 29 | 30 | ## TODO 31 | 32 | ### 1. Data update/reading: 33 | 34 | - [x] `getGraph(): SerializedFullGraph` / `importGraph(graph: FullGraph)` 35 | - [x] `setGraphDataset` / `getGraphDataset` / `mergeGraphDataset` 36 | - [x] `setGraphAppearance` / `getGraphAppearance` / `mergeGraphAppearance` 37 | - [x] `setFilters` / `getFilters` 38 | - [ ] `setSelection` / `getSelection` 39 | 40 | ### 2. Other methods: 41 | 42 | - [x] `ping` (to check broadcast status) 43 | - [x] `getVersion` 44 | - [ ] `zoomToNodes` / `resetZoom` 45 | - [ ] `computeMetric` 46 | - [ ] `computeLayout` / `startLayout` / `stopLayout` 47 | - [ ] `notify` 48 | - [ ] `exportGraph` 49 | - [ ] methods to handle UI elements (right panel, left tabs, caption, 50 | fullscreen) 51 | 52 | ### 3. Events 53 | 54 | - [x] `instanceCreation` 55 | - [ ] `graphUpdate` 56 | - [ ] `graphModelUpdate` 57 | - [ ] `graphAppearanceUpdate` 58 | - [ ] `filtersUpdate` 59 | - [ ] `selectionUpdate` 60 | -------------------------------------------------------------------------------- /packages/broadcast/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gephi/gephi-lite-broadcast", 3 | "description": "A browser library to control a Gephi Lite instance using the Broadcast Channel API", 4 | "version": "0.6.2", 5 | "main": "dist/gephi-gephi-lite-broadcast.cjs.js", 6 | "module": "dist/gephi-gephi-lite-broadcast.esm.js", 7 | "files": [ 8 | "/dist" 9 | ], 10 | "sideEffects": false, 11 | "homepage": "https://gephi.org/gephi-lite", 12 | "bugs": "http://github.com/jacomyal/sigma.js/issues", 13 | "repository": { 14 | "type": "git", 15 | "url": "http://github.com/jacomyal/sigma.js.git", 16 | "directory": "packages/template" 17 | }, 18 | "dependencies": { 19 | "@gephi/gephi-lite-sdk": "^0.6.2", 20 | "lodash": "^4.17.21", 21 | "uuid": "^11.0.5" 22 | }, 23 | "devDependencies": { 24 | "@types/lodash": "^4.17.15", 25 | "graphology-types": "^0.24.8" 26 | }, 27 | "preconstruct": { 28 | "entrypoints": [ 29 | "index.ts" 30 | ] 31 | }, 32 | "license": "gpl-3.0", 33 | "exports": { 34 | ".": { 35 | "module": "./dist/gephi-gephi-lite-broadcast.esm.js", 36 | "import": "./dist/gephi-gephi-lite-broadcast.cjs.mjs", 37 | "default": "./dist/gephi-gephi-lite-broadcast.cjs.js" 38 | }, 39 | "./package.json": "./package.json" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/broadcast/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./types"; 2 | export * from "./driver"; 3 | -------------------------------------------------------------------------------- /packages/broadcast/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "declaration": true, 20 | "types": ["vitest/globals"] 21 | }, 22 | "include": ["src"] 23 | } 24 | -------------------------------------------------------------------------------- /packages/gephi-lite/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /packages/gephi-lite/e2e/load-graph.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | import { BASE_URL } from "../../../config"; 4 | 5 | const FILES = ["Java.gexf", "Les Miserables.gexf", "Power Grid.gexf", "airlines.graphml"]; 6 | 7 | FILES.forEach((file) => { 8 | test(`Loading '${file}' should work`, async ({ page }) => { 9 | // Load gephi-lite with the given gexf file 10 | await page.goto(`/?file=${BASE_URL}/samples/${file}`); 11 | 12 | // Wait for the graph to be fully loaded 13 | await expect(page).toHaveTitle(`Gephi Lite - ${file}`, { timeout: 30000 }); 14 | 15 | // Check the screenshot 16 | await expect(page).toHaveScreenshot(`${file}.png`, { maxDiffPixelRatio: 0.01 }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/Java-gexf-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/Java-gexf-chromium-linux.png -------------------------------------------------------------------------------- /packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/Les-Miserables-gexf-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/Les-Miserables-gexf-chromium-linux.png -------------------------------------------------------------------------------- /packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/Power-Grid-gexf-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/Power-Grid-gexf-chromium-linux.png -------------------------------------------------------------------------------- /packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/airlines-graphml-chromium-linux.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/e2e/load-graph.spec.ts-snapshots/airlines-graphml-chromium-linux.png -------------------------------------------------------------------------------- /packages/gephi-lite/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 14 | 15 | Gephi Lite 16 | 17 | 18 | 19 |
20 |
21 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /packages/gephi-lite/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // require('dotenv').config(); 8 | 9 | /** 10 | * See https://playwright.dev/docs/test-configuration. 11 | */ 12 | export default defineConfig({ 13 | testDir: "./e2e", 14 | /* Run tests in files in parallel */ 15 | fullyParallel: true, 16 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 17 | forbidOnly: !!process.env.CI, 18 | /* Retry on CI only */ 19 | retries: process.env.CI ? 2 : 0, 20 | /* Opt out of parallel tests on CI. */ 21 | workers: process.env.CI ? 1 : undefined, 22 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 23 | reporter: "html", 24 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 25 | use: { 26 | /* Base URL to use in actions like `await page.goto('/')`. */ 27 | baseURL: process.env.CI ? "http://localhost:4173/gephi-lite/" : "http://127.0.0.1:5173/gephi-lite/", 28 | 29 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 30 | trace: "on-first-retry", 31 | }, 32 | 33 | /* Configure projects for major browsers */ 34 | projects: [ 35 | { 36 | name: "chromium", 37 | use: { ...devices["Desktop Chrome"] }, 38 | }, 39 | ], 40 | 41 | /* Run your local dev server before starting the tests */ 42 | webServer: { 43 | command: "npm run build && npm run serve", 44 | url: process.env.CI ? "http://localhost:4173/gephi-lite/" : "http://127.0.0.1:5173/gephi-lite/", 45 | reuseExistingServer: !process.env.CI, 46 | timeout: 120000, 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /packages/gephi-lite/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/public/favicon.ico -------------------------------------------------------------------------------- /packages/gephi-lite/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/public/logo192.png -------------------------------------------------------------------------------- /packages/gephi-lite/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/public/logo512.png -------------------------------------------------------------------------------- /packages/gephi-lite/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /packages/gephi-lite/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-light-italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-light-italic-webfont.eot -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-light-italic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-light-italic-webfont.ttf -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-light-italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-light-italic-webfont.woff -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-semibold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-semibold-webfont.eot -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-semibold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-semibold-webfont.ttf -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-semibold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-semibold-webfont.woff -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-webfont.eot -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-webfont.ttf -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/gibson-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/gibson-webfont.woff -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/mfglabsiconset-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/mfglabsiconset-webfont.eot -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/mfglabsiconset-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/mfglabsiconset-webfont.ttf -------------------------------------------------------------------------------- /packages/gephi-lite/src/assets/font/mfglabsiconset-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gephi/gephi-lite/f34d9ff41bc5603599704b058cd53b75f9f1fc5f/packages/gephi-lite/src/assets/font/mfglabsiconset-webfont.woff -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/ColorPicker.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useRef } from "react"; 2 | import { SketchPicker } from "react-color"; 3 | import { AiOutlineCheck, AiOutlineClose } from "react-icons/ai"; 4 | 5 | import { hexToRgba, rgbaToHex } from "../utils/colors"; 6 | import Tooltip, { TooltipAPI } from "./Tooltip"; 7 | 8 | const ColorPicker: FC< 9 | ( 10 | | { color: string | undefined; onChange: (color: string | undefined) => void; clearable: true } 11 | | { color: string; onChange: (color: string) => void; clearable?: false } 12 | ) & { className?: string } 13 | > = ({ color, onChange, clearable, className }) => { 14 | const tooltipRef = useRef(null); 15 | 16 | return ( 17 | 18 | 21 |
22 | onChange(rgbaToHex(color.rgb))} 25 | styles={{ 26 | default: { 27 | picker: { 28 | boxShadow: "none", 29 | padding: 0, 30 | }, 31 | }, 32 | }} 33 | /> 34 |
35 | {clearable && ( 36 | 39 | )} 40 | 43 |
44 |
45 |
46 | ); 47 | }; 48 | 49 | export default ColorPicker; 50 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/DropInput.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import React, { FC } from "react"; 3 | import { Accept, useDropzone } from "react-dropzone"; 4 | import { useTranslation } from "react-i18next"; 5 | import { FaTimes } from "react-icons/fa"; 6 | 7 | interface DropInputProperties { 8 | value: File | null; 9 | onChange: (file: File | null) => void; 10 | helpText: string; 11 | accept: Accept; 12 | } 13 | 14 | export const DropInput: FC = ({ value, onChange, accept, helpText }) => { 15 | const { t } = useTranslation(); 16 | const { getRootProps, getInputProps } = useDropzone({ 17 | maxFiles: 1, 18 | accept: accept, 19 | onDrop: (acceptedFiles) => { 20 | const value = acceptedFiles[0] || null; 21 | onChange(value); 22 | }, 23 | }); 24 | 25 | return ( 26 |
27 | 28 | 29 |

{value ? value.name : helpText}

30 | 31 | {value && ( 32 | 43 | )} 44 |
45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Dropdown.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { FC, ReactNode } from "react"; 3 | 4 | import Tooltip from "./Tooltip"; 5 | 6 | export type Option = 7 | | { 8 | type?: "option"; 9 | label: ReactNode; 10 | title?: ReactNode; 11 | onClick: () => void; 12 | disabled?: boolean; 13 | } 14 | | { type: "divider" }; 15 | 16 | const Dropdown: FC<{ children: ReactNode; options: Option[] }> = ({ children: target, options }) => { 17 | return ( 18 | 19 | {target} 20 |
21 | {options.map((option, i) => 22 | option.type === "divider" ? ( 23 |
24 | ) : ( 25 | 34 | ), 35 | )} 36 |
37 | 38 | ); 39 | }; 40 | 41 | export default Dropdown; 42 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Error.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { FC } from "react"; 3 | import { FallbackProps } from "react-error-boundary"; 4 | import { useTranslation } from "react-i18next"; 5 | import { TbFaceIdError } from "react-icons/tb"; 6 | 7 | import { GitHubIcon, RetryIcon } from "./common-icons"; 8 | 9 | function errorToGithubLink(error: unknown): string { 10 | let body = ` 11 | ## Description 12 | 13 | Please provide a description of what you were doing while the error occurs`; 14 | 15 | if (error instanceof Error) { 16 | body = `${body} 17 | 18 | ## Stack trace 19 | 20 | \`\`\` 21 | ${error.message} 22 | ${error.stack} 23 | \`\`\``; 24 | } 25 | return `https://github.com/gephi/gephi-lite/issues/new?labels=bug,prod&body=${encodeURIComponent(body)}`; 26 | } 27 | 28 | export const ErrorComponent: FC = ({ error, resetErrorBoundary }) => { 29 | const { t } = useTranslation("translation"); 30 | 31 | return ( 32 |
33 |
34 |
35 |

36 | 37 | {t("error.title")} 38 |

39 |

{error.message || t("error.unknown")}

40 | 41 |

{t("error.message")}

42 | 43 |
44 | 50 | {t("error.report")} 51 | 52 | 55 |
56 |
57 |
58 |
59 | ); 60 | }; 61 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/TransformationMethodPreview.tsx: -------------------------------------------------------------------------------- 1 | import { range } from "lodash"; 2 | import { FC } from "react"; 3 | 4 | import { TransformationMethod } from "../../core/appearance/types"; 5 | import { makeGetValue } from "../../core/appearance/utils"; 6 | 7 | export const TransformationMethodPreview: FC<{ method?: TransformationMethod }> = ({ method }) => { 8 | const getValue = makeGetValue(method); 9 | const size = 40; 10 | const margin = 2; 11 | return ( 12 | <> 13 | 20 | {/* Example of a polyline with the default fill */} 21 | 28 | `${n},${ 29 | size - 30 | (((getValue(n) || 1) - (getValue(0.1) || 1)) * size) / ((getValue(size) || 1) - (getValue(0.1) || 1)) 31 | }`, 32 | ) 33 | .join(" ")} 34 | /> 35 | 36 | 37 | ); 38 | }; 39 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/TransformationMethodSelect.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { TransformationMethod } from "../../core/appearance/types"; 5 | import { TransformationMethodPreview } from "./TransformationMethodPreview"; 6 | 7 | export const TransformationMethodsSelect: FC<{ 8 | id?: string; 9 | method?: TransformationMethod; 10 | onChange: (name: TransformationMethod | null) => void; 11 | }> = ({ id, method, onChange }) => { 12 | const { t } = useTranslation(); 13 | 14 | return ( 15 | <> 16 | 17 |
18 | 49 | 50 | 51 |
52 | 53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/color/ColorFixedEditor.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { FixedColor } from "../../../core/appearance/types"; 5 | import { ItemType } from "../../../core/types"; 6 | import ColorPicker from "../../ColorPicker"; 7 | 8 | export const ColorFixedEditor: FC<{ 9 | itemType: ItemType; 10 | color: FixedColor; 11 | setColor: (newColor: FixedColor) => void; 12 | }> = ({ itemType, color, setColor }) => { 13 | const { t } = useTranslation(); 14 | 15 | return ( 16 |
17 | setColor({ ...color, value: v })} /> 18 | 21 |
22 | ); 23 | }; 24 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/color/ColorPartitionEditor.tsx: -------------------------------------------------------------------------------- 1 | import { map } from "lodash"; 2 | import { FC, useState } from "react"; 3 | import AnimateHeight from "react-animate-height"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { PartitionColor } from "../../../core/appearance/types"; 7 | import { ItemType } from "../../../core/types"; 8 | import ColorPicker from "../../ColorPicker"; 9 | 10 | export const ColorPartitionEditor: FC<{ 11 | itemType: ItemType; 12 | color: PartitionColor; 13 | setColor: (newColor: PartitionColor) => void; 14 | }> = ({ itemType, color, setColor }) => { 15 | const { t } = useTranslation(); 16 | const [expanded, setExpanded] = useState(false); 17 | 18 | return ( 19 |
20 | 21 | {map(color.colorPalette, (c, value) => ( 22 |
23 | 26 | setColor({ 27 | ...color, 28 | colorPalette: { 29 | ...color.colorPalette, 30 | [value]: v, 31 | }, 32 | }) 33 | } 34 | /> 35 | 36 |
37 | ))} 38 | 39 |
40 | setColor({ ...color, missingColor: v })} /> 41 | 44 |
45 | 46 |
47 | 48 | {!expanded &&
} 49 |
50 | 53 |
54 | 55 |
56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/color/utils.ts: -------------------------------------------------------------------------------- 1 | import iwanthue from "iwanthue"; 2 | import { every } from "lodash"; 3 | 4 | export function isColor(strColor: string): boolean { 5 | const s = new Option().style; 6 | s.color = strColor; 7 | return s.color !== ""; 8 | } 9 | 10 | export function getPalette(values: string[]): Record { 11 | if (every(values, (v) => isColor(v))) { 12 | return values.reduce((iter, v) => ({ ...iter, [v]: v }), {}); 13 | } else { 14 | const palette = iwanthue(values.length); 15 | return values.reduce((iter, v, i) => ({ ...iter, [v]: palette[i] }), {}); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/label/LabelEllipsis.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { useAppearance, useAppearanceActions } from "../../../core/context/dataContexts"; 5 | import { ItemType } from "../../../core/types"; 6 | 7 | export const LabelEllipsis: FC<{ itemType: ItemType }> = ({ itemType }) => { 8 | const { t } = useTranslation(); 9 | const { nodesLabelEllipsis, edgesLabelEllipsis } = useAppearance(); 10 | const { setNodesLabelEllipsisAppearance, setEdgesLabelEllipsisAppearance } = useAppearanceActions(); 11 | 12 | const labelEllipsis = itemType === "nodes" ? nodesLabelEllipsis : edgesLabelEllipsis; 13 | const setLabelEllipsis = itemType === "nodes" ? setNodesLabelEllipsisAppearance : setEdgesLabelEllipsisAppearance; 14 | 15 | return ( 16 |
17 |
18 | setLabelEllipsis({ ...labelEllipsis, enabled: e.target.checked })} 24 | /> 25 | 26 |
27 | 28 | {labelEllipsis.enabled && ( 29 |
30 | setLabelEllipsis({ ...labelEllipsis, maxLength: e.target.valueAsNumber })} 39 | /> 40 | 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/size/SizeFixedEditor.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { FixedSize } from "../../../core/appearance/types"; 5 | import { ItemType } from "../../../core/types"; 6 | 7 | export const SizeFixedEditor: FC<{ 8 | itemType: ItemType; 9 | size: FixedSize; 10 | setSize: (newSize: FixedSize) => void; 11 | }> = ({ itemType, size, setSize }) => { 12 | const { t } = useTranslation(); 13 | const id = `${itemType}-fixedSizeInput`; 14 | 15 | return ( 16 |
17 | setSize({ ...size, value: +v.target.value })} 23 | id={id} 24 | /> 25 | 28 |
29 | ); 30 | }; 31 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/size/SizeRankingEditor.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { RankingSize } from "../../../core/appearance/types"; 5 | import { ItemType } from "../../../core/types"; 6 | import { TransformationMethodsSelect } from "../TransformationMethodSelect"; 7 | 8 | export const SizeRankingEditor: FC<{ 9 | itemType: ItemType; 10 | size: RankingSize; 11 | setSize: (newSize: RankingSize) => void; 12 | }> = ({ itemType, size, setSize }) => { 13 | const { t } = useTranslation(); 14 | const minId = `${itemType}-rankingSizeInput-min`; 15 | const maxId = `${itemType}-rankingSizeInput-max`; 16 | const defaultId = `${itemType}-rankingSizeInput-default`; 17 | 18 | return ( 19 | <> 20 |
21 | setSize({ ...size, minSize: +v.target.value })} 28 | id={minId} 29 | /> 30 | 33 |
34 |
35 | setSize({ ...size, maxSize: +v.target.value })} 41 | id={maxId} 42 | /> 43 | 46 |
47 |
48 | setSize({ ...size, missingSize: +v.target.value })} 54 | id={defaultId} 55 | /> 56 | 59 |
60 |
61 | setSize({ ...size, transformationMethod: method || undefined })} 64 | /> 65 |
66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphAppearance/size/utils.ts: -------------------------------------------------------------------------------- 1 | import { isNumber as _isNumber } from "lodash"; 2 | 3 | export const isNumber = (value: string | number): boolean => { 4 | if (_isNumber(value) || !isNaN(+value)) return true; 5 | return false; 6 | }; 7 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphCaption/CaptionItemTitle.tsx: -------------------------------------------------------------------------------- 1 | import { ItemType } from "@gephi/gephi-lite-sdk"; 2 | import cx from "classnames"; 3 | import { FC } from "react"; 4 | import { useTranslation } from "react-i18next"; 5 | 6 | import { TransformationMethod } from "../../core/appearance/types"; 7 | 8 | const ICON_NAMES = { 9 | color: { 10 | nodes: "icon-node_color", 11 | edges: "icon-node_link_color", 12 | }, 13 | size: { 14 | nodes: "icon-node_size", 15 | edges: "icon-node_link_weight", 16 | }, 17 | }; 18 | 19 | const TransformationMethodLabel: FC<{ field: string; transformationMethod?: TransformationMethod }> = ({ 20 | field, 21 | transformationMethod, 22 | }) => { 23 | const methodLabelProps = { className: "text-muted", style: { fontSize: "0.75em" } }; 24 | if (!transformationMethod) return <>{field}; 25 | if (transformationMethod === "log") 26 | return ( 27 | <> 28 | log( 29 | {field} 30 | ) 31 | 32 | ); 33 | if ("pow" in transformationMethod) { 34 | if (transformationMethod.pow === 0.5) 35 | return ( 36 | <> 37 | √( 38 | {field} 39 | ) 40 | 41 | ); 42 | else 43 | return ( 44 | <> 45 | {field} 46 | {transformationMethod.pow} 47 | 48 | ); 49 | } 50 | return <>{field}; 51 | }; 52 | 53 | export const CaptionItemTitle: FC<{ 54 | itemType: ItemType; 55 | vizVariable: "color" | "size"; 56 | field: string; 57 | transformationMethod?: TransformationMethod; 58 | }> = ({ itemType, field, vizVariable, transformationMethod }) => { 59 | const { t } = useTranslation(); 60 | const label = t(`graph.caption.${vizVariable}`, { 61 | itemType: t(`graph.model.${itemType}`, { count: 2 }) + "", 62 | }).toString(); 63 | 64 | return ( 65 |
66 | 67 |
68 | {label} 69 |
70 | 71 |
72 |
73 |
74 | ); 75 | }; 76 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphCaption/LayoutQualityCaption.tsx: -------------------------------------------------------------------------------- 1 | import { isNaN } from "lodash"; 2 | import { FC, useCallback } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | import { useLayoutState, usePreferences } from "../../core/context/dataContexts"; 6 | import { LayoutsIcon } from "../common-icons"; 7 | 8 | export const LayoutQualityCaption: FC = () => { 9 | const { t } = useTranslation(); 10 | const { quality } = useLayoutState(); 11 | const { locale } = usePreferences(); 12 | const error = quality.metric?.cMax === undefined || isNaN(quality.metric?.cMax); 13 | const formatNumber = useCallback( 14 | (n?: number) => (n ? Math.round(n * 100).toLocaleString(locale, { compactDisplay: "short" }) + "%" : "N/A"), 15 | [locale], 16 | ); 17 | 18 | if (!quality.enabled) return null; 19 | 20 | return ( 21 |
22 |
23 | 24 |
25 | {t("layouts.quality.title")} 26 |
Connected Closeness
27 |
28 |
29 |
30 | {error ? ( 31 | "ERROR" 32 | ) : ( 33 | <> 34 |
35 | {formatNumber(quality.metric?.cMax)} 36 | {t("layouts.quality.caption_CMax")} 37 |
38 | {quality.showGrid && ( 39 |
40 | {t("layouts.quality.caption_deltaMax")} 41 | {quality.metric?.deltaMax 42 | ? quality.metric?.deltaMax.toLocaleString(locale, { 43 | compactDisplay: "short", 44 | maximumFractionDigits: 0, 45 | }) 46 | : "N/A"} 47 |
48 | )} 49 | 50 | )} 51 |
52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphFilters/FilteredGraphSummary.tsx: -------------------------------------------------------------------------------- 1 | import { useReadAtom } from "@ouestware/atoms"; 2 | import { FC } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | import { filteredGraphsAtom } from "../../core/graph"; 6 | 7 | export const FilteredGraphSummary: FC<{ filterIndex: number }> = ({ filterIndex }) => { 8 | const { t } = useTranslation(); 9 | const filteredGraphs = useReadAtom(filteredGraphsAtom); 10 | const relatedGraph = filteredGraphs[filterIndex]?.graph; 11 | 12 | return ( 13 |
14 | {relatedGraph.order} {t("graph.model.nodes", { count: relatedGraph.order })}, {relatedGraph.size}{" "} 15 | {t("graph.model.edges", { count: relatedGraph.size })} 16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphFilters/utils.ts: -------------------------------------------------------------------------------- 1 | // copied from https://gitlab.com/ouestware/retina/-/blob/main/src/utils/number.ts#L3-20 2 | import { inRange, round } from "lodash"; 3 | 4 | export function findRanges(min: number, max: number): { unit: number; ranges: [number, number][] } { 5 | if (max <= min) return { ranges: [[Math.min(min, max), Math.max(min, max)]], unit: Math.abs(max - min) }; 6 | 7 | const ranges: [number, number][] = []; 8 | 9 | const diff = max - min; 10 | const digits = Math.floor(Math.log10(diff)) - 1; 11 | const p = Math.pow(10, digits); 12 | const unit = [0.1, 0.2, 0.5, 1, 2, 5, 10, 20, 50, 100, 1000].map((n) => n * p).find((n) => inRange(diff / n, 5, 15)); 13 | 14 | if (!unit) return { ranges: [[min, max]], unit: max - min }; 15 | 16 | for (let i = Math.floor(min / unit); i <= max / unit; i++) { 17 | ranges.push([round(i * unit, -digits), round((i + 1) * unit, -digits)]); 18 | } 19 | 20 | return { unit, ranges }; 21 | } 22 | export function shortenNumber(n: number, extendSize?: number): string { 23 | if (n === 0) return "0"; 24 | if (n < 0) return "-" + shortenNumber(-n, extendSize); 25 | const suffixes = ["", "k", "m", "b", "t"]; 26 | const suffixNum = Math.floor(Math.log10(extendSize || n) / 3); 27 | const shortValue = suffixNum ? +(n / Math.pow(1000, suffixNum)).toFixed(2) : n; 28 | const label = 29 | suffixes[suffixNum] !== undefined 30 | ? (shortValue % 1 ? shortValue.toFixed(1) : shortValue) + suffixes[suffixNum] 31 | : n.toPrecision(3).replace(/\.?0+$/, ""); 32 | return label; 33 | } 34 | 35 | export function isNumber(v: unknown): boolean { 36 | if (typeof v === "number") return true; 37 | if (typeof v === "string") { 38 | return !isNaN(+v); 39 | } 40 | 41 | return false; 42 | } 43 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphPartitioning/GraphPartitioningForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { ItemType } from "../../core/types"; 5 | import { AttributeSelect } from "../forms/AttributeSelect"; 6 | import { GraphPartitioningStatus } from "./GraphPartitioningStatus"; 7 | 8 | export interface Attribute { 9 | id: string; 10 | qualitative?: boolean; 11 | quantitative?: boolean; 12 | } 13 | 14 | export const GraphPartitioningForm: FC<{ 15 | itemType: ItemType; 16 | partitionAttributeId: string | undefined; 17 | setPartitionAttributeId: (nodeAttId: string | undefined) => void; 18 | closeForm: () => void; 19 | }> = ({ itemType, partitionAttributeId, setPartitionAttributeId, closeForm }) => { 20 | const { t } = useTranslation(); 21 | const [newPartAttId, setNewPartAttId] = useState(); 22 | 23 | return ( 24 |
{ 26 | e.preventDefault(); 27 | }} 28 | > 29 |
30 | 35 | !!a.qualitative && a.id !== partitionAttributeId} 39 | onChange={setNewPartAttId} 40 | /> 41 | {newPartAttId && } 42 |
43 | 52 | 63 |
64 |
65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/GraphPartitioning/GraphPartitioningStatus.tsx: -------------------------------------------------------------------------------- 1 | import { t } from "i18next"; 2 | import { FC } from "react"; 3 | import { IoWarning } from "react-icons/io5"; 4 | 5 | import { ItemType } from "../../core/types"; 6 | 7 | // TODO: MOVE TO CONTEXT 8 | type AttributeValueStatistics = Record; 9 | const nodeAttributesIndex: AttributeValueStatistics = { 10 | att_dual: { nbValues: 23, nbMissingValues: 0 }, 11 | att_quali: { nbValues: 3, nbMissingValues: 100 }, 12 | }; 13 | const edgeAttributesIndex: AttributeValueStatistics = { 14 | weight: { nbValues: 23, nbMissingValues: 0 }, 15 | type: { nbValues: 2, nbMissingValues: 3 }, 16 | }; 17 | 18 | export const GraphPartitioningStatus: FC<{ 19 | itemType: ItemType; 20 | partitionAttributeId: string; 21 | preview?: boolean; 22 | }> = ({ itemType, partitionAttributeId, preview }) => { 23 | const attributeStats = 24 | itemType === "nodes" ? nodeAttributesIndex[partitionAttributeId] : edgeAttributesIndex[partitionAttributeId]; 25 | return ( 26 |
27 |
28 | {t("graph.partitioning.status_item", { 29 | context: preview && "preview", 30 | items: itemType === "nodes" ? t("graph.model.nodes") : t("graph.model.edges"), 31 | })}{" "} 32 | {t("graph.partitioning.status_partition", { 33 | nbPartitions: attributeStats?.nbValues + (attributeStats?.nbMissingValues !== 0 ? 1 : 0), 34 | attribute: partitionAttributeId, 35 | })} 36 |
37 | {attributeStats?.nbMissingValues !== 0 && ( 38 |
39 | {" "} 40 | {t("graph.partitioning.warning_nbMissing", { 41 | nbMissingValues: attributeStats?.nbMissingValues, 42 | items: itemType === "nodes" ? t("graph.model.nodes") : t("graph.model.edges"), 43 | })} 44 |
45 | {t("graph.partitioning.missing_partition", { context: preview && "preview" })} 46 |
47 | )} 48 |
49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/InfiniteScroll.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useMemo, useState } from "react"; 2 | import InfiniteScrollComponent from "react-infinite-scroll-component"; 3 | 4 | import { LoaderFill } from "./Loader"; 5 | 6 | const DEFAULT_PAGE_SIZE = 50; 7 | 8 | interface InfiniteScrollProps { 9 | data: T[]; 10 | renderItem: (data: T) => ReactNode; 11 | pageSize?: number; 12 | scrollableTarget?: ReactNode; 13 | } 14 | 15 | export function InfiniteScroll({ data, renderItem, pageSize, scrollableTarget }: InfiniteScrollProps) { 16 | const [itemNumber, setItemNumber] = useState(pageSize || DEFAULT_PAGE_SIZE); 17 | 18 | const next = () => setItemNumber((prev) => prev + (pageSize || DEFAULT_PAGE_SIZE)); 19 | 20 | const visibleItems = useMemo( 21 | () => 22 | data.slice(0, itemNumber).map((d) => { 23 | return renderItem(d); 24 | }), 25 | [renderItem, data, itemNumber], 26 | ); 27 | 28 | return ( 29 | } 34 | next={next} 35 | > 36 | {visibleItems} 37 | 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/InformationTooltip.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | import { IoInformationCircleOutline } from "react-icons/io5"; 3 | 4 | import Tooltip from "./Tooltip"; 5 | 6 | export const InformationTooltip: FC = ({ children }) => { 7 | return ( 8 | 14 | 15 |
16 | {children} 17 |
18 |
19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Loader.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { CSSProperties, FC } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | export const Spinner: FC<{ className?: string; style?: CSSProperties }> = ({ className, style }) => { 6 | const { t } = useTranslation(); 7 | return ( 8 |
9 | {t("common.loading").toString()}... 10 |
11 | ); 12 | }; 13 | 14 | /** 15 | * Display a loader that takes the full screen size. 16 | */ 17 | export const Loader: FC = () => ( 18 |
19 | 20 |
21 | ); 22 | 23 | /** 24 | * Display a loader that takes the size of its parent container. 25 | */ 26 | export const LoaderFill: FC = () => ( 27 |
28 | 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/LocalSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import localeEmoji from "locale-emoji"; 2 | import { toPairs } from "lodash"; 3 | import { FC, ReactNode } from "react"; 4 | import { HiMiniLanguage } from "react-icons/hi2"; 5 | 6 | import { usePreferences, usePreferencesActions } from "../core/context/dataContexts"; 7 | import { LOCALES } from "../locales/LOCALES"; 8 | import Tooltip from "./Tooltip"; 9 | 10 | const DEFAULT_FLAG = ; 11 | 12 | function getIcon(locale: string): ReactNode { 13 | return locale === "dev" ? DEFAULT_FLAG : localeEmoji(locale) || DEFAULT_FLAG; 14 | } 15 | const AVAILABLE_LOCALES = toPairs(LOCALES) 16 | .filter(([key]) => import.meta.env.MODE === "development" || key !== "dev") 17 | .map(([key, locale]) => ({ 18 | value: key, 19 | label: ( 20 | <> 21 | {getIcon(key)} {locale.label} 22 | 23 | ), 24 | })); 25 | 26 | const LocalSwitcher: FC = () => { 27 | const { locale } = usePreferences(); 28 | const { changeLocale } = usePreferencesActions(); 29 | return ( 30 | 31 | 32 |
33 | {AVAILABLE_LOCALES.map((option, i) => ( 34 | 43 | ))} 44 |
45 |
46 | ); 47 | }; 48 | 49 | export default LocalSwitcher; 50 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Matomo.tsx: -------------------------------------------------------------------------------- 1 | import { MatomoProvider as MatomoProviderOrginal, createInstance, useMatomo } from "@datapunt/matomo-tracker-react"; 2 | import { MatomoInstance } from "@datapunt/matomo-tracker-react/lib/types"; 3 | import { FC, PropsWithChildren, useEffect, useMemo } from "react"; 4 | import { useLocation } from "react-router"; 5 | 6 | import { config } from "../config"; 7 | 8 | /** 9 | * Fix definition of MatomoProvider to allow children 10 | */ 11 | type MatomoProviderFixedType = FC>; 12 | const MatomoProviderFixed: MatomoProviderFixedType = MatomoProviderOrginal as MatomoProviderFixedType; 13 | 14 | /** 15 | * Component to track page changes 16 | */ 17 | export const MatomoTracker: FC = () => { 18 | const { trackPageView } = useMatomo(); 19 | const location = useLocation(); 20 | 21 | useEffect(() => { 22 | trackPageView({}); 23 | }, [trackPageView, location]); 24 | 25 | return null; 26 | }; 27 | 28 | export const MatomoProvider: FC> = ({ children }) => { 29 | const instance = useMemo(() => { 30 | if (config.matomo.urlBase) return createInstance(config.matomo); 31 | return null; 32 | }, []); 33 | 34 | return ( 35 | <> 36 | {instance ? ( 37 | 38 | {children} 39 | 40 | 41 | ) : ( 42 | <>{children} 43 | )} 44 | 45 | ); 46 | }; 47 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/MessageTooltip.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import React, { FC, ReactNode, useEffect, useRef, useState } from "react"; 3 | import { IconType } from "react-icons"; 4 | import { AiFillWarning, AiOutlineCheckCircle, AiOutlineInfoCircle } from "react-icons/ai"; 5 | 6 | import Tooltip, { TooltipAPI } from "./Tooltip"; 7 | 8 | const DEFAULT_ICONS = { 9 | success: AiOutlineCheckCircle, 10 | info: AiOutlineInfoCircle, 11 | warning: AiFillWarning, 12 | error: AiFillWarning, 13 | } as const; 14 | type MessageType = keyof typeof DEFAULT_ICONS; 15 | 16 | const MessageTooltip: FC<{ 17 | message: ReactNode; 18 | type?: MessageType; 19 | icon?: IconType; 20 | openOnMount?: number; 21 | className?: string; 22 | iconClassName?: string; 23 | }> = ({ message, type = "info", icon: IconComponent = DEFAULT_ICONS[type], openOnMount, className, iconClassName }) => { 24 | const tooltipRef = useRef(null); 25 | const [timeout, setTimeout] = useState(null); 26 | 27 | useEffect(() => { 28 | let timeoutID: number | undefined; 29 | if (tooltipRef.current && openOnMount && !tooltipRef.current.isOpened()) { 30 | tooltipRef.current.open(); 31 | timeoutID = window.setTimeout(() => { 32 | tooltipRef.current?.close(); 33 | }, openOnMount); 34 | 35 | setTimeout(timeoutID); 36 | } 37 | 38 | return () => { 39 | if (timeoutID) window.clearTimeout(timeoutID); 40 | }; 41 | // eslint-disable-next-line react-hooks/exhaustive-deps 42 | }, []); 43 | 44 | return ( 45 | 46 | 55 |
{ 59 | if (timeout) window.clearTimeout(timeout); 60 | }} 61 | > 62 |
{message}
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default MessageTooltip; 69 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Node.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { FC, ReactNode, useMemo } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | import { getItemAttributes } from "../core/appearance/utils"; 6 | import { useDynamicItemData, useFilteredGraph, useGraphDataset, useVisualGetters } from "../core/context/dataContexts"; 7 | import { mergeStaticDynamicData } from "../core/graph/dynamicAttributes"; 8 | 9 | export const NodeComponent: FC<{ label: ReactNode; color: string; hidden?: boolean }> = ({ label, color, hidden }) => { 10 | const { t } = useTranslation(); 11 | return ( 12 |
13 | 14 | 15 | {label || t("selection.node_no_label")} 16 | 17 |
18 | ); 19 | }; 20 | 21 | export const NodeComponentById: FC<{ id: string }> = ({ id }) => { 22 | const graphDataset = useGraphDataset(); 23 | const dynamicItemData = useDynamicItemData(); 24 | const visualGetters = useVisualGetters(); 25 | const filteredGraph = useFilteredGraph(); 26 | 27 | const data = useMemo( 28 | () => 29 | getItemAttributes( 30 | "nodes", 31 | id, 32 | filteredGraph, 33 | mergeStaticDynamicData(graphDataset.nodeData, dynamicItemData.dynamicNodeData)[id], 34 | graphDataset, 35 | visualGetters, 36 | ), 37 | [id, graphDataset, visualGetters, dynamicItemData, filteredGraph], 38 | ); 39 | 40 | return ; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import classNames from "classnames"; 2 | import { chunk } from "lodash"; 3 | import React, { ComponentType, FC, ReactNode, useState } from "react"; 4 | 5 | export interface Tab { 6 | value: string; 7 | content: ComponentType; 8 | } 9 | 10 | export const Tabs: FC<{ children: [ReactNode, ReactNode, ...ReactNode[]] }> = ({ children }) => { 11 | const tabs = chunk(children, 2) as [ReactNode, ReactNode][]; 12 | 13 | if (children.length % 2) throw new Error("Tabs: This component should have an even number of children."); 14 | 15 | const [currentTabIndex, setCurrentTabIndex] = useState(0); 16 | 17 | return ( 18 |
19 |
    20 | {tabs.map((tab, i) => ( 21 |
  • 22 | 28 |
  • 29 | ))} 30 |
31 | {tabs[currentTabIndex][1]} 32 |
33 | ); 34 | }; 35 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/ThemeSwitcher.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { MdContrast, MdDarkMode, MdLightMode } from "react-icons/md"; 3 | 4 | import { usePreferences, usePreferencesActions } from "../core/context/dataContexts"; 5 | import { Preferences } from "../core/preferences/types"; 6 | import Tooltip from "./Tooltip"; 7 | 8 | export const ThemeSwicther: FC = () => { 9 | const { theme } = usePreferences(); 10 | const { changeTheme } = usePreferencesActions(); 11 | return ( 12 | 13 | 18 |
19 | {(["auto", "light", "dark"] as Preferences["theme"][]).map((theme) => ( 20 | 37 | ))} 38 |
39 |
40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Toggle.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { FC, Fragment } from "react"; 3 | 4 | /** 5 | * This toggle button displays and controls a boolean value, with two "left" and 6 | * "right" labels: 7 | * - `true` is right 8 | * - `false` is left 9 | */ 10 | export const Toggle: FC<{ 11 | value: boolean; 12 | onChange: (newValue: boolean) => void; 13 | leftLabel: JSX.Element | string; 14 | rightLabel: JSX.Element | string; 15 | className?: string; 16 | disabled?: boolean; 17 | }> = ({ value, onChange, leftLabel, rightLabel, className, disabled }) => { 18 | return ( 19 |
20 | 29 | 30 | onChange(e.target.checked)} 36 | disabled={disabled} 37 | /> 38 | 39 | 48 |
49 | ); 50 | }; 51 | 52 | export function ToggleBar(props: { 53 | value: T; 54 | options: Array<{ label: JSX.Element | string; value: T }>; 55 | onChange: (value: T) => void; 56 | className?: string; 57 | disabled?: boolean; 58 | }) { 59 | const { value, onChange, options, className, disabled } = props; 60 | return ( 61 |
    62 | {options.map((option) => ( 63 | 64 |
  • 65 | 72 |
  • 73 |
    74 | ))} 75 |
76 | ); 77 | } 78 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/Transition.tsx: -------------------------------------------------------------------------------- 1 | import { Property } from "csstype"; 2 | import React, { PropsWithChildren, forwardRef, useEffect, useState } from "react"; 3 | 4 | const Transition = forwardRef< 5 | HTMLDivElement, 6 | PropsWithChildren<{ show: unknown; mountTransition?: Property.Animation; unmountTransition?: Property.Animation }> 7 | >(({ children, show, mountTransition, unmountTransition }, ref) => { 8 | const [shouldRender, setRender] = useState(show); 9 | 10 | useEffect(() => { 11 | if (show) setRender(true); 12 | else if (!show && !unmountTransition) setRender(false); 13 | }, [show, unmountTransition]); 14 | 15 | return show || shouldRender ? ( 16 |
{ 20 | if (!show) setRender(false); 21 | }} 22 | > 23 | {children} 24 |
25 | ) : null; 26 | }); 27 | 28 | export default Transition; 29 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/common-icons.tsx: -------------------------------------------------------------------------------- 1 | import { IconType } from "react-icons"; 2 | import { BiNetworkChart } from "react-icons/bi"; 3 | import { BsCircle, BsCodeSlash, BsFillPlayFill, BsGithub, BsPalette, BsSearch, BsSlashLg } from "react-icons/bs"; 4 | import { BsExclamationTriangle } from "react-icons/bs"; 5 | import { GrOverview, GrScorecard } from "react-icons/gr"; 6 | import { ImFilesEmpty } from "react-icons/im"; 7 | import { IoSettingsOutline } from "react-icons/io5"; 8 | import { MdClose, MdLogin, MdLogout, MdOutlineRefresh, MdOutlineSaveAlt } from "react-icons/md"; 9 | import { RiFilterFill } from "react-icons/ri"; 10 | import { TbCircles } from "react-icons/tb"; 11 | 12 | import { ItemType } from "../core/types"; 13 | 14 | export const GraphIcon = BiNetworkChart; 15 | export const StatisticsIcon = GrScorecard; 16 | export const AppearanceIcon = BsPalette; 17 | export const FiltersIcon = RiFilterFill; 18 | export const LayoutsIcon = TbCircles; 19 | export const FileIcon = ImFilesEmpty; 20 | export const GitHubIcon = BsGithub; 21 | export const SingInIcon = MdLogin; 22 | export const SignOutIcon = MdLogout; 23 | export const ContextIcon = GrOverview; 24 | export const RetryIcon = MdOutlineRefresh; 25 | export const SettingsIcon = IoSettingsOutline; 26 | export const DangerIcon = BsExclamationTriangle; 27 | 28 | export const NodeIcon = BsCircle; 29 | export const EdgeIcon = BsSlashLg; 30 | export const ItemIcons: Record = { 31 | nodes: NodeIcon, 32 | edges: EdgeIcon, 33 | }; 34 | 35 | export const CodeEditorIcon = BsCodeSlash; 36 | export const SaveIcon = MdOutlineSaveAlt; 37 | export const RunIcon = BsFillPlayFill; 38 | export const CloseIcon = MdClose; 39 | export const SearchIcon = BsSearch; 40 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/consts.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_SELECT_PROPS = { 2 | classNamePrefix: "react-select", 3 | }; 4 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/forms/AttributeSelect.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | 3 | import { ItemType } from "../../core/types"; 4 | import { Attribute } from "../GraphPartitioning/GraphPartitioningForm"; 5 | 6 | type AttributeSelectProps = { 7 | id?: string; 8 | itemType: ItemType; 9 | attributeId: string | undefined; 10 | onChange: (v: string | undefined) => void; 11 | attributesFilter?: (a: Attribute) => boolean; 12 | disabled?: boolean; 13 | defaultToFirstAttribute?: boolean; 14 | emptyOptionLabel?: string; 15 | }; 16 | 17 | export const AttributeSelect: FC = ({ 18 | id, 19 | attributeId, 20 | onChange, 21 | itemType, 22 | attributesFilter = () => true, 23 | disabled, 24 | defaultToFirstAttribute, 25 | emptyOptionLabel, 26 | }) => { 27 | //TODO: replace by core.model types once done 28 | 29 | const nodeAttributes: Attribute[] = [ 30 | { id: "att_dual", qualitative: true, quantitative: true }, 31 | { id: "att_quanti", qualitative: false, quantitative: true }, 32 | { id: "att_quali", qualitative: true, quantitative: false }, 33 | { id: "att_color", qualitative: true, quantitative: false }, 34 | { id: "att_partial_fail_color", qualitative: true, quantitative: false }, 35 | ]; 36 | const edgeAttributes: Attribute[] = [ 37 | { id: "weight", qualitative: false, quantitative: true }, 38 | { id: "type", qualitative: true, quantitative: false }, 39 | ]; 40 | const attributes = (itemType === "nodes" ? nodeAttributes : edgeAttributes).filter(attributesFilter); 41 | 42 | useEffect(() => { 43 | if (defaultToFirstAttribute && !attributeId) onChange(attributes[0]?.id); 44 | }, [defaultToFirstAttribute, attributeId, onChange, attributes]); 45 | return ( 46 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/forms/LayoutQualityForm.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { useLayoutActions } from "../../core/context/dataContexts"; 5 | import { layoutStateAtom } from "../../core/layouts"; 6 | 7 | export const LayoutQualityForm: FC = () => { 8 | const { t } = useTranslation(); 9 | const { quality } = layoutStateAtom.get(); 10 | const { setQuality } = useLayoutActions(); 11 | 12 | return ( 13 |
14 | {t("layouts.quality.title")} 15 |

16 | {t("layouts.quality.description")}{" "} 17 | 18 | (Jacomy 2023) 19 | 20 |

21 | 22 |
23 | setQuality({ ...quality, enabled: e.target.checked })} 29 | /> 30 | 31 |
32 |
33 | setQuality({ ...quality, showGrid: e.target.checked })} 39 | /> 40 | 41 |
42 |
43 | ); 44 | }; 45 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/user/SignInModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useEffect } from "react"; 2 | 3 | import { GithubLoginModal } from "../../core/cloud/github/GithubLoginModal"; 4 | import { useModal } from "../../core/modals"; 5 | 6 | /** 7 | * For now this modal is just a redirect to the one for github. 8 | * But in a near feature, we will have other cloud provider, 9 | * so we need a modal to choose the cloud provider 10 | */ 11 | export const SignInModal: FC = () => { 12 | const { openModal } = useModal(); 13 | 14 | useEffect(() => { 15 | openModal({ component: GithubLoginModal, arguments: {} }); 16 | }, [openModal]); 17 | 18 | return null; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/components/user/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import cx from "classnames"; 2 | import { CSSProperties, FC } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | import { FaUser } from "react-icons/fa"; 5 | 6 | import { useConnectedUser } from "../../core/user"; 7 | 8 | export const UserAvatar: FC<{ className?: string; style?: CSSProperties }> = ({ className, style }) => { 9 | const [user] = useConnectedUser(); 10 | const { t } = useTranslation(); 11 | 12 | return ( 13 |
14 | {user && user.avatar ? ( 15 | <> 16 | {t("user.avatar_alt", 17 | {user.provider && ( 18 | 19 | {user.provider.icon} 20 | 21 | )} 22 | 23 | ) : ( 24 | 25 | )} 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/config.ts: -------------------------------------------------------------------------------- 1 | import { version } from "../package.json"; 2 | 3 | export const config = { 4 | version, 5 | website_url: "https://github.com/gephi/gephi-lite#readme", 6 | notificationTimeoutMs: 3000, 7 | github_proxy: import.meta.env.VITE_GITHUB_PROXY || "/_github", 8 | github: { 9 | client_id: "938f561199e6e55c739b", 10 | scopes: ["gist"], 11 | }, 12 | matomo: { 13 | urlBase: import.meta.env.VITE_MATOMO_URL, 14 | siteId: import.meta.env.VITE_MATOMO_SITEID || 0, 15 | heartBeat: { 16 | active: true, 17 | seconds: 15, 18 | }, 19 | configurations: { 20 | disableCookies: true, 21 | setSecureCookie: true, 22 | setRequestMethod: "POST", 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/Root.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo } from "react"; 2 | import { ErrorBoundary } from "react-error-boundary"; 3 | import { HashRouter, Route, Routes } from "react-router-dom"; 4 | 5 | import { ErrorComponent } from "../components/Error"; 6 | import { MatomoProvider } from "../components/Matomo"; 7 | import { NotFoundPage } from "../views/NotFoundPage"; 8 | import { GraphPage } from "../views/graphPage"; 9 | import { Initialize } from "./Initialize"; 10 | import { AtomsContextsRoot } from "./context/dataContexts"; 11 | import { UIContext, emptyUIContext } from "./context/uiContext"; 12 | 13 | export const Root: FC = () => { 14 | const portalTarget = useMemo(() => document.getElementById("portal-target") as HTMLDivElement, []); 15 | 16 | return ( 17 | { 20 | // Reset the state of your app so the error doesn't happen again 21 | console.debug(details); 22 | }} 23 | > 24 | 25 | 26 | 32 | 33 | 34 | 35 | } /> 36 | 37 | {/* Error pages: */} 38 | } /> 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/appearance/types.ts: -------------------------------------------------------------------------------- 1 | export { 2 | type DataSize, 3 | type FixedSize, 4 | type TransformationMethod, 5 | type RankingSize, 6 | type Size, 7 | type DataColor, 8 | type SourceNodeColor, 9 | type TargetNodeColor, 10 | type FixedColor, 11 | type ColorScalePointType, 12 | type RankingColor, 13 | type PartitionColor, 14 | type ShadingColor, 15 | type Color, 16 | type EdgeColor, 17 | type NoStringAttr, 18 | type DataStringAttr, 19 | type FixedStringAttr, 20 | type FieldStringAttr, 21 | type StringAttr, 22 | type BaseLabelSize, 23 | type FixedLabelSize, 24 | type ItemLabelSize, 25 | type LabelSize, 26 | type LabelEllipsis, 27 | type BooleanAppearance, 28 | type ZIndexFieldAttr, 29 | type ZIndexAttr, 30 | type AppearanceState, 31 | type NumberGetter, 32 | type ColorGetter, 33 | type StringAttrGetter, 34 | type VisualGetters, 35 | type CustomNodeDisplayData, 36 | type CustomEdgeDisplayData, 37 | } from "@gephi/gephi-lite-sdk"; 38 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/broadcast/actions.ts: -------------------------------------------------------------------------------- 1 | import { Producer, asyncAction, producerToAction } from "@ouestware/atoms"; 2 | import Graph from "graphology"; 3 | import { SerializedGraph } from "graphology-types"; 4 | 5 | import { appearanceAtom } from "../appearance"; 6 | import { AppearanceState } from "../appearance/types"; 7 | import { resetStates } from "../context/dataContexts"; 8 | import { fileAtom } from "../file"; 9 | import { graphDatasetActions } from "../graph"; 10 | import { initializeGraphDataset } from "../graph/utils"; 11 | import { resetCamera } from "../sigma"; 12 | 13 | /** 14 | * Actions: 15 | * ******** 16 | */ 17 | // TODO: this code has a lot in commons with core/file/index.ts/open 18 | // we should mutialize it 19 | const importGraph = asyncAction(async (data: SerializedGraph, title?: string) => { 20 | if (fileAtom.get().status.type === "loading") throw new Error("A file is already being loaded"); 21 | fileAtom.set((prev) => ({ ...prev, status: { type: "loading" } })); 22 | try { 23 | const graph = Graph.from(data); 24 | if (title) graph.setAttribute("title", title); 25 | 26 | const { setGraphDataset } = graphDatasetActions; 27 | resetStates(false); 28 | setGraphDataset({ ...initializeGraphDataset(graph) }); 29 | resetCamera({ forceRefresh: true }); 30 | } catch (e) { 31 | fileAtom.set((prev) => ({ ...prev, status: { type: "error", message: (e as Error).message } })); 32 | throw e; 33 | } finally { 34 | fileAtom.set((prev) => ({ ...prev, status: { type: "idle" } })); 35 | } 36 | }); 37 | 38 | const updateAppearance: Producer]> = (newState) => { 39 | return (state) => ({ ...state, ...newState }); 40 | }; 41 | 42 | export const broadcastActions = { 43 | importGraph, 44 | updateAppearance: producerToAction(updateAppearance, appearanceAtom), 45 | }; 46 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/broadcast/useBroadcast.tsx: -------------------------------------------------------------------------------- 1 | import { GephiLiteEvents } from "@gephi/gephi-lite-broadcast"; 2 | import { useEffect, useState } from "react"; 3 | 4 | import { useNotifications } from "../notifications"; 5 | import { BroadcastClient } from "./client"; 6 | 7 | export function useBroadcast(broadcastID?: string | null) { 8 | const { notify } = useNotifications(); 9 | const [client, setClient] = useState(null); 10 | 11 | /** 12 | * Broadcast client lifecycle: 13 | */ 14 | useEffect(() => { 15 | if (!broadcastID) return; 16 | 17 | const client = new BroadcastClient(broadcastID); 18 | client.broadcastEvent("newInstance"); 19 | setClient(client); 20 | 21 | return () => { 22 | client.destroy(); 23 | setClient(null); 24 | }; 25 | }, [broadcastID, notify]); 26 | 27 | /** 28 | * Events: 29 | */ 30 | useEffect(() => { 31 | if (!client) return; 32 | 33 | const _handlers: Omit = { 34 | // TODO 35 | }; 36 | // TODO: Bind handlers 37 | 38 | return () => { 39 | // TODO: Unbind handlers 40 | }; 41 | }); 42 | } 43 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/broadcast/utils.ts: -------------------------------------------------------------------------------- 1 | import { GephiLiteDriver } from "@gephi/gephi-lite-broadcast"; 2 | import { AppearanceState, FiltersState } from "@gephi/gephi-lite-sdk"; 3 | 4 | import { appearanceAtom } from "../appearance"; 5 | import { filtersAtom } from "../filters"; 6 | import { graphDatasetAtom } from "../graph"; 7 | import { GraphDataset } from "../graph/types"; 8 | 9 | export async function openInNewTab({ 10 | dataset = graphDatasetAtom.get(), 11 | appearance = appearanceAtom.get(), 12 | filters = filtersAtom.get(), 13 | }: { dataset?: GraphDataset; appearance?: AppearanceState; filters?: FiltersState } = {}) { 14 | const driver = new GephiLiteDriver(); 15 | 16 | await new Promise((resolve) => { 17 | // Wait for new instance to be fully working: 18 | driver.on("newInstance", () => { 19 | resolve(); 20 | }); 21 | driver.openGephiLite({ 22 | baseUrl: location.pathname, 23 | }); 24 | }); 25 | 26 | await Promise.all([driver.setAppearance(appearance), driver.setFilters(filters)]); 27 | await driver.setGraphDataset(dataset); 28 | 29 | driver.destroy(); 30 | } 31 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/cloud/types.ts: -------------------------------------------------------------------------------- 1 | import { AbstractFile } from "../file/types"; 2 | 3 | export interface CloudFile extends AbstractFile { 4 | type: "cloud"; 5 | id: string; 6 | description?: string; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | isPublic: boolean; 10 | size: number; 11 | webUrl?: string; 12 | } 13 | 14 | export interface CloudProvider { 15 | /** 16 | * type is use for TS but also displayed on the application 17 | */ 18 | type: string; 19 | /** 20 | * Icon is use for TS but also displayed on the application 21 | */ 22 | icon: JSX.Element; 23 | 24 | /** 25 | * Make a call to the provider to find files 26 | */ 27 | getFiles(skip?: number, limit?: number): Promise>>; 28 | 29 | /** 30 | * Make a call to retrieve the cloudfile by its id. 31 | */ 32 | getFile(id: string): Promise | null>; 33 | 34 | /** 35 | * Make a call to retrieve the content of the file. 36 | */ 37 | getFileContent(id: string): Promise; 38 | 39 | /** 40 | * Create a file. 41 | */ 42 | createFile( 43 | file: Pick, 44 | content: string, 45 | ): Promise; 46 | 47 | /** 48 | * Save/Update a file. 49 | */ 50 | saveFile(file: CloudFile, content: string): Promise; 51 | 52 | /** 53 | * Delete a file. 54 | */ 55 | deleteFile(file: CloudFile): Promise; 56 | 57 | /** 58 | * Serialize the cloud provider. 59 | * It is used when we save the user (with the provider) in the localstorage 60 | */ 61 | serialize(): string; 62 | } 63 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/context/uiContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | /** 4 | * Type definition of the context 5 | */ 6 | export interface UIContextType { 7 | portalTarget: HTMLDivElement; 8 | } 9 | 10 | export const emptyUIContext: UIContextType = { 11 | portalTarget: document.createElement("div"), 12 | }; 13 | 14 | /** 15 | * UI context 16 | */ 17 | export const UIContext = createContext(emptyUIContext); 18 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/file/types.ts: -------------------------------------------------------------------------------- 1 | import { AppearanceState } from "@gephi/gephi-lite-sdk"; 2 | 3 | import { CloudFile } from "../cloud/types"; 4 | import { FiltersState } from "../filters/types"; 5 | import { GraphDataset } from "../graph/types"; 6 | 7 | /** 8 | * A serializable structure, to allow Gephi Lite to load and save graphs, with their surrounding context. 9 | * This includes: 10 | * - The full graph dataset 11 | * - The filters state 12 | * - The appearance state 13 | */ 14 | export type GephiLiteFileFormat = { 15 | type: "gephi-lite"; 16 | version: string; 17 | graphDataset: GraphDataset; 18 | filters: FiltersState; 19 | appearance: AppearanceState; 20 | }; 21 | 22 | export type FileFormat = "gexf" | "gephi-lite" | "graphology" | "graphml"; 23 | export const fileFormatExt: Record = { 24 | gexf: "gexf", 25 | "gephi-lite": "json", 26 | graphology: "json", 27 | graphml: "graphml", 28 | }; 29 | 30 | export interface AbstractFile { 31 | type: "local" | "remote" | "cloud"; 32 | format: FileFormat; 33 | filename: string; 34 | } 35 | export interface RemoteFile extends AbstractFile { 36 | type: "remote"; 37 | url: string; 38 | } 39 | export interface LocalFile extends AbstractFile { 40 | type: "local"; 41 | updatedAt: Date; 42 | size: number; 43 | source: File; 44 | } 45 | 46 | export type FileType = CloudFile | RemoteFile | LocalFile; 47 | export type FileTypeWithoutFormat = Omit | Omit | Omit; 48 | 49 | export type FileState = { 50 | current: FileType | null; 51 | recentFiles: Array; 52 | status: { type: "idle" } | { type: "loading" } | { type: "error"; message?: string }; 53 | }; 54 | 55 | export type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue }; 56 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/filters/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { deleteCurrentFilter } from "./index"; 4 | import { getEmptyFiltersState } from "./utils"; 5 | 6 | describe("Filters producers", () => { 7 | describe("#deleteCurrentFilter", () => { 8 | it("should throw when there is no filter to delete", () => { 9 | expect(() => { 10 | deleteCurrentFilter()(getEmptyFiltersState()); 11 | }).toThrow(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/filters/topological/index.ts: -------------------------------------------------------------------------------- 1 | import { TopologicalFilterDefinition } from "../types"; 2 | import { buildEgoFilterDefinition } from "./ego"; 3 | import { buildKCoreFilterDefinition } from "./kCore"; 4 | import { buildLargestConnectedComponentFilterDefinition } from "./largestConnectedComponentFilter"; 5 | 6 | export const buildTopologicalFiltersDefinitions: (directed: boolean) => TopologicalFilterDefinition[] = (directed) => [ 7 | buildKCoreFilterDefinition(), 8 | buildLargestConnectedComponentFilterDefinition(), 9 | buildEgoFilterDefinition(directed), 10 | ]; 11 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/filters/topological/kCore.ts: -------------------------------------------------------------------------------- 1 | import { kCore } from "graphology-cores"; 2 | import { t } from "i18next"; 3 | 4 | import { FilterNumberParameter, TopologicalFilterDefinition } from "../types"; 5 | 6 | export const buildKCoreFilterDefinition = (): TopologicalFilterDefinition<[FilterNumberParameter]> => ({ 7 | type: "topological", 8 | id: "kCore", 9 | label: t("filters.topology.kCore.label"), 10 | summary: ([core]) => t("filters.topology.kCore.summary", { core: core }), 11 | parameters: [ 12 | { 13 | id: "core", 14 | type: "number", 15 | label: t("filters.topology.kCore.core"), 16 | required: true, 17 | defaultValue: 2, 18 | min: 1, 19 | }, 20 | ], 21 | filter([core], graph) { 22 | return kCore(graph, core); 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/filters/topological/largestConnectedComponentFilter.ts: -------------------------------------------------------------------------------- 1 | import { connectedComponents } from "graphology-components"; 2 | import { subgraph } from "graphology-operators"; 3 | import { t } from "i18next"; 4 | import { flatten, sortBy } from "lodash"; 5 | 6 | import { FilterNumberParameter, TopologicalFilterDefinition } from "../types"; 7 | 8 | export const buildLargestConnectedComponentFilterDefinition = (): TopologicalFilterDefinition< 9 | [FilterNumberParameter] 10 | > => ({ 11 | type: "topological", 12 | id: "largestConnectedComponent", 13 | label: t("filters.topology.largestConnectedComponent.label"), 14 | summary: ([numberOfComponents]) => 15 | t("filters.topology.largestConnectedComponent.summary", { number: numberOfComponents }), 16 | parameters: [ 17 | { 18 | id: "numberOfComponents", 19 | type: "number", 20 | label: t("filters.topology.largestConnectedComponent.number"), 21 | required: true, 22 | defaultValue: 1, 23 | min: 1, 24 | }, 25 | ], 26 | filter([numberOfComponents], graph) { 27 | const components = connectedComponents(graph); 28 | const componentstoKeep = sortBy(components, (c) => -1 * c.length).slice(0, numberOfComponents); 29 | 30 | return subgraph(graph, flatten(componentstoKeep)); 31 | }, 32 | }); 33 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/filters/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import { DatalessGraph } from "../graph/types"; 4 | 5 | export { 6 | type BaseFilter, 7 | type RangeFilterType, 8 | type TermsFilterType, 9 | type ScriptFilterType, 10 | type TopologicalFilterType, 11 | type FilterType, 12 | type FiltersState, 13 | type FilteredGraph, 14 | } from "@gephi/gephi-lite-sdk"; 15 | 16 | /** 17 | * Topological filters definitions 18 | * ******************************* 19 | */ 20 | interface BaseFilterParameter { 21 | id: string; 22 | type: string; 23 | label: string; 24 | required: boolean; 25 | defaultValue?: unknown; 26 | hidden?: boolean; 27 | } 28 | 29 | export interface FilterBooleanParameter extends BaseFilterParameter { 30 | type: "boolean"; 31 | defaultValue: boolean; 32 | } 33 | 34 | export interface FilterNumberParameter extends BaseFilterParameter { 35 | type: "number"; 36 | min?: number; 37 | max?: number; 38 | step?: number; 39 | defaultValue: number; 40 | } 41 | 42 | export interface FilterEnumParameter extends BaseFilterParameter { 43 | type: "enum"; 44 | options: { value: E; label: string }[]; 45 | defaultValue: E; 46 | } 47 | 48 | export interface FilterNodeParameter extends BaseFilterParameter { 49 | type: "node"; 50 | } 51 | 52 | export type FilterParameter = 53 | | FilterBooleanParameter 54 | | FilterNumberParameter 55 | | FilterEnumParameter 56 | | FilterNodeParameter; 57 | 58 | export type FilterParameterValueArray

= { 59 | [I in keyof P]: P[I]["defaultValue"]; 60 | }; 61 | 62 | export interface TopologicalFilterDefinition { 63 | type: "topological"; 64 | id: string; 65 | label: string; 66 | parameters: ParametersType; 67 | summary: (parameters: FilterParameterValueArray) => ReactNode; 68 | filter: (parameters: FilterParameterValueArray, graph: DatalessGraph) => DatalessGraph; 69 | } 70 | 71 | // largest connected components 72 | // arguments number 73 | 74 | // connected components by size 75 | // arguments minimumsize number 76 | 77 | // degree 78 | // k-core 79 | // ego-network 80 | // shortest path 81 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/filters/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { filterValue } from "./utils"; 4 | 5 | describe("Filters utilities", () => { 6 | describe("#filterValue", () => { 7 | it("should work as expected with ranges", () => { 8 | expect(filterValue(150, { type: "range", min: 100, max: 200 })).toBe(true); 9 | expect(filterValue(50, { type: "range", min: 100, max: 200 })).toBe(false); 10 | expect(filterValue(250, { type: "range", min: 100, max: 200 })).toBe(false); 11 | 12 | expect(filterValue(150, { type: "range", min: 100 })).toBe(true); 13 | expect(filterValue(50, { type: "range", min: 100 })).toBe(false); 14 | expect(filterValue(250, { type: "range", min: 100 })).toBe(true); 15 | 16 | expect(filterValue(150, { type: "range", max: 200 })).toBe(true); 17 | expect(filterValue(50, { type: "range", max: 200 })).toBe(true); 18 | expect(filterValue(250, { type: "range", max: 200 })).toBe(false); 19 | 20 | expect(filterValue("150", { type: "range", min: 100, max: 200 })).toBe(true); 21 | expect(filterValue("50", { type: "range", min: 100, max: 200 })).toBe(false); 22 | expect(filterValue("250", { type: "range", min: 100, max: 200 })).toBe(false); 23 | 24 | expect(filterValue(null, { type: "range", min: 100, max: 200 })).toBe(false); 25 | expect(filterValue(null, { type: "range", min: 100, max: 200, keepMissingValues: true })).toBe(true); 26 | }); 27 | 28 | it("should work as expected with terms", () => { 29 | const terms = new Set(["toto", "tata", "tutu"]); 30 | 31 | expect(filterValue("toto", { type: "terms", terms })).toBe(true); 32 | expect(filterValue("tonton", { type: "terms", terms })).toBe(false); 33 | 34 | expect(filterValue(null, { type: "terms", terms })).toBe(false); 35 | expect(filterValue(null, { type: "terms", terms, keepMissingValues: true })).toBe(true); 36 | }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/graph/dynamicAttributes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DynamicItemsDataSpec, 3 | ItemData, 4 | ItemDataField, 5 | ItemType, 6 | Scalar, 7 | StaticDynamicItemData, 8 | } from "@gephi/gephi-lite-sdk"; 9 | import { fromPairs, mapValues } from "lodash"; 10 | 11 | import { DatalessGraph } from "./types"; 12 | 13 | /** 14 | * Dynamic attributes are recomputed at every graph topology change. 15 | * Do not add heavy or random-based one in dynamic attribute! 16 | */ 17 | export const dynamicAttributes: DynamicItemsDataSpec = { 18 | nodes: [ 19 | { 20 | field: { id: "degree", itemType: "nodes", qualitative: null, quantitative: { unit: null }, dynamic: true }, 21 | compute: (nodeId: string, graph: DatalessGraph) => { 22 | return graph.degree(nodeId); 23 | }, 24 | }, 25 | ], 26 | edges: [], 27 | }; 28 | 29 | export const computeAllDynamicAttributes = (itemType: ItemType, graph: DatalessGraph) => { 30 | const itemData = fromPairs( 31 | graph[itemType]().map((n) => { 32 | return [ 33 | n, // keyby itemid 34 | dynamicAttributes[itemType].reduce( 35 | (result, dan) => { 36 | return { ...result, [dan.field.id]: dan.compute(n, graph) }; 37 | }, 38 | {} as Record, 39 | ), // reduce sepcs to an object {id: value} 40 | ]; 41 | }), 42 | ); 43 | return itemData; 44 | }; 45 | 46 | export const mergeStaticDynamicData = ( 47 | staticData: Record, 48 | dynamicData: Record, 49 | ): Record => { 50 | return mapValues(staticData, (staticItemData, id) => ({ static: staticItemData, dynamic: dynamicData[id] || {} })); 51 | }; 52 | 53 | export const staticDynamicAttributeKey = (field: ItemDataField) => 54 | `${field.dynamic ? "dynamic" : "static"}.${field.field}`; 55 | 56 | export const staticDynamicAttributeLabel = (field: ItemDataField) => 57 | `${field.field} ${field.dynamic ? " (dynamic)" : ""}`; 58 | 59 | export const getFieldValue = (data: StaticDynamicItemData, field: ItemDataField) => { 60 | return field.dynamic ? data.dynamic[field.field] : data.static[field.field]; 61 | }; 62 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/graph/types.ts: -------------------------------------------------------------------------------- 1 | import { EdgeRenderingData, NodeRenderingData } from "@gephi/gephi-lite-sdk"; 2 | import { Attributes } from "graphology-types"; 3 | import Sigma from "sigma"; 4 | 5 | export { 6 | type DataGraph, 7 | type DatalessGraph, 8 | type DynamicItemData, 9 | type EdgeRenderingData, 10 | type FieldModel, 11 | type FieldModelWithStats, 12 | type FullGraph, 13 | type GraphDataset, 14 | type GraphMetadata, 15 | type ItemData, 16 | type NodeRenderingData, 17 | type SigmaGraph, 18 | } from "@gephi/gephi-lite-sdk"; 19 | 20 | export type GephiLiteSigma = Sigma; 21 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/graph/utils.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { guessSeparator, inferFieldType } from "./utils"; 4 | 5 | describe("Graph utilities", () => { 6 | describe("#inferFieldType", () => { 7 | it("should properly handle a list of differing numbers", () => { 8 | expect(inferFieldType([123, -123, 456, 789, Infinity], 5)).toEqual({ 9 | quantitative: { unit: null }, 10 | qualitative: null, 11 | }); 12 | }); 13 | 14 | it("should properly handle a list of differing strings", () => { 15 | expect(inferFieldType(["Alexis", "Benoit", "Paul", "Guillaume", "Mathieu"], 5)).toEqual({ 16 | quantitative: null, 17 | qualitative: null, 18 | }); 19 | }); 20 | 21 | it("should properly handle a list of repeating strings", () => { 22 | expect(inferFieldType(["Nantes", "Nantes", "Nantes", "Paris", "Paris"], 5)).toEqual({ 23 | quantitative: null, 24 | qualitative: { separator: null }, 25 | }); 26 | }); 27 | 28 | it("should properly handle a list of repeating numbers", () => { 29 | expect(inferFieldType([44, 44, 44, 75, 75], 5)).toEqual({ 30 | quantitative: { unit: null }, 31 | qualitative: { separator: null }, 32 | }); 33 | }); 34 | 35 | it("should properly detect separators", () => { 36 | expect( 37 | inferFieldType(["TypeScript", "Neo4J,TypeScript", "Python,TypeScript", "TypeScript,Python", "TypeScript"], 5), 38 | ).toEqual({ 39 | quantitative: null, 40 | qualitative: { separator: "," }, 41 | }); 42 | }); 43 | }); 44 | 45 | describe("#guessSeparator", () => { 46 | it("should properly detect classic detectors", () => { 47 | expect( 48 | guessSeparator([ 49 | "TypeScript", 50 | "Neo4J,TypeScript", 51 | "Python,TypeScript", 52 | "TypeScript,Python", 53 | "TypeScript", 54 | "TypeScript", 55 | ]), 56 | ).toEqual(","); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/layouts/collection.ts: -------------------------------------------------------------------------------- 1 | import { CirclePackLayout } from "./collection/circlePack"; 2 | import { CircularLayout } from "./collection/circular"; 3 | import { ForceLayout } from "./collection/force"; 4 | import { ForceAtlas2Layout } from "./collection/forceAtlas2"; 5 | import { NOverlapLayout } from "./collection/noverlap"; 6 | import { RandomLayout } from "./collection/random"; 7 | import { ScriptLayout } from "./collection/script"; 8 | import { Layout } from "./types"; 9 | 10 | /** 11 | * List of available layouts 12 | */ 13 | export const LAYOUTS: Array = [ 14 | RandomLayout, 15 | CircularLayout, 16 | CirclePackLayout, 17 | ForceAtlas2Layout, 18 | ForceLayout, 19 | NOverlapLayout, 20 | ScriptLayout, 21 | ]; 22 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/layouts/collection/circlePack.ts: -------------------------------------------------------------------------------- 1 | import Graph from "graphology"; 2 | import circlepack from "graphology-layout/circlepack"; 3 | 4 | import { SyncLayout } from "../types"; 5 | 6 | export const CirclePackLayout = { 7 | id: "circlePack", 8 | type: "sync", 9 | description: true, 10 | parameters: [ 11 | { 12 | id: "groupingField", 13 | type: "attribute", 14 | itemType: "nodes", 15 | required: false, 16 | }, 17 | { 18 | id: "center", 19 | type: "number", 20 | description: true, 21 | defaultValue: 0.5, 22 | step: 0.1, 23 | }, 24 | { 25 | id: "scale", 26 | type: "number", 27 | description: true, 28 | defaultValue: 1, 29 | }, 30 | ], 31 | run(graph: Graph, options) { 32 | const { groupingField, center, scale } = options?.settings || {}; 33 | 34 | return circlepack(graph, { 35 | center, 36 | scale, 37 | hierarchyAttributes: groupingField ? [groupingField] : [], 38 | }); 39 | }, 40 | } as SyncLayout<{ scale?: number; groupingField?: string; center?: number }>; 41 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/layouts/collection/circular.ts: -------------------------------------------------------------------------------- 1 | import circular, { CircularLayoutOptions } from "graphology-layout/circular"; 2 | 3 | import { LayoutMapping, SyncLayout } from "../types"; 4 | 5 | export const CircularLayout = { 6 | id: "circular", 7 | type: "sync", 8 | description: true, 9 | parameters: [ 10 | { 11 | id: "center", 12 | type: "number", 13 | description: true, 14 | defaultValue: 0, 15 | step: 1, 16 | }, 17 | { 18 | id: "scale", 19 | type: "number", 20 | description: true, 21 | defaultValue: 1000, 22 | }, 23 | ], 24 | run: (graph, options) => circular(graph, options?.settings) as unknown as LayoutMapping, 25 | } as SyncLayout; 26 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/layouts/collection/force.ts: -------------------------------------------------------------------------------- 1 | import ForceSupervisor, { ForceLayoutSupervisorParameters } from "graphology-layout-force/worker"; 2 | 3 | import { WorkerLayout } from "../types"; 4 | 5 | export const ForceLayout = { 6 | id: "force", 7 | type: "worker", 8 | supervisor: ForceSupervisor, 9 | parameters: [ 10 | { id: "attraction", type: "number", description: true, defaultValue: 0.0005, min: 0, step: 0.0001 }, 11 | { id: "repulsion", type: "number", description: true, defaultValue: 0.1, min: 0, step: 0.1 }, 12 | { id: "gravity", type: "number", description: true, defaultValue: 0.0001, min: 0, step: 0.0001 }, 13 | { id: "inertia", type: "number", description: true, defaultValue: 0.6, min: 0, max: 1, step: 0.1 }, 14 | { id: "maxMove", type: "number", description: true, defaultValue: 200 }, 15 | ], 16 | } as WorkerLayout; 17 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/layouts/collection/noverlap.ts: -------------------------------------------------------------------------------- 1 | import NoverlapLayout, { NoverlapLayoutSupervisorParameters } from "graphology-layout-noverlap/worker"; 2 | 3 | import { WorkerLayout } from "../types"; 4 | 5 | export const NOverlapLayout = { 6 | id: "noverlap", 7 | type: "worker", 8 | description: true, 9 | supervisor: NoverlapLayout, 10 | parameters: [ 11 | { id: "gridSize", type: "number", description: true, defaultValue: 20 }, 12 | { id: "margin", type: "number", description: true, defaultValue: 5 }, 13 | { id: "expansion", type: "number", description: true, defaultValue: 1.1, step: 0.1 }, 14 | { id: "ratio", type: "number", description: true, defaultValue: 1 }, 15 | { id: "speed", type: "number", description: true, defaultValue: 3 }, 16 | ], 17 | } as WorkerLayout; 18 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/layouts/collection/random.ts: -------------------------------------------------------------------------------- 1 | import random, { RandomLayoutOptions } from "graphology-layout/random"; 2 | 3 | import { LayoutMapping, SyncLayout } from "../types"; 4 | 5 | export const RandomLayout = { 6 | id: "random", 7 | type: "sync", 8 | description: true, 9 | parameters: [ 10 | { 11 | id: "center", 12 | type: "number", 13 | description: true, 14 | defaultValue: 0.5, 15 | }, 16 | { 17 | id: "scale", 18 | type: "number", 19 | description: true, 20 | defaultValue: 1000, 21 | }, 22 | ], 23 | run: (graph, options) => random(graph, options?.settings) as unknown as LayoutMapping, 24 | } as SyncLayout; 25 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/collections.ts: -------------------------------------------------------------------------------- 1 | import { disparityMetric } from "./edges/disparityMetric"; 2 | import { edgeScript } from "./edges/edgeScript"; 3 | import { simmelianStrengthMetric } from "./edges/simmelianStrength"; 4 | import { louvainEdgeAmbiguity } from "./mixed/louvainEdgeAmbiguity"; 5 | import { betweennessCentralityMetric } from "./nodes/betweennessCentralityMetric"; 6 | import { degreeMetric } from "./nodes/degreeMetric"; 7 | import { hitsMetric } from "./nodes/hitsMetric"; 8 | import { louvainMetric } from "./nodes/louvainMetric"; 9 | import { nodeScript } from "./nodes/nodeScript"; 10 | import { pageRankMetric } from "./nodes/pagerankMetric"; 11 | import { Metric } from "./types"; 12 | 13 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 14 | export const NODE_METRICS: Metric<{ nodes: any }>[] = [ 15 | louvainMetric, 16 | pageRankMetric, 17 | betweennessCentralityMetric, 18 | degreeMetric, 19 | hitsMetric, 20 | nodeScript, 21 | ]; 22 | 23 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 24 | export const EDGE_METRICS: Metric<{ edges: any }>[] = [disparityMetric, simmelianStrengthMetric, edgeScript]; 25 | 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | export const MIXED_METRICS: Metric<{ edges: any; nodes: any }>[] = [louvainEdgeAmbiguity]; 28 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/edges/disparityMetric.ts: -------------------------------------------------------------------------------- 1 | import { disparity } from "graphology-metrics/edge"; 2 | import { toSimple } from "graphology-operators"; 3 | 4 | import { EdgeRenderingData, FullGraph } from "../../graph/types"; 5 | import { Metric } from "../types"; 6 | import { quantitativeOnly } from "../utils"; 7 | 8 | export const disparityMetric: Metric<{ edges: ["disparity"] }> = { 9 | id: "disparity", 10 | outputs: { edges: { disparity: quantitativeOnly } }, 11 | parameters: [ 12 | { 13 | id: "getEdgeWeight", 14 | type: "attribute", 15 | itemType: "edges", 16 | restriction: "quantitative", 17 | }, 18 | ], 19 | fn( 20 | parameters: { 21 | getEdgeWeight?: keyof EdgeRenderingData; 22 | }, 23 | graph: FullGraph, 24 | ) { 25 | return { edges: { disparity: disparity(toSimple(graph), parameters) } }; 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/edges/edgeScript.ts: -------------------------------------------------------------------------------- 1 | import { isScalar } from "../../../utils/check"; 2 | import { graphDatasetAtom } from "../../graph"; 3 | import { FullGraph } from "../../graph/types"; 4 | import { dataGraphToFullGraph } from "../../graph/utils"; 5 | import { Scalar } from "../../types"; 6 | import { Metric, MetricScriptFunction } from "../types"; 7 | 8 | // Definition of a custom metric function for edges 9 | const edgeMetricCustomFn = new Function(`return ( 10 | function edgeMetric(id, attributes, index, graph) { 11 | // Your code goes here 12 | return Math.random(); 13 | } 14 | )`)(); 15 | 16 | export const edgeScript: Metric<{ edges: ["custom"] }> = { 17 | id: "edgescript", 18 | outputs: { edges: { custom: undefined } }, 19 | parameters: [ 20 | { 21 | id: "script", 22 | type: "script", 23 | functionJsDoc: `/** 24 | * Function that return the metric value for the specified edge. 25 | * 26 | * @param {string} id The ID of the edge 27 | * @param {Object.} attributes Attributes of the node 28 | * @param {number} index The index position of the node in the graph 29 | * @param {Graph} graph The graphology instance (documentation: https://graphology.github.io/) 30 | * @returns number|string The computed metric of the edge 31 | */`, 32 | functionCheck: (fn) => { 33 | if (!fn) throw new Error("Function is not defined"); 34 | const fullGraph = dataGraphToFullGraph(graphDatasetAtom.get()); 35 | const id = fullGraph.edges()[0]; 36 | const attributes = fullGraph.getEdgeAttributes(id); 37 | const result = fn(id, attributes, 0, fullGraph); 38 | if (!isScalar(result)) 39 | throw new Error("Function must returns a number, a string, a boolean, null or undefined"); 40 | }, 41 | defaultValue: edgeMetricCustomFn, 42 | }, 43 | ], 44 | fn(parameters: { script?: MetricScriptFunction }, graph: FullGraph) { 45 | const fn = parameters.script; 46 | if (fn) { 47 | // we copy the graph to avoid user to modify it 48 | const graphCopy = graph.copy(); 49 | Object.freeze(graphCopy); 50 | 51 | const custom: Record = {}; 52 | graph.edges().forEach((id, index) => { 53 | custom[id] = fn(id, graph.getEdgeAttributes(id), index, graphCopy); 54 | }); 55 | return { edges: { custom } }; 56 | } 57 | return { 58 | edges: { 59 | custom: {}, 60 | }, 61 | }; 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/edges/simmelianStrength.ts: -------------------------------------------------------------------------------- 1 | import { simmelianStrength } from "graphology-metrics/edge"; 2 | 3 | import { FullGraph } from "../../graph/types"; 4 | import { Metric } from "../types"; 5 | import { quantitativeOnly } from "../utils"; 6 | 7 | export const simmelianStrengthMetric: Metric<{ edges: ["simmelianStrength"] }> = { 8 | id: "simmelianStrength", 9 | outputs: { edges: { simmelianStrength: quantitativeOnly } }, 10 | parameters: [], 11 | fn(_parameters: unknown, graph: FullGraph) { 12 | return { edges: { simmelianStrength: simmelianStrength(graph) } }; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/index.ts: -------------------------------------------------------------------------------- 1 | import { DatalessGraph, FieldModel, GraphDataset } from "../graph/types"; 2 | import { dataGraphToFullGraph, inferFieldType } from "../graph/utils"; 3 | import { ItemType } from "../types"; 4 | import { Metric, MetricReport } from "./types"; 5 | 6 | /** 7 | * Compute a metric and mutate the graph dataset state directly for better performance. 8 | * 9 | * @param metric metric object to apply 10 | * @param params metric params from metric form 11 | * @param attributeNames attribute⋅s where the result will be stored 12 | * @param filteredGraph 13 | * @param dataset 14 | * @returns 15 | */ 16 | export function computeMetric( 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | metric: Metric, 19 | params: Record, 20 | attributeNames: Record, 21 | filteredGraph: DatalessGraph, 22 | dataset: GraphDataset, 23 | ): { fieldModels: FieldModel[]; report: MetricReport } { 24 | // get the full filtered graph 25 | const graph = dataGraphToFullGraph(dataset, filteredGraph); 26 | 27 | const scores = metric.fn(params, graph); 28 | const report = {}; // TODO 29 | const updatedFieldModels: FieldModel[] = []; 30 | for (const key in metric.outputs) { 31 | const itemType = key as ItemType; 32 | const itemsCount = itemType === "nodes" ? dataset.fullGraph.order : dataset.fullGraph.size; 33 | const dataKey = itemType === "nodes" ? "nodeData" : "edgeData"; 34 | const data = dataset[dataKey]; 35 | 36 | for (const score in scores[itemType]) { 37 | const values = scores[itemType][score]; 38 | const attributeName = attributeNames[score]; 39 | 40 | if (!attributeName) throw new Error("missing_attribute_name"); 41 | 42 | // Update item values: 43 | for (const itemId in values) { 44 | data[itemId][attributeName] = values[itemId]; 45 | } 46 | 47 | // Update field model: 48 | let qualiQuanti = metric.outputs[itemType][score]; 49 | if (qualiQuanti === undefined) qualiQuanti = inferFieldType(Object.values(values), itemsCount); 50 | 51 | updatedFieldModels.push({ 52 | itemType, 53 | id: attributeName, 54 | ...qualiQuanti, 55 | }); 56 | } 57 | } 58 | 59 | return { 60 | report, 61 | fieldModels: updatedFieldModels, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/nodes/betweennessCentralityMetric.ts: -------------------------------------------------------------------------------- 1 | import betweennessCentrality from "graphology-metrics/centrality/betweenness"; 2 | 3 | import { EdgeRenderingData, FullGraph } from "../../graph/types"; 4 | import { Metric } from "../types"; 5 | import { quantitativeOnly } from "../utils"; 6 | 7 | export const betweennessCentralityMetric: Metric<{ nodes: ["betweennessCentrality"] }> = { 8 | id: "betweennessCentrality", 9 | outputs: { 10 | nodes: { 11 | betweennessCentrality: quantitativeOnly, 12 | }, 13 | }, 14 | parameters: [ 15 | { 16 | id: "getEdgeWeight", 17 | type: "attribute", 18 | itemType: "edges", 19 | restriction: "quantitative", 20 | }, 21 | { 22 | id: "normalize", 23 | type: "boolean", 24 | defaultValue: true, 25 | }, 26 | ], 27 | fn( 28 | parameters: { 29 | getEdgeWeight?: keyof EdgeRenderingData; 30 | normalize?: boolean; 31 | }, 32 | graph: FullGraph, 33 | ) { 34 | return { 35 | nodes: { 36 | betweennessCentrality: betweennessCentrality(graph, { 37 | ...parameters, 38 | getEdgeWeight: parameters.getEdgeWeight || null, 39 | normalized: parameters.normalize, 40 | }), 41 | }, 42 | }; 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/nodes/degreeMetric.ts: -------------------------------------------------------------------------------- 1 | import { toNumber } from "lodash"; 2 | 3 | import { EdgeRenderingData, FullGraph } from "../../graph/types"; 4 | import { Metric } from "../types"; 5 | import { quantitativeOnly } from "../utils"; 6 | 7 | export const degreeMetric: Metric<{ nodes: ["degree"] }> = { 8 | id: "degree", 9 | description: true, 10 | outputs: { 11 | nodes: { 12 | degree: quantitativeOnly, 13 | }, 14 | }, 15 | parameters: [ 16 | { 17 | id: "kind", 18 | type: "enum", 19 | values: [{ id: "degree" }, { id: "inDegree" }, { id: "outDegree" }], 20 | defaultValue: "degree", 21 | }, 22 | { 23 | id: "getEdgeWeight", 24 | type: "attribute", 25 | itemType: "edges", 26 | restriction: "quantitative", 27 | }, 28 | ], 29 | fn( 30 | { 31 | kind, 32 | getEdgeWeight, 33 | }: { 34 | kind?: "degree" | "inDegree" | "outDegree"; 35 | getEdgeWeight?: keyof EdgeRenderingData; 36 | }, 37 | graph: FullGraph, 38 | ) { 39 | const collection: Record = {}; 40 | 41 | if (getEdgeWeight) { 42 | const reduceEdges = ( 43 | kind === "inDegree" ? graph.reduceInEdges : kind === "outDegree" ? graph.reduceOutEdges : graph.reduceEdges 44 | ).bind(graph); 45 | graph.forEachNode((node) => { 46 | collection[node] = reduceEdges(node, (acc, _edge, attr) => acc + (toNumber(attr[getEdgeWeight]) || 0), 0); 47 | }); 48 | } else { 49 | const getDegree = graph[kind || "degree"].bind(graph); 50 | graph.forEachNode((node) => { 51 | collection[node] = getDegree(node); 52 | }); 53 | } 54 | 55 | return { 56 | nodes: { 57 | degree: collection, 58 | }, 59 | }; 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/nodes/hitsMetric.ts: -------------------------------------------------------------------------------- 1 | import { hits } from "graphology-metrics/centrality"; 2 | import { toSimple } from "graphology-operators"; 3 | 4 | import { EdgeRenderingData, FullGraph } from "../../graph/types"; 5 | import { Metric } from "../types"; 6 | import { quantitativeOnly } from "../utils"; 7 | 8 | export const hitsMetric: Metric<{ nodes: ["hubs", "authorities"] }> = { 9 | id: "hits", 10 | outputs: { nodes: { hubs: quantitativeOnly, authorities: quantitativeOnly } }, 11 | parameters: [ 12 | { 13 | id: "getEdgeWeight", 14 | type: "attribute", 15 | itemType: "edges", 16 | restriction: "quantitative", 17 | }, 18 | { 19 | id: "maxIterations", 20 | type: "number", 21 | defaultValue: 100, 22 | }, 23 | { 24 | id: "tolerance", 25 | type: "number", 26 | defaultValue: 1e-8, 27 | }, 28 | { 29 | id: "normalize", 30 | type: "boolean", 31 | defaultValue: true, 32 | }, 33 | ], 34 | fn( 35 | parameters: { 36 | getEdgeWeight?: keyof EdgeRenderingData; 37 | maxIterations?: number; 38 | normalize?: boolean; 39 | tolerance?: number; 40 | }, 41 | graph: FullGraph, 42 | ) { 43 | return { nodes: hits(toSimple(graph), parameters) }; 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/nodes/louvainMetric.ts: -------------------------------------------------------------------------------- 1 | import louvain from "graphology-communities-louvain"; 2 | 3 | import { EdgeRenderingData, FullGraph } from "../../graph/types"; 4 | import { Metric } from "../types"; 5 | import { qualitativeOnly } from "../utils"; 6 | 7 | export const louvainMetric: Metric<{ nodes: ["modularityClass"] }> = { 8 | id: "louvain", 9 | description: true, 10 | outputs: { nodes: { modularityClass: qualitativeOnly } }, 11 | parameters: [ 12 | { 13 | id: "getEdgeWeight", 14 | type: "attribute", 15 | itemType: "edges", 16 | restriction: "quantitative", 17 | description: true, 18 | }, 19 | { 20 | id: "fastLocalMoves", 21 | type: "boolean", 22 | defaultValue: true, 23 | description: true, 24 | }, 25 | { 26 | id: "randomWalk", 27 | type: "boolean", 28 | defaultValue: true, 29 | description: true, 30 | }, 31 | { 32 | id: "resolution", 33 | type: "number", 34 | defaultValue: 1, 35 | description: true, 36 | }, 37 | ], 38 | fn( 39 | parameters: { 40 | getEdgeWeight?: keyof EdgeRenderingData; 41 | fastLocalMoves?: boolean; 42 | randomWalk?: boolean; 43 | resolution?: number; 44 | }, 45 | graph: FullGraph, 46 | ) { 47 | return { 48 | nodes: { modularityClass: louvain(graph, { ...parameters, getEdgeWeight: parameters.getEdgeWeight || null }) }, 49 | }; 50 | }, 51 | }; 52 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/nodes/nodeScript.ts: -------------------------------------------------------------------------------- 1 | import { isScalar } from "../../../utils/check"; 2 | import { graphDatasetAtom } from "../../graph"; 3 | import { FullGraph } from "../../graph/types"; 4 | import { dataGraphToFullGraph } from "../../graph/utils"; 5 | import { Scalar } from "../../types"; 6 | import { Metric, MetricScriptFunction } from "../types"; 7 | 8 | // Definition of a custom metric function for nodes 9 | const nodeMetricCustomFn = new Function(`return ( 10 | function nodeMetric(id, attributes, index, graph) { 11 | // Your code goes here 12 | return Math.random(); 13 | } 14 | )`)(); 15 | 16 | export const nodeScript: Metric<{ nodes: ["custom"] }> = { 17 | id: "nodescript", 18 | outputs: { nodes: { custom: undefined } }, 19 | parameters: [ 20 | { 21 | id: "script", 22 | type: "script", 23 | functionJsDoc: `/** 24 | * Function that return the metric value for the specified node. 25 | * 26 | * @param {string} id The ID of the node 27 | * @param {Object.} attributes Attributes of the node 28 | * @param {number} index The index position of the node in the graph 29 | * @param {Graph} graph The graphology instance (documentation: https://graphology.github.io/) 30 | * @returns number|string The computed metric of the node 31 | */`, 32 | functionCheck: (fn) => { 33 | if (!fn) throw new Error("Function is not defined"); 34 | const fullGraph = dataGraphToFullGraph(graphDatasetAtom.get()); 35 | const id = fullGraph.nodes()[0]; 36 | const attributs = fullGraph.getNodeAttributes(id); 37 | const result = fn(id, attributs, 0, fullGraph); 38 | if (!isScalar(result)) 39 | throw new Error("Function must returns a number, a string, a boolean, null or undefined"); 40 | }, 41 | defaultValue: nodeMetricCustomFn, 42 | }, 43 | ], 44 | fn(parameters: { script?: MetricScriptFunction }, graph: FullGraph) { 45 | const fn = parameters.script; 46 | if (fn) { 47 | // we copy the graph to avoid user to modify it 48 | const graphCopy = graph.copy(); 49 | Object.freeze(graphCopy); 50 | 51 | const custom: Record = {}; 52 | graph.nodes().forEach((id, index) => { 53 | custom[id] = fn(id, graph.getNodeAttributes(id), index, graphCopy); 54 | }); 55 | return { nodes: { custom } }; 56 | } 57 | return { 58 | nodes: { 59 | custom: {}, 60 | }, 61 | }; 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/nodes/pagerankMetric.ts: -------------------------------------------------------------------------------- 1 | import { pagerank } from "graphology-metrics/centrality"; 2 | 3 | import { EdgeRenderingData, FullGraph } from "../../graph/types"; 4 | import { Metric } from "../types"; 5 | import { quantitativeOnly } from "../utils"; 6 | 7 | export const pageRankMetric: Metric<{ nodes: ["pagerank"] }> = { 8 | id: "pagerank", 9 | outputs: { nodes: { pagerank: quantitativeOnly } }, 10 | parameters: [ 11 | { 12 | id: "getEdgeWeight", 13 | type: "attribute", 14 | itemType: "edges", 15 | restriction: "quantitative", 16 | }, 17 | { 18 | id: "alpha", 19 | type: "number", 20 | defaultValue: 0.85, 21 | }, 22 | { 23 | id: "maxIterations", 24 | type: "number", 25 | defaultValue: 100, 26 | min: 1, 27 | }, 28 | { 29 | id: "tolerance", 30 | type: "number", 31 | defaultValue: 1e-6, 32 | }, 33 | ], 34 | fn( 35 | parameters: { 36 | getEdgeWeight?: keyof EdgeRenderingData; 37 | alpha?: number; 38 | maxIterations?: number; 39 | tolerance?: number; 40 | }, 41 | graph: FullGraph, 42 | ) { 43 | return { nodes: { pagerank: pagerank(graph, { ...parameters, getEdgeWeight: parameters.getEdgeWeight || null }) } }; 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from "react"; 2 | 3 | import { FieldModel, FullGraph, ItemData } from "../graph/types"; 4 | import { ItemType, Scalar } from "../types"; 5 | 6 | interface BaseMetricParameter { 7 | id: string; 8 | type: string; 9 | description?: boolean; 10 | required?: boolean; 11 | defaultValue?: unknown; 12 | } 13 | 14 | export interface MetricBooleanParameter extends BaseMetricParameter { 15 | type: "boolean"; 16 | defaultValue: boolean; 17 | } 18 | 19 | export interface MetricNumberParameter extends BaseMetricParameter { 20 | type: "number"; 21 | min?: number; 22 | max?: number; 23 | step?: number; 24 | defaultValue: number; 25 | } 26 | 27 | export interface MetricEnumParameter extends BaseMetricParameter { 28 | type: "enum"; 29 | values: { id: string }[]; 30 | defaultValue: string; 31 | } 32 | 33 | export interface MetricAttributeParameter extends BaseMetricParameter { 34 | type: "attribute"; 35 | itemType: ItemType; 36 | restriction?: "qualitative" | "quantitative"; 37 | } 38 | 39 | export type MetricScriptFunction = (id: string, attributes: ItemData, index: number, graph: FullGraph) => Scalar; 40 | export interface MetricScriptParameter extends BaseMetricParameter { 41 | type: "script"; 42 | defaultValue: MetricScriptFunction; 43 | functionJsDoc: string; 44 | functionCheck: (fn?: MetricScriptFunction) => void; 45 | } 46 | 47 | export type MetricParameter = 48 | | MetricBooleanParameter 49 | | MetricNumberParameter 50 | | MetricEnumParameter 51 | | MetricAttributeParameter 52 | | MetricScriptParameter; 53 | 54 | export type MetricType = { string: "qualitative"; type: string | boolean } | { string: "quantitative"; type: number }; 55 | 56 | export interface Metric>> { 57 | id: string; 58 | outputs: { 59 | [Key in keyof Outputs]: Outputs[Key] extends string[] 60 | ? Record | undefined> 61 | : never; 62 | }; 63 | parameters: MetricParameter[]; 64 | description?: boolean; 65 | fn: ( 66 | parameters: Record, 67 | graph: FullGraph, 68 | ) => { 69 | [Key in keyof Outputs]: Outputs[Key] extends string[] 70 | ? Record> 71 | : never; 72 | }; 73 | additionalControl?: ComponentType<{ 74 | parameters: Record; 75 | attributeNames: Record; 76 | submitCount: number; 77 | }>; 78 | } 79 | 80 | //eslint-disable-next-line 81 | export interface MetricReport { 82 | // TODO 83 | } 84 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/metrics/utils.ts: -------------------------------------------------------------------------------- 1 | export const quantitativeOnly = { quantitative: { unit: null }, qualitative: null }; 2 | export const qualitativeOnly = { quantitative: null, qualitative: { separator: null } }; 3 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/modals/index.ts: -------------------------------------------------------------------------------- 1 | import { atom, useAtom } from "@ouestware/atoms"; 2 | import { useCallback } from "react"; 3 | 4 | import { ModalRequest, ModalState } from "./types"; 5 | 6 | export const modalStateAtom = atom({}); 7 | 8 | export function useModal() { 9 | const [modalState, setModalState] = useAtom(modalStateAtom); 10 | 11 | const openModal = useCallback( 12 | (request: ModalRequest) => { 13 | setModalState((modalState) => ({ ...modalState, modal: request })); 14 | }, 15 | [setModalState], 16 | ); 17 | 18 | const closeModal = useCallback(() => { 19 | setModalState((modalState) => ({ ...modalState, modal: undefined })); 20 | }, [setModalState]); 21 | 22 | return { 23 | modal: modalState.modal, 24 | openModal, 25 | closeModal, 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/modals/types.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from "react"; 2 | 3 | export interface ModalProps { 4 | arguments: ArgumentsType; 5 | cancel: () => void; 6 | submit: (args: SubmitArgumentsType) => void; 7 | } 8 | 9 | export interface ModalRequest { 10 | component: ComponentType>; 11 | arguments: ArgumentsType; 12 | beforeCancel?: () => void; 13 | afterCancel?: () => void; 14 | beforeSubmit?: (args: SubmitArgumentsType) => void; 15 | afterSubmit?: (args: SubmitArgumentsType) => void; 16 | } 17 | 18 | export interface ModalState { 19 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 20 | modal?: ModalRequest; 21 | } 22 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/notifications/index.ts: -------------------------------------------------------------------------------- 1 | import { atom, useWriteAtom } from "@ouestware/atoms"; 2 | import { useCallback } from "react"; 3 | 4 | import { NotificationData, NotificationsState } from "./types"; 5 | 6 | export const notificationsStateAtom = atom({ notifications: [] }); 7 | 8 | let INCREMENTAL_ID = 1; 9 | export function useNotifications() { 10 | const setNotificationsState = useWriteAtom(notificationsStateAtom); 11 | 12 | const notify = useCallback( 13 | (notif: NotificationData) => { 14 | const id = ++INCREMENTAL_ID; 15 | setNotificationsState((state) => ({ 16 | ...state, 17 | notifications: [{ id, createdAt: new Date(), ...notif }, ...state.notifications], 18 | })); 19 | }, 20 | [setNotificationsState], 21 | ); 22 | 23 | return { notify }; 24 | } 25 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/notifications/types.ts: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export interface NotificationData { 4 | title?: ReactNode; 5 | message: ReactNode; 6 | type: "success" | "info" | "warning" | "error"; 7 | } 8 | 9 | export type NotificationType = NotificationData & { id: number; createdAt: Date }; 10 | 11 | export interface NotificationsState { 12 | notifications: NotificationType[]; 13 | } 14 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/preferences/index.ts: -------------------------------------------------------------------------------- 1 | import { Producer, atom, producerToAction } from "@ouestware/atoms"; 2 | 3 | import { Preferences } from "./types"; 4 | import { getAppliedTheme, getCurrentPreferences, serializePreferences } from "./utils"; 5 | 6 | /** 7 | * Producers: 8 | * ********** 9 | */ 10 | const changeLocale: Producer = (locale) => { 11 | // save the new locale in the state 12 | return (preferences) => ({ 13 | ...preferences, 14 | locale, 15 | }); 16 | }; 17 | 18 | const changeTheme: Producer = (theme) => { 19 | return (preferences) => ({ 20 | ...preferences, 21 | theme, 22 | }); 23 | }; 24 | 25 | /** 26 | * Public API: 27 | * *********** 28 | */ 29 | export const preferencesAtom = atom(getCurrentPreferences()); 30 | 31 | export const preferencesActions = { 32 | changeLocale: producerToAction(changeLocale, preferencesAtom), 33 | changeTheme: producerToAction(changeTheme, preferencesAtom), 34 | }; 35 | 36 | /** 37 | * Bindings: 38 | * ********* 39 | */ 40 | preferencesAtom.bind((preferences, prevPreferences) => { 41 | localStorage.setItem("preferences", serializePreferences(preferences)); 42 | 43 | // Apply theme change 44 | if (prevPreferences.theme !== preferences.theme || !document.documentElement.getAttribute("data-bs-theme")) { 45 | document.documentElement.setAttribute("data-bs-theme", getAppliedTheme(preferences.theme)); 46 | } 47 | }); 48 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/preferences/types.ts: -------------------------------------------------------------------------------- 1 | export interface Preferences { 2 | // for each layout, we save the parameters 3 | layoutsParameters: { [layout: string]: Record }; 4 | // for each metrics, we save the parameters 5 | metrics: { 6 | [metric: string]: { 7 | parameters: Record; 8 | attributeNames: Record; 9 | }; 10 | }; 11 | // current locale 12 | locale: string; 13 | // theme 14 | theme: "light" | "dark" | "auto"; 15 | } 16 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/preferences/utils.ts: -------------------------------------------------------------------------------- 1 | import { gephiLiteParse, gephiLiteStringify } from "@gephi/gephi-lite-sdk"; 2 | 3 | import { i18n } from "../../locales/provider"; 4 | import { Preferences } from "./types"; 5 | 6 | export function getEmptyPreferences(): Preferences { 7 | return { 8 | layoutsParameters: {}, 9 | metrics: {}, 10 | // default is the local detected by i18n 11 | locale: i18n.language, 12 | theme: "auto", 13 | }; 14 | } 15 | 16 | export function getCurrentPreferences(): Preferences { 17 | try { 18 | const rawPreferences = localStorage.getItem("preferences"); 19 | const preferences = rawPreferences ? parsePreferences(rawPreferences) : null; 20 | return { ...getEmptyPreferences(), ...preferences }; 21 | } catch (e) { 22 | console.error(e); 23 | return getEmptyPreferences(); 24 | } 25 | } 26 | 27 | /** 28 | * Preferences lifecycle helpers (state serialization / deserialization): 29 | */ 30 | export function serializePreferences(preferences: Preferences): string { 31 | return gephiLiteStringify(preferences); 32 | } 33 | 34 | export function parsePreferences(rawPreferences: string): Preferences | null { 35 | try { 36 | // TODO: 37 | // Validate the actual data 38 | return gephiLiteParse(rawPreferences); 39 | } catch (e) { 40 | console.error(e); 41 | return null; 42 | } 43 | } 44 | 45 | export function getAppliedTheme(theme: Preferences["theme"]): "light" | "dark" { 46 | if (theme === "auto") { 47 | if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) return "dark"; 48 | else return "light"; 49 | } 50 | return theme; 51 | } 52 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/search/types.ts: -------------------------------------------------------------------------------- 1 | import MiniSearch from "minisearch"; 2 | 3 | import { ItemType } from "../types"; 4 | 5 | export type Document = { itemId: string; id: string; type: ItemType; label?: string | null } & { 6 | [key: string]: unknown; 7 | }; 8 | export interface SearchState { 9 | index: MiniSearch; 10 | } 11 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/search/utils.ts: -------------------------------------------------------------------------------- 1 | import { mapKeys } from "lodash"; 2 | import MiniSearch from "minisearch"; 3 | 4 | import { GraphDataset } from "../graph/types"; 5 | import { Document, SearchState } from "./types"; 6 | 7 | export function getEmptySearchState(): SearchState { 8 | return { 9 | index: new MiniSearch({ fields: [] }), 10 | }; 11 | } 12 | 13 | export function nodeToDocument(graphDataset: GraphDataset, id: string): Document { 14 | return { 15 | itemId: `nodes-${id}`, 16 | id: id, 17 | type: "nodes", 18 | label: graphDataset.nodeRenderingData[id].label, 19 | // to avoid collision with our internal data, we prefix properties 20 | ...mapKeys(graphDataset.nodeData[id], (_value, key) => `prop_${key}`), 21 | }; 22 | } 23 | 24 | export function edgeToDocument(graphDataset: GraphDataset, id: string): Document { 25 | return { 26 | itemId: `edges-${id}`, 27 | id: id, 28 | type: "edges", 29 | label: graphDataset.edgeRenderingData[id].label, 30 | // to avoid collision with our internal data, we prefix properties 31 | ...mapKeys(graphDataset.edgeData[id], (_value, key) => `prop_${key}`), 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/selection/index.ts: -------------------------------------------------------------------------------- 1 | import { Producer, atom, producerToAction } from "@ouestware/atoms"; 2 | import { without } from "lodash"; 3 | 4 | import { ItemType } from "../types"; 5 | import { SelectionState } from "./types"; 6 | import { getEmptySelectionState } from "./utils"; 7 | 8 | /** 9 | * Producers: 10 | * ********** 11 | */ 12 | export const select: Producer; replace?: boolean }]> = ({ 13 | type, 14 | items, 15 | replace, 16 | }) => { 17 | return (state) => ({ 18 | ...state, 19 | type, 20 | items: state.type !== type || replace ? items : new Set([...Array.from(state.items), ...Array.from(items)]), 21 | }); 22 | }; 23 | 24 | export const unselect: Producer }]> = ({ type, items }) => { 25 | return (state) => ({ 26 | ...state, 27 | type, 28 | items: 29 | state.type !== type ? new Set() : new Set(without(Array.from(state.items), ...Array.from(items))), 30 | }); 31 | }; 32 | 33 | export const toggle: Producer = ({ type, item }) => { 34 | return (state) => ({ 35 | ...state, 36 | type, 37 | items: 38 | state.type !== type 39 | ? new Set([item]) 40 | : state.items.has(item) 41 | ? new Set(without(Array.from(state.items), item)) 42 | : new Set(Array.from(state.items).concat(item)), 43 | }); 44 | }; 45 | 46 | export const reset: Producer = () => { 47 | return () => getEmptySelectionState(); 48 | }; 49 | 50 | /** 51 | * Public API: 52 | * *********** 53 | */ 54 | export const selectionAtom = atom(getEmptySelectionState()); 55 | 56 | export const selectionActions = { 57 | select: producerToAction(select, selectionAtom), 58 | unselect: producerToAction(unselect, selectionAtom), 59 | toggle: producerToAction(toggle, selectionAtom), 60 | reset: producerToAction(reset, selectionAtom), 61 | } as const; 62 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/selection/types.ts: -------------------------------------------------------------------------------- 1 | import { ItemType } from "../types"; 2 | 3 | export interface SelectionState { 4 | type: ItemType; 5 | items: Set; 6 | } 7 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/selection/utils.ts: -------------------------------------------------------------------------------- 1 | import { SelectionState } from "./types"; 2 | 3 | /** 4 | * Returns an empty selection state: 5 | */ 6 | export function getEmptySelectionState(): SelectionState { 7 | return { 8 | type: "nodes", 9 | items: new Set(), 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/session/index.ts: -------------------------------------------------------------------------------- 1 | import { Producer, atom, producerToAction } from "@ouestware/atoms"; 2 | 3 | import { Session } from "./types"; 4 | import { getEmptySession, serializeSession } from "./utils"; 5 | 6 | /** 7 | * Producers: 8 | * ********** 9 | */ 10 | 11 | /** 12 | * Public API: 13 | * *********** 14 | */ 15 | export const sessionAtom = atom(getEmptySession()); 16 | 17 | export const reset: Producer = () => { 18 | return () => getEmptySession(); 19 | }; 20 | 21 | export const sessionActions = { 22 | reset: producerToAction(reset, sessionAtom), 23 | }; 24 | 25 | /** 26 | * Bindings: 27 | * ********* 28 | */ 29 | sessionAtom.bind((session) => { 30 | sessionStorage.setItem("session", serializeSession(session)); 31 | }); 32 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/session/types.ts: -------------------------------------------------------------------------------- 1 | export interface Session { 2 | // for each layout, we save the parameters 3 | layoutsParameters: { [layout: string]: Record }; 4 | // for each metrics, we save the parameters 5 | metrics: { 6 | [metric: string]: { 7 | parameters: Record; 8 | attributeNames: Record; 9 | }; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/session/utils.ts: -------------------------------------------------------------------------------- 1 | import { gephiLiteParse, gephiLiteStringify } from "@gephi/gephi-lite-sdk"; 2 | 3 | import { Session } from "./types"; 4 | 5 | export function getEmptySession(): Session { 6 | return { 7 | layoutsParameters: {}, 8 | metrics: {}, 9 | }; 10 | } 11 | 12 | /** 13 | * Preferences lifecycle helpers (state serialization / deserialization): 14 | */ 15 | export function serializeSession(session: Session): string { 16 | return gephiLiteStringify(session); 17 | } 18 | 19 | export function parseSession(rawSession: string): Session | null { 20 | try { 21 | // Validate the actual data 22 | return gephiLiteParse(rawSession); 23 | } catch (e) { 24 | console.error(e); 25 | return null; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/sigma/types.ts: -------------------------------------------------------------------------------- 1 | export interface SigmaState { 2 | highlightedNodes: Set | null; 3 | emphasizedNodes: Set | null; 4 | emphasizedEdges: Set | null; 5 | hoveredNode: string | null; 6 | hoveredEdge: string | null; 7 | } 8 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/sigma/utils.ts: -------------------------------------------------------------------------------- 1 | import { Attributes } from "graphology-types"; 2 | import { drawDiscNodeLabel } from "sigma/rendering"; 3 | import { Settings } from "sigma/settings"; 4 | import { NodeDisplayData, PartialButFor } from "sigma/types"; 5 | 6 | import { SigmaState } from "./types"; 7 | 8 | /** 9 | * Returns an empty sigma state: 10 | */ 11 | export function getEmptySigmaState(): SigmaState { 12 | return { 13 | emphasizedNodes: null, 14 | emphasizedEdges: null, 15 | hoveredNode: null, 16 | hoveredEdge: null, 17 | highlightedNodes: null, 18 | }; 19 | } 20 | 21 | export function drawDiscNodeHover< 22 | N extends Attributes = Attributes, 23 | E extends Attributes = Attributes, 24 | G extends Attributes = Attributes, 25 | >( 26 | context: CanvasRenderingContext2D, 27 | data: PartialButFor, 28 | settings: Settings, 29 | ): void { 30 | const size = settings.labelSize, 31 | font = settings.labelFont, 32 | weight = settings.labelWeight; 33 | 34 | context.font = `${weight} ${size}px ${font}`; 35 | 36 | // Then we draw the label background 37 | context.fillStyle = (settings as Settings & { nodeHoverBackgroundColor?: string }).nodeHoverBackgroundColor || "#FFF"; 38 | context.shadowOffsetX = 0; 39 | context.shadowOffsetY = 0; 40 | context.shadowBlur = 8; 41 | context.shadowColor = "#000"; 42 | 43 | const PADDING = 2; 44 | 45 | if (typeof data.label === "string") { 46 | const textWidth = context.measureText(data.label).width, 47 | boxWidth = Math.round(textWidth + 5), 48 | boxHeight = Math.round(size + 2 * PADDING), 49 | radius = Math.max(data.size, size / 2) + PADDING; 50 | 51 | const angleRadian = Math.asin(boxHeight / 2 / radius); 52 | const xDeltaCoord = Math.sqrt(Math.abs(Math.pow(radius, 2) - Math.pow(boxHeight / 2, 2))); 53 | 54 | context.beginPath(); 55 | context.moveTo(data.x + xDeltaCoord, data.y + boxHeight / 2); 56 | context.lineTo(data.x + radius + boxWidth, data.y + boxHeight / 2); 57 | context.lineTo(data.x + radius + boxWidth, data.y - boxHeight / 2); 58 | context.lineTo(data.x + xDeltaCoord, data.y - boxHeight / 2); 59 | context.arc(data.x, data.y, radius, angleRadian, -angleRadian); 60 | context.closePath(); 61 | context.fill(); 62 | } else { 63 | context.beginPath(); 64 | context.arc(data.x, data.y, data.size + PADDING, 0, Math.PI * 2); 65 | context.closePath(); 66 | context.fill(); 67 | } 68 | 69 | context.shadowOffsetX = 0; 70 | context.shadowOffsetY = 0; 71 | context.shadowBlur = 0; 72 | 73 | // And finally we draw the label 74 | drawDiscNodeLabel(context, data, settings); 75 | } 76 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/types.ts: -------------------------------------------------------------------------------- 1 | export { type ItemType, type Scalar, type ItemData, SCALAR_TYPES } from "@gephi/gephi-lite-sdk"; 2 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/user/AuthInit.tsx: -------------------------------------------------------------------------------- 1 | import { isNil } from "lodash"; 2 | import { FC, useEffect } from "react"; 3 | import { useTranslation } from "react-i18next"; 4 | 5 | import { ghProviderDeserialize } from "../cloud/github/provider"; 6 | import { useNotifications } from "../notifications"; 7 | import { LS_USER_KEY, useConnectedUser } from "./index"; 8 | 9 | /** 10 | * Sync user saved in localstorage with the atom. 11 | * Used when the application is loaded. 12 | */ 13 | export const AuthInit: FC = () => { 14 | const { t } = useTranslation(); 15 | const { notify } = useNotifications(); 16 | const [, setUser] = useConnectedUser(); 17 | 18 | useEffect(() => { 19 | const lsUserString = localStorage.getItem(LS_USER_KEY); 20 | if (!isNil(lsUserString)) { 21 | try { 22 | const lsUser = JSON.parse(lsUserString); 23 | // TODO: need to check the validity of the user 24 | // before to set it and also to find a better way to deserialize provider 25 | setUser({ ...lsUser, provider: ghProviderDeserialize(lsUser.provider) }); 26 | } catch (e) { 27 | console.error("Failed to load user from localstorage", e); 28 | notify({ 29 | type: "warning", 30 | title: `${t("gephi-lite.title")}`, 31 | message: "TODO", 32 | }); 33 | setUser(null); 34 | } 35 | } 36 | }, [setUser, notify, t]); 37 | 38 | return null; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/user/index.tsx: -------------------------------------------------------------------------------- 1 | import { Producer, atom, producerToAction, useAtom } from "@ouestware/atoms"; 2 | import { isNil } from "lodash"; 3 | 4 | import { User } from "./types"; 5 | 6 | export const LS_USER_KEY = "user"; 7 | type UserState = User | null; 8 | 9 | export function useConnectedUser() { 10 | return useAtom(userAtom); 11 | } 12 | 13 | export const reset: Producer = () => { 14 | return () => null; 15 | }; 16 | 17 | /** 18 | * Public API: 19 | * *********** 20 | */ 21 | export const userAtom = atom(null); 22 | 23 | export const userActions = { 24 | reset: producerToAction(reset, userAtom), 25 | }; 26 | 27 | /** 28 | * Sync. user atom in the localstorage 29 | */ 30 | userAtom.bind((user) => { 31 | if (!isNil(user)) localStorage.setItem(LS_USER_KEY, JSON.stringify({ ...user, provider: user.provider.serialize() })); 32 | else localStorage.removeItem(LS_USER_KEY); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/core/user/types.ts: -------------------------------------------------------------------------------- 1 | import { CloudProvider } from "../cloud/types"; 2 | 3 | export interface User { 4 | id: string; 5 | name: string; 6 | avatar?: string; 7 | provider: CloudProvider; 8 | } 9 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/hooks/useTimeout.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from "react"; 2 | 3 | function isTimeoutValid(timeout: number): boolean { 4 | return !isNaN(timeout) && timeout >= 0 && timeout !== Infinity; 5 | } 6 | 7 | /** 8 | * React hook for delaying calls in time. 9 | * 10 | * @param callback The function to execute for the timeout 11 | * @param timeout The time to wait before to execute the callback 12 | * @returns a function to cancel the timeout, and an other to reschedule it 13 | */ 14 | export function useTimeout(callback: () => void, timeout: number = 0): { cancel: () => void; reschedule: () => void } { 15 | // reference the current timeoutId 16 | const timeoutIdRef = useRef(null); 17 | 18 | /** 19 | * Function that clear the current timeout. 20 | */ 21 | const cancel = useCallback(() => { 22 | const timeoutId = timeoutIdRef.current; 23 | if (timeoutId) { 24 | timeoutIdRef.current = null; 25 | clearTimeout(timeoutId); 26 | } 27 | }, [timeoutIdRef]); 28 | 29 | /** 30 | * Function that reschedule the timeout. 31 | */ 32 | const reschedule = useCallback(() => { 33 | cancel(); 34 | timeoutIdRef.current = isTimeoutValid(timeout) ? window.setTimeout(callback, timeout) : null; 35 | }, [callback, timeout, cancel]); 36 | 37 | /** 38 | * When the hook props change 39 | * => create the new timeout 40 | */ 41 | useEffect(() => { 42 | timeoutIdRef.current = isTimeoutValid(timeout) ? window.setTimeout(callback, timeout) : null; 43 | return cancel; 44 | }, [callback, timeout, cancel]); 45 | 46 | return { cancel, reschedule }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | 4 | import { Root } from "./core/Root"; 5 | import "./styles/index.scss"; 6 | 7 | const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement); 8 | 9 | root.render( 10 | 11 | 12 | , 13 | ); 14 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/locales/LOCALES.ts: -------------------------------------------------------------------------------- 1 | import dev from "./dev.json"; 2 | import en from "./en.json"; 3 | import fr from "./fr.json"; 4 | import hu from "./hu.json"; 5 | import ko from "./ko.json"; 6 | 7 | export const LOCALES = { 8 | dev: { 9 | translation: dev, 10 | label: "Dev language", 11 | }, 12 | en: { 13 | translation: en, 14 | label: "English", 15 | }, 16 | fr: { 17 | translation: fr, 18 | label: "Français", 19 | }, 20 | hu: { 21 | translation: hu, 22 | label: "Magyar", 23 | }, 24 | ko: { 25 | translation: ko, 26 | label: "한국인", 27 | }, 28 | }; 29 | 30 | export const DEFAULT_LOCALE = import.meta.env.NODE_ENV !== "production" ? "dev" : "en"; 31 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/locales/fa.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "logo": "لوگوی گفی", 4 | "title": "به گفی لایت خوش آمدید", 5 | "disclaimer-1": "گفی لایت یک پروژه در حال پیشرفت است." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/locales/provider.tsx: -------------------------------------------------------------------------------- 1 | import i18next from "i18next"; 2 | import LngDetector from "i18next-browser-languagedetector"; 3 | import { capitalize } from "lodash"; 4 | import { FC, PropsWithChildren, useEffect } from "react"; 5 | import { I18nextProvider, initReactI18next } from "react-i18next"; 6 | 7 | import { usePreferences } from "../core/context/dataContexts"; 8 | import { DEFAULT_LOCALE, LOCALES } from "./LOCALES"; 9 | 10 | const i18n = i18next.use(initReactI18next).use(LngDetector); 11 | 12 | i18n 13 | .init({ 14 | debug: import.meta.env.MODE !== "production", 15 | fallbackLng: DEFAULT_LOCALE, 16 | resources: LOCALES, 17 | detection: { 18 | order: ["querystring", "navigator"], 19 | lookupQuerystring: "lang", 20 | convertDetectedLanguage: (lng) => (lng in LOCALES ? lng : DEFAULT_LOCALE), 21 | }, 22 | }) 23 | .then(() => { 24 | i18next.services.formatter?.add("lowercase", (value, _lng, _options) => { 25 | return value.toLowerCase(); 26 | }); 27 | i18next.services.formatter?.add("capitalize", (value, _lng, _options) => { 28 | return capitalize(value); 29 | }); 30 | }); 31 | 32 | export { i18n }; 33 | 34 | export const I18n: FC> = ({ children }) => { 35 | const { locale } = usePreferences(); 36 | 37 | useEffect(() => { 38 | if (locale) { 39 | i18n.changeLanguage(locale); 40 | } 41 | }, [locale]); 42 | 43 | return {children}; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/locales/pt.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "title": "Bem vindo ao Gephi Lite" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/locales/uk.json: -------------------------------------------------------------------------------- 1 | { 2 | "welcome": { 3 | "logo": "Логотип Gephi", 4 | "title": "Вітаємо у Gephi Lite", 5 | "disclaimer-2": "Ви можете також переглянути проект на GitHub, щоб побачити більше про нові функції або повідомити про проблеми.", 6 | "open_recent": "Відкрити нещодавні", 7 | "disclaimer-1": "Gephi Lite знаходиться на стадії розробки.", 8 | "samples": "Приклади" 9 | }, 10 | "appearance": { 11 | "show_edges": "Показати ребра", 12 | "color": { 13 | "title": "Колір" 14 | } 15 | }, 16 | "providers": { 17 | "github": "GitHub" 18 | }, 19 | "selection": { 20 | "nodes_one": "Один вибраний вузол", 21 | "focus_edges": "Вибрати тільки це ребро", 22 | "edges": "{{count}} вибраних ребер", 23 | "edges_zero": "Нема вибраних ребер", 24 | "select_all": "Вибрати все", 25 | "nodes": "{{count}} вибраних вузлів", 26 | "focus_nodes": "Вибрати тільки цей вузол", 27 | "edges_one": "Одне вибране ребро", 28 | "nodes_zero": "Немає вибраних вузлів" 29 | }, 30 | "github": { 31 | "select_ui_language": "Вибрати мову додатку" 32 | }, 33 | "gephi-lite": { 34 | "title": "Gephi Lite", 35 | "info": "Більше інформації про Gephi Lite" 36 | }, 37 | "error": { 38 | "title": "Помилка", 39 | "not_found": { 40 | "title": "Сторінку не знайдено" 41 | } 42 | }, 43 | "menu": { 44 | "save": { 45 | "default": "Зберегти" 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/locales/zh-Hans.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_dark.scss: -------------------------------------------------------------------------------- 1 | @include color-mode(dark) { 2 | --toolbar-bg: #{$black}; 3 | --panels-bg: #{$gray-700}; 4 | --panels-bg-rgb: 73, 80, 87; 5 | --range-in-bg: #{$gray-800}; 6 | --range-in-body: #{$gray-800}; 7 | --range-out-bg: #{$gray-200}; 8 | --range-out-body: #{$gray-200}; 9 | } 10 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_filters.scss: -------------------------------------------------------------------------------- 1 | .filter-item { 2 | position: relative; 3 | 4 | @extend .mt-2; 5 | @extend .px-2; 6 | @extend .py-2; 7 | &.inactive { 8 | @extend .text-muted; 9 | } 10 | 11 | .button-container { 12 | // we center buttons vertically on two lines of fs-5 text (filter title) 13 | height: calc(var(--bs-body-line-height) * #{$h5-font-size} * 2); 14 | margin: 0 0.5rem; 15 | display: flex; 16 | align-items: center; 17 | .btn-icon { 18 | padding: 0; 19 | } 20 | } 21 | 22 | &::after { 23 | content: " "; 24 | position: absolute; 25 | inset: 0; 26 | border: 1px solid $gray-500; 27 | z-index: -1; 28 | } 29 | 30 | &:not(.inactive)::after { 31 | border-color: $gray-600; 32 | } 33 | &.edited::after { 34 | border-width: 2px; 35 | } 36 | } 37 | 38 | .range-filter { 39 | margin-top: 0.8rem; 40 | height: 80px; 41 | 42 | display: flex; 43 | flex-direction: row; 44 | justify-content: space-between; 45 | 46 | .bar { 47 | position: relative; 48 | height: 100%; 49 | flex-grow: 1; 50 | 51 | &:not(:last-child) { 52 | margin-right: 1px; 53 | } 54 | } 55 | 56 | .global, 57 | .filtered { 58 | position: absolute; 59 | left: 0; 60 | right: 0; 61 | bottom: 0; 62 | transition: height ease-in-out 0.2s; 63 | } 64 | 65 | .global { 66 | background: var(--range-in-bg); 67 | } 68 | .filtered { 69 | background: var(--range-out-bg); 70 | } 71 | .label { 72 | position: absolute; 73 | text-align: center; 74 | width: 100%; 75 | font-size: 0.8em; 76 | 77 | &.inside { 78 | top: 0; 79 | color: var(--range-in-body); 80 | } 81 | &.outside { 82 | bottom: 100%; 83 | color: var(--range-out-body); 84 | } 85 | } 86 | } 87 | .rc-slider { 88 | .rc-slider-handle { 89 | background-color: white; 90 | border-color: black; 91 | } 92 | } 93 | .rc-slider-disabled { 94 | background-color: unset !important; 95 | .rc-slider-handle { 96 | background-color: $gray-400 !important; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_forms.scss: -------------------------------------------------------------------------------- 1 | .form-control.react-select.is-invalid { 2 | border: none; 3 | padding-left: 0; 4 | padding-top: 0; 5 | padding-bottom: 0; 6 | 7 | > div { 8 | border-color: var(--bs-form-invalid-border-color); 9 | box-shadow: none; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_highlightjs.scss: -------------------------------------------------------------------------------- 1 | @import "highlight.js/styles/default.css"; 2 | 3 | div.code-thumb { 4 | font-size: 10px; 5 | min-height: 80px; 6 | max-height: 120px; 7 | overflow: hidden; 8 | pre { 9 | margin-bottom: 0; 10 | white-space: pre-wrap; /* css-3 */ 11 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 12 | white-space: -pre-wrap; /* Opera 4-6 */ 13 | white-space: -o-pre-wrap; /* Opera 7 */ 14 | word-wrap: break-word; /* Internet Explorer 5.5+ */ 15 | code.hljs { 16 | background: none; 17 | padding: calc($spacer / 4); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_layout.scss: -------------------------------------------------------------------------------- 1 | #app-root { 2 | height: 100vh; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | header { 7 | flex-shrink: 0; 8 | 9 | ul.nav { 10 | li.dropdown { 11 | ul.dropdown-menu { 12 | inset: 40px 0px auto auto; 13 | } 14 | &:hover { 15 | ul.dropdown-menu { 16 | display: block; 17 | } 18 | } 19 | } 20 | 21 | .dropdown-item { 22 | display: flex; 23 | align-items: center; 24 | } 25 | } 26 | } 27 | 28 | main { 29 | flex-shrink: 1; 30 | flex-grow: 1; 31 | overflow: hidden; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_loader.scss: -------------------------------------------------------------------------------- 1 | .loader { 2 | position: fixed; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | z-index: $zindex-tooltip + 1; 11 | background-color: rgba(0, 0, 0, 0.25); 12 | margin: 0 !important; 13 | padding: 0 !important; 14 | } 15 | 16 | .loader-fill { 17 | position: absolute; 18 | inset: 0; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | z-index: 9; 24 | background: rgba(255, 255, 255, 0.5); 25 | } 26 | 27 | .z-over-loader { 28 | z-index: 10; 29 | position: relative; 30 | } 31 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_slider.scss: -------------------------------------------------------------------------------- 1 | .horizontal-slider { 2 | width: 100%; 3 | height: 18px; 4 | .track { 5 | top: 25%; 6 | height: 50%; 7 | cursor: pointer; 8 | } 9 | .track:first-child, 10 | .track:nth-child(2) { 11 | margin-left: 5px; 12 | } 13 | .track:last-child, 14 | .track:nth-child(n-1) { 15 | margin-right: 5px; 16 | } 17 | } 18 | .thumb { 19 | border-radius: 50%; 20 | border: #d3d3d3 2px solid; 21 | // /!\ changing thumb size must also change minDistance setting in ReactSlider AND slider height 22 | height: 18px; 23 | width: 18px; 24 | cursor: pointer; 25 | } 26 | 27 | .custom-color-picker { 28 | background-color: var(--bs-body-bg); 29 | width: 220px; 30 | padding: 10px; 31 | border-radius: 4px; 32 | .sketch-picker { 33 | background: var(--bs-body-bg) !important; 34 | 35 | .flexbox-fix label { 36 | color: var(--bs-body-color) !important; 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_transition.scss: -------------------------------------------------------------------------------- 1 | @keyframes fade-in { 2 | 0% { 3 | opacity: 0; 4 | } 5 | 100% { 6 | opacity: 1; 7 | } 8 | } 9 | @keyframes fade-out { 10 | 0% { 11 | opacity: 1; 12 | } 13 | 100% { 14 | opacity: 0; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_user.scss: -------------------------------------------------------------------------------- 1 | .user { 2 | @extend .rounded-circle; 3 | display: flex; 4 | align-items: center; 5 | justify-content: center; 6 | border: 2px solid $primary; 7 | height: $user-avatar-size; 8 | width: $user-avatar-size; 9 | background-color: $light; 10 | overflow: hidden; 11 | 12 | &.user-sm { 13 | height: 0.8 * $user-avatar-size; 14 | width: 0.8 * $user-avatar-size; 15 | } 16 | 17 | img, 18 | svg { 19 | height: 100%; 20 | width: 100%; 21 | 22 | &.default { 23 | padding: 5px; 24 | color: $secondary; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_variables-override.scss: -------------------------------------------------------------------------------- 1 | @use "sass:map"; 2 | 3 | // Bootstrap variables: 4 | $position-values: map.merge( 5 | $position-values, 6 | ( 7 | 5: 0.5em, 8 | ) 9 | ); 10 | 11 | // Switch customization: 12 | $form-switch-checked-color: $form-switch-color; 13 | $form-switch-checked-bg-image: $form-switch-bg-image; 14 | .form-switch .form-check-input:checked { 15 | background-color: $input-bg; 16 | border-color: $input-border-color; 17 | } 18 | 19 | $toolbar-bg: $gray-400; 20 | $panels-bg: $gray-200; 21 | :root { 22 | --toolbar-bg: #{$gray-400}; 23 | --panels-bg: #{$gray-200}; 24 | --panels-bg-rgb: 233, 236, 239; 25 | --range-in-bg: #{$gray-400}; 26 | --range-in-body: #{$gray-100}; 27 | --range-out-bg: #{$gray-800}; 28 | --range-out-body: #{$gray-800}; 29 | } 30 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $user-avatar-size: 2rem; 2 | 3 | // Bootstrap variables: 4 | $font-family-sans-serif: Poppins, Arial, Helvetica, Geneva; 5 | 6 | $graph-caption-items-height: 55px; 7 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // Fonts: 2 | @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@200;300;400;600;700&display=swap"); 3 | 4 | // Custom bootstrap variables tuning: 5 | @import "variables"; 6 | 7 | // Bootstrap files: 8 | @import "bootstrap/scss/functions"; 9 | @import "bootstrap/scss/variables"; 10 | @import "bootstrap/scss/variables-dark"; 11 | 12 | @import "variables-override"; 13 | 14 | @import "bootstrap/scss/maps"; 15 | @import "bootstrap/scss/mixins"; 16 | @import "bootstrap/scss/utilities"; 17 | @import "bootstrap/scss/root"; 18 | @import "bootstrap/scss/reboot"; 19 | @import "bootstrap/scss/type"; 20 | @import "bootstrap/scss/images"; 21 | @import "bootstrap/scss/containers"; 22 | @import "bootstrap/scss/grid"; 23 | @import "bootstrap/scss/tables"; 24 | @import "bootstrap/scss/forms"; 25 | @import "bootstrap/scss/buttons"; 26 | @import "bootstrap/scss/transitions"; 27 | @import "bootstrap/scss/dropdown"; 28 | @import "bootstrap/scss/button-group"; 29 | @import "bootstrap/scss/nav"; 30 | @import "bootstrap/scss/navbar"; 31 | @import "bootstrap/scss/card"; 32 | @import "bootstrap/scss/accordion"; 33 | @import "bootstrap/scss/breadcrumb"; 34 | @import "bootstrap/scss/pagination"; 35 | @import "bootstrap/scss/badge"; 36 | @import "bootstrap/scss/alert"; 37 | @import "bootstrap/scss/progress"; 38 | @import "bootstrap/scss/list-group"; 39 | @import "bootstrap/scss/close"; 40 | @import "bootstrap/scss/toasts"; 41 | @import "bootstrap/scss/modal"; 42 | @import "bootstrap/scss/tooltip"; 43 | @import "bootstrap/scss/popover"; 44 | @import "bootstrap/scss/carousel"; 45 | @import "bootstrap/scss/spinners"; 46 | @import "bootstrap/scss/offcanvas"; 47 | @import "bootstrap/scss/placeholders"; 48 | @import "bootstrap/scss/helpers"; 49 | @import "bootstrap/scss/utilities/api"; 50 | 51 | // External libs 52 | @import "@react-sigma/core/lib/style.css"; 53 | @import "rc-slider/assets/index.css"; 54 | @import "mfglabs_iconset.css"; 55 | 56 | // Application styles: 57 | @import "base"; 58 | @import "layout"; 59 | @import "graph-page"; 60 | @import "loader"; 61 | @import "user"; 62 | @import "filters"; 63 | @import "forms"; 64 | @import "slider"; 65 | @import "highlightjs"; 66 | @import "graph-caption"; 67 | @import "react-select"; 68 | @import "rc-slider"; 69 | @import "transition"; 70 | @import "dark"; 71 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/rc-slider.scss: -------------------------------------------------------------------------------- 1 | .rc-slider { 2 | .rc-slider-rail { 3 | background-color: var(--range-in-bg) !important; 4 | } 5 | .rc-slider-track { 6 | background-color: var(--range-out-bg) !important; 7 | } 8 | .rc-slider-dot { 9 | background-color: var(--panels-bg); 10 | border-color: var(--range-in-bg) !important; 11 | } 12 | .rc-slider-dot-active { 13 | border-color: var(--range-out-bg) !important; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/styles/react-select.scss: -------------------------------------------------------------------------------- 1 | .react-select__control { 2 | background-color: var(--bs-body-bg) !important; 3 | } 4 | .react-select__input, 5 | .react-select__single-value { 6 | color: var(--bs-body-color) !important; 7 | } 8 | .react-select__menu-list { 9 | background-color: var(--bs-body-bg) !important; 10 | } 11 | .react-select__option { 12 | background-color: var(--bs-body-bg) !important; 13 | &:hover { 14 | cursor: pointer; 15 | background-color: var(--bs-border-color) !important; 16 | } 17 | } 18 | .react-select__option--is-selected, 19 | .react-select__option--is-focused, 20 | .selected { 21 | background-color: var(--bs-primary) !important; 22 | &:hover { 23 | cursor: pointer; 24 | background-color: var(--bs-primary) !important; 25 | } 26 | } 27 | .react-select__multi-value__remove { 28 | color: var(--bs-body-bg) !important; 29 | } 30 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/bordered-node-program/index.ts: -------------------------------------------------------------------------------- 1 | import { NodeProgram, ProgramInfo } from "sigma/rendering"; 2 | import { RenderParams } from "sigma/types"; 3 | import { floatColor } from "sigma/utils"; 4 | 5 | import { CustomNodeDisplayData } from "../../core/appearance/types"; 6 | import { fragmentShaderSource as FRAGMENT_SHADER_SOURCE } from "./program.frag"; 7 | import { vertexShaderSource as VERTEX_SHADER_SOURCE } from "./program.vert"; 8 | 9 | const { UNSIGNED_BYTE, FLOAT } = WebGLRenderingContext; 10 | 11 | const UNIFORMS = ["u_sizeRatio", "u_correctionRatio", "u_matrix"] as const; 12 | 13 | export default class NodeProgramBorder extends NodeProgram<(typeof UNIFORMS)[number]> { 14 | static readonly ANGLE_1 = 0; 15 | static readonly ANGLE_2 = (2 * Math.PI) / 3; 16 | static readonly ANGLE_3 = (4 * Math.PI) / 3; 17 | 18 | getDefinition() { 19 | return { 20 | VERTICES: 3, 21 | VERTEX_SHADER_SOURCE, 22 | FRAGMENT_SHADER_SOURCE, 23 | METHOD: WebGLRenderingContext.TRIANGLES, 24 | UNIFORMS, 25 | ATTRIBUTES: [ 26 | { name: "a_position", size: 2, type: FLOAT }, 27 | { name: "a_size", size: 1, type: FLOAT }, 28 | { name: "a_color", size: 4, type: UNSIGNED_BYTE, normalized: true }, 29 | { name: "a_borderColor", size: 4, type: UNSIGNED_BYTE, normalized: true }, 30 | { name: "a_id", size: 4, type: UNSIGNED_BYTE, normalized: true }, 31 | ], 32 | CONSTANT_ATTRIBUTES: [{ name: "a_angle", size: 1, type: FLOAT }], 33 | CONSTANT_DATA: [[NodeProgramBorder.ANGLE_1], [NodeProgramBorder.ANGLE_2], [NodeProgramBorder.ANGLE_3]], 34 | }; 35 | } 36 | 37 | processVisibleItem(nodeIndex: number, startIndex: number, data: CustomNodeDisplayData) { 38 | const array = this.array; 39 | 40 | const color = floatColor(data.color); 41 | const borderColor = floatColor(data.borderColor || data.color); 42 | 43 | array[startIndex++] = data.x; 44 | array[startIndex++] = data.y; 45 | array[startIndex++] = data.size; 46 | array[startIndex++] = color; 47 | array[startIndex++] = borderColor; 48 | array[startIndex++] = nodeIndex; 49 | } 50 | 51 | setUniforms(params: RenderParams, { gl, uniformLocations }: ProgramInfo): void { 52 | const { u_sizeRatio, u_correctionRatio, u_matrix } = uniformLocations; 53 | 54 | gl.uniform1f(u_correctionRatio, params.correctionRatio); 55 | gl.uniform1f(u_sizeRatio, params.sizeRatio); 56 | gl.uniformMatrix3fv(u_matrix, false, params.matrix); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/bordered-node-program/program.frag.ts: -------------------------------------------------------------------------------- 1 | export const fragmentShaderSource = /*glsl*/ ` 2 | precision mediump float; 3 | 4 | varying vec4 v_color; 5 | varying vec4 v_borderColor; 6 | varying vec2 v_diffVector; 7 | varying float v_radius; 8 | varying float v_borderThickness; 9 | varying float v_antiAliasingBorder; 10 | 11 | const vec4 transparent = vec4(0.0, 0.0, 0.0, 0.0); 12 | 13 | void main(void) { 14 | float dist = length(v_diffVector); 15 | 16 | float insideRadius = v_radius - v_borderThickness; 17 | 18 | 19 | // No antialiasing for picking mode: 20 | #ifdef PICKING_MODE 21 | if (dist > v_radius) 22 | gl_FragColor = transparent; 23 | else 24 | gl_FragColor = v_color; 25 | 26 | #else 27 | if (dist < insideRadius - v_antiAliasingBorder) 28 | gl_FragColor = v_color; 29 | else if (dist < insideRadius) 30 | gl_FragColor = mix(v_borderColor, v_color, (insideRadius - dist) / v_antiAliasingBorder); 31 | else if (dist < v_radius - v_antiAliasingBorder) 32 | gl_FragColor = v_borderColor; 33 | else if (dist < v_radius) 34 | gl_FragColor = mix(transparent, v_borderColor, (v_radius - dist) / v_antiAliasingBorder); 35 | else 36 | gl_FragColor = transparent; 37 | #endif 38 | } 39 | `; 40 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/bordered-node-program/program.vert.ts: -------------------------------------------------------------------------------- 1 | export const vertexShaderSource = /*glsl*/ ` 2 | attribute vec4 a_id; 3 | attribute vec2 a_position; 4 | attribute float a_size; 5 | attribute float a_angle; 6 | attribute vec4 a_color; 7 | attribute vec4 a_borderColor; 8 | 9 | uniform mat3 u_matrix; 10 | uniform float u_sizeRatio; 11 | uniform float u_correctionRatio; 12 | 13 | varying vec4 v_color; 14 | varying vec4 v_borderColor; 15 | varying vec2 v_diffVector; 16 | varying float v_radius; 17 | varying float v_borderThickness; 18 | varying float v_antiAliasingBorder; 19 | 20 | const float bias = 255.0 / 254.0; 21 | const float marginRatio = 1.05; 22 | 23 | void main() { 24 | float size = a_size * u_correctionRatio / u_sizeRatio * 4.0; 25 | vec2 diffVector = size * vec2(cos(a_angle), sin(a_angle)); 26 | vec2 position = a_position + diffVector * marginRatio; 27 | gl_Position = vec4( 28 | (u_matrix * vec3(position, 1)).xy, 29 | 0, 30 | 1 31 | ); 32 | 33 | v_antiAliasingBorder = u_correctionRatio; 34 | v_diffVector = diffVector; 35 | v_radius = size / 2.0 / marginRatio; 36 | v_borderThickness = min(5.0 * u_correctionRatio, v_radius / 2.0); 37 | 38 | #ifdef PICKING_MODE 39 | // For picking mode, we use the ID as both colors: 40 | v_color = a_id; 41 | v_borderColor = a_id; 42 | #else 43 | // For normal mode, we use the color: 44 | v_color = a_color; 45 | v_borderColor = a_borderColor; 46 | #endif 47 | 48 | v_color.a *= bias; 49 | v_borderColor.a *= bias; 50 | } 51 | `; 52 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/check.ts: -------------------------------------------------------------------------------- 1 | import { Scalar } from "@gephi/gephi-lite-sdk"; 2 | import checkIsUrl from "is-url"; 3 | import { isNil } from "lodash"; 4 | 5 | const NON_NIL_SCALAR_TYPES = new Set(["boolean", "number", "string"]); 6 | 7 | /** 8 | * Check if a value is a scalar value. 9 | */ 10 | export function isScalar(value: unknown): value is Scalar { 11 | return isNil(value) || NON_NIL_SCALAR_TYPES.has(typeof value); 12 | } 13 | 14 | /** 15 | * Check if a string is a valid url. 16 | */ 17 | export function isUrl(url: string): boolean { 18 | return checkIsUrl(url); 19 | } 20 | 21 | /** 22 | * Check if the given filename has the specified extension. 23 | * Exemple `checkFilenameExtension(file.filename, "gexf")` 24 | */ 25 | export function checkFilenameExtension(filename: string, extension: string): boolean { 26 | const fileExt = filename.split(".").pop()?.toLocaleLowerCase() || ""; 27 | return extension.toLocaleLowerCase() === fileExt; 28 | } 29 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | import chroma from "chroma-js"; 2 | import Color from "color"; 3 | import { memoize } from "lodash"; 4 | import { RGBColor } from "react-color"; 5 | 6 | export const memoizedBrighten = memoize((color: string) => chroma.mix(color, "white", 0.75).hex()); 7 | export const memoizedDarken = memoize((color: string) => chroma.mix(color, "black", 0.75).hex()); 8 | 9 | export function hexToRgba(value: string): RGBColor { 10 | const parsed = Color(value); 11 | return { r: parsed.red(), g: parsed.green(), b: parsed.blue(), a: parsed.alpha() }; 12 | } 13 | 14 | export function rgbaToHex(value: RGBColor): string { 15 | const parsed = Color({ r: value.r, g: value.g, b: value.b }).alpha(value.a || 1); 16 | return parsed.hexa(); 17 | } 18 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/date.ts: -------------------------------------------------------------------------------- 1 | export function displayDateTime(value: string | Date, options?: { time: boolean }): string | undefined { 2 | try { 3 | const date = new Date(value); 4 | if (date) 5 | return new Intl.DateTimeFormat("en-GB", { 6 | dateStyle: "short", 7 | timeStyle: options?.time === false ? undefined : "short", 8 | }).format(date); 9 | } catch (_e) { 10 | // nothing todo 11 | } 12 | return undefined; 13 | } 14 | 15 | /** 16 | * Display the date to the "from ago" format. 17 | */ 18 | export function dateToFromAgo(date: Date): string { 19 | const seconds = Math.round((Date.now() - date.getTime()) / 1000); 20 | const prefix = seconds < 0 ? "in " : ""; 21 | const suffix = seconds < 0 ? "" : " ago"; 22 | const absSecond = Math.abs(seconds); 23 | 24 | const times = [ 25 | absSecond / 60 / 60 / 24 / 365, // years 26 | absSecond / 60 / 60 / 24 / 30, // months 27 | absSecond / 60 / 60 / 24 / 7, // weeks 28 | absSecond / 60 / 60 / 24, // days 29 | absSecond / 60 / 60, // hours 30 | absSecond / 60, // minutes 31 | absSecond, // seconds 32 | ]; 33 | 34 | return ( 35 | ["year", "month", "week", "day", "hour", "minute", "second"] 36 | .map((name, index) => { 37 | const time = Math.floor(times[index]); 38 | if (time > 0) return `${prefix}${time} ${name}${time > 1 ? "s" : ""}${suffix}`; 39 | return null; 40 | }) 41 | .reduce((acc, curr) => (acc === null && curr !== null ? curr : null), null) || "now" 42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/labels.ts: -------------------------------------------------------------------------------- 1 | export const MIN_LABEL_THRESHOLD = 0.1; 2 | export const MAX_LABEL_THRESHOLD = 10; 3 | 4 | export function stateToInputThreshold(v: number): number { 5 | if (v === Infinity) return MIN_LABEL_THRESHOLD; 6 | if (v === 0) return MAX_LABEL_THRESHOLD; 7 | return 6 / v; 8 | } 9 | 10 | export function inputToStateThreshold(v: number): number { 11 | if (v <= MIN_LABEL_THRESHOLD) return Infinity; 12 | if (v >= MAX_LABEL_THRESHOLD) return 0; 13 | return 6 / v; 14 | } 15 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/promises.ts: -------------------------------------------------------------------------------- 1 | export function wait(delay: number) { 2 | return new Promise(function (resolve) { 3 | setTimeout(resolve, delay); 4 | }); 5 | } 6 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/sigma.ts: -------------------------------------------------------------------------------- 1 | import Graph from "graphology"; 2 | import { Dimensions } from "graphology-layout/conversion"; 3 | import Sigma from "sigma"; 4 | import { Settings } from "sigma/settings"; 5 | import { CameraState } from "sigma/types"; 6 | 7 | import { wait } from "./promises"; 8 | 9 | type LightAttributes = { [key: string]: unknown }; 10 | 11 | export async function getGraphSnapshot( 12 | graph: Graph, 13 | settings: Partial>, 14 | { 15 | width, 16 | height, 17 | ratio, 18 | backgroundColor, 19 | cameraState, 20 | }: Dimensions & { ratio: number; backgroundColor: string; cameraState?: Partial }, 21 | ): Promise { 22 | const pixelRatio = window.devicePixelRatio || 1; 23 | 24 | const div = document.createElement("DIV"); 25 | div.style.width = `${width * ratio}px`; 26 | div.style.height = `${height * ratio}px`; 27 | div.style.position = "absolute"; 28 | div.style.right = "101%"; 29 | div.style.bottom = "101%"; 30 | div.style.background = backgroundColor; 31 | document.body.append(div); 32 | 33 | // Instantiate sigma: 34 | const renderer = new Sigma(graph, div, settings); 35 | 36 | if (cameraState) renderer.getCamera().setState(cameraState); 37 | 38 | await wait(0); 39 | 40 | // Capture sigma rendering: 41 | const canvas = document.createElement("CANVAS") as HTMLCanvasElement; 42 | canvas.setAttribute("width", width * pixelRatio + ""); 43 | canvas.setAttribute("height", height * pixelRatio + ""); 44 | const ctx = canvas.getContext("2d") as CanvasRenderingContext2D; 45 | 46 | // This weird casting allows accessing "private" sigma properties: 47 | const elements = (renderer as unknown as { elements: Record }).elements; 48 | 49 | ctx.fillStyle = backgroundColor; 50 | ctx.fillRect(0, 0, width * ratio * pixelRatio, height * ratio * pixelRatio); 51 | [elements.edges, elements.edgeLabels, elements.nodes, elements.labels].forEach((canvas) => 52 | ctx.drawImage( 53 | canvas, 54 | 0, 55 | 0, 56 | width * ratio * pixelRatio, 57 | height * ratio * pixelRatio, 58 | 0, 59 | 0, 60 | width * pixelRatio, 61 | height * pixelRatio, 62 | ), 63 | ); 64 | 65 | const blob: Blob = await new Promise((resolve, reject) => { 66 | canvas.toBlob((blob) => { 67 | if (blob) resolve(blob); 68 | else reject("Cannot generate sigma screenshot"); 69 | }, "image/png"); 70 | }); 71 | 72 | renderer.kill(); 73 | div.remove(); 74 | 75 | return blob; 76 | } 77 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/utils/url.tsx: -------------------------------------------------------------------------------- 1 | import { Props as LinkifyProps } from "react-linkify"; 2 | 3 | /** 4 | * Given an url, returns the filename 5 | */ 6 | export function extractFilename(url: string): string { 7 | return url.split("/").pop() || url; 8 | } 9 | /** 10 | * Configuration of Linkify 11 | * Given a string, return a proper a href if this string is a URL 12 | */ 13 | export const DEFAULT_LINKIFY_PROPS: Partial = { 14 | textDecorator: (url: string) => url.replace(/^https?:\/\//, ""), 15 | componentDecorator: (decoratedHref: string, decoratedText: string, key: number) => ( 16 | 17 | {decoratedText} 18 | 19 | ), 20 | }; 21 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { Layout } from "./layout"; 5 | 6 | export const ErrorPage: FC = () => { 7 | const { t } = useTranslation("translation"); 8 | return ( 9 | 10 |

{t("error.title")}

11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { Layout } from "./layout"; 5 | 6 | export const NotFoundPage: FC = () => { 7 | const { t } = useTranslation("translation"); 8 | return ( 9 | 10 |

{t("error.not_found.title")}

11 |

{t("error.not_found.subtitle")}

12 |

{t("error.not_found.paragraph")}

13 |
14 | ); 15 | }; 16 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/graphPage/AppearancePanel.tsx: -------------------------------------------------------------------------------- 1 | import { FC, useMemo, useState } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { GraphGraphAppearance, GraphItemAppearance } from "../../components/GraphAppearance"; 5 | import { ToggleBar } from "../../components/Toggle"; 6 | import { AppearanceIcon, EdgeIcon, GraphIcon, NodeIcon } from "../../components/common-icons"; 7 | 8 | export const AppearancePanel: FC = () => { 9 | const { t } = useTranslation(); 10 | const [selected, setSelected] = useState("nodes"); 11 | const tabs = useMemo(() => { 12 | return [ 13 | { 14 | value: "nodes", 15 | label: ( 16 | <> 17 | {t("graph.model.nodes")} 18 | 19 | ), 20 | }, 21 | { 22 | value: "edges", 23 | label: ( 24 | <> 25 | {t("graph.model.edges")} 26 | 27 | ), 28 | }, 29 | { 30 | value: "graph", 31 | label: ( 32 | <> 33 | {t("graph.model.graph")} 34 | 35 | ), 36 | }, 37 | ]; 38 | }, [t]); 39 | return ( 40 | <> 41 |
42 |

43 | {t("appearance.title")}{" "} 44 |

45 | setSelected(e)} 49 | options={tabs} 50 | /> 51 |
52 |
53 | 54 | {selected === "nodes" && } 55 | {selected === "edges" && } 56 | {selected === "graph" && } 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/graphPage/FiltersPanel.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import GraphFilters from "../../components/GraphFilters"; 5 | import { InformationTooltip } from "../../components/InformationTooltip"; 6 | import { FiltersIcon } from "../../components/common-icons"; 7 | 8 | export const FiltersPanel: FC = () => { 9 | const { t } = useTranslation(); 10 | return ( 11 | <> 12 |
13 |

14 | {t("filters.title")} 15 | 16 |

{t("filters.description")}

17 |
18 |

19 |

{t("filters.description")}

20 |
21 | 22 |
23 | 24 | 25 | 26 | ); 27 | }; 28 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/graphPage/GraphDataPanel.tsx: -------------------------------------------------------------------------------- 1 | import { FC } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { GraphIcon } from "../../components/common-icons"; 5 | import { GraphMetadataForm } from "../../components/forms/GraphMetadataForm"; 6 | import { GraphModelForm } from "../../components/forms/GraphModelForm"; 7 | 8 | export const GraphDataPanel: FC = () => { 9 | const { t } = useTranslation(); 10 | return ( 11 | <> 12 |
13 |

14 | {t("graph.title")} 15 |

16 |
17 | 18 |
19 | 20 |
21 |

{t("graph.metadata.title")}

22 | 23 | 24 |
25 | 26 |

{t("graph.model.title")}

27 | 28 |
29 | 30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/graphPage/controllers/SettingsController.tsx: -------------------------------------------------------------------------------- 1 | import { useSigma } from "@react-sigma/core"; 2 | import { FC, useEffect } from "react"; 3 | import { drawDiscNodeLabel, drawStraightEdgeLabel } from "sigma/rendering"; 4 | import { DEFAULT_SETTINGS, Settings } from "sigma/settings"; 5 | 6 | import { getDrawEdgeLabel, getDrawNodeLabel } from "../../../core/appearance/utils"; 7 | import { useAppearance, useGraphDataset, usePreferences } from "../../../core/context/dataContexts"; 8 | import { getAppliedTheme } from "../../../core/preferences/utils"; 9 | import { GephiLiteSigma, resetCamera, sigmaAtom } from "../../../core/sigma"; 10 | import { drawDiscNodeHover } from "../../../core/sigma/utils"; 11 | import { inputToStateThreshold } from "../../../utils/labels"; 12 | 13 | export const SettingsController: FC<{ setIsReady: () => void }> = ({ setIsReady }) => { 14 | const sigma = useSigma() as GephiLiteSigma; 15 | const graphDataset = useGraphDataset(); 16 | const graphAppearance = useAppearance(); 17 | const { theme } = usePreferences(); 18 | 19 | useEffect(() => { 20 | sigmaAtom.set(sigma); 21 | resetCamera({ forceRefresh: true }); 22 | }, [sigma]); 23 | 24 | useEffect(() => { 25 | const mode = getAppliedTheme(theme); 26 | sigma.setSetting("labelColor", { color: mode === "dark" ? "#FFF" : "#000" }); 27 | sigma.setSetting("edgeLabelColor", { color: mode === "dark" ? "#495057" : "#CCC" }); 28 | sigma.setSetting("nodeHoverBackgroundColor" as keyof Settings, mode === "dark" ? "#000" : "#FFF"); 29 | sigma.setSetting("renderEdgeLabels", graphAppearance.edgesLabel.type !== "none"); 30 | sigma.setSetting("zIndex", graphAppearance.edgesZIndex.type !== "none"); 31 | sigma.setSetting("defaultDrawNodeLabel", getDrawNodeLabel(graphAppearance, drawDiscNodeLabel)); 32 | sigma.setSetting("defaultDrawNodeHover", getDrawNodeLabel(graphAppearance, drawDiscNodeHover)); 33 | sigma.setSetting("defaultDrawEdgeLabel", getDrawEdgeLabel(graphAppearance, drawStraightEdgeLabel)); 34 | 35 | const labelThreshold = inputToStateThreshold(graphAppearance.nodesLabelSize.density); 36 | const labelDensity = labelThreshold === 0 ? Infinity : DEFAULT_SETTINGS.labelDensity; 37 | sigma.setSetting("labelRenderedSizeThreshold", labelThreshold); 38 | sigma.setSetting("labelDensity", labelDensity); 39 | 40 | setIsReady(); 41 | }, [graphAppearance, graphDataset, setIsReady, sigma, theme]); 42 | 43 | return null; 44 | }; 45 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/graphPage/modals/ConfirmModal.tsx: -------------------------------------------------------------------------------- 1 | import { FC, ReactNode } from "react"; 2 | import { useTranslation } from "react-i18next"; 3 | 4 | import { Modal } from "../../../components/modals"; 5 | import { ModalProps } from "../../../core/modals/types"; 6 | import { useNotifications } from "../../../core/notifications"; 7 | 8 | const ConfirmModal: FC< 9 | ModalProps<{ 10 | title: ReactNode; 11 | message: ReactNode; 12 | confirmMsg?: ReactNode; 13 | cancelMsg?: ReactNode; 14 | successMsg?: ReactNode; 15 | }> 16 | > = ({ cancel, submit, arguments: { title, message, confirmMsg, cancelMsg, successMsg } }) => { 17 | const { t } = useTranslation(); 18 | const { notify } = useNotifications(); 19 | 20 | return ( 21 | cancel()} 24 | doNotPreserveData 25 | onSubmit={() => { 26 | submit({}); 27 | notify({ message: successMsg, type: "success" }); 28 | }} 29 | > 30 | <>{message} 31 | <> 32 | 35 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export default ConfirmModal; 44 | -------------------------------------------------------------------------------- /packages/gephi-lite/src/views/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { FC, PropsWithChildren } from "react"; 2 | 3 | import { Modals } from "../../components/modals"; 4 | import Notifications from "../../components/notifications"; 5 | 6 | export const Layout: FC = ({ children }) => { 7 | return ( 8 |
9 |
{children}
10 | 11 | 12 |
13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/gephi-lite/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["./src", "./types"], 4 | "compilerOptions": { 5 | "jsx": "react-jsx", 6 | "types": ["vite/client"], 7 | "esModuleInterop": true, 8 | "typeRoots": ["node_modules", "../../node_modules"], 9 | "plugins": [ 10 | { 11 | "transform": "typia/lib/transform" 12 | } 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/gephi-lite/types/locale-emoji.d.ts: -------------------------------------------------------------------------------- 1 | declare module "locale-emoji"; 2 | -------------------------------------------------------------------------------- /packages/gephi-lite/types/react-tether.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-tether" { 2 | import ReactTether from "react-tether/lib/react-tether.d.ts"; 3 | export default ReactTether; 4 | } 5 | -------------------------------------------------------------------------------- /packages/gephi-lite/types/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.svg?react" { 2 | import type { FunctionComponent, SVGProps } from "react"; 3 | export const ReactComponent: FunctionComponent>; 4 | export default ReactComponent; 5 | } 6 | -------------------------------------------------------------------------------- /packages/gephi-lite/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /packages/gephi-lite/vite.config.mts: -------------------------------------------------------------------------------- 1 | import UnpluginTypia from "@ryoppippi/unplugin-typia/vite"; 2 | import react from "@vitejs/plugin-react-swc"; 3 | import checker from "vite-plugin-checker"; 4 | import svgr from "vite-plugin-svgr"; 5 | import { defineConfig } from "vitest/config"; 6 | 7 | import { BASE_URL } from "../../config"; 8 | 9 | export default defineConfig({ 10 | base: BASE_URL, 11 | plugins: [ 12 | UnpluginTypia({}), 13 | react(), 14 | svgr(), 15 | checker({ 16 | typescript: { 17 | buildMode: true, 18 | }, 19 | eslint: { 20 | useFlatConfig: true, 21 | lintCommand: "eslint src --max-warnings=0", 22 | }, 23 | }), 24 | ], 25 | resolve: { 26 | alias: { 27 | global: "window", 28 | "node-fetch": "isomorphic-fetch", 29 | }, 30 | }, 31 | test: { 32 | root: ".", 33 | globals: true, 34 | exclude: ["e2e", "node_modules"], 35 | }, 36 | build: { 37 | outDir: "build", 38 | }, 39 | server: { 40 | open: false, 41 | host: process.env.VITE_HOST || "localhost", 42 | allowedHosts: process.env.VITE_ALLOWED_HOSTS?.split(","), 43 | proxy: { 44 | "^/_github/*": { 45 | target: "https://github.com", 46 | changeOrigin: true, 47 | rewrite: (path) => path.replace(/^\/_github/, ""), 48 | }, 49 | }, 50 | }, 51 | }); 52 | -------------------------------------------------------------------------------- /packages/gephi-lite/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | browser: { 7 | provider: "playwright", 8 | instances: [ 9 | { 10 | browser: "chromium", 11 | }, 12 | ], 13 | enabled: true, 14 | headless: true, 15 | }, 16 | }, 17 | optimizeDeps: { 18 | exclude: ["chromium-bidi"], 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /packages/sdk/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /packages/sdk/.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | node_modules 3 | src 4 | tsconfig.json 5 | -------------------------------------------------------------------------------- /packages/sdk/README.md: -------------------------------------------------------------------------------- 1 | # @gephi/gephi-lite-sdk 2 | 3 | The package [@gephi/gephi-lite-sdk](https://www.npmjs.com/package/@gephi/gephi-lite-sdk) contains various TypeScript typings and utils for [Gephi Lite](https://github.com/gephi/gephi-lite). 4 | 5 | > [!WARNING] 6 | > Use this carefully! 7 | > It is not stable, as for now, its main purpose was to make typings available outside Gephi Lite to allow to develop [@gephi/gephi-lite-broadcast](../broadcast). 8 | > Its scope and functional boundaries (what should be in and out) is still to be determined. 9 | -------------------------------------------------------------------------------- /packages/sdk/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@gephi/gephi-lite-sdk", 3 | "description": "Gephi Lite core types and utils functions", 4 | "version": "0.6.2", 5 | "main": "dist/gephi-gephi-lite-sdk.cjs.js", 6 | "module": "dist/gephi-gephi-lite-sdk.esm.js", 7 | "files": [ 8 | "/dist" 9 | ], 10 | "scripts": { 11 | "test": "vitest run src" 12 | }, 13 | "sideEffects": false, 14 | "homepage": "https://gephi.org/gephi-lite", 15 | "bugs": "http://github.com/jacomyal/sigma.js/issues", 16 | "repository": { 17 | "type": "git", 18 | "url": "http://github.com/jacomyal/sigma.js.git", 19 | "directory": "packages/template" 20 | }, 21 | "preconstruct": { 22 | "entrypoints": [ 23 | "index.ts" 24 | ] 25 | }, 26 | "peerDependencies": { 27 | "graphology": "^0.25.4" 28 | }, 29 | "devDependencies": { 30 | "graphology-types": "^0.24.8" 31 | }, 32 | "license": "gpl-3.0", 33 | "exports": { 34 | ".": { 35 | "module": "./dist/gephi-gephi-lite-sdk.esm.js", 36 | "import": "./dist/gephi-gephi-lite-sdk.cjs.mjs", 37 | "default": "./dist/gephi-gephi-lite-sdk.cjs.js" 38 | }, 39 | "./package.json": "./package.json" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/sdk/src/appearance/index.ts: -------------------------------------------------------------------------------- 1 | import { gephiLiteParse, gephiLiteStringify } from "../utils"; 2 | import { type AppearanceState } from "./types"; 3 | 4 | export * from "./types"; 5 | 6 | export const DEFAULT_NODE_COLOR = "#999999"; 7 | export const DEFAULT_EDGE_COLOR = "#cccccc"; 8 | export const DEFAULT_NODE_SIZE = 20; 9 | export const DEFAULT_EDGE_SIZE = 6; 10 | export const DEFAULT_NODE_LABEL_SIZE = 14; 11 | export const DEFAULT_EDGE_LABEL_SIZE = 14; 12 | export const DEFAULT_BACKGROUND_COLOR = "#FFFFFF00"; 13 | export const DEFAULT_LAYOUT_GRID_COLOR = "#666666"; 14 | export const DEFAULT_SHADING_COLOR = "#ffffff"; 15 | 16 | export function getEmptyAppearanceState(): AppearanceState { 17 | return { 18 | showEdges: { 19 | value: true, 20 | }, 21 | nodesSize: { 22 | type: "data", 23 | }, 24 | edgesSize: { 25 | type: "data", 26 | }, 27 | backgroundColor: DEFAULT_BACKGROUND_COLOR, 28 | layoutGridColor: DEFAULT_LAYOUT_GRID_COLOR, 29 | nodesColor: { 30 | type: "data", 31 | }, 32 | edgesColor: { 33 | type: "data", 34 | }, 35 | nodesLabel: { 36 | type: "data", 37 | }, 38 | edgesLabel: { 39 | type: "data", 40 | }, 41 | nodesLabelSize: { 42 | type: "fixed", 43 | value: DEFAULT_NODE_LABEL_SIZE, 44 | zoomCorrelation: 0, 45 | density: 1, 46 | }, 47 | edgesLabelSize: { 48 | type: "fixed", 49 | value: DEFAULT_EDGE_LABEL_SIZE, 50 | zoomCorrelation: 0, 51 | density: 1, 52 | }, 53 | nodesLabelEllipsis: { 54 | type: "ellipsis", 55 | enabled: false, 56 | maxLength: 25, 57 | }, 58 | edgesLabelEllipsis: { 59 | type: "ellipsis", 60 | enabled: false, 61 | maxLength: 25, 62 | }, 63 | nodesImage: { 64 | type: "none", 65 | }, 66 | edgesZIndex: { 67 | type: "none", 68 | }, 69 | }; 70 | } 71 | 72 | /** 73 | * Appearance lifecycle helpers (state serialization / deserialization): 74 | */ 75 | export function serializeAppearanceState(appearance: AppearanceState): string { 76 | return gephiLiteStringify(appearance); 77 | } 78 | export function parseAppearanceState(rawAppearance: string): AppearanceState | null { 79 | try { 80 | // TODO: 81 | // Validate the actual data 82 | return { ...getEmptyAppearanceState(), ...gephiLiteParse(rawAppearance) }; 83 | } catch (_e) { 84 | return null; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /packages/sdk/src/filters/index.ts: -------------------------------------------------------------------------------- 1 | import { gephiLiteParse, gephiLiteStringify } from "../utils"; 2 | import { FiltersState } from "./types"; 3 | 4 | export * from "./types"; 5 | 6 | /** 7 | * Returns an empty filters state: 8 | */ 9 | export function getEmptyFiltersState(): FiltersState { 10 | return { 11 | past: [], 12 | future: [], 13 | }; 14 | } 15 | 16 | /** 17 | * Filters lifecycle helpers (state serialization / deserialization): 18 | */ 19 | export function serializeFiltersState(filters: FiltersState): string { 20 | return gephiLiteStringify(filters); 21 | } 22 | export function parseFiltersState(rawFilters: string): FiltersState | null { 23 | try { 24 | // TODO: 25 | // Validate the actual data 26 | return gephiLiteParse(rawFilters); 27 | } catch (e) { 28 | console.error(e); 29 | return null; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/sdk/src/filters/types.ts: -------------------------------------------------------------------------------- 1 | import { DatalessGraph, FullGraph, ItemData, ItemDataField, ItemType } from "../graph"; 2 | 3 | export interface BaseFilter { 4 | type: string; 5 | itemType: ItemType; 6 | field: ItemDataField; 7 | } 8 | 9 | export type RangeFilterType = BaseFilter & { 10 | type: "range"; 11 | itemType: ItemType; 12 | keepMissingValues?: boolean; 13 | } & { min?: number; max?: number }; 14 | 15 | export interface TermsFilterType extends BaseFilter { 16 | type: "terms"; 17 | itemType: ItemType; 18 | terms?: Set; 19 | keepMissingValues?: boolean; 20 | } 21 | 22 | export interface ScriptFilterType extends Omit { 23 | type: "script"; 24 | script?: (itemID: string, attributes: ItemData, fullGraph: FullGraph) => boolean; 25 | } 26 | 27 | export interface TopologicalFilterType extends Omit { 28 | type: "topological"; 29 | topologicalFilterId: string; 30 | parameters: unknown[]; 31 | } 32 | 33 | export type FilterType = RangeFilterType | TermsFilterType | TopologicalFilterType | ScriptFilterType; 34 | 35 | export interface FilteredGraph { 36 | filterFingerprint: string; 37 | graph: DatalessGraph; 38 | } 39 | 40 | export interface FiltersState { 41 | past: FilterType[]; 42 | future: FilterType[]; 43 | } 44 | -------------------------------------------------------------------------------- /packages/sdk/src/graph/index.ts: -------------------------------------------------------------------------------- 1 | import { MultiGraph } from "graphology"; 2 | 3 | import { gephiLiteParse, gephiLiteStringify } from "../utils"; 4 | import { GraphDataset, SerializedGraphDataset } from "./types"; 5 | 6 | export * from "./types"; 7 | 8 | export function getEmptyGraphDataset(): GraphDataset { 9 | return { 10 | nodeRenderingData: {}, 11 | edgeRenderingData: {}, 12 | nodeData: {}, 13 | edgeData: {}, 14 | metadata: { type: "mixed" }, 15 | nodeFields: [], 16 | edgeFields: [], 17 | fullGraph: new MultiGraph(), 18 | }; 19 | } 20 | 21 | export function serializeDataset(dataset: GraphDataset): SerializedGraphDataset; 22 | export function serializeDataset(dataset: Partial): Partial; 23 | export function serializeDataset( 24 | dataset: Partial | GraphDataset, 25 | ): SerializedGraphDataset | Partial { 26 | return dataset.fullGraph 27 | ? { ...dataset, fullGraph: dataset.fullGraph.export() } 28 | : (dataset as Omit, "fullGraph">); 29 | } 30 | export function deserializeDataset(dataset: SerializedGraphDataset): GraphDataset; 31 | export function deserializeDataset(dataset: Partial): Partial; 32 | export function deserializeDataset( 33 | dataset: SerializedGraphDataset | Partial, 34 | ): Partial | GraphDataset { 35 | if (!dataset.fullGraph) return dataset as Omit, "fullGraph">; 36 | 37 | const fullGraph = new MultiGraph(); 38 | fullGraph.import(dataset.fullGraph); 39 | 40 | return { 41 | ...dataset, 42 | fullGraph, 43 | }; 44 | } 45 | 46 | export function datasetToString(dataset: GraphDataset): string { 47 | return gephiLiteStringify(serializeDataset(dataset)); 48 | } 49 | export function parseDataset(rawDataset: string): GraphDataset | null { 50 | try { 51 | // TODO: 52 | // Validate the actual data 53 | return deserializeDataset(gephiLiteParse(rawDataset) as SerializedGraphDataset); 54 | } catch (e) { 55 | console.error(e); 56 | return null; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/sdk/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./utils"; 2 | 3 | export * from "./graph"; 4 | export * from "./appearance"; 5 | export * from "./filters"; 6 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/casting.ts: -------------------------------------------------------------------------------- 1 | import { SCALAR_TYPES, Scalar } from "../graph"; 2 | 3 | export function toScalar(o: unknown): Scalar { 4 | if (SCALAR_TYPES.has(typeof o)) return o as Scalar; 5 | 6 | // some special cases 7 | if (o instanceof Date) return o.toString(); 8 | if (o instanceof Object) return JSON.stringify(o); 9 | 10 | if (o === null) return undefined; 11 | // fallback to toString 12 | return `${o}`; 13 | } 14 | 15 | export function toNumber(o: unknown): number | undefined { 16 | if (typeof o === "number" && !isNaN(o)) return o; 17 | if (typeof o === "string") { 18 | const n = +o; 19 | if (!isNaN(n)) return n; 20 | } 21 | 22 | return undefined; 23 | } 24 | 25 | export function toString(o: Scalar): string | undefined { 26 | if (typeof o === "string") return o; 27 | if (typeof o === "number") return o + ""; 28 | if (typeof o === "boolean") return o.toString(); 29 | return undefined; 30 | } 31 | 32 | export function notEmpty(value: TValue | null | undefined): value is TValue { 33 | return value !== null && value !== undefined; 34 | } 35 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./json"; 2 | export * from "./casting"; 3 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/json.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { gephiLiteParse, gephiLiteStringify } from "./json"; 4 | 5 | const CLASSIC_DATASET = { 6 | name: "John Doe", 7 | age: 42, 8 | isHuman: true, 9 | links: [{ url: "http://somewhere.com", label: "somewhere" }], 10 | favoriteMovie: null, 11 | }; 12 | const CLASSIC_DATASET_STRING = 13 | '{"name":"John Doe","age":42,"isHuman":true,"links":[{"url":"http://somewhere.com","label":"somewhere"}],"favoriteMovie":null}'; 14 | 15 | const SETS_DATASET = { 16 | abc: { 17 | numbersSet: new Set([123, 456, 789]), 18 | stringsSet: new Set(["abc", "def", "ghi"]), 19 | objectsSet: new Set([{ abc: 123 }, [false, true]]), 20 | }, 21 | }; 22 | const SETS_DATASET_STRING = 23 | '{"abc":{"numbersSet":["<>"],"stringsSet":["<>"],"objectsSet":["<>"]}}'; 24 | 25 | describe("JSON utilities", () => { 26 | describe("#parse and #stringify together", () => { 27 | it("should work with 'classic' data", () => { 28 | expect(gephiLiteParse(gephiLiteStringify(CLASSIC_DATASET))).toEqual(CLASSIC_DATASET); 29 | }); 30 | 31 | it("should work with sets", () => { 32 | expect(gephiLiteParse(gephiLiteStringify(SETS_DATASET))).toEqual(SETS_DATASET); 33 | }); 34 | }); 35 | 36 | describe("#stringify", () => { 37 | it("should work with 'classic' data", () => { 38 | expect(gephiLiteStringify(CLASSIC_DATASET)).toBe(CLASSIC_DATASET_STRING); 39 | }); 40 | 41 | it("should work with sets", () => { 42 | expect(gephiLiteStringify(SETS_DATASET)).toBe(SETS_DATASET_STRING); 43 | }); 44 | }); 45 | 46 | describe("#parse", () => { 47 | it("should work with 'classic' data", () => { 48 | expect(gephiLiteParse(CLASSIC_DATASET_STRING)).toEqual(CLASSIC_DATASET); 49 | }); 50 | 51 | it("should work with sets", () => { 52 | expect(gephiLiteParse(SETS_DATASET_STRING)).toEqual(SETS_DATASET); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/sdk/src/utils/json.ts: -------------------------------------------------------------------------------- 1 | import Graph from "graphology"; 2 | import { SerializedGraph } from "graphology-types"; 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | export function deserializer(_: string, value: any): any { 6 | if (Array.isArray(value) && value.length === 3 && value[0] === "<>") { 7 | return new Set(gephiLiteParse(value[1])); 8 | } 9 | if (Array.isArray(value) && value.length === 3 && value[0] === "<>") { 10 | return new Function(`return ${value[1]}`)(); 11 | } 12 | if (value && typeof value === "object" && "nodes" in value && "edges" in value) { 13 | return Graph.from(value as SerializedGraph); 14 | } 15 | return value; 16 | } 17 | 18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 19 | export function serializer(_: string, value: unknown): any { 20 | if (value instanceof Set) { 21 | return ["<>"]; 22 | } 23 | if (value instanceof Function) { 24 | return ["<>"]; 25 | } 26 | return value; 27 | } 28 | 29 | /** 30 | * Use the following functions to serialize/deserialize data structures that may 31 | * include Sets, serializable functions & graph. 32 | */ 33 | export function gephiLiteStringify(value: unknown): string { 34 | return JSON.stringify(value, serializer); 35 | } 36 | 37 | /** 38 | * Deserialize JSON data in JS, using custom reviver. 39 | */ 40 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 41 | export function gephiLiteParse(value: string): T { 42 | return JSON.parse(value, deserializer) as T; 43 | } 44 | -------------------------------------------------------------------------------- /packages/sdk/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "useDefineForClassFields": true, 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "module": "ESNext", 8 | "skipLibCheck": true, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "strict": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "declaration": true 20 | }, 21 | "include": ["src"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/sdk/vitest.config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | browser: { 7 | provider: "playwright", 8 | instances: [ 9 | { 10 | browser: "chromium", 11 | }, 12 | ], 13 | enabled: true, 14 | headless: true, 15 | }, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "strictFunctionTypes": false, 17 | "noEmit": true, 18 | "typeRoots": ["node_modules"], 19 | "strictNullChecks": true 20 | }, 21 | "watchOptions": { 22 | "excludeDirectories": ["**/node_modules"] 23 | }, 24 | "files": [], 25 | "references": [ 26 | { 27 | "path": "./packages/sdk" 28 | }, 29 | { 30 | "path": "./packages/broadcast" 31 | }, 32 | { 33 | "path": "./packages/gephi-lite" 34 | } 35 | ] 36 | } 37 | --------------------------------------------------------------------------------