完成世界书、骰子、apiconfig页面处理

This commit is contained in:
2026-04-30 01:35:10 +08:00
parent a3e3711b2b
commit ba9b925c32
4602 changed files with 785225 additions and 23 deletions

View File

@@ -0,0 +1,29 @@
**Cross-references to related issues.** If there is no existing issue that describes your bug or feature request, then [create an issue](https://github.com/cytoscape/cytoscape.js/issues/new/choose) before making your pull request.
Associated issues:
- #X
- #Y
- #Z
**Notes re. the content of the pull request.** Give context to reviewers or serve as a general record of the changes made. Add a screenshot or video to demonstrate your new feature, if possible.
- This PR adds this.
- This PR does that.
- This PR allows us to do some other thing.
**Checklist**
Author:
- [ ] The proper base branch has been selected. New features go on `unstable`. Bug-fix patches can go on either `unstable` or `master`.
- [ ] Automated tests have been included in this pull request, if possible, for the new feature(s) or bug fix. Check this box if tests are not pragmatically possible (e.g. rendering features could include screenshots or videos instead of automated tests).
- [ ] The associated GitHub issues are included (above).
- [ ] Notes have been included (above).
- [ ] For new or updated API, the `index.d.ts` Typescript definition file has been appropriately updated.
Reviewers:
- [ ] All automated checks are passing (green check next to latest commit).
- [ ] At least one reviewer has signed off on the pull request.
- [ ] For bug fixes: Just after this pull request is merged, it should be applied to both the `master` branch and the `unstable` branch. Normally, this just requires cherry-picking the corresponding merge commit from `master` to `unstable` -- or vice versa.

View File

@@ -0,0 +1,108 @@
# Patch/Backport Release GitHub Action - README
## Introduction
The "Patch Release Test" GitHub Action automates the process of creating a patch release for the [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) repository. This action allows you to define the target branch for the patch release and performs various tasks, including version updating, testing, publishing to npmjs and GitHub Releases, deploying to GitHub Pages, and more.
## Prerequisites
Before using the "Patch Release Test" GitHub Action, ensure you have the following prerequisites:
1. Access to the [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) repository.
2. Necessary access tokens and secrets stored as GitHub repository secrets:
- `NPM_TOKEN`: Token for npmjs package publishing. Ref: [How to create legacy token in npm](https://docs.npmjs.com/creating-and-viewing-access-tokens#creating-legacy-tokens-on-the-website)
- `MAIN_GH_TOKEN`: Token for accessing GitHub API to publish GitHub Releases on Cytoscape/Cytoscape.js repo. Ref: [Create fine-grained-personal-access-tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens)
## Usage
1. Navigate to the [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) repository.
2. Go to the "Actions" tab.
3. Click on the "Patch Release Test" workflow.
4. Click the "Run workflow" button.
5. Provide the target branch name for the patch release when prompted.
6. For backport release: Make a corresponding patch release.
## Workflow Steps
The "Patch Release Test" GitHub Action comprises the following steps:
1. **Get Branch**:
- Action: Retrieves the target branch for the patch release.
- Script: Sets the `BRANCH` environment variable based on user input.
- Uses the `github.event.inputs.branch` input for branch selection.
2. **Checkout Patch Branch**:
- Action: Checks out the specified patch branch.
- Uses: `actions/checkout@v3`.
- Branch: The branch specified by the `BRANCH` environment variable.
3. **Setup Node.js Environment**:
- Action: Sets up Node.js environment for the workflow.
- Uses: `actions/setup-node@v3`.
- Node Version: 18.
- Caches npm packages.
4. **Get New Version String**:
- Action: Determines the new version for the patch release.
- Script: Retrieves the new version from a script.
- Uses a custom script to calculate the new version.
5. **See Patch Branch**:
- Action: Displays the selected branch for the patch release.
- Command: Outputs the branch stored in the `BRANCH` environment variable.
6. **See New Patch Version**:
- Action: Displays the calculated version for the patch release.
- Command: Outputs the calculated version using the `VERSION` environment variable.
7. **Checkout Master Branch**:
- Action: Checks out the `master` branch.
- Uses: `actions/checkout@v3`.
8. **Update Version on Master**:
- Action: Updates the `versions.json` file on the `master` branch.
- Script: Uses `jq` to add the new version to the `versions.json` file.
- Commits and pushes the updated `versions.json` file.
9. **Checkout Patch Branch Again**:
- Action: Checks out the specified patch branch.
- Uses: `actions/checkout@v3`.
- Branch: The branch specified by the `BRANCH` environment variable.
10. **Update Version on Unstable**:
- Action: Updates the `versions.json` file on the `unstable` branch.
- Script: Uses `jq` to add the new version to the `versions.json` file.
- Commits and pushes the updated `versions.json` file.
- Checks out the original patch branch again.
11. **Install Dependencies**:
- Action: Installs project dependencies.
- Command: `npm install`.
12. **Run Tests**:
- Action: Executes tests for the project.
- Command: `npm test`.
13. **Set Git Config**:
- Action: Configures Git with user information.
- Sets the user name and email based on the GitHub actor.
14. **Pre Release Tests**:
- Action: Executes pre-release tests.
- Uses a custom script to run pre-release tests on the specified branch.
15. **Archive Code Coverage Results**:
- Action: Archives code coverage results in case of test failure.
- Uses: `actions/upload-artifact@v3`.
16. **Publish Package to npmjs**:
- Action: Publishes the package to npmjs.
- Command: `npm publish`.
17. **Publish Package to GitHub Releases**:
- Action: Publishes the package to GitHub Releases.
- Uses GitHub API to create a release with provided information.
18. **Deploy to Github Pages**:
- Action: Deploys documentation to GitHub Pages.
- Uses: `JamesIves/github-pages-deploy-action@v4`.

View File

@@ -0,0 +1,115 @@
name: Patch Release
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to run the action on'
required: true
default: 'master'
jobs:
patch-release:
runs-on: ubuntu-latest
environment: prod
steps:
- name: Extract branch name
shell: bash
id: extract_branch
run: |
echo "BRANCH=${{ github.event.inputs.branch }}" >> $GITHUB_ENV
echo "Branch: " $BRANCH
- name: checkout patch branch
uses: actions/checkout@v3
with:
ref: ${{ env.BRANCH }}
- uses: actions/setup-node@v3
with:
node-version: 22
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm install
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run tests
run : |
npm test
- name: Get new version string
id: get_new_patch_version
run: |
chmod +rx ./.github/workflows/scripts/new-patch-version.sh
. ./.github/workflows/scripts/new-patch-version.sh
shell: bash
- name: See patch branch
run: echo branch ${{ env.BRANCH }}
shell: bash
- name: See new patch version
run: echo "# version:" ${{ env.VERSION }}
shell: bash
- name: checkout master branch
uses: actions/checkout@v3
with:
ref: master
fetch-depth: 0
- name: Set Git Config
run : |
# Set git configs
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- name: Update Version on master
id: update_backport_version_master
run: |
jq --arg ver "${{ env.VERSION }}" '.versions += [$ver]' ./documentation/versions.json >> /tmp/temp.json
mv /tmp/temp.json ./documentation/versions.json
git add . && git commit -m "Docs: Add ${{ env.VERSION }} to versions.json"
git push
- name: checkout unstable branch
uses: actions/checkout@v3
with:
ref: unstable
fetch-depth: 0
- name: Update Version on unstable
id: update_backport_version_unstable
run: |
jq --arg ver "${{ env.VERSION }}" '.versions += [$ver]' ./documentation/versions.json >> /tmp/temp.json
mv /tmp/temp.json ./documentation/versions.json
git add . && git commit -m "Docs: Add ${{ env.VERSION }} to versions.json"
git push
git checkout ${{ env.BRANCH }}
- name: Set Git Config
run : |
# Set git configs
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
- name: Make Release
id: release
run: |
chmod +rx .github/workflows/scripts/pre_release_test.sh
. .github/workflows/scripts/pre_release_test.sh ${{ env.BRANCH }}
- name: Archive action failure results
uses: actions/upload-artifact@v4
if: ${{ failure() && steps.release.conclusion == 'failure' }}
with:
name: npm-release--failure-report
path: /home/runner/.npm/_logs/
- name: Publish Package To npmjs
run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish Package to GitHub Releases
run: |
curl -L \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.MAIN_GH_TOKEN }}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/${{ github.repository }}/releases \
-d '{"tag_name":"v${{ env.VERSION }}","target_commitish":"master","name":"v${{ env.VERSION }}","body":"Release version v${{ env.VERSION }}","draft":false,"prerelease":false,"generate_release_notes":false}'
- name: Deploy to Github Pages 🚀
if: ${{ env.BRANCH == 'master' }}
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: documentation

173
frontend/node_modules/cytoscape/README.md generated vendored Normal file
View File

@@ -0,0 +1,173 @@
<img style="width: 200px; height: 200px;" src="https://raw.githubusercontent.com/cytoscape/cytoscape.js/unstable/documentation/img/cytoscape-logo.png" width="200" height="200">
[![GitHub repo](https://img.shields.io/badge/Repo-GitHub-yellow.svg)](https://github.com/cytoscape/cytoscape.js)
[![Ask a question with Phind](https://img.shields.io/badge/Get%20help-Phind-yellow.svg)](https://www.phind.com/search?q=I%27m%20using%20the%20Cytoscape.js%20graph%20theory%20JS%20library.%20How%20do%20I%20create%20a%20graph%20in%20my%20HTML%20page)
[![News and tutorials](https://img.shields.io/badge/News%20%26%20tutorials-Blog-yellow.svg)](https://blog.js.cytoscape.org)
[![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://raw.githubusercontent.com/cytoscape/cytoscape.js/master/LICENSE)
[![npm](https://img.shields.io/npm/v/cytoscape.svg)](https://www.npmjs.com/package/cytoscape)
[![DOI](https://zenodo.org/badge/2255947.svg)](https://zenodo.org/badge/latestdoi/2255947)
[![npm installs](https://img.shields.io/npm/dm/cytoscape.svg?label=npm%20installs)](https://www.npmjs.com/package/cytoscape)
[![Automated tests](https://github.com/cytoscape/cytoscape.js/actions/workflows/tests.yml/badge.svg)](https://github.com/cytoscape/cytoscape.js/actions/workflows/tests.yml)
[![Extensions](https://img.shields.io/badge/Extensions-70-brightgreen.svg)](https://js.cytoscape.org/#extensions)
[![Cloudflare](https://img.shields.io/badge/Powered%20by-Cloudflare-orange.svg)](https://cloudflare.com)
Created at the [University of Toronto](https://utoronto.ca) and published in [Oxford Bioinformatics](https://js.cytoscape.org/#introduction/citation) ([2016](https://academic.oup.com/bioinformatics/article/32/2/309/1744007), [2023](https://academic.oup.com/bioinformatics/article/39/1/btad031/6988031)). <br />
Authored by: [Max Franz](https://github.com/maxkfranz), [Christian Lopes](https://github.com/chrtannus), [Dylan Fong](https://github.com/d2fong), [Mike Kucera](https://github.com/mikekucera), ..., [Gary Bader](https://baderlab.org)
# Cytoscape.js
Graph theory (network) library for visualisation and analysis : [https://js.cytoscape.org](https://js.cytoscape.org)
## Description
Cytoscape.js is a fully featured [graph theory](https://en.wikipedia.org/wiki/Graph_theory) library. Do you need to model and/or visualise relational data, like biological data or social networks? If so, Cytoscape.js is just what you need.
Cytoscape.js contains a graph theory model and an optional renderer to display interactive graphs. This library was designed to make it as easy as possible for programmers and scientists to use graph theory in their apps, whether it's for server-side analysis in a Node.js app or for a rich user interface.
You can get started with Cytoscape.js with one line:
```js
var cy = cytoscape({ elements: myElements, container: myDiv });
```
Learn more about the features of Cytoscape.js by reading [its documentation](https://js.cytoscape.org).
## Example
The Tokyo railway stations network can be visualised with Cytoscape:
<img style="width: 300px; height: 126px;" src="https://raw.githubusercontent.com/cytoscape/cytoscape.js/unstable/documentation/img/tokyo-big.png" width="300" height="126">
<img style="width: 300px; height: 126px;" src="https://raw.githubusercontent.com/cytoscape/cytoscape.js/unstable/documentation/img/tokyo-big-zoomed-in.png" width="300" height="126">
A [live demo](https://js.cytoscape.org/demos/tokyo-railways/) and [source code](https://github.com/cytoscape/cytoscape.js/tree/master/documentation/demos/tokyo-railways) are available for the Tokyo railway stations graph. More demos are available in the [documentation](https://js.cytoscape.org/#demos).
## Documentation
You can find the documentation and downloads on the [project website](https://js.cytoscape.org).
## Roadmap
Future versions of Cytoscape.js are planned in the [milestones of the Github issue tracker](https://github.com/cytoscape/cytoscape.js/milestones). You can use the milestones to see what's currently planned for future releases.
## Contributing to Cytoscape.js
Would you like to become a Cytoscape.js contributor? You can contribute in technical roles (e.g. features, testing) or non-technical roles (e.g. documentation, outreach), depending on your interests. [Get in touch with us by posting a GitHub discussion](https://github.com/cytoscape/cytoscape.js/discussions).
For the mechanics of contributing a pull request, refer to [CONTRIBUTING.md](CONTRIBUTING.md).
Feature releases are made monthly, while patch releases are made weekly. This allows for rapid releases of first- and third-party contributions.
## Citation
To cite Cytoscape.js in a paper, please cite the Oxford Bioinformatics issue:
*Cytoscape.js: a graph theory library for visualisation and analysis*
Franz M, Lopes CT, Huck G, Dong Y, Sumer O, Bader GD
[Bioinformatics (2016) 32 (2): 309-311 first published online September 28, 2015 doi:10.1093/bioinformatics/btv557](https://bioinformatics.oxfordjournals.org/content/32/2/309) [(PDF)](http://bioinformatics.oxfordjournals.org/content/32/2/309.full.pdf)
- [PubMed abstract for the original 2016 article](http://www.ncbi.nlm.nih.gov/pubmed/26415722)
- [PubMed abstract for the 2023 update article](https://pubmed.ncbi.nlm.nih.gov/36645249)
## Build dependencies
Install `node` and `npm`. Run `npm install` before using `npm run`.
## Build instructions
Run `npm run <target>` in the console. The main targets are:
**Building:**
* `build`: do all builds of the library (umd, min, umd, esm)
* `build:min` : do the unminified build with bundled dependencies (for simple html pages, good for novices)
* `build:umd` : do the umd (cjs/amd/globals) build
* `build:esm` : do the esm (ES 2015 modules) build
* `clean` : clean the `build` directory
* `docs` : build the docs into `documentation`
* `release` : build all release artifacts
* `watch` : automatically build lib for debugging (with sourcemap, no babel, very quick)
* good for general testing on `debug/index.html`
* served on `http://localhost:8080` or the first available port thereafter, with livereload on `debug/index.html`
* `watch:babel` : automatically build lib for debugging (with sourcemap, with babel, a bit slower)
* good for testing performance or for testing out of date browsers
* served on `http://localhost:8080` or the first available port thereafter, with livereload on `debug/index.html`
* `watch:umd` : automatically build prod umd bundle (no sourcemap, with babel)
* good for testing cytoscape in another project (with a `"cytoscape": "file:./path/to/cytoscape"` reference in your project's `package.json`)
* no http server
* `dist` : update the distribution js for npm etc.
**Testing:**
The default test scripts run directly against the source code. Tests can alternatively be run on a built bundle. The library can be built on `node>=6`, but the library's bundle can be tested on `node>=0.10`.
* `test` : run all testing & linting
* `test:js` : run the mocha tests on the public API of the lib (directly on source files)
* `npm run test:js -- -g "my test name"` runs tests on only the matching test cases
* `test:build` : run the mocha tests on the public API of the lib (on a built bundle)
* `npm run build` should be run beforehand on a recent version of node
* `npm run test:build -- -g "my test name"` runs build tests on only the matching test cases
* `test:modules` : run unit tests on private, internal API
* `npm run test:modules -- -g "my test name"` runs modules tests on only the matching test cases
* `lint` : lint the js sources via eslint
* `benchmark` : run all benchmarks
* `benchmark:single` : run benchmarks only for the suite specified in `benchmark/single`
## Release instructions
### Background
- Ensure that a milestone exists for the release you want to make, with all the issues for that release assigned in the milestone.
- Bug fixes should be applied to both the `master` and `unstable` branches. PRs can go on either branch, with the patch applied to the other branch after merging.
- When a patch release is made concurrently with a feature release, the patch release should be made first. Wait 5 minutes after the patch release completes before starting the feature release -- otherwise Zenodo doesn't pick up releases properly.
### Patch version
1. Go to [Actions > Patch release](https://github.com/cytoscape/cytoscape.js/actions/workflows/patch-release.yml)
1. Go to the 'Run workflow' dropdown
1. [Optional] The 'master' branch should be preselected for you
1. Press the green 'Run workflow' button
1. Close the milestone for the release
<img style="width: 300px; height: auto;" src="https://raw.githubusercontent.com/cytoscape/cytoscape.js/unstable/documentation/img/preview-patch.png" width="300">
### Feature version
1. Go to [Actions > Feature release](https://github.com/cytoscape/cytoscape.js/actions/workflows/feature-release.yml)
1. Go to the 'Run workflow' dropdown
1. [Optional] The 'unstable' branch should be preselected for you
1. Press the green 'Run workflow' button
1. Close the milestone for the release
1. Make the release announcement [on the blog](https://github.com/cytoscape/cytoscape.js-blog)
<img style="width: 300px; height: auto;" src="https://raw.githubusercontent.com/cytoscape/cytoscape.js/unstable/documentation/img/preview-feature.png" width="300">
### Notes on GitHub Actions UI
- 'Use workflow from' in the GitHub UI selects the branch from which the workflow YML file is selected. Since the workflow files should usually be the same on the master and unstable branches, it shouldn't matter what's selected.
- 'Branch to run the action on' in the GitHub UI is preselected for you. You don't need to change it.
## Tests
Mocha tests are found in the [test directory](https://github.com/cytoscape/cytoscape.js/tree/master/test). The tests can be run in the browser or they can be run via Node.js (`npm run test:js`).

File diff suppressed because one or more lines are too long

35580
frontend/node_modules/cytoscape/dist/cytoscape.esm.mjs generated vendored Normal file

File diff suppressed because one or more lines are too long

143
frontend/node_modules/cytoscape/package.json generated vendored Normal file
View File

@@ -0,0 +1,143 @@
{
"name": "cytoscape",
"version": "3.33.1",
"license": "MIT",
"description": "Graph theory (a.k.a. network) library for analysis and visualisation",
"homepage": "http://js.cytoscape.org",
"repository": {
"type": "git",
"url": "https://github.com/cytoscape/cytoscape.js.git"
},
"bugs": {
"url": "https://github.com/cytoscape/cytoscape.js/issues"
},
"keywords": [
"graph",
"graph-theory",
"network",
"node",
"edge",
"vertex",
"link",
"analysis",
"visualisation",
"visualization",
"draw",
"render",
"biojs",
"cytoscape"
],
"engines": {
"node": ">=0.10"
},
"types": "index.d.ts",
"main": "dist/cytoscape.cjs.js",
"module": "dist/cytoscape.esm.mjs",
"exports": {
".": {
"import": "./dist/cytoscape.esm.mjs",
"require": "./dist/cytoscape.cjs.js",
"types": "./index.d.ts"
},
"./dist/cytoscape.esm.mjs": {
"import": "./dist/cytoscape.esm.mjs"
},
"./dist/cytoscape.esm": {
"import": "./dist/cytoscape.esm.mjs"
},
"./dist/cytoscape.esm.min.mjs": {
"import": "./dist/cytoscape.esm.min.mjs"
},
"./dist/cytoscape.esm.min": {
"import": "./dist/cytoscape.esm.min.mjs"
},
"./dist/cytoscape.cjs.js": {
"require": "./dist/cytoscape.cjs.js"
},
"./dist/cytoscape.umd.js": {
"require": "./dist/cytoscape.umd.js"
},
"./dist/cytoscape.min.js": {
"require": "./dist/cytoscape.min.js"
}
},
"unpkg": "dist/cytoscape.min.js",
"jsdelivr": "dist/cytoscape.min.js",
"scripts": {
"lint": "eslint src/**/*.mjs",
"build": "rollup -c",
"build:esm": "cross-env FILE=esm rollup -c",
"build:esm.min": "cross-env FILE=esm.min rollup -c",
"build:cjs": "cross-env FILE=cjs rollup -c",
"build:umd": "cross-env FILE=umd rollup -c",
"build:min": "cross-env FILE=min rollup -c",
"clean": "rimraf build/*",
"copyright": "node license-update.mjs",
"dist:copy": "cpy --flat build/cytoscape.umd.js build/cytoscape.min.js build/cytoscape.cjs.js build/cytoscape.esm.mjs build/cytoscape.esm.min.mjs dist",
"dist": "cross-env NODE_ENV=production run-s build dist:*",
"release": "run-s copyright dist docs",
"watch": "run-s watch:fast",
"watch:sync": "livereload \"build, debug\" -w 500",
"watch:http": "http-server -p 3333 -s -c -1 -o debug",
"watch:fast": "run-p watch:sync watch:http watch:build:fast",
"watch:umd": "run-p watch:sync watch:http watch:build:umd",
"watch:build:fast": "cross-env FILE=umd SOURCEMAPS=true BABEL=false NODE_ENV=development rollup -c -w",
"watch:build:umd": "cross-env FILE=umd SOURCEMAPS=true NODE_ENV=development rollup -c -w",
"watch:build:cjs": "cross-env FILE=cjs SOURCEMAPS=true NODE_ENV=development rollup -c -w",
"test": "run-s test:js test:modules test:playwright lint",
"test:js": "mocha --recursive",
"test:js:debug": "mocha inspect --recursive",
"test:build": "cross-env TEST_BUILD=true mocha",
"test:modules": "mocha test/modules",
"test:modules:debug": "mocha inspect test/modules",
"test:playwright:http": "http-server -p 3333 -s -c -1",
"test:playwright:build": "cross-env FILE=umd SOURCEMAPS=true BABEL=false NODE_ENV=development rollup -c",
"test:playwright:setup": "run-s test:playwright:build test:playwright:http",
"test:playwright": "playwright test",
"docs": "run-s docs:build docs:js",
"docs:js": "cpy --flat build/cytoscape.min.js documentation/js",
"docs:build": "node documentation/docmaker.mjs",
"docs:push": "gh-pages -d documentation",
"benchmark": "run-s benchmark:all",
"benchmark:download": "download https://raw.githubusercontent.com/cytoscape/cytoscape.js/master/dist/cytoscape.cjs.js --out build --filename cytoscape.benchmark.js",
"benchmark:all:exec": "node benchmark/all",
"benchmark:all": "run-s benchmark:download benchmark:all:exec",
"benchmark:single:exec": "node benchmark/single",
"benchmark:single": "run-s benchmark:download benchmark:single:exec"
},
"devDependencies": {
"@babel/core": "^7.26.0",
"@babel/preset-env": "^7.26.0",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.49.1",
"@rollup/plugin-babel": "^6.0.4",
"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-replace": "^6.0.2",
"@rollup/plugin-terser": "^0.4.4",
"@types/node": "^22.10.7",
"benchmark": "^2.1.4",
"bluebird": "^3.5.0",
"chai": "^5.1.2",
"cpy-cli": "^5.0.0",
"cross-env": "^7.0.0",
"download-cli": "^1.0.5",
"eslint": "^9.18.0",
"gh-pages": "^5.0.0",
"gl-matrix": "^3.4.3",
"globals": "^15.14.0",
"handlebars": "^4.7.6",
"heap": "^0.2.7",
"highlight.js": "^10.0.0",
"http-server": "^14.1.1",
"jsonlint": "^1.6.2",
"livereload": "^0.9.1",
"lodash": "^4.17.21",
"marked": "^4.0.10",
"mocha": "^11.1.0",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.0",
"rollup": "^4.31.0",
"rollup-plugin-license": "^3.5.3"
}
}

View File

@@ -0,0 +1,329 @@
// Implemented by Zoe Xi @zoexi for GSOC 2016
// https://github.com/cytoscape/cytoscape.js-affinity-propagation
// Implemented from the reference library: https://github.com/juhis/affinity-propagation
// Additional reference: http://www.psi.toronto.edu/affinitypropagation/faq.html
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
import clusteringDistance from './clustering-distances.mjs';
let defaults = util.defaults({
distance: 'euclidean', // distance metric to compare attributes between two nodes
preference: 'median', // suitability of a data point to serve as an exemplar
damping: 0.8, // damping factor between [0.5, 1)
maxIterations: 1000, // max number of iterations to run
minIterations: 100, // min number of iterations to run in order for clustering to stop
attributes: [ // functions to quantify the similarity between any two points
// e.g. node => node.data('weight')
]
});
let setOptions = function( options ) {
let dmp = options.damping;
let pref = options.preference;
if( !(0.5 <= dmp && dmp < 1) ){
util.error(`Damping must range on [0.5, 1). Got: ${dmp}`);
}
let validPrefs = ['median', 'mean', 'min', 'max'];
if( !( validPrefs.some(v => v === pref) || is.number(pref) ) ){
util.error(`Preference must be one of [${validPrefs.map( p => `'${p}'` ).join(', ')}] or a number. Got: ${pref}`);
}
return defaults( options );
};
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
var printMatrix = function( M ) { // used for debugging purposes only
let str = '';
let log = s => str = str + s + '\n';
let n = Math.sqrt(M.length);
for ( let i = 0; i < n; i++ ) {
let row = '';
for ( let j = 0; j < n; j++ ) {
row += M[i*n+j] + ' ';
}
log(row);
}
console.log(str);
};
} /* eslint-enable */
let getSimilarity = function( type, n1, n2, attributes ) {
let attr = (n, i) => attributes[i](n);
// nb negative because similarity should have an inverse relationship to distance
return -clusteringDistance( type, attributes.length, i => attr(n1, i), i => attr(n2, i), n1, n2 );
};
let getPreference = function( S, preference ) { // larger preference = greater # of clusters
let p = null;
if( preference === 'median' ){
p = math.median( S );
} else if( preference === 'mean' ){
p = math.mean( S );
} else if ( preference === 'min' ){
p = math.min( S );
} else if ( preference === 'max' ){
p = math.max( S );
} else { // Custom preference number, as set by user
p = preference;
}
return p;
};
let findExemplars = function( n, R, A ) {
let indices = [];
for ( let i = 0; i < n; i++ ) {
if ( R[i * n + i] + A[i * n + i] > 0 ) {
indices.push(i);
}
}
return indices;
};
let assignClusters = function( n, S, exemplars ) {
let clusters = [];
for ( let i = 0; i < n; i++ ) {
let index = -1;
let max = -Infinity;
for ( let ei = 0; ei < exemplars.length; ei++ ) {
let e = exemplars[ei];
if ( S[i * n + e] > max ) {
index = e;
max = S[i * n + e];
}
}
if( index > 0 ){
clusters.push(index);
}
}
for (let ei = 0; ei < exemplars.length; ei++) {
clusters[ exemplars[ei] ] = exemplars[ei];
}
return clusters;
};
let assign = function( n, S, exemplars ) {
let clusters = assignClusters( n, S, exemplars );
for ( let ei = 0; ei < exemplars.length; ei++ ) {
let ii = [];
for ( let c = 0; c < clusters.length; c++ ) {
if (clusters[c] === exemplars[ei]) {
ii.push(c);
}
}
let maxI = -1;
let maxSum = -Infinity;
for ( let i = 0; i < ii.length; i++ ) {
let sum = 0;
for ( let j = 0; j < ii.length; j++ ) {
sum += S[ii[j] * n + ii[i]];
}
if ( sum > maxSum ) {
maxI = i;
maxSum = sum;
}
}
exemplars[ei] = ii[maxI];
}
clusters = assignClusters( n, S, exemplars );
return clusters;
};
let affinityPropagation = function( options ) {
let cy = this.cy();
let nodes = this.nodes();
let opts = setOptions( options );
// Map each node to its position in node array
let id2position = {};
for( let i = 0; i < nodes.length; i++ ){
id2position[ nodes[i].id() ] = i;
}
// Begin affinity propagation algorithm
let n; // number of data points
let n2; // size of matrices
let S; // similarity matrix (1D array)
let p; // preference/suitability of a data point to serve as an exemplar
let R; // responsibility matrix (1D array)
let A; // availability matrix (1D array)
n = nodes.length;
n2 = n * n;
// Initialize and build S similarity matrix
S = new Array(n2);
for ( let i = 0; i < n2; i++ ) {
S[i] = -Infinity; // for cases where two data points shouldn't be linked together
}
for ( let i = 0; i < n; i++ ) {
for ( let j = 0; j < n; j++ ) {
if ( i !== j ) {
S[i * n + j] = getSimilarity( opts.distance, nodes[i], nodes[j], opts.attributes );
}
}
}
// Place preferences on the diagonal of S
p = getPreference( S, opts.preference );
for ( let i = 0; i < n; i++ ) {
S[i * n + i] = p;
}
// Initialize R responsibility matrix
R = new Array(n2);
for ( let i = 0; i < n2; i++ ) {
R[i] = 0.0;
}
// Initialize A availability matrix
A = new Array(n2);
for ( let i = 0; i < n2; i++ ) {
A[i] = 0.0;
}
let old = new Array(n);
let Rp = new Array(n);
let se = new Array(n);
for ( let i = 0; i < n; i ++ ) {
old[i] = 0.0;
Rp[i] = 0.0;
se[i] = 0;
}
let e = new Array(n * opts.minIterations);
for ( let i = 0; i < e.length; i++ ) {
e[i] = 0;
}
let iter;
for ( iter = 0; iter < opts.maxIterations; iter++ ) { // main algorithmic loop
// Update R responsibility matrix
for ( let i = 0; i < n; i++ ) {
let max = -Infinity,
max2 = -Infinity,
maxI = -1,
AS = 0.0;
for ( let j = 0; j < n; j++ ) {
old[j] = R[i * n + j];
AS = A[i * n + j] + S[i * n + j];
if ( AS >= max ) {
max2 = max;
max = AS;
maxI = j;
}
else if ( AS > max2 ) {
max2 = AS;
}
}
for ( let j = 0; j < n; j++ ) {
R[i * n + j] = (1 - opts.damping) * (S[i * n + j] - max) + opts.damping * old[j];
}
R[i * n + maxI] = (1 - opts.damping) * (S[i * n + maxI] - max2) + opts.damping * old[maxI];
}
// Update A availability matrix
for ( let i = 0; i < n; i++ ) {
let sum = 0;
for ( let j = 0; j < n; j++ ) {
old[j] = A[j * n + i];
Rp[j] = Math.max(0, R[j * n + i]);
sum += Rp[j];
}
sum -= Rp[i];
Rp[i] = R[i * n + i];
sum += Rp[i];
for ( let j = 0; j < n; j++ ) {
A[j * n + i] = (1 - opts.damping) * Math.min(0, sum - Rp[j]) + opts.damping * old[j];
}
A[i * n + i] = (1 - opts.damping) * (sum - Rp[i]) + opts.damping * old[i];
}
// Check for convergence
let K = 0;
for ( let i = 0; i < n; i++ ) {
let E = A[i * n + i] + R[i * n + i] > 0 ? 1 : 0;
e[(iter % opts.minIterations) * n + i] = E;
K += E;
}
if ( K > 0 && (iter >= opts.minIterations - 1 || iter == opts.maxIterations - 1) ) {
let sum = 0;
for ( let i = 0; i < n; i++ ) {
se[i] = 0;
for ( let j = 0; j < opts.minIterations; j++ ) {
se[i] += e[j * n + i];
}
if ( se[i] === 0 || se[i] === opts.minIterations ) {
sum++;
}
}
if ( sum === n ) { // then we have convergence
break;
}
}
}
// Identify exemplars (cluster centers)
let exemplarsIndices = findExemplars( n, R, A );
// Assign nodes to clusters
let clusterIndices = assign( n, S, exemplarsIndices, nodes, id2position );
let clusters = {};
for ( let c = 0; c < exemplarsIndices.length; c++ ) {
clusters[ exemplarsIndices[c] ] = [];
}
for (let i = 0; i < nodes.length; i++) {
let pos = id2position[ nodes[i].id() ];
let clusterIndex = clusterIndices[pos];
if( clusterIndex != null ){ // the node may have not been assigned a cluster if no valid attributes were specified
clusters[ clusterIndex ].push( nodes[i] );
}
}
let retClusters = new Array(exemplarsIndices.length);
for ( let c = 0; c < exemplarsIndices.length; c++ ) {
retClusters[c] = cy.collection( clusters[ exemplarsIndices[c] ] );
}
return retClusters;
};
export default { affinityPropagation, ap: affinityPropagation };

View File

@@ -0,0 +1,174 @@
import Heap from '../../heap.mjs';
import * as util from '../../util/index.mjs';
const defaults = util.defaults({
weight: null,
directed: false
});
let elesfn = ({
// Implemented from the algorithm in the paper "On Variants of Shortest-Path Betweenness Centrality and their Generic Computation" by Ulrik Brandes
betweennessCentrality: function( options ){
let { directed, weight } = defaults(options);
let weighted = weight != null;
let cy = this.cy();
// starting
let V = this.nodes();
let A = {};
let _C = {};
let max = 0;
let C = {
set: function( key, val ){
_C[ key ] = val;
if( val > max ){ max = val; }
},
get: function( key ){ return _C[ key ]; }
};
// A contains the neighborhoods of every node
for( let i = 0; i < V.length; i++ ){
let v = V[ i ];
let vid = v.id();
if( directed ){
A[ vid ] = v.outgoers().nodes(); // get outgoers of every node
} else {
A[ vid ] = v.openNeighborhood().nodes(); // get neighbors of every node
}
C.set( vid, 0 );
}
for( let s = 0; s < V.length; s++ ){
let sid = V[s].id();
let S = []; // stack
let P = {};
let g = {};
let d = {};
let Q = new Heap(function( a, b ){
return d[a] - d[b];
}); // queue
// init dictionaries
for( let i = 0; i < V.length; i++ ){
let vid = V[ i ].id();
P[ vid ] = [];
g[ vid ] = 0;
d[ vid ] = Infinity;
}
g[ sid ] = 1; // sigma
d[ sid ] = 0; // distance to s
Q.push( sid );
while( !Q.empty() ){
let v = Q.pop();
S.push( v );
if( weighted ){
for( let j = 0; j < A[v].length; j++ ){
let w = A[v][j];
let vEle = cy.getElementById( v );
let edge;
if( vEle.edgesTo( w ).length > 0 ){
edge = vEle.edgesTo( w )[0];
} else {
edge = w.edgesTo( vEle )[0];
}
let edgeWeight = weight( edge );
w = w.id();
if( d[w] > d[v] + edgeWeight ){
d[w] = d[v] + edgeWeight;
if( Q.nodes.indexOf( w ) < 0 ){ //if w is not in Q
Q.push( w );
} else { // update position if w is in Q
Q.updateItem( w );
}
g[w] = 0;
P[w] = [];
}
if( d[w] == d[v] + edgeWeight ){
g[w] = g[w] + g[v];
P[w].push( v );
}
}
} else {
for( let j = 0; j < A[v].length; j++ ){
let w = A[v][j].id();
if( d[w] == Infinity ){
Q.push( w );
d[w] = d[v] + 1;
}
if( d[w] == d[v] + 1 ){
g[w] = g[w] + g[v];
P[w].push( v );
}
}
}
}
let e = {};
for( let i = 0; i < V.length; i++ ){
e[ V[ i ].id() ] = 0;
}
while( S.length > 0 ){
let w = S.pop();
for( let j = 0; j < P[w].length; j++ ){
let v = P[w][j];
e[v] = e[v] + (g[v] / g[w]) * (1 + e[w]);
}
if( w != V[s].id() ){
C.set( w, C.get( w ) + e[w] );
}
}
}
let ret = {
betweenness: function( node ){
let id = cy.collection(node).id();
return C.get( id );
},
betweennessNormalized: function( node ){
if ( max == 0 ){ return 0; }
let id = cy.collection(node).id();
return C.get( id ) / max;
}
};
// alias
ret.betweennessNormalised = ret.betweennessNormalized;
return ret;
} // betweennessCentrality
}); // elesfn
// nice, short mathematical alias
elesfn.bc = elesfn.betweennessCentrality;
export default elesfn;

View File

@@ -0,0 +1,101 @@
import * as is from '../../is.mjs';
import * as util from '../../util/index.mjs';
const defaults = util.defaults({
harmonic: true,
weight: () => 1,
directed: false,
root: null
});
const elesfn = ({
closenessCentralityNormalized: function( options ){
let { harmonic, weight, directed } = defaults(options);
let cy = this.cy();
let closenesses = {};
let maxCloseness = 0;
let nodes = this.nodes();
let fw = this.floydWarshall({ weight, directed });
// Compute closeness for every node and find the maximum closeness
for( let i = 0; i < nodes.length; i++ ){
let currCloseness = 0;
let node_i = nodes[i];
for( let j = 0; j < nodes.length; j++ ){
if( i !== j ){
let d = fw.distance( node_i, nodes[j] );
if( harmonic ){
currCloseness += 1 / d;
} else {
currCloseness += d;
}
}
}
if( !harmonic ){
currCloseness = 1 / currCloseness;
}
if( maxCloseness < currCloseness ){
maxCloseness = currCloseness;
}
closenesses[ node_i.id() ] = currCloseness;
}
return {
closeness: function( node ){
if( maxCloseness == 0 ){ return 0; }
if( is.string( node ) ){
// from is a selector string
node = (cy.filter( node )[0]).id();
} else {
// from is a node
node = node.id();
}
return closenesses[ node ] / maxCloseness;
}
};
},
// Implemented from pseudocode from wikipedia
closenessCentrality: function( options ){
let { root, weight, directed, harmonic } = defaults(options);
root = this.filter(root)[0];
// we need distance from this node to every other node
let dijkstra = this.dijkstra({ root, weight, directed });
let totalDistance = 0;
let nodes = this.nodes();
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[i];
if( !n.same(root) ){
let d = dijkstra.distanceTo(n);
if( harmonic ){
totalDistance += 1 / d;
} else {
totalDistance += d;
}
}
}
return harmonic ? totalDistance : 1 / totalDistance;
} // closenessCentrality
}); // elesfn
// nice, short mathematical alias
elesfn.cc = elesfn.closenessCentrality;
elesfn.ccn = elesfn.closenessCentralityNormalised = elesfn.closenessCentralityNormalized;
export default elesfn;

View File

@@ -0,0 +1,133 @@
import * as is from '../../is.mjs';
import Heap from '../../heap.mjs';
import { defaults } from '../../util/index.mjs';
const dijkstraDefaults = defaults({
root: null,
weight: edge => 1,
directed: false
});
let elesfn = ({
dijkstra: function( options ){
if( !is.plainObject(options) ){
let args = arguments;
options = { root: args[0], weight: args[1], directed: args[2] };
}
let { root, weight, directed } = dijkstraDefaults(options);
let eles = this;
let weightFn = weight;
let source = is.string( root ) ? this.filter( root )[0] : root[0];
let dist = {};
let prev = {};
let knownDist = {};
let { nodes, edges } = this.byGroup();
edges.unmergeBy( ele => ele.isLoop() );
let getDist = node => dist[ node.id() ];
let setDist = ( node, d ) => {
dist[ node.id() ] = d;
Q.updateItem( node );
};
let Q = new Heap( (a, b) => getDist(a) - getDist(b) );
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
dist[ node.id() ] = node.same( source ) ? 0 : Infinity;
Q.push( node );
}
let distBetween = ( u, v ) => {
let uvs = ( directed ? u.edgesTo(v) : u.edgesWith(v) ).intersect( edges );
let smallestDistance = Infinity;
let smallestEdge;
for( let i = 0; i < uvs.length; i++ ){
let edge = uvs[ i ];
let weight = weightFn( edge );
if( weight < smallestDistance || !smallestEdge ){
smallestDistance = weight;
smallestEdge = edge;
}
}
return {
edge: smallestEdge,
dist: smallestDistance
};
};
while( Q.size() > 0 ){
let u = Q.pop();
let smalletsDist = getDist( u );
let uid = u.id();
knownDist[ uid ] = smalletsDist;
if( smalletsDist === Infinity ){
continue;
}
let neighbors = u.neighborhood().intersect( nodes );
for( let i = 0; i < neighbors.length; i++ ){
let v = neighbors[ i ];
let vid = v.id();
let vDist = distBetween( u, v );
let alt = smalletsDist + vDist.dist;
if( alt < getDist( v ) ){
setDist( v, alt );
prev[ vid ] = {
node: u,
edge: vDist.edge
};
}
} // for
} // while
return {
distanceTo: function( node ){
let target = is.string( node ) ? nodes.filter( node )[0] : node[0];
return knownDist[ target.id() ];
},
pathTo: function( node ){
let target = is.string( node ) ? nodes.filter( node )[0] : node[0];
let S = [];
let u = target;
let uid = u.id();
if( target.length > 0 ){
S.unshift( target );
while( prev[ uid ] ){
let p = prev[ uid ];
S.unshift( p.edge );
S.unshift( p.node );
u = p.node;
uid = u.id();
}
}
return eles.spawn( S );
}
};
}
});
export default elesfn;

View File

@@ -0,0 +1,118 @@
let hopcroftTarjanBiconnected = function() {
let eles = this;
let nodes = {};
let id = 0;
let edgeCount = 0;
let components = [];
let stack = [];
let visitedEdges = {};
const buildComponent = (x, y) => {
let i = stack.length-1;
let cutset = [];
let component = eles.spawn();
while (stack[i].x != x || stack[i].y != y) {
cutset.push(stack.pop().edge);
i--;
}
cutset.push(stack.pop().edge);
cutset.forEach(edge => {
let connectedNodes = edge.connectedNodes()
.intersection(eles);
component.merge(edge);
connectedNodes.forEach(node => {
const nodeId = node.id();
const connectedEdges = node.connectedEdges()
.intersection(eles);
component.merge(node);
if (!nodes[nodeId].cutVertex) {
component.merge(connectedEdges);
} else {
component.merge(connectedEdges.filter(edge => edge.isLoop()));
}
});
});
components.push(component);
};
const biconnectedSearch = (root, currentNode, parent) => {
if (root === parent) edgeCount += 1;
nodes[currentNode] = {
id : id,
low : id++,
cutVertex : false
};
let edges = eles.getElementById(currentNode)
.connectedEdges()
.intersection(eles);
if (edges.size() === 0) {
components.push(eles.spawn(eles.getElementById(currentNode)));
} else {
let sourceId, targetId, otherNodeId, edgeId;
edges.forEach(edge => {
sourceId = edge.source().id();
targetId = edge.target().id();
otherNodeId = (sourceId === currentNode) ? targetId : sourceId;
if (otherNodeId !== parent) {
edgeId = edge.id();
if (!visitedEdges[edgeId]) {
visitedEdges[edgeId] = true;
stack.push({
x : currentNode,
y : otherNodeId,
edge
});
}
if (!(otherNodeId in nodes)) {
biconnectedSearch(root, otherNodeId, currentNode);
nodes[currentNode].low = Math.min(nodes[currentNode].low,
nodes[otherNodeId].low);
if (nodes[currentNode].id <= nodes[otherNodeId].low) {
nodes[currentNode].cutVertex = true;
buildComponent(currentNode, otherNodeId);
}
} else {
nodes[currentNode].low = Math.min(nodes[currentNode].low,
nodes[otherNodeId].id);
}
}
});
}
};
eles.forEach(ele => {
if (ele.isNode()) {
let nodeId = ele.id();
if (!(nodeId in nodes)) {
edgeCount = 0;
biconnectedSearch(nodeId, nodeId);
nodes[nodeId].cutVertex = (edgeCount > 1);
}
}
});
let cutVertices = Object.keys(nodes)
.filter(id => nodes[id].cutVertex)
.map(id => eles.getElementById(id));
return {
cut: eles.spawn(cutVertices),
components
};
};
export default {
hopcroftTarjanBiconnected,
htbc: hopcroftTarjanBiconnected,
htb: hopcroftTarjanBiconnected,
hopcroftTarjanBiconnectedComponents: hopcroftTarjanBiconnected
};

View File

@@ -0,0 +1,34 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
let cache = function( fn, name ){
return function traversalCache( arg1, arg2, arg3, arg4 ){
let selectorOrEles = arg1;
let eles = this;
let key;
if( selectorOrEles == null ){
key = '';
} else if( is.elementOrCollection( selectorOrEles ) && selectorOrEles.length === 1 ){
key = selectorOrEles.id();
}
if( eles.length === 1 && key ){
let _p = eles[0]._private;
let tch = _p.traversalCache = _p.traversalCache || {};
let ch = tch[ name ] = tch[ name ] || [];
let hash = util.hashString( key );
let cacheHit = ch[ hash ];
if( cacheHit ){
return cacheHit;
} else {
return ( ch[ hash ] = fn.call( eles, arg1, arg2, arg3, arg4 ) );
}
} else {
return fn.call( eles, arg1, arg2, arg3, arg4 );
}
};
};
export default cache;

View File

@@ -0,0 +1,86 @@
import define from '../define/index.mjs';
let fn, elesfn;
fn = elesfn = ({
data: define.data( {
field: 'data',
bindingEvent: 'data',
allowBinding: true,
allowSetting: true,
settingEvent: 'data',
settingTriggersEvent: true,
triggerFnName: 'trigger',
allowGetting: true,
immutableKeys: {
'id': true,
'source': true,
'target': true,
'parent': true
},
updateStyle: true
} ),
removeData: define.removeData( {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: true,
immutableKeys: {
'id': true,
'source': true,
'target': true,
'parent': true
},
updateStyle: true
} ),
scratch: define.data( {
field: 'scratch',
bindingEvent: 'scratch',
allowBinding: true,
allowSetting: true,
settingEvent: 'scratch',
settingTriggersEvent: true,
triggerFnName: 'trigger',
allowGetting: true,
updateStyle: true
} ),
removeScratch: define.removeData( {
field: 'scratch',
event: 'scratch',
triggerFnName: 'trigger',
triggerEvent: true,
updateStyle: true
} ),
rscratch: define.data( {
field: 'rscratch',
allowBinding: false,
allowSetting: true,
settingTriggersEvent: false,
allowGetting: true
} ),
removeRscratch: define.removeData( {
field: 'rscratch',
triggerEvent: false
} ),
id: function(){
let ele = this[0];
if( ele ){
return ele._private.data.id;
}
}
});
// aliases
fn.attr = fn.data;
fn.removeAttr = fn.removeData;
export default elesfn;

View File

@@ -0,0 +1,56 @@
import * as math from '../../math.mjs';
const ifEdge = (ele, getValue) => {
if( ele.isEdge() && ele.takesUpSpace() ){
return getValue( ele );
}
};
const ifEdgeRenderedPosition = (ele, getPoint) => {
if( ele.isEdge() && ele.takesUpSpace() ){
let cy = ele.cy();
return math.modelToRenderedPosition( getPoint( ele ), cy.zoom(), cy.pan() );
}
};
const ifEdgeRenderedPositions = (ele, getPoints) => {
if( ele.isEdge() && ele.takesUpSpace() ){
let cy = ele.cy();
let pan = cy.pan();
let zoom = cy.zoom();
return getPoints( ele ).map( p => math.modelToRenderedPosition( p, zoom, pan ) );
}
};
const controlPoints = ele => ele.renderer().getControlPoints( ele );
const segmentPoints = ele => ele.renderer().getSegmentPoints( ele );
const sourceEndpoint = ele => ele.renderer().getSourceEndpoint( ele );
const targetEndpoint = ele => ele.renderer().getTargetEndpoint( ele );
const midpoint = ele => ele.renderer().getEdgeMidpoint( ele );
const pts = {
controlPoints: { get: controlPoints, mult: true },
segmentPoints: { get: segmentPoints, mult: true },
sourceEndpoint: { get: sourceEndpoint },
targetEndpoint: { get: targetEndpoint },
midpoint: { get: midpoint }
};
const renderedName = name => 'rendered' + name[0].toUpperCase() + name.substr(1);
export default Object.keys( pts ).reduce( ( obj, name ) => {
let spec = pts[ name ];
let rName = renderedName( name );
obj[ name ] = function(){ return ifEdge( this, spec.get ); };
if( spec.mult ){
obj[ rName ] = function(){ return ifEdgeRenderedPositions( this, spec.get ); };
} else {
obj[ rName ] = function(){ return ifEdgeRenderedPosition( this, spec.get ); };
}
return obj;
}, {} );

View File

@@ -0,0 +1,130 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import Set from '../set.mjs';
// represents a node or an edge
let Element = function( cy, params, restore = true ){
if( cy === undefined || params === undefined || !is.core( cy ) ){
util.error( 'An element must have a core reference and parameters set' );
return;
}
let group = params.group;
// try to automatically infer the group if unspecified
if( group == null ){
if( params.data && params.data.source != null && params.data.target != null ){
group = 'edges';
} else {
group = 'nodes';
}
}
// validate group
if( group !== 'nodes' && group !== 'edges' ){
util.error( 'An element must be of type `nodes` or `edges`; you specified `' + group + '`' );
return;
}
// make the element array-like, just like a collection
this.length = 1;
this[0] = this;
// NOTE: when something is added here, add also to ele.json()
let _p = this._private = {
cy: cy,
single: true, // indicates this is an element
data: params.data || {}, // data object
position: params.position || { x: 0, y: 0 }, // (x, y) position pair
autoWidth: undefined, // width and height of nodes calculated by the renderer when set to special 'auto' value
autoHeight: undefined,
autoPadding: undefined,
compoundBoundsClean: false, // whether the compound dimensions need to be recalculated the next time dimensions are read
listeners: [], // array of bound listeners
group: group, // string; 'nodes' or 'edges'
style: {}, // properties as set by the style
rstyle: {}, // properties for style sent from the renderer to the core
styleCxts: [], // applied style contexts from the styler
styleKeys: {}, // per-group keys of style property values
removed: true, // whether it's inside the vis; true if removed (set true here since we call restore)
selected: params.selected ? true : false, // whether it's selected
selectable: params.selectable === undefined ? true : ( params.selectable ? true : false ), // whether it's selectable
locked: params.locked ? true : false, // whether the element is locked (cannot be moved)
grabbed: false, // whether the element is grabbed by the mouse; renderer sets this privately
grabbable: params.grabbable === undefined ? true : ( params.grabbable ? true : false ), // whether the element can be grabbed
pannable: params.pannable === undefined ? (group === 'edges' ? true : false) : ( params.pannable ? true : false ), // whether the element has passthrough panning enabled
active: false, // whether the element is active from user interaction
classes: new Set(), // map ( className => true )
animation: { // object for currently-running animations
current: [],
queue: []
},
rscratch: {}, // object in which the renderer can store information
scratch: params.scratch || {}, // scratch objects
edges: [], // array of connected edges
children: [], // array of children
parent: params.parent && params.parent.isNode() ? params.parent : null, // parent ref
traversalCache: {}, // cache of output of traversal functions
backgrounding: false, // whether background images are loading
bbCache: null, // cache of the current bounding box
bbCacheShift: { x: 0, y: 0 }, // shift applied to cached bb to be applied on next get
bodyBounds: null, // bounds cache of element body, w/o overlay
overlayBounds: null, // bounds cache of element body, including overlay
labelBounds: { // bounds cache of labels
all: null,
source: null,
target: null,
main: null
},
arrowBounds: { // bounds cache of edge arrows
source: null,
target: null,
'mid-source': null,
'mid-target': null
}
};
if( _p.position.x == null ){ _p.position.x = 0; }
if( _p.position.y == null ){ _p.position.y = 0; }
// renderedPosition overrides if specified
if( params.renderedPosition ){
let rpos = params.renderedPosition;
let pan = cy.pan();
let zoom = cy.zoom();
_p.position = {
x: (rpos.x - pan.x) / zoom,
y: (rpos.y - pan.y) / zoom
};
}
let classes = [];
if( is.array( params.classes ) ){
classes = params.classes;
} else if( is.string( params.classes ) ){
classes = params.classes.split( /\s+/ );
}
for( let i = 0, l = classes.length; i < l; i++ ){
let cls = classes[ i ];
if( !cls || cls === '' ){ continue; }
_p.classes.add(cls);
}
this.createEmitter();
if( restore === undefined || restore ){
this.restore();
}
let bypass = params.style || params.css;
if( bypass ){
util.warn('Setting a `style` bypass at element creation should be done only when absolutely necessary. Try to use the stylesheet instead.');
this.style(bypass);
}
};
export default Element;

View File

@@ -0,0 +1,196 @@
import * as util from '../util/index.mjs';
import Promise from '../promise.mjs';
import * as math from '../math.mjs';
const getLayoutDimensionOptions = util.defaults({
nodeDimensionsIncludeLabels: false
});
let elesfn = ({
// Calculates and returns node dimensions { x, y } based on options given
layoutDimensions: function( options ){
options = getLayoutDimensionOptions( options );
let dims;
if( !this.takesUpSpace() ){
dims = { w: 0, h: 0 };
} else if( options.nodeDimensionsIncludeLabels ){
let bbDim = this.boundingBox();
dims = {
w: bbDim.w,
h: bbDim.h
};
} else {
dims = {
w: this.outerWidth(),
h: this.outerHeight()
};
}
// sanitise the dimensions for external layouts (avoid division by zero)
if( dims.w === 0 || dims.h === 0 ){
dims.w = dims.h = 1;
}
return dims;
},
// using standard layout options, apply position function (w/ or w/o animation)
layoutPositions: function( layout, options, fn ){
let nodes = this.nodes().filter(n => !n.isParent());
let cy = this.cy();
let layoutEles = options.eles; // nodes & edges
let getMemoizeKey = node => node.id();
let fnMem = util.memoize( fn, getMemoizeKey ); // memoized version of position function
layout.emit( { type: 'layoutstart', layout: layout } );
layout.animations = [];
let calculateSpacing = function( spacing, nodesBb, pos ){
let center = {
x: nodesBb.x1 + nodesBb.w / 2,
y: nodesBb.y1 + nodesBb.h / 2
};
let spacingVector = { // scale from center of bounding box (not necessarily 0,0)
x: (pos.x - center.x) * spacing,
y: (pos.y - center.y) * spacing
};
return {
x: center.x + spacingVector.x,
y: center.y + spacingVector.y
};
};
let useSpacingFactor = options.spacingFactor && options.spacingFactor !== 1;
let spacingBb = function(){
if( !useSpacingFactor ){ return null; }
let bb = math.makeBoundingBox();
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[i];
let pos = fnMem( node, i );
math.expandBoundingBoxByPoint( bb, pos.x, pos.y );
}
return bb;
};
let bb = spacingBb();
let getFinalPos = util.memoize( function( node, i ){
let newPos = fnMem( node, i );
if( useSpacingFactor ){
let spacing = Math.abs( options.spacingFactor );
newPos = calculateSpacing( spacing, bb, newPos );
}
if( options.transform != null ){
newPos = options.transform( node, newPos );
}
return newPos;
}, getMemoizeKey );
if( options.animate ){
for( let i = 0; i < nodes.length; i++ ){
let node = nodes[ i ];
let newPos = getFinalPos( node, i );
let animateNode = options.animateFilter == null || options.animateFilter( node, i );
if( animateNode ){
let ani = node.animation( {
position: newPos,
duration: options.animationDuration,
easing: options.animationEasing
} );
layout.animations.push( ani );
} else {
node.position( newPos );
}
}
if( options.fit ){
let fitAni = cy.animation({
fit: {
boundingBox: layoutEles.boundingBoxAt( getFinalPos ),
padding: options.padding
},
duration: options.animationDuration,
easing: options.animationEasing
});
layout.animations.push( fitAni );
} else if( options.zoom !== undefined && options.pan !== undefined ){
let zoomPanAni = cy.animation({
zoom: options.zoom,
pan: options.pan,
duration: options.animationDuration,
easing: options.animationEasing
});
layout.animations.push( zoomPanAni );
}
layout.animations.forEach(ani => ani.play());
layout.one( 'layoutready', options.ready );
layout.emit( { type: 'layoutready', layout: layout } );
Promise.all( layout.animations.map(function( ani ){
return ani.promise();
}) ).then(function(){
layout.one( 'layoutstop', options.stop );
layout.emit( { type: 'layoutstop', layout: layout } );
});
} else {
nodes.positions( getFinalPos );
if( options.fit ){
cy.fit( options.eles, options.padding );
}
if( options.zoom != null ){
cy.zoom( options.zoom );
}
if( options.pan ){
cy.pan( options.pan );
}
layout.one( 'layoutready', options.ready );
layout.emit( { type: 'layoutready', layout: layout } );
layout.one( 'layoutstop', options.stop );
layout.emit( { type: 'layoutstop', layout: layout } );
}
return this; // chaining
},
layout: function( options ){
let cy = this.cy();
return cy.makeLayout( util.extend( {}, options, {
eles: this
} ) );
}
});
// aliases:
elesfn.createLayout = elesfn.makeLayout = elesfn.layout;
export default elesfn;

View File

@@ -0,0 +1,457 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
function styleCache( key, fn, ele ){
var _p = ele._private;
var cache = _p.styleCache = _p.styleCache || [];
var val;
if( (val = cache[key]) != null ){
return val;
} else {
val = cache[key] = fn( ele );
return val;
}
}
function cacheStyleFunction( key, fn ){
key = util.hashString( key );
return function cachedStyleFunction( ele ){
return styleCache( key, fn, ele );
};
}
function cachePrototypeStyleFunction( key, fn ){
key = util.hashString( key );
let selfFn = ele => fn.call( ele );
return function cachedPrototypeStyleFunction(){
var ele = this[0];
if( ele ){
return styleCache( key, selfFn, ele );
}
};
}
let elesfn = ({
recalculateRenderedStyle: function( useCache ){
let cy = this.cy();
let renderer = cy.renderer();
let styleEnabled = cy.styleEnabled();
if( renderer && styleEnabled ){
renderer.recalculateRenderedStyle( this, useCache );
}
return this;
},
dirtyStyleCache: function(){
let cy = this.cy();
let dirty = ele => ele._private.styleCache = null;
if( cy.hasCompoundNodes() ){
let eles;
eles = this.spawnSelf()
.merge( this.descendants() )
.merge( this.parents() )
;
eles.merge( eles.connectedEdges() );
eles.forEach( dirty );
} else {
this.forEach( ele => {
dirty( ele );
ele.connectedEdges().forEach( dirty );
} );
}
return this;
},
// fully updates (recalculates) the style for the elements
updateStyle: function( notifyRenderer ){
let cy = this._private.cy;
if( !cy.styleEnabled() ){ return this; }
if( cy.batching() ){
let bEles = cy._private.batchStyleEles;
bEles.merge( this );
return this; // chaining and exit early when batching
}
let hasCompounds = cy.hasCompoundNodes();
let updatedEles = this;
notifyRenderer = notifyRenderer || notifyRenderer === undefined ? true : false;
if( hasCompounds ){ // then add everything up and down for compound selector checks
updatedEles = this.spawnSelf().merge( this.descendants() ).merge( this.parents() );
}
// let changedEles = style.apply( updatedEles );
let changedEles = updatedEles;
if( notifyRenderer ){
changedEles.emitAndNotify( 'style' ); // let renderer know we changed style
} else {
changedEles.emit( 'style' ); // just fire the event
}
updatedEles.forEach(ele => ele._private.styleDirty = true);
return this; // chaining
},
// private: clears dirty flag and recalculates style
cleanStyle: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return; }
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
if( ele._private.styleDirty ){
// n.b. this flag should be set before apply() to avoid potential infinite recursion
ele._private.styleDirty = false;
cy.style().apply(ele);
}
}
},
// get the internal parsed style object for the specified property
parsedStyle: function( property, includeNonDefault = true ){
let ele = this[0];
let cy = ele.cy();
if( !cy.styleEnabled() ){ return; }
if( ele ){
// this.cleanStyle();
// Inline the important part of cleanStyle(), for raw performance
if( ele._private.styleDirty ){
// n.b. this flag should be set before apply() to avoid potential infinite recursion
ele._private.styleDirty = false;
cy.style().apply(ele);
}
let overriddenStyle = ele._private.style[ property ];
if( overriddenStyle != null ){
return overriddenStyle;
} else if( includeNonDefault ){
return cy.style().getDefaultProperty( property );
} else {
return null;
}
}
},
numericStyle: function( property ){
let ele = this[0];
if( !ele.cy().styleEnabled() ){ return; }
if( ele ){
let pstyle = ele.pstyle( property );
return pstyle.pfValue !== undefined ? pstyle.pfValue : pstyle.value;
}
},
numericStyleUnits: function( property ){
let ele = this[0];
if( !ele.cy().styleEnabled() ){ return; }
if( ele ){
return ele.pstyle( property ).units;
}
},
// get the specified css property as a rendered value (i.e. on-screen value)
// or get the whole rendered style if no property specified (NB doesn't allow setting)
renderedStyle: function( property ){
let cy = this.cy();
if( !cy.styleEnabled() ){ return this; }
let ele = this[0];
if( ele ){
return cy.style().getRenderedStyle( ele, property );
}
},
// read the calculated css style of the element or override the style (via a bypass)
style: function( name, value ){
let cy = this.cy();
if( !cy.styleEnabled() ){ return this; }
let updateTransitions = false;
let style = cy.style();
if( is.plainObject( name ) ){ // then extend the bypass
let props = name;
style.applyBypass( this, props, updateTransitions );
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
} else if( is.string( name ) ){
if( value === undefined ){ // then get the property from the style
let ele = this[0];
if( ele ){
return style.getStylePropertyValue( ele, name );
} else { // empty collection => can't get any value
return;
}
} else { // then set the bypass with the property value
style.applyBypass( this, name, value, updateTransitions );
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
}
} else if( name === undefined ){
let ele = this[0];
if( ele ){
return style.getRawStyle( ele );
} else { // empty collection => can't get any value
return;
}
}
return this; // chaining
},
removeStyle: function( names ){
let cy = this.cy();
if( !cy.styleEnabled() ){ return this; }
let updateTransitions = false;
let style = cy.style();
let eles = this;
if( names === undefined ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
style.removeAllBypasses( ele, updateTransitions );
}
} else {
names = names.split( /\s+/ );
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
style.removeBypasses( ele, names, updateTransitions );
}
}
this.emitAndNotify( 'style' ); // let the renderer know we've updated style
return this; // chaining
},
show: function(){
this.css( 'display', 'element' );
return this; // chaining
},
hide: function(){
this.css( 'display', 'none' );
return this; // chaining
},
effectiveOpacity: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return 1; }
let hasCompoundNodes = cy.hasCompoundNodes();
let ele = this[0];
if( ele ){
let _p = ele._private;
let parentOpacity = ele.pstyle( 'opacity' ).value;
if( !hasCompoundNodes ){ return parentOpacity; }
let parents = !_p.data.parent ? null : ele.parents();
if( parents ){
for( let i = 0; i < parents.length; i++ ){
let parent = parents[ i ];
let opacity = parent.pstyle( 'opacity' ).value;
parentOpacity = opacity * parentOpacity;
}
}
return parentOpacity;
}
},
transparent: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return false; }
let ele = this[0];
let hasCompoundNodes = ele.cy().hasCompoundNodes();
if( ele ){
if( !hasCompoundNodes ){
return ele.pstyle( 'opacity' ).value === 0;
} else {
return ele.effectiveOpacity() === 0;
}
}
},
backgrounding: function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return false; }
let ele = this[0];
return ele._private.backgrounding ? true : false;
}
});
function checkCompound( ele, parentOk ){
let _p = ele._private;
let parents = _p.data.parent ? ele.parents() : null;
if( parents ){ for( let i = 0; i < parents.length; i++ ){
let parent = parents[ i ];
if( !parentOk( parent ) ){ return false; }
} }
return true;
}
function defineDerivedStateFunction( specs ){
let ok = specs.ok;
let edgeOkViaNode = specs.edgeOkViaNode || specs.ok;
let parentOk = specs.parentOk || specs.ok;
return function(){
let cy = this.cy();
if( !cy.styleEnabled() ){ return true; }
let ele = this[0];
let hasCompoundNodes = cy.hasCompoundNodes();
if( ele ){
let _p = ele._private;
if( !ok( ele ) ){ return false; }
if( ele.isNode() ){
return !hasCompoundNodes || checkCompound( ele, parentOk );
} else {
let src = _p.source;
let tgt = _p.target;
return ( edgeOkViaNode(src) && (!hasCompoundNodes || checkCompound(src, edgeOkViaNode)) ) &&
( src === tgt || ( edgeOkViaNode(tgt) && (!hasCompoundNodes || checkCompound(tgt, edgeOkViaNode)) ) );
}
}
};
}
let eleTakesUpSpace = cacheStyleFunction( 'eleTakesUpSpace', function( ele ){
return (
ele.pstyle( 'display' ).value === 'element'
&& ele.width() !== 0
&& ( ele.isNode() ? ele.height() !== 0 : true )
);
} );
elesfn.takesUpSpace = cachePrototypeStyleFunction( 'takesUpSpace', defineDerivedStateFunction({
ok: eleTakesUpSpace
}) );
let eleInteractive = cacheStyleFunction( 'eleInteractive', function( ele ){
return (
ele.pstyle('events').value === 'yes'
&& ele.pstyle('visibility').value === 'visible'
&& eleTakesUpSpace( ele )
);
} );
let parentInteractive = cacheStyleFunction( 'parentInteractive', function( parent ){
return (
parent.pstyle('visibility').value === 'visible'
&& eleTakesUpSpace( parent )
);
} );
elesfn.interactive = cachePrototypeStyleFunction( 'interactive', defineDerivedStateFunction({
ok: eleInteractive,
parentOk: parentInteractive,
edgeOkViaNode: eleTakesUpSpace
}) );
elesfn.noninteractive = function(){
let ele = this[0];
if( ele ){
return !ele.interactive();
}
};
let eleVisible = cacheStyleFunction( 'eleVisible', function( ele ){
return (
ele.pstyle( 'visibility' ).value === 'visible'
&& ele.pstyle( 'opacity' ).pfValue !== 0
&& eleTakesUpSpace( ele )
);
} );
let edgeVisibleViaNode = eleTakesUpSpace;
elesfn.visible = cachePrototypeStyleFunction( 'visible', defineDerivedStateFunction({
ok: eleVisible,
edgeOkViaNode: edgeVisibleViaNode
}) );
elesfn.hidden = function(){
let ele = this[0];
if( ele ){
return !ele.visible();
}
};
elesfn.isBundledBezier = cachePrototypeStyleFunction('isBundledBezier', function(){
if( !this.cy().styleEnabled() ){ return false; }
return !this.removed() && this.pstyle('curve-style').value === 'bezier' && this.takesUpSpace();
});
elesfn.bypass = elesfn.css = elesfn.style;
elesfn.renderedCss = elesfn.renderedStyle;
elesfn.removeBypass = elesfn.removeCss = elesfn.removeStyle;
elesfn.pstyle = elesfn.parsedStyle;
export default elesfn;

View File

@@ -0,0 +1,82 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
import Collection from '../collection/index.mjs';
import Element from '../collection/element.mjs';
let corefn = {
add: function( opts ){
let elements;
let cy = this;
// add the elements
if( is.elementOrCollection( opts ) ){
let eles = opts;
if( eles._private.cy === cy ){ // same instance => just restore
elements = eles.restore();
} else { // otherwise, copy from json
let jsons = [];
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
jsons.push( ele.json() );
}
elements = new Collection( cy, jsons );
}
}
// specify an array of options
else if( is.array( opts ) ){
let jsons = opts;
elements = new Collection( cy, jsons );
}
// specify via opts.nodes and opts.edges
else if( is.plainObject( opts ) && (is.array( opts.nodes ) || is.array( opts.edges )) ){
let elesByGroup = opts;
let jsons = [];
let grs = [ 'nodes', 'edges' ];
for( let i = 0, il = grs.length; i < il; i++ ){
let group = grs[ i ];
let elesArray = elesByGroup[ group ];
if( is.array( elesArray ) ){
for( let j = 0, jl = elesArray.length; j < jl; j++ ){
let json = util.extend( { group: group }, elesArray[ j ] );
jsons.push( json );
}
}
}
elements = new Collection( cy, jsons );
}
// specify options for one element
else {
let json = opts;
elements = (new Element( cy, json )).collection();
}
return elements;
},
remove: function( collection ){
if( is.elementOrCollection( collection ) ){
// already have right ref
} else if( is.string( collection ) ){
let selector = collection;
collection = this.$( selector );
}
return collection.remove();
}
};
export default corefn;

View File

@@ -0,0 +1,82 @@
import * as is from '../../is.mjs';
function getEasedValue( type, start, end, percent, easingFn ){
if( percent === 1 ){
return end;
}
if( start === end ){
return end;
}
let val = easingFn( start, end, percent );
if( type == null ){
return val;
}
if( type.roundValue || type.color ){
val = Math.round( val );
}
if( type.min !== undefined ){
val = Math.max( val, type.min );
}
if( type.max !== undefined ){
val = Math.min( val, type.max );
}
return val;
}
function getValue( prop, spec ){
if( prop.pfValue != null || prop.value != null ){
if( prop.pfValue != null && (spec == null || spec.type.units !== '%') ){
return prop.pfValue;
} else {
return prop.value;
}
} else {
return prop;
}
}
function ease( startProp, endProp, percent, easingFn, propSpec ){
let type = propSpec != null ? propSpec.type : null;
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
let start = getValue( startProp, propSpec );
let end = getValue( endProp, propSpec );
if( is.number( start ) && is.number( end ) ){
return getEasedValue( type, start, end, percent, easingFn );
} else if( is.array( start ) && is.array( end ) ){
let easedArr = [];
for( let i = 0; i < end.length; i++ ){
let si = start[ i ];
let ei = end[ i ];
if( si != null && ei != null ){
let val = getEasedValue( type, si, ei, percent, easingFn );
easedArr.push( val );
} else {
easedArr.push( ei );
}
}
return easedArr;
}
return undefined;
}
export default ease;

View File

@@ -0,0 +1,76 @@
import generateCubicBezier from './cubic-bezier.mjs';
import generateSpringRK4 from './spring.mjs';
let cubicBezier = function( t1, p1, t2, p2 ){
let bezier = generateCubicBezier( t1, p1, t2, p2 );
return function( start, end, percent ){
return start + ( end - start ) * bezier( percent );
};
};
let easings = {
'linear': function( start, end, percent ){
return start + (end - start) * percent;
},
// default easings
'ease': cubicBezier( 0.25, 0.1, 0.25, 1 ),
'ease-in': cubicBezier( 0.42, 0, 1, 1 ),
'ease-out': cubicBezier( 0, 0, 0.58, 1 ),
'ease-in-out': cubicBezier( 0.42, 0, 0.58, 1 ),
// sine
'ease-in-sine': cubicBezier( 0.47, 0, 0.745, 0.715 ),
'ease-out-sine': cubicBezier( 0.39, 0.575, 0.565, 1 ),
'ease-in-out-sine': cubicBezier( 0.445, 0.05, 0.55, 0.95 ),
// quad
'ease-in-quad': cubicBezier( 0.55, 0.085, 0.68, 0.53 ),
'ease-out-quad': cubicBezier( 0.25, 0.46, 0.45, 0.94 ),
'ease-in-out-quad': cubicBezier( 0.455, 0.03, 0.515, 0.955 ),
// cubic
'ease-in-cubic': cubicBezier( 0.55, 0.055, 0.675, 0.19 ),
'ease-out-cubic': cubicBezier( 0.215, 0.61, 0.355, 1 ),
'ease-in-out-cubic': cubicBezier( 0.645, 0.045, 0.355, 1 ),
// quart
'ease-in-quart': cubicBezier( 0.895, 0.03, 0.685, 0.22 ),
'ease-out-quart': cubicBezier( 0.165, 0.84, 0.44, 1 ),
'ease-in-out-quart': cubicBezier( 0.77, 0, 0.175, 1 ),
// quint
'ease-in-quint': cubicBezier( 0.755, 0.05, 0.855, 0.06 ),
'ease-out-quint': cubicBezier( 0.23, 1, 0.32, 1 ),
'ease-in-out-quint': cubicBezier( 0.86, 0, 0.07, 1 ),
// expo
'ease-in-expo': cubicBezier( 0.95, 0.05, 0.795, 0.035 ),
'ease-out-expo': cubicBezier( 0.19, 1, 0.22, 1 ),
'ease-in-out-expo': cubicBezier( 1, 0, 0, 1 ),
// circ
'ease-in-circ': cubicBezier( 0.6, 0.04, 0.98, 0.335 ),
'ease-out-circ': cubicBezier( 0.075, 0.82, 0.165, 1 ),
'ease-in-out-circ': cubicBezier( 0.785, 0.135, 0.15, 0.86 ),
// user param easings...
'spring': function( tension, friction, duration ){
if( duration === 0 ){ // can't get a spring w/ duration 0
return easings.linear; // duration 0 => jump to end so impl doesn't matter
}
let spring = generateSpringRK4( tension, friction, duration );
return function( start, end, percent ){
return start + (end - start) * spring( percent );
};
},
'cubic-bezier': cubicBezier
};
export default easings;

522
frontend/node_modules/cytoscape/src/core/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,522 @@
import window from '../window.mjs';
import * as util from '../util/index.mjs';
import Collection from '../collection/index.mjs';
import * as is from '../is.mjs';
import Promise from '../promise.mjs';
import addRemove from './add-remove.mjs';
import animation from './animation/index.mjs';
import events from './events.mjs';
import exportFormat from './export.mjs';
import layout from './layout.mjs';
import notification from './notification.mjs';
import renderer from './renderer.mjs';
import search from './search.mjs';
import style from './style.mjs';
import viewport from './viewport.mjs';
import data from './data.mjs';
let Core = function( opts ){
let cy = this;
opts = util.extend( {}, opts );
let container = opts.container;
// allow for passing a wrapped jquery object
// e.g. cytoscape({ container: $('#cy') })
if( container && !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
container = container[0];
}
let reg = container ? container._cyreg : null; // e.g. already registered some info (e.g. readies) via jquery
reg = reg || {};
if( reg && reg.cy ){
reg.cy.destroy();
reg = {}; // old instance => replace reg completely
}
let readies = reg.readies = reg.readies || [];
if( container ){ container._cyreg = reg; } // make sure container assoc'd reg points to this cy
reg.cy = cy;
let head = window !== undefined && container !== undefined && !opts.headless;
let options = opts;
options.layout = util.extend( { name: head ? 'grid' : 'null' }, options.layout );
options.renderer = util.extend( { name: head ? 'canvas' : 'null' }, options.renderer );
let defVal = function( def, val, altVal ){
if( val !== undefined ){
return val;
} else if( altVal !== undefined ){
return altVal;
} else {
return def;
}
};
let _p = this._private = {
container: container, // html dom ele container
ready: false, // whether ready has been triggered
options: options, // cached options
elements: new Collection( this ), // elements in the graph
listeners: [], // list of listeners
aniEles: new Collection( this ), // elements being animated
data: options.data || {}, // data for the core
scratch: {}, // scratch object for core
layout: null,
renderer: null,
destroyed: false, // whether destroy was called
notificationsEnabled: true, // whether notifications are sent to the renderer
minZoom: 1e-50,
maxZoom: 1e50,
zoomingEnabled: defVal( true, options.zoomingEnabled ),
userZoomingEnabled: defVal( true, options.userZoomingEnabled ),
panningEnabled: defVal( true, options.panningEnabled ),
userPanningEnabled: defVal( true, options.userPanningEnabled ),
boxSelectionEnabled: defVal( true, options.boxSelectionEnabled ),
autolock: defVal( false, options.autolock, options.autolockNodes ),
autoungrabify: defVal( false, options.autoungrabify, options.autoungrabifyNodes ),
autounselectify: defVal( false, options.autounselectify ),
styleEnabled: options.styleEnabled === undefined ? head : options.styleEnabled,
zoom: is.number( options.zoom ) ? options.zoom : 1,
pan: {
x: is.plainObject( options.pan ) && is.number( options.pan.x ) ? options.pan.x : 0,
y: is.plainObject( options.pan ) && is.number( options.pan.y ) ? options.pan.y : 0
},
animation: { // object for currently-running animations
current: [],
queue: []
},
hasCompoundNodes: false,
multiClickDebounceTime: defVal(250, options.multiClickDebounceTime)
};
this.createEmitter();
// set selection type
this.selectionType( options.selectionType );
// init zoom bounds
this.zoomRange({ min: options.minZoom, max: options.maxZoom });
let loadExtData = function( extData, next ){
let anyIsPromise = extData.some( is.promise );
if( anyIsPromise ){
return Promise.all( extData ).then( next ); // load all data asynchronously, then exec rest of init
} else {
next( extData ); // exec synchronously for convenience
}
};
// start with the default stylesheet so we have something before loading an external stylesheet
if( _p.styleEnabled ){
cy.setStyle([]);
}
// create the renderer
let rendererOptions = util.assign({}, options, options.renderer); // allow rendering hints in top level options
cy.initRenderer( rendererOptions );
let setElesAndLayout = function( elements, onload, ondone ){
cy.notifications( false );
// remove old elements
let oldEles = cy.mutableElements();
if( oldEles.length > 0 ){
oldEles.remove();
}
if( elements != null ){
if( is.plainObject( elements ) || is.array( elements ) ){
cy.add( elements );
}
}
cy.one( 'layoutready', function( e ){
cy.notifications( true );
cy.emit( e ); // we missed this event by turning notifications off, so pass it on
cy.one( 'load', onload );
cy.emitAndNotify( 'load' );
} ).one( 'layoutstop', function(){
cy.one( 'done', ondone );
cy.emit( 'done' );
} );
let layoutOpts = util.extend( {}, cy._private.options.layout );
layoutOpts.eles = cy.elements();
cy.layout( layoutOpts ).run();
};
loadExtData([ options.style, options.elements ], function( thens ){
let initStyle = thens[0];
let initEles = thens[1];
// init style
if( _p.styleEnabled ){
cy.style().append( initStyle );
}
// initial load
setElesAndLayout( initEles, function(){ // onready
cy.startAnimationLoop();
_p.ready = true;
// if a ready callback is specified as an option, the bind it
if( is.fn( options.ready ) ){
cy.on( 'ready', options.ready );
}
// bind all the ready handlers registered before creating this instance
for( let i = 0; i < readies.length; i++ ){
let fn = readies[ i ];
cy.on( 'ready', fn );
}
if( reg ){ reg.readies = []; } // clear b/c we've bound them all and don't want to keep it around in case a new core uses the same div etc
cy.emit( 'ready' );
}, options.done );
} );
};
let corefn = Core.prototype; // short alias
util.extend( corefn, {
instanceString: function(){
return 'core';
},
isReady: function(){
return this._private.ready;
},
destroyed: function(){
return this._private.destroyed;
},
ready: function( fn ){
if( this.isReady() ){
this.emitter().emit( 'ready', [], fn ); // just calls fn as though triggered via ready event
} else {
this.on( 'ready', fn );
}
return this;
},
destroy: function(){
let cy = this;
if( cy.destroyed() ) return;
cy.stopAnimationLoop();
cy.destroyRenderer();
this.emit( 'destroy' );
cy._private.destroyed = true;
return cy;
},
hasElementWithId: function( id ){
return this._private.elements.hasElementWithId( id );
},
getElementById: function( id ){
return this._private.elements.getElementById( id );
},
hasCompoundNodes: function(){
return this._private.hasCompoundNodes;
},
headless: function(){
return this._private.renderer.isHeadless();
},
styleEnabled: function(){
return this._private.styleEnabled;
},
addToPool: function( eles ){
this._private.elements.merge( eles );
return this; // chaining
},
removeFromPool: function( eles ){
this._private.elements.unmerge( eles );
return this;
},
container: function(){
return this._private.container || null;
},
window: function() {
let container = this._private.container;
if (container == null) return window;
let ownerDocument = this._private.container.ownerDocument;
if (ownerDocument === undefined || ownerDocument == null) {
return window;
}
return ownerDocument.defaultView || window;
},
mount: function( container ){
if( container == null ){ return; }
let cy = this;
let _p = cy._private;
let options = _p.options;
if( !is.htmlElement( container ) && is.htmlElement( container[0] ) ){
container = container[0];
}
cy.stopAnimationLoop();
cy.destroyRenderer();
_p.container = container;
_p.styleEnabled = true;
cy.invalidateSize();
cy.initRenderer( util.assign({}, options, options.renderer, {
// allow custom renderer name to be re-used, otherwise use canvas
name: options.renderer.name === 'null' ? 'canvas' : options.renderer.name
}) );
cy.startAnimationLoop();
cy.style( options.style );
cy.emit( 'mount' );
return cy;
},
unmount: function(){
let cy = this;
cy.stopAnimationLoop();
cy.destroyRenderer();
cy.initRenderer( { name: 'null' } );
cy.emit( 'unmount' );
return cy;
},
options: function(){
return util.copy( this._private.options );
},
json: function( obj ){
let cy = this;
let _p = cy._private;
let eles = cy.mutableElements();
let getFreshRef = ele => cy.getElementById(ele.id());
if( is.plainObject( obj ) ){ // set
cy.startBatch();
if( obj.elements ){
let idInJson = {};
let updateEles = function( jsons, gr ){
let toAdd = [];
let toMod = [];
for( let i = 0; i < jsons.length; i++ ){
let json = jsons[ i ];
if( !json.data.id ){
util.warn( 'cy.json() cannot handle elements without an ID attribute' );
continue;
}
let id = '' + json.data.id; // id must be string
let ele = cy.getElementById( id );
idInJson[ id ] = true;
if( ele.length !== 0 ){ // existing element should be updated
toMod.push({ ele, json });
} else { // otherwise should be added
if( gr ){
json.group = gr;
toAdd.push( json );
} else {
toAdd.push( json );
}
}
}
cy.add( toAdd );
for( let i = 0; i < toMod.length; i++ ){
let { ele, json } = toMod[i];
ele.json(json);
}
};
if( is.array( obj.elements ) ){ // elements: []
updateEles( obj.elements );
} else { // elements: { nodes: [], edges: [] }
let grs = [ 'nodes', 'edges' ];
for( let i = 0; i < grs.length; i++ ){
let gr = grs[ i ];
let elements = obj.elements[ gr ];
if( is.array( elements ) ){
updateEles( elements, gr );
}
}
}
let parentsToRemove = cy.collection();
(eles
.filter(ele => !idInJson[ ele.id() ])
.forEach(ele => {
if ( ele.isParent() ) {
parentsToRemove.merge(ele);
} else {
ele.remove();
}
})
);
// so that children are not removed w/parent
parentsToRemove.forEach(ele => ele.children().move({ parent: null }));
// intermediate parents may be moved by prior line, so make sure we remove by fresh refs
parentsToRemove.forEach(ele => getFreshRef(ele).remove());
}
if( obj.style ){
cy.style( obj.style );
}
if( obj.zoom != null && obj.zoom !== _p.zoom ){
cy.zoom( obj.zoom );
}
if( obj.pan ){
if( obj.pan.x !== _p.pan.x || obj.pan.y !== _p.pan.y ){
cy.pan( obj.pan );
}
}
if( obj.data ){
cy.data( obj.data );
}
let fields = [
'minZoom', 'maxZoom', 'zoomingEnabled', 'userZoomingEnabled',
'panningEnabled', 'userPanningEnabled',
'boxSelectionEnabled',
'autolock', 'autoungrabify', 'autounselectify',
'multiClickDebounceTime'
];
for( let i = 0; i < fields.length; i++ ){
let f = fields[ i ];
if( obj[ f ] != null ){
cy[ f ]( obj[ f ] );
}
}
cy.endBatch();
return this; // chaining
} else { // get
let flat = !!obj;
let json = {};
if( flat ){
json.elements = this.elements().map( ele => ele.json() );
} else {
json.elements = {};
eles.forEach( function( ele ){
let group = ele.group();
if( !json.elements[ group ] ){
json.elements[ group ] = [];
}
json.elements[ group ].push( ele.json() );
} );
}
if( this._private.styleEnabled ){
json.style = cy.style().json();
}
json.data = util.copy( cy.data() );
let options = _p.options;
json.zoomingEnabled = _p.zoomingEnabled;
json.userZoomingEnabled = _p.userZoomingEnabled;
json.zoom = _p.zoom;
json.minZoom = _p.minZoom;
json.maxZoom = _p.maxZoom;
json.panningEnabled = _p.panningEnabled;
json.userPanningEnabled = _p.userPanningEnabled;
json.pan = util.copy( _p.pan );
json.boxSelectionEnabled = _p.boxSelectionEnabled;
json.renderer = util.copy( options.renderer );
json.hideEdgesOnViewport = options.hideEdgesOnViewport;
json.textureOnViewport = options.textureOnViewport;
json.wheelSensitivity = options.wheelSensitivity;
json.motionBlur = options.motionBlur;
json.multiClickDebounceTime = options.multiClickDebounceTime;
return json;
}
}
} );
corefn.$id = corefn.getElementById;
[
addRemove,
animation,
events,
exportFormat,
layout,
notification,
renderer,
search,
style,
viewport,
data
].forEach( function( props ){
util.extend( corefn, props );
} );
export default Core;

208
frontend/node_modules/cytoscape/src/define/data.mjs generated vendored Normal file
View File

@@ -0,0 +1,208 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import get from 'lodash/get.js';
import set from 'lodash/set.js';
import toPath from 'lodash/toPath.js';
let define = {
// access data field
data: function( params ){
let defaults = {
field: 'data',
bindingEvent: 'data',
allowBinding: false,
allowSetting: false,
allowGetting: false,
settingEvent: 'data',
settingTriggersEvent: false,
triggerFnName: 'trigger',
immutableKeys: {}, // key => true if immutable
updateStyle: false,
beforeGet: function( self ){},
beforeSet: function( self, obj ){},
onSet: function( self ){},
canSet: function( self ){ return true; }
};
params = util.extend( {}, defaults, params );
return function dataImpl( name, value ){
let p = params;
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
let single = selfIsArrayLike ? self[0] : self;
// .data('foo', ...)
if (is.string(name)) { // set or get property
let isPathLike = name.indexOf('.') !== -1; // there might be a normal field with a dot
let path = isPathLike && toPath(name);
// .data('foo')
if( p.allowGetting && value === undefined ){ // get
let ret;
if( single ){
p.beforeGet( single );
// check if it's path and a field with the same name doesn't exist
if (path && single._private[ p.field ][ name ] === undefined) {
ret = get(single._private[ p.field ], path);
} else {
ret = single._private[ p.field ][ name ];
}
}
return ret;
// .data('foo', 'bar')
} else if( p.allowSetting && value !== undefined ){ // set
let valid = !p.immutableKeys[ name ];
if( valid ){
let change = { [name]: value };
p.beforeSet( self, change );
for( let i = 0, l = all.length; i < l; i++ ){
let ele = all[i];
if( p.canSet( ele ) ){
if (path && single._private[ p.field ][ name ] === undefined) {
set(ele._private[ p.field ], path, value);
} else {
ele._private[ p.field ][ name ] = value;
}
}
}
// update mappers if asked
if( p.updateStyle ){ self.updateStyle(); }
// call onSet callback
p.onSet( self );
if( p.settingTriggersEvent ){
self[ p.triggerFnName ]( p.settingEvent );
}
}
}
// .data({ 'foo': 'bar' })
} else if( p.allowSetting && is.plainObject( name ) ){ // extend
let obj = name;
let k, v;
let keys = Object.keys( obj );
p.beforeSet( self, obj );
for( let i = 0; i < keys.length; i++ ){
k = keys[ i ];
v = obj[ k ];
let valid = !p.immutableKeys[ k ];
if( valid ){
for( let j = 0; j < all.length; j++ ){
let ele = all[j];
if( p.canSet( ele ) ){
ele._private[ p.field ][ k ] = v;
}
}
}
}
// update mappers if asked
if( p.updateStyle ){ self.updateStyle(); }
// call onSet callback
p.onSet( self );
if( p.settingTriggersEvent ){
self[ p.triggerFnName ]( p.settingEvent );
}
// .data(function(){ ... })
} else if( p.allowBinding && is.fn( name ) ){ // bind to event
let fn = name;
self.on( p.bindingEvent, fn );
// .data()
} else if( p.allowGetting && name === undefined ){ // get whole object
let ret;
if( single ){
p.beforeGet( single );
ret = single._private[ p.field ];
}
return ret;
}
return self; // maintain chainability
}; // function
}, // data
// remove data field
removeData: function( params ){
let defaults = {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: false,
immutableKeys: {} // key => true if immutable
};
params = util.extend( {}, defaults, params );
return function removeDataImpl( names ){
let p = params;
let self = this;
let selfIsArrayLike = self.length !== undefined;
let all = selfIsArrayLike ? self : [ self ]; // put in array if not array-like
// .removeData('foo bar')
if( is.string( names ) ){ // then get the list of keys, and delete them
let keys = names.split( /\s+/ );
let l = keys.length;
for( let i = 0; i < l; i++ ){ // delete each non-empty key
let key = keys[ i ];
if( is.emptyString( key ) ){ continue; }
let valid = !p.immutableKeys[ key ]; // not valid if immutable
if( valid ){
for( let i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
all[ i_a ]._private[ p.field ][ key ] = undefined;
}
}
}
if( p.triggerEvent ){
self[ p.triggerFnName ]( p.event );
}
// .removeData()
} else if( names === undefined ){ // then delete all keys
for( let i_a = 0, l_a = all.length; i_a < l_a; i_a++ ){
let _privateFields = all[ i_a ]._private[ p.field ];
let keys = Object.keys( _privateFields );
for( let i = 0; i < keys.length; i++ ){
let key = keys[i];
let validKeyToDelete = !p.immutableKeys[ key ];
if( validKeyToDelete ){
_privateFields[ key ] = undefined;
}
}
}
if( p.triggerEvent ){
self[ p.triggerFnName ]( p.event );
}
}
return self; // maintain chaining
}; // function
}, // removeData
}; // define
export default define;

111
frontend/node_modules/cytoscape/src/event.mjs generated vendored Normal file
View File

@@ -0,0 +1,111 @@
/*!
Event object based on jQuery events, MIT license
https://jquery.org/license/
https://tldrlegal.com/license/mit-license
https://github.com/jquery/jquery/blob/master/src/event.js
*/
let Event = function( src, props ){
this.recycle( src, props );
};
function returnFalse(){
return false;
}
function returnTrue(){
return true;
}
// http://www.w3.org/TR/2003/WD-DOM-Level-3-Events-20030331/ecma-script-binding.html
Event.prototype = {
instanceString: function(){
return 'event';
},
recycle: function( src, props ){
this.isImmediatePropagationStopped = this.isPropagationStopped = this.isDefaultPrevented = returnFalse;
if( src != null && src.preventDefault ){ // Browser Event object
this.type = src.type;
// Events bubbling up the document may have been marked as prevented
// by a handler lower down the tree; reflect the correct value.
this.isDefaultPrevented = ( src.defaultPrevented ) ? returnTrue : returnFalse;
} else if( src != null && src.type ){ // Plain object containing all event details
props = src;
} else { // Event string
this.type = src;
}
// Put explicitly provided properties onto the event object
if( props != null ){
// more efficient to manually copy fields we use
this.originalEvent = props.originalEvent;
this.type = props.type != null ? props.type : this.type;
this.cy = props.cy;
this.target = props.target;
this.position = props.position;
this.renderedPosition = props.renderedPosition;
this.namespace = props.namespace;
this.layout = props.layout;
}
if( this.cy != null && this.position != null && this.renderedPosition == null ){
// create a rendered position based on the passed position
let pos = this.position;
let zoom = this.cy.zoom();
let pan = this.cy.pan();
this.renderedPosition = {
x: pos.x * zoom + pan.x,
y: pos.y * zoom + pan.y
};
}
// Create a timestamp if incoming event doesn't have one
this.timeStamp = src && src.timeStamp || Date.now();
},
preventDefault: function(){
this.isDefaultPrevented = returnTrue;
let e = this.originalEvent;
if( !e ){
return;
}
// if preventDefault exists run it on the original event
if( e.preventDefault ){
e.preventDefault();
}
},
stopPropagation: function(){
this.isPropagationStopped = returnTrue;
let e = this.originalEvent;
if( !e ){
return;
}
// if stopPropagation exists run it on the original event
if( e.stopPropagation ){
e.stopPropagation();
}
},
stopImmediatePropagation: function(){
this.isImmediatePropagationStopped = returnTrue;
this.stopPropagation();
},
isDefaultPrevented: returnFalse,
isPropagationStopped: returnFalse,
isImmediatePropagationStopped: returnFalse
};
export default Event;

246
frontend/node_modules/cytoscape/src/extension.mjs generated vendored Normal file
View File

@@ -0,0 +1,246 @@
import * as util from'./util/index.mjs';
import define from './define/index.mjs';
import Collection from './collection/index.mjs';
import Core from './core/index.mjs';
import incExts from './extensions/index.mjs';
import * as is from './is.mjs';
import Emitter from './emitter.mjs';
// registered extensions to cytoscape, indexed by name
let extensions = {};
// registered modules for extensions, indexed by name
let modules = {};
function setExtension( type, name, registrant ){
let ext = registrant;
let overrideErr = function( field ){
util.warn( 'Can not register `' + name + '` for `' + type + '` since `' + field + '` already exists in the prototype and can not be overridden' );
};
if( type === 'core' ){
if( Core.prototype[ name ] ){
return overrideErr( name );
} else {
Core.prototype[ name ] = registrant;
}
} else if( type === 'collection' ){
if( Collection.prototype[ name ] ){
return overrideErr( name );
} else {
Collection.prototype[ name ] = registrant;
}
} else if( type === 'layout' ){
// fill in missing layout functions in the prototype
let Layout = function( options ){
this.options = options;
registrant.call( this, options );
// make sure layout has _private for use w/ std apis like .on()
if( !is.plainObject( this._private ) ){
this._private = {};
}
this._private.cy = options.cy;
this._private.listeners = [];
this.createEmitter();
};
let layoutProto = Layout.prototype = Object.create( registrant.prototype );
let optLayoutFns = [];
for( let i = 0; i < optLayoutFns.length; i++ ){
let fnName = optLayoutFns[ i ];
layoutProto[ fnName ] = layoutProto[ fnName ] || function(){ return this; };
}
// either .start() or .run() is defined, so autogen the other
if( layoutProto.start && !layoutProto.run ){
layoutProto.run = function(){ this.start(); return this; };
} else if( !layoutProto.start && layoutProto.run ){
layoutProto.start = function(){ this.run(); return this; };
}
let regStop = registrant.prototype.stop;
layoutProto.stop = function(){
let opts = this.options;
if( opts && opts.animate ){
let anis = this.animations;
if( anis ){
for( let i = 0; i < anis.length; i++ ){
anis[ i ].stop();
}
}
}
if( regStop ){
regStop.call( this );
} else {
this.emit( 'layoutstop' );
}
return this;
};
if( !layoutProto.destroy ){
layoutProto.destroy = function(){
return this;
};
}
layoutProto.cy = function(){
return this._private.cy;
};
let getCy = layout => layout._private.cy;
let emitterOpts = {
addEventFields: function( layout, evt ){
evt.layout = layout;
evt.cy = getCy(layout);
evt.target = layout;
},
bubble: function(){ return true; },
parent: function( layout ){ return getCy(layout); }
};
util.assign( layoutProto, {
createEmitter: function(){
this._private.emitter = new Emitter( emitterOpts, this );
return this;
},
emitter: function(){ return this._private.emitter; },
on: function( evt, cb ){ this.emitter().on( evt, cb ); return this; },
one: function( evt, cb ){ this.emitter().one( evt, cb ); return this; },
once: function( evt, cb ){ this.emitter().one( evt, cb ); return this; },
removeListener: function( evt, cb ){ this.emitter().removeListener( evt, cb ); return this; },
removeAllListeners: function(){ this.emitter().removeAllListeners(); return this; },
emit: function( evt, params ){ this.emitter().emit( evt, params ); return this; }
} );
define.eventAliasesOn( layoutProto );
ext = Layout; // replace with our wrapped layout
} else if( type === 'renderer' && name !== 'null' && name !== 'base' ){
// user registered renderers inherit from base
let BaseRenderer = getExtension( 'renderer', 'base' );
let bProto = BaseRenderer.prototype;
let RegistrantRenderer = registrant;
let rProto = registrant.prototype;
let Renderer = function(){
BaseRenderer.apply( this, arguments );
RegistrantRenderer.apply( this, arguments );
};
let proto = Renderer.prototype;
for( let pName in bProto ){
let pVal = bProto[ pName ];
let existsInR = rProto[ pName ] != null;
if( existsInR ){
return overrideErr( pName );
}
proto[ pName ] = pVal; // take impl from base
}
for( let pName in rProto ){
proto[ pName ] = rProto[ pName ]; // take impl from registrant
}
bProto.clientFunctions.forEach( function( name ){
proto[ name ] = proto[ name ] || function(){
util.error( 'Renderer does not implement `renderer.' + name + '()` on its prototype' );
};
} );
ext = Renderer;
} else if (type === '__proto__' || type === 'constructor' || type === 'prototype'){
// to avoid potential prototype pollution
return util.error( type + ' is an illegal type to be registered, possibly lead to prototype pollutions' );
}
return util.setMap( {
map: extensions,
keys: [ type, name ],
value: ext
} );
}
function getExtension( type, name ){
return util.getMap( {
map: extensions,
keys: [ type, name ]
} );
}
function setModule( type, name, moduleType, moduleName, registrant ){
return util.setMap( {
map: modules,
keys: [ type, name, moduleType, moduleName ],
value: registrant
} );
}
function getModule( type, name, moduleType, moduleName ){
return util.getMap( {
map: modules,
keys: [ type, name, moduleType, moduleName ]
} );
}
let extension = function(){
// e.g. extension('renderer', 'svg')
if( arguments.length === 2 ){
return getExtension.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', { ... })
else if( arguments.length === 3 ){
return setExtension.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', 'nodeShape', 'ellipse')
else if( arguments.length === 4 ){
return getModule.apply( null, arguments );
}
// e.g. extension('renderer', 'svg', 'nodeShape', 'ellipse', { ... })
else if( arguments.length === 5 ){
return setModule.apply( null, arguments );
}
else {
util.error( 'Invalid extension access syntax' );
}
};
// allows a core instance to access extensions internally
Core.prototype.extension = extension;
// included extensions
incExts.forEach( function( group ){
group.extensions.forEach( function( ext ){
setExtension( group.type, ext.name, ext.impl );
} );
} );
export default extension;

View File

@@ -0,0 +1,105 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
import * as is from '../../is.mjs';
let defaults = {
fit: true, // whether to fit the viewport to the graph
padding: 30, // the padding on fit
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
avoidOverlap: true, // prevents node overlap, may overflow boundingBox and radius if not enough space
nodeDimensionsIncludeLabels: false, // Excludes the label when calculating node bounding boxes for the layout algorithm
spacingFactor: undefined, // Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
radius: undefined, // the radius of the circle
startAngle: 3 / 2 * Math.PI, // where nodes start in radians
sweep: undefined, // how many radians should be between the first and last node (defaults to full circle)
clockwise: true, // whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)
sort: undefined, // a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function CircleLayout( options ){
this.options = util.extend( {}, defaults, options );
}
CircleLayout.prototype.run = function(){
let params = this.options;
let options = params;
let cy = params.cy;
let eles = options.eles;
let clockwise = options.counterclockwise !== undefined ? !options.counterclockwise : options.clockwise;
let nodes = eles.nodes().not( ':parent' );
if( options.sort ){
nodes = nodes.sort( options.sort );
}
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let center = {
x: bb.x1 + bb.w / 2,
y: bb.y1 + bb.h / 2
};
let sweep = options.sweep === undefined ? 2 * Math.PI - 2 * Math.PI / nodes.length : options.sweep;
let dTheta = sweep / ( Math.max( 1, nodes.length - 1 ) );
let r;
let minDistance = 0;
for( let i = 0; i < nodes.length; i++ ){
let n = nodes[ i ];
let nbb = n.layoutDimensions( options );
let w = nbb.w;
let h = nbb.h;
minDistance = Math.max( minDistance, w, h );
}
if( is.number( options.radius ) ){
r = options.radius;
} else if( nodes.length <= 1 ){
r = 0;
} else {
r = Math.min( bb.h, bb.w ) / 2 - minDistance;
}
// calculate the radius
if( nodes.length > 1 && options.avoidOverlap ){ // but only if more than one node (can't overlap)
minDistance *= 1.75; // just to have some nice spacing
let dcos = Math.cos( dTheta ) - Math.cos( 0 );
let dsin = Math.sin( dTheta ) - Math.sin( 0 );
let rMin = Math.sqrt( minDistance * minDistance / ( dcos * dcos + dsin * dsin ) ); // s.t. no nodes overlapping
r = Math.max( rMin, r );
}
let getPos = function( ele, i ){
let theta = options.startAngle + i * dTheta * ( clockwise ? 1 : -1 );
let rx = r * Math.cos( theta );
let ry = r * Math.sin( theta );
let pos = {
x: center.x + rx,
y: center.y + ry
};
return pos;
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default CircleLayout;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,43 @@
import * as util from '../../util/index.mjs';
import * as math from '../../math.mjs';
let defaults = {
fit: true, // whether to fit to viewport
padding: 30, // fit padding
boundingBox: undefined, // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: undefined, // callback on layoutready
stop: undefined, // callback on layoutstop
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
};
function RandomLayout( options ){
this.options = util.extend( {}, defaults, options );
}
RandomLayout.prototype.run = function(){
let options = this.options;
let cy = options.cy;
let eles = options.eles;
let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : {
x1: 0, y1: 0, w: cy.width(), h: cy.height()
} );
let getPos = function( node, i ){
return {
x: bb.x1 + Math.round( Math.random() * bb.w ),
y: bb.y1 + Math.round( Math.random() * bb.h )
};
};
eles.nodes().layoutPositions( this, options, getPos );
return this; // chaining
};
export default RandomLayout;

View File

@@ -0,0 +1,29 @@
import * as util from '../../../../util/index.mjs';
import coords from './coords.mjs';
import edgeArrows from './edge-arrows.mjs';
import edgeControlPoints from './edge-control-points.mjs';
import edgeEndpoints from './edge-endpoints.mjs';
import edgeProjection from './edge-projection.mjs';
import labels from './labels.mjs';
import nodes from './nodes.mjs';
import renderedStyle from './rendered-style.mjs';
import zOrdering from './z-ordering.mjs';
var BRp = {};
[
coords,
edgeArrows,
edgeControlPoints,
edgeEndpoints,
edgeProjection,
labels,
nodes,
renderedStyle,
zOrdering
].forEach(function( props ){
util.extend( BRp, props );
});
export default BRp;

View File

@@ -0,0 +1,38 @@
var BRp = {};
BRp.getCachedImage = function( url, crossOrigin, onLoad ){
var r = this;
var imageCache = r.imageCache = r.imageCache || {};
var cache = imageCache[ url ];
if( cache ){
if( !cache.image.complete ){
cache.image.addEventListener('load', onLoad);
}
return cache.image;
} else {
cache = imageCache[ url ] = imageCache[ url ] || {};
var image = cache.image = new Image(); // eslint-disable-line no-undef
image.addEventListener('load', onLoad);
image.addEventListener('error', function(){ image.error = true; });
// #1582 safari doesn't load data uris with crossOrigin properly
// https://bugs.webkit.org/show_bug.cgi?id=123978
var dataUriPrefix = 'data:';
var isDataUri = url.substring( 0, dataUriPrefix.length ).toLowerCase() === dataUriPrefix;
if( !isDataUri ){
// if crossorigin is 'null'(stringified), then manually set it to null
crossOrigin = crossOrigin === 'null' ? null : crossOrigin;
image.crossOrigin = crossOrigin; // prevent tainted canvas
}
image.src = url;
return image;
}
};
export default BRp;

View File

@@ -0,0 +1,224 @@
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import arrowShapes from './arrow-shapes.mjs';
import coordEleMath from './coord-ele-math/index.mjs';
import images from './images.mjs';
import loadListeners from './load-listeners.mjs';
import nodeShapes from './node-shapes.mjs';
import redraw from './redraw.mjs';
var BaseRenderer = function( options ){ this.init( options ); };
var BR = BaseRenderer;
var BRp = BR.prototype;
BRp.clientFunctions = [ 'redrawHint', 'render', 'renderTo', 'matchCanvasSize', 'nodeShapeImpl', 'arrowShapeImpl' ];
BRp.init = function( options ){
var r = this;
r.options = options;
r.cy = options.cy;
var ctr = r.container = options.cy.container();
var containerWindow = r.cy.window();
// prepend a stylesheet in the head such that
if( containerWindow ){
var document = containerWindow.document;
var head = document.head;
var stylesheetId = '__________cytoscape_stylesheet';
var className = '__________cytoscape_container';
var stylesheetAlreadyExists = document.getElementById( stylesheetId ) != null;
if( ctr.className.indexOf( className ) < 0 ){
ctr.className = ( ctr.className || '' ) + ' ' + className;
}
if( !stylesheetAlreadyExists ){
var stylesheet = document.createElement('style');
stylesheet.id = stylesheetId;
stylesheet.textContent = '.'+className+' { position: relative; }';
head.insertBefore( stylesheet, head.children[0] ); // first so lowest priority
}
var computedStyle = containerWindow.getComputedStyle( ctr );
var position = computedStyle.getPropertyValue('position');
if( position === 'static' ){
util.warn('A Cytoscape container has style position:static and so can not use UI extensions properly');
}
}
r.selection = [ undefined, undefined, undefined, undefined, 0]; // Coordinates for selection box, plus enabled flag
r.bezierProjPcts = [ 0.05, 0.225, 0.4, 0.5, 0.6, 0.775, 0.95 ];
//--Pointer-related data
r.hoverData = {down: null, last: null,
downTime: null, triggerMode: null,
dragging: false,
initialPan: [ null, null ], capture: false};
r.dragData = {possibleDragElements: []};
r.touchData = {
start: null, capture: false,
// These 3 fields related to tap, taphold events
startPosition: [ null, null, null, null, null, null ],
singleTouchStartTime: null,
singleTouchMoved: true,
now: [ null, null, null, null, null, null ],
earlier: [ null, null, null, null, null, null ]
};
r.redraws = 0;
r.showFps = options.showFps;
r.debug = options.debug;
r.webgl = options.webgl;
r.hideEdgesOnViewport = options.hideEdgesOnViewport;
r.textureOnViewport = options.textureOnViewport;
r.wheelSensitivity = options.wheelSensitivity;
r.motionBlurEnabled = options.motionBlur; // on by default
r.forcedPixelRatio = is.number(options.pixelRatio) ? options.pixelRatio : null;
r.motionBlur = options.motionBlur; // for initial kick off
r.motionBlurOpacity = options.motionBlurOpacity;
r.motionBlurTransparency = 1 - r.motionBlurOpacity;
r.motionBlurPxRatio = 1;
r.mbPxRBlurry = 1; //0.8;
r.minMbLowQualFrames = 4;
r.fullQualityMb = false;
r.clearedForMotionBlur = [];
r.desktopTapThreshold = options.desktopTapThreshold;
r.desktopTapThreshold2 = options.desktopTapThreshold * options.desktopTapThreshold;
r.touchTapThreshold = options.touchTapThreshold;
r.touchTapThreshold2 = options.touchTapThreshold * options.touchTapThreshold;
r.tapholdDuration = 500;
r.bindings = [];
r.beforeRenderCallbacks = [];
r.beforeRenderPriorities = { // higher priority execs before lower one
animations: 400,
eleCalcs: 300,
eleTxrDeq: 200,
lyrTxrDeq: 150,
lyrTxrSkip: 100,
};
r.registerNodeShapes();
r.registerArrowShapes();
r.registerCalculationListeners();
};
BRp.notify = function( eventName, eles ) {
var r = this;
var cy = r.cy;
// the renderer can't be notified after it's destroyed
if( this.destroyed ){ return; }
if( eventName === 'init' ){
r.load();
return;
}
if( eventName === 'destroy' ){
r.destroy();
return;
}
if(
eventName === 'add'
|| eventName === 'remove'
|| (eventName === 'move' && cy.hasCompoundNodes())
|| eventName === 'load'
|| eventName === 'zorder'
|| eventName === 'mount'
){
r.invalidateCachedZSortedEles();
}
if( eventName === 'viewport' ){
r.redrawHint( 'select', true );
}
if( eventName === 'gc' ){
r.redrawHint( 'gc', true );
}
if( eventName === 'load' || eventName === 'resize' || eventName === 'mount' ){
r.invalidateContainerClientCoordsCache();
r.matchCanvasSize( r.container );
}
r.redrawHint( 'eles', true );
r.redrawHint( 'drag', true );
this.startRenderLoop();
this.redraw();
};
BRp.destroy = function(){
var r = this;
r.destroyed = true;
r.cy.stopAnimationLoop();
for( var i = 0; i < r.bindings.length; i++ ){
var binding = r.bindings[ i ];
var b = binding;
var tgt = b.target;
( tgt.off || tgt.removeEventListener ).apply( tgt, b.args );
}
r.bindings = [];
r.beforeRenderCallbacks = [];
r.onUpdateEleCalcsFns = [];
if( r.removeObserver ){
r.removeObserver.disconnect();
}
if( r.styleObserver ){
r.styleObserver.disconnect();
}
if( r.resizeObserver ){
r.resizeObserver.disconnect();
}
if( r.labelCalcDiv ){
try {
document.body.removeChild( r.labelCalcDiv ); // eslint-disable-line no-undef
} catch( e ){
// ie10 issue #1014
}
}
};
BRp.isHeadless = function(){
return false;
};
[
arrowShapes,
coordEleMath,
images,
loadListeners,
nodeShapes,
redraw
].forEach( function( props ){
util.extend( BRp, props );
} );
export default BR;

View File

@@ -0,0 +1,431 @@
/* global Path2D */
import * as util from '../../../util/index.mjs';
import {drawPreparedRoundCorner} from "../../../round.mjs";
let CRp = {};
CRp.drawEdge = function( context, edge, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let rs = edge._private.rscratch;
if( shouldDrawOpacity && !edge.visible() ){ return; }
// if bezier ctrl pts can not be calculated, then die
if( rs.badLine || rs.allpts == null || isNaN(rs.allpts[0]) ){ // isNaN in case edge is impossible and browser bugs (e.g. safari)
return;
}
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
let opacity = shouldDrawOpacity ? edge.pstyle('opacity').value : 1;
let lineOpacity = shouldDrawOpacity ? edge.pstyle('line-opacity').value : 1;
let curveStyle = edge.pstyle('curve-style').value;
let lineStyle = edge.pstyle('line-style').value;
let edgeWidth = edge.pstyle('width').pfValue;
let lineCap = edge.pstyle('line-cap').value;
let lineOutlineWidth = edge.pstyle('line-outline-width').value;
let lineOutlineColor = edge.pstyle('line-outline-color').value;
let effectiveLineOpacity = opacity * lineOpacity;
// separate arrow opacity would require arrow-opacity property
let effectiveArrowOpacity = opacity * lineOpacity;
let drawLine = ( strokeOpacity = effectiveLineOpacity) => {
if (curveStyle === 'straight-triangle') {
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
context.lineWidth = edgeWidth;
context.lineCap = lineCap;
r.eleStrokeStyle( context, edge, strokeOpacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawLineOutline = ( strokeOpacity = effectiveLineOpacity) => {
context.lineWidth = edgeWidth + lineOutlineWidth;
context.lineCap = lineCap;
if (lineOutlineWidth > 0) {
r.colorStrokeStyle( context, lineOutlineColor[0], lineOutlineColor[1], lineOutlineColor[2], strokeOpacity );
} else {
// do not draw any lineOutline
context.lineCap = 'butt'; // reset for other drawing functions
return;
}
if (curveStyle === 'straight-triangle') {
r.drawEdgeTrianglePath(
edge,
context,
rs.allpts
);
} else {
r.drawEdgePath(
edge,
context,
rs.allpts,
lineStyle
);
context.lineCap = 'butt'; // reset for other drawing functions
}
};
let drawOverlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeOverlay( context, edge );
};
let drawUnderlay = () => {
if( !shouldDrawOverlay ){ return; }
r.drawEdgeUnderlay( context, edge );
};
let drawArrows = ( arrowOpacity = effectiveArrowOpacity) => {
r.drawArrowheads( context, edge, arrowOpacity );
};
let drawText = () => {
r.drawElementText( context, edge, null, drawLabel );
};
context.lineJoin = 'round';
let ghost = edge.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = edge.pstyle('ghost-offset-x').pfValue;
let gy = edge.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = edge.pstyle('ghost-opacity').value;
let effectiveGhostOpacity = effectiveLineOpacity * ghostOpacity;
context.translate( gx, gy );
drawLine( effectiveGhostOpacity );
drawArrows( effectiveGhostOpacity );
context.translate( -gx, -gy );
} else {
drawLineOutline();
}
drawUnderlay();
drawLine();
drawArrows();
drawOverlay();
drawText();
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawEdgeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, edge ){
if( !edge.visible() ){ return; }
let opacity = edge.pstyle(`${overlayOrUnderlay}-opacity`).value;
if( opacity === 0 ){ return; }
let r = this;
let usePaths = r.usePaths();
let rs = edge._private.rscratch;
let padding = edge.pstyle(`${overlayOrUnderlay}-padding`).pfValue;
let width = 2 * padding;
let color = edge.pstyle(`${overlayOrUnderlay}-color`).value;
context.lineWidth = width;
if( rs.edgeType === 'self' && !usePaths ){
context.lineCap = 'butt';
} else {
context.lineCap = 'round';
}
r.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
r.drawEdgePath(
edge,
context,
rs.allpts,
'solid'
);
};
};
CRp.drawEdgeOverlay = drawEdgeOverlayUnderlay('overlay');
CRp.drawEdgeUnderlay = drawEdgeOverlayUnderlay('underlay');
CRp.drawEdgePath = function( edge, context, pts, type ){
let rs = edge._private.rscratch;
let canvasCxt = context;
let path;
let pathCacheHit = false;
let usePaths = this.usePaths();
let lineDashPattern = edge.pstyle('line-dash-pattern').pfValue;
let lineDashOffset = edge.pstyle('line-dash-offset').pfValue;
if( usePaths ){
let pathCacheKey = pts.join( '$' );
let keyMatches = rs.pathCacheKey && rs.pathCacheKey === pathCacheKey;
if( keyMatches ){
path = context = rs.pathCache;
pathCacheHit = true;
} else {
path = context = new Path2D();
rs.pathCacheKey = pathCacheKey;
rs.pathCache = path;
}
}
if( canvasCxt.setLineDash ){ // for very outofdate browsers
switch( type ){
case 'dotted':
canvasCxt.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
canvasCxt.setLineDash( lineDashPattern );
canvasCxt.lineDashOffset = lineDashOffset;
break;
case 'solid':
canvasCxt.setLineDash( [ ] );
break;
}
}
if( !pathCacheHit && !rs.badLine ){
if( context.beginPath ){ context.beginPath(); }
context.moveTo( pts[0], pts[1] );
switch( rs.edgeType ){
case 'bezier':
case 'self':
case 'compound':
case 'multibezier':
for( let i = 2; i + 3 < pts.length; i += 4 ){
context.quadraticCurveTo( pts[ i ], pts[ i + 1], pts[ i + 2], pts[ i + 3] );
}
break;
case 'straight':
case 'haystack':
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
break;
case 'segments':
if (rs.isRound) {
for( let corner of rs.roundCorners ){
drawPreparedRoundCorner(context, corner);
}
context.lineTo( pts[ pts.length - 2 ], pts[ pts.length - 1] );
} else {
for( let i = 2; i + 1 < pts.length; i += 2 ) {
context.lineTo( pts[ i ], pts[ i + 1] );
}
}
break;
}
}
context = canvasCxt;
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
// reset any line dashes
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
};
CRp.drawEdgeTrianglePath = function( edge, context, pts ){
// use line stroke style for triangle fill style
context.fillStyle = context.strokeStyle;
let edgeWidth = edge.pstyle('width').pfValue;
for( let i = 0; i + 1 < pts.length; i += 2 ){
const vector = [ pts[ i + 2 ] - pts[ i ], pts[ i + 3 ] - pts[ i + 1 ] ];
const length = Math.sqrt( vector[0] * vector[0] + vector[1] * vector[1] );
const normal = [ vector[1] / length, -vector[0] / length ];
const triangleHead = [ normal[0] * edgeWidth / 2, normal[1] * edgeWidth / 2 ];
context.beginPath();
context.moveTo( pts[ i ] - triangleHead[0], pts[ i + 1 ] - triangleHead[1] );
context.lineTo( pts[ i ] + triangleHead[0], pts[ i + 1 ] + triangleHead[1] );
context.lineTo( pts[ i + 2 ], pts[ i + 3 ] );
context.closePath();
context.fill();
}
};
CRp.drawArrowheads = function( context, edge, opacity ){
let rs = edge._private.rscratch;
let isHaystack = rs.edgeType === 'haystack';
if( !isHaystack ){
this.drawArrowhead( context, edge, 'source', rs.arrowStartX, rs.arrowStartY, rs.srcArrowAngle, opacity );
}
this.drawArrowhead( context, edge, 'mid-target', rs.midX, rs.midY, rs.midtgtArrowAngle, opacity );
this.drawArrowhead( context, edge, 'mid-source', rs.midX, rs.midY, rs.midsrcArrowAngle, opacity );
if( !isHaystack ){
this.drawArrowhead( context, edge, 'target', rs.arrowEndX, rs.arrowEndY, rs.tgtArrowAngle, opacity );
}
};
CRp.drawArrowhead = function( context, edge, prefix, x, y, angle, opacity ){
if( isNaN( x ) || x == null || isNaN( y ) || y == null || isNaN( angle ) || angle == null ){ return; }
let self = this;
let arrowShape = edge.pstyle( prefix + '-arrow-shape' ).value;
if( arrowShape === 'none' ) { return; }
let arrowClearFill = edge.pstyle( prefix + '-arrow-fill' ).value === 'hollow' ? 'both' : 'filled';
let arrowFill = edge.pstyle( prefix + '-arrow-fill' ).value;
let edgeWidth = edge.pstyle( 'width' ).pfValue;
let pArrowWidth = edge.pstyle( prefix + '-arrow-width' );
let arrowWidth = pArrowWidth.value === 'match-line' ? edgeWidth : pArrowWidth.pfValue;
if (pArrowWidth.units === '%') arrowWidth *= edgeWidth;
let edgeOpacity = edge.pstyle( 'opacity' ).value;
if( opacity === undefined ){
opacity = edgeOpacity;
}
let gco = context.globalCompositeOperation;
if( opacity !== 1 || arrowFill === 'hollow' ){ // then extra clear is needed
context.globalCompositeOperation = 'destination-out';
self.colorFillStyle( context, 255, 255, 255, 1 );
self.colorStrokeStyle( context, 255, 255, 255, 1 );
self.drawArrowShape( edge, context,
arrowClearFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
context.globalCompositeOperation = gco;
} // otherwise, the opaque arrow clears it for free :)
let color = edge.pstyle( prefix + '-arrow-color' ).value;
self.colorFillStyle( context, color[0], color[1], color[2], opacity );
self.colorStrokeStyle( context, color[0], color[1], color[2], opacity );
self.drawArrowShape( edge, context,
arrowFill, edgeWidth, arrowShape, arrowWidth, x, y, angle
);
};
CRp.drawArrowShape = function( edge, context, fill, edgeWidth, shape, shapeWidth, x, y, angle ){
let r = this;
let usePaths = this.usePaths() && shape !== 'triangle-cross';
let pathCacheHit = false;
let path;
let canvasContext = context;
let translation = { x, y };
let scale = edge.pstyle( 'arrow-scale' ).value;
let size = this.getArrowWidth( edgeWidth, scale );
let shapeImpl = r.arrowShapes[ shape ];
if( usePaths ){
let cache = r.arrowPathCache = r.arrowPathCache || [];
let key = util.hashString(shape);
let cachedPath = cache[ key ];
if( cachedPath != null ){
path = context = cachedPath;
pathCacheHit = true;
} else {
path = context = new Path2D();
cache[ key ] = path;
}
}
if( !pathCacheHit ){
if( context.beginPath ){ context.beginPath(); }
if( usePaths ){ // store in the path cache with values easily manipulated later
shapeImpl.draw( context, 1, 0, { x: 0, y: 0 }, 1 );
} else {
shapeImpl.draw( context, size, angle, translation, edgeWidth );
}
if( context.closePath ){ context.closePath(); }
}
context = canvasContext;
if( usePaths ){ // set transform to arrow position/orientation
context.translate( x, y );
context.rotate( angle );
context.scale( size, size );
}
if( fill === 'filled' || fill === 'both' ){
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
if( fill === 'hollow' || fill === 'both' ){
context.lineWidth = shapeWidth / (usePaths ? size : 1);
context.lineJoin = 'miter';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
}
if( usePaths ){ // reset transform by applying inverse
context.scale( 1/size, 1/size );
context.rotate( -angle );
context.translate( -x, -y );
}
};
export default CRp;

View File

@@ -0,0 +1,218 @@
import * as math from '../../../math.mjs';
let CRp = {};
CRp.drawElement = function( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity ){
let r = this;
if( ele.isNode() ){
r.drawNode( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity );
} else {
r.drawEdge( context, ele, shiftToOriginWithBb, showLabel, showOverlay, showOpacity );
}
};
CRp.drawElementOverlay = function( context, ele ){
let r = this;
if( ele.isNode() ){
r.drawNodeOverlay( context, ele );
} else {
r.drawEdgeOverlay( context, ele );
}
};
CRp.drawElementUnderlay = function( context, ele ){
let r = this;
if( ele.isNode() ){
r.drawNodeUnderlay( context, ele );
} else {
r.drawEdgeUnderlay( context, ele );
}
};
CRp.drawCachedElementPortion = function( context, ele, eleTxrCache, pxRatio, lvl, reason, getRotation, getOpacity ){
let r = this;
let bb = eleTxrCache.getBoundingBox(ele);
if( bb.w === 0 || bb.h === 0 ){ return; } // ignore zero size case
let eleCache = eleTxrCache.getElement( ele, bb, pxRatio, lvl, reason );
if( eleCache != null ){
let opacity = getOpacity(r, ele);
if( opacity === 0 ){ return; }
let theta = getRotation(r, ele);
let { x1, y1, w, h } = bb;
let x, y, sx, sy, smooth;
if( theta !== 0 ){
let rotPt = eleTxrCache.getRotationPoint(ele);
sx = rotPt.x;
sy = rotPt.y;
context.translate(sx, sy);
context.rotate(theta);
smooth = r.getImgSmoothing(context);
if( !smooth ){ r.setImgSmoothing(context, true); }
let off = eleTxrCache.getRotationOffset(ele);
x = off.x;
y = off.y;
} else {
x = x1;
y = y1;
}
let oldGlobalAlpha;
if( opacity !== 1 ){
oldGlobalAlpha = context.globalAlpha;
context.globalAlpha = oldGlobalAlpha * opacity;
}
context.drawImage( eleCache.texture.canvas, eleCache.x, 0, eleCache.width, eleCache.height, x, y, w, h );
if( opacity !== 1 ){
context.globalAlpha = oldGlobalAlpha;
}
if( theta !== 0 ){
context.rotate(-theta);
context.translate(-sx, -sy);
if( !smooth ){ r.setImgSmoothing(context, false); }
}
} else {
eleTxrCache.drawElement( context, ele ); // direct draw fallback
}
};
const getZeroRotation = () => 0;
const getLabelRotation = (r, ele) => r.getTextAngle(ele, null);
const getSourceLabelRotation = (r, ele) => r.getTextAngle(ele, 'source');
const getTargetLabelRotation = (r, ele) => r.getTextAngle(ele, 'target');
const getOpacity = (r, ele) => ele.effectiveOpacity();
const getTextOpacity = (e, ele) => ele.pstyle('text-opacity').pfValue * ele.effectiveOpacity();
CRp.drawCachedElement = function( context, ele, pxRatio, extent, lvl, requestHighQuality ){
let r = this;
let { eleTxrCache, lblTxrCache, slbTxrCache, tlbTxrCache } = r.data;
let bb = ele.boundingBox();
let reason = requestHighQuality === true ? eleTxrCache.reasons.highQuality : null;
if( bb.w === 0 || bb.h === 0 || !ele.visible() ){ return; }
if( !extent || math.boundingBoxesIntersect( bb, extent ) ){
let isEdge = ele.isEdge();
let badLine = ele.element()._private.rscratch.badLine;
r.drawElementUnderlay( context, ele );
r.drawCachedElementPortion( context, ele, eleTxrCache, pxRatio, lvl, reason, getZeroRotation, getOpacity );
if( !isEdge || !badLine ){
r.drawCachedElementPortion( context, ele, lblTxrCache, pxRatio, lvl, reason, getLabelRotation, getTextOpacity );
}
if( isEdge && !badLine ){
r.drawCachedElementPortion( context, ele, slbTxrCache, pxRatio, lvl, reason, getSourceLabelRotation, getTextOpacity );
r.drawCachedElementPortion( context, ele, tlbTxrCache, pxRatio, lvl, reason, getTargetLabelRotation, getTextOpacity );
}
r.drawElementOverlay( context, ele );
}
};
CRp.drawElements = function( context, eles ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
r.drawElement( context, ele );
}
};
CRp.drawCachedElements = function( context, eles, pxRatio, extent ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
r.drawCachedElement( context, ele, pxRatio, extent );
}
};
CRp.drawCachedNodes = function( context, eles, pxRatio, extent ){
let r = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
if( !ele.isNode() ){ continue; }
r.drawCachedElement( context, ele, pxRatio, extent );
}
};
CRp.drawLayeredElements = function( context, eles, pxRatio, extent ){
let r = this;
let layers = r.data.lyrTxrCache.getLayers( eles, pxRatio );
if( layers ){
for( let i = 0; i < layers.length; i++ ){
let layer = layers[i];
let bb = layer.bb;
if( bb.w === 0 || bb.h === 0 ){ continue; }
context.drawImage( layer.canvas, bb.x1, bb.y1, bb.w, bb.h );
}
} else { // fall back on plain caching if no layers
r.drawCachedElements( context, eles, pxRatio, extent );
}
};
if( process.env.NODE_ENV !== 'production' ){
CRp.drawDebugPoints = function( context, eles ){
let draw = function( x, y, color ){
context.fillStyle = color;
context.fillRect( x - 1, y - 1, 3, 3 );
};
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
let rs = ele._private.rscratch;
if( ele.isNode() ){
let p = ele.position();
draw( rs.labelX, rs.labelY, 'red' );
draw( p.x, p.y, 'magenta' );
} else {
let pts = rs.allpts;
for( let j = 0; j + 1 < pts.length; j += 2 ){
let x = pts[ j ];
let y = pts[ j + 1 ];
draw( x, y, 'cyan' );
}
draw( rs.midX, rs.midY, 'yellow' );
}
}
};
}
export default CRp;

View File

@@ -0,0 +1,804 @@
/* global Path2D */
import * as is from '../../../is.mjs';
import {expandPolygon, joinLines} from '../../../math.mjs';
import * as util from '../../../util/index.mjs';
import * as round from "../../../round.mjs";
import * as math from "../../../math.mjs";
let CRp = {};
CRp.drawNode = function( context, node, shiftToOriginWithBb, drawLabel = true, shouldDrawOverlay = true, shouldDrawOpacity = true ){
let r = this;
let nodeWidth, nodeHeight;
let _p = node._private;
let rs = _p.rscratch;
let pos = node.position();
if( !is.number( pos.x ) || !is.number( pos.y ) ){
return; // can't draw node with undefined position
}
if( shouldDrawOpacity && !node.visible() ){ return; }
let eleOpacity = shouldDrawOpacity ? node.effectiveOpacity() : 1;
let usePaths = r.usePaths();
let path;
let pathCacheHit = false;
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
//
// setup shift
let bb;
if( shiftToOriginWithBb ){
bb = shiftToOriginWithBb;
context.translate( -bb.x1, -bb.y1 );
}
//
// load bg image
let bgImgProp = node.pstyle( 'background-image' );
let urls = bgImgProp.value;
let urlDefined = new Array( urls.length );
let image = new Array( urls.length );
let numImages = 0;
for( let i = 0; i < urls.length; i++ ){
let url = urls[i];
let defd = urlDefined[i] = url != null && url !== 'none';
if( defd ){
let bgImgCrossOrigin = node.cy().style().getIndexedStyle(node, 'background-image-crossorigin', 'value', i);
numImages++;
// get image, and if not loaded then ask to redraw when later loaded
image[i] = r.getCachedImage( url, bgImgCrossOrigin, function(){
_p.backgroundTimestamp = Date.now();
node.emitAndNotify('background');
} );
}
}
//
// setup styles
let darkness = node.pstyle('background-blacken').value;
let borderWidth = node.pstyle('border-width').pfValue;
let bgOpacity = node.pstyle('background-opacity').value * eleOpacity;
let borderColor = node.pstyle('border-color').value;
let borderStyle = node.pstyle('border-style').value;
let borderJoin = node.pstyle('border-join').value;
let borderCap = node.pstyle('border-cap').value;
let borderPosition = node.pstyle('border-position').value;
let borderPattern = node.pstyle('border-dash-pattern').pfValue;
let borderOffset = node.pstyle('border-dash-offset').pfValue;
let borderOpacity = node.pstyle('border-opacity').value * eleOpacity;
let outlineWidth = node.pstyle('outline-width').pfValue;
let outlineColor = node.pstyle('outline-color').value;
let outlineStyle = node.pstyle('outline-style').value;
let outlineOpacity = node.pstyle('outline-opacity').value * eleOpacity;
let outlineOffset = node.pstyle('outline-offset').value;
let cornerRadius = node.pstyle('corner-radius').value;
if (cornerRadius !== 'auto') cornerRadius = node.pstyle('corner-radius').pfValue;
let setupShapeColor = ( bgOpy = bgOpacity ) => {
r.eleFillStyle( context, node, bgOpy );
};
let setupBorderColor = ( bdrOpy = borderOpacity ) => {
r.colorStrokeStyle( context, borderColor[0], borderColor[1], borderColor[2], bdrOpy );
};
let setupOutlineColor = ( otlnOpy = outlineOpacity ) => {
r.colorStrokeStyle( context, outlineColor[0], outlineColor[1], outlineColor[2], otlnOpy );
};
//
// setup shape
let getPath = (width, height, shape, points) => {
let pathCache = r.nodePathCache = r.nodePathCache || [];
let key = util.hashStrings(
shape === 'polygon' ? shape + ',' + points.join(',') : shape,
'' + height,
'' + width,
'' + cornerRadius
);
let cachedPath = pathCache[ key ];
let path;
let cacheHit = false;
if( cachedPath != null ){
path = cachedPath;
cacheHit = true;
rs.pathCache = path;
} else {
path = new Path2D();
pathCache[ key ] = rs.pathCache = path;
}
return {
path,
cacheHit
};
};
let styleShape = node.pstyle('shape').strValue;
let shapePts = node.pstyle('shape-polygon-points').pfValue;
if( usePaths ){
context.translate( pos.x, pos.y );
const shapePath = getPath(nodeWidth, nodeHeight, styleShape, shapePts);
path = shapePath.path;
pathCacheHit = shapePath.cacheHit;
}
let drawShape = () => {
if( !pathCacheHit ){
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
r.nodeShapes[ r.getNodeShape( node ) ].draw(
( path || context ),
npos.x,
npos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
};
let drawImages = ( nodeOpacity = eleOpacity, inside = true ) => {
let prevBging = _p.backgrounding;
let totalCompleted = 0;
for( let i = 0; i < image.length; i++ ){
const bgContainment = node.cy().style().getIndexedStyle(node, 'background-image-containment', 'value', i);
if( inside && bgContainment === 'over' || !inside && bgContainment === 'inside' ){
totalCompleted++;
continue;
}
if( urlDefined[i] && image[i].complete && !image[i].error ){
totalCompleted++;
r.drawInscribedImage( context, image[i], node, i, nodeOpacity );
}
}
_p.backgrounding = !(totalCompleted === numImages);
if( prevBging !== _p.backgrounding ){ // update style b/c :backgrounding state changed
node.updateStyle( false );
}
};
let drawPie = ( redrawShape = false, pieOpacity = eleOpacity ) => {
if( r.hasPie( node ) ){
r.drawPie( context, node, pieOpacity );
// redraw/restore path if steps after pie need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
}
}
};
let drawStripe = (redrawShape = false, stripeOpacity = eleOpacity) => {
if( r.hasStripe( node ) ){
context.save();
if (usePaths) {
context.clip( rs.pathCache );
} else {
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
context.clip();
}
r.drawStripe( context, node, stripeOpacity );
context.restore();
// redraw/restore path if steps after stripes need it
if( redrawShape ){
if( !usePaths ){
r.nodeShapes[ r.getNodeShape( node ) ].draw(
context,
pos.x,
pos.y,
nodeWidth,
nodeHeight, cornerRadius, rs );
}
}
}
};
let darken = ( darkenOpacity = eleOpacity ) => {
let opacity = ( darkness > 0 ? darkness : -darkness ) * darkenOpacity;
let c = darkness > 0 ? 0 : 255;
if( darkness !== 0 ){
r.colorFillStyle( context, c, c, c, opacity );
if( usePaths ){
context.fill( path );
} else {
context.fill();
}
}
};
let drawBorder = () => {
if( borderWidth > 0 ){
context.lineWidth = borderWidth;
context.lineCap = borderCap;
context.lineJoin = borderJoin;
if( context.setLineDash ){ // for very outofdate browsers
switch( borderStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( borderPattern );
context.lineDashOffset = borderOffset;
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
if ( borderPosition !== 'center') {
context.save();
context.lineWidth *= 2;
if (borderPosition === 'inside') {
usePaths ? context.clip(path) : context.clip();
} else {
const region = new Path2D();
region.rect(
-nodeWidth / 2 - borderWidth,
-nodeHeight / 2 - borderWidth,
nodeWidth + 2 * borderWidth,
nodeHeight + 2 * borderWidth
);
region.addPath(path);
context.clip(region, 'evenodd');
}
usePaths ? context.stroke(path) : context.stroke();
context.restore();
} else {
usePaths ? context.stroke(path) : context.stroke();
}
if( borderStyle === 'double' ){
context.lineWidth = borderWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOutline = () => {
if( outlineWidth > 0 ){
context.lineWidth = outlineWidth;
context.lineCap = 'butt';
if( context.setLineDash ){ // for very outofdate browsers
switch( outlineStyle ){
case 'dotted':
context.setLineDash( [ 1, 1 ] );
break;
case 'dashed':
context.setLineDash( [ 4, 2 ] );
break;
case 'solid':
case 'double':
context.setLineDash( [ ] );
break;
}
}
let npos = pos;
if( usePaths ){
npos = {
x: 0,
y: 0
};
}
let shape = r.getNodeShape( node );
let bWidth = borderWidth;
if( borderPosition === 'inside' ) bWidth = 0;
if( borderPosition === 'outside' ) bWidth *= 2;
let scaleX = (nodeWidth + bWidth + (outlineWidth + outlineOffset)) / nodeWidth;
let scaleY = (nodeHeight + bWidth + (outlineWidth + outlineOffset)) / nodeHeight;
let sWidth = nodeWidth * scaleX;
let sHeight = nodeHeight * scaleY;
let points = r.nodeShapes[ shape ].points;
let path;
if (usePaths) {
let outlinePath = getPath(sWidth, sHeight, shape, points);
path = outlinePath.path;
}
// draw the outline path, either by using expanded points or by scaling
// the dimensions, depending on shape
if (shape === "ellipse") {
r.drawEllipsePath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if ([
'round-diamond', 'round-heptagon', 'round-hexagon', 'round-octagon',
'round-pentagon', 'round-polygon', 'round-triangle', 'round-tag'
].includes(shape)) {
let sMult = 0;
let offsetX = 0;
let offsetY = 0;
if (shape === 'round-diamond') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.4;
} else if (shape === 'round-heptagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.075;
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 35;
} else if (shape === 'round-hexagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12;
} else if (shape === 'round-pentagon') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.13;
offsetY = -(bWidth/2 + outlineOffset + outlineWidth) / 15;
} else if (shape === 'round-tag') {
sMult = (bWidth + outlineOffset + outlineWidth) * 1.12;
offsetX = (bWidth/2 + outlineWidth + outlineOffset) * .07;
} else if (shape === 'round-triangle') {
sMult = (bWidth + outlineOffset + outlineWidth) * (Math.PI/2);
offsetY = -(bWidth + outlineOffset/2 + outlineWidth) / Math.PI;
}
if (sMult !== 0) {
scaleX = (nodeWidth + sMult)/nodeWidth;
sWidth = nodeWidth * scaleX;
if ( ! ['round-hexagon', 'round-tag'].includes(shape) ) {
scaleY = (nodeHeight + sMult)/nodeHeight;
sHeight = nodeHeight * scaleY;
}
}
cornerRadius = cornerRadius === 'auto' ? math.getRoundPolygonRadius( sWidth, sHeight ) : cornerRadius;
const halfW = sWidth / 2;
const halfH = sHeight / 2;
const radius = cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2;
const p = new Array( points.length / 2 );
const corners = new Array( points.length / 2 );
for ( let i = 0; i < points.length / 2; i++ ){
p[i] = {
x: npos.x + offsetX + halfW * points[ i * 2 ],
y: npos.y + offsetY + halfH * points[ i * 2 + 1 ]
};
}
let i, p1, p2, p3, len = p.length;
p1 = p[ len - 1 ];
// for each point
for( i = 0; i < len; i++ ){
p2 = p[ (i) % len ];
p3 = p[ (i + 1) % len ];
corners[ i ] = round.getRoundCorner( p1, p2, p3, radius );
p1 = p2;
p2 = p3;
}
r.drawRoundPolygonPath(path || context, npos.x + offsetX, npos.y + offsetY, nodeWidth * scaleX, nodeHeight * scaleY, points, corners );
} else if (['roundrectangle', 'round-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius;
r.drawRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 );
} else if (['cutrectangle', 'cut-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getCutRectangleCornerLength( sWidth, sHeight ) : cornerRadius;
r.drawCutRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, null ,cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 4 );
} else if (['bottomroundrectangle', 'bottom-round-rectangle'].includes(shape)) {
cornerRadius = cornerRadius === 'auto' ? math.getRoundRectangleRadius( sWidth, sHeight ) : cornerRadius;
r.drawBottomRoundRectanglePath(path || context, npos.x, npos.y, sWidth, sHeight, cornerRadius + ( bWidth + outlineWidth + outlineOffset ) / 2 );
} else if (shape === "barrel") {
r.drawBarrelPath(path || context, npos.x, npos.y, sWidth, sHeight);
} else if (shape.startsWith("polygon") || ['rhomboid', 'right-rhomboid', 'round-tag', 'tag', 'vee'].includes(shape)) {
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
} else {
let pad = (bWidth + outlineWidth + outlineOffset) / nodeWidth;
points = joinLines(expandPolygon(points, -pad));
r.drawPolygonPath(path || context, npos.x, npos.y, nodeWidth, nodeHeight, points);
}
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
if( outlineStyle === 'double' ){
context.lineWidth = bWidth / 3;
let gco = context.globalCompositeOperation;
context.globalCompositeOperation = 'destination-out';
if( usePaths ){
context.stroke( path );
} else {
context.stroke();
}
context.globalCompositeOperation = gco;
}
// reset in case we changed the border style
if( context.setLineDash ){ // for very outofdate browsers
context.setLineDash( [ ] );
}
}
};
let drawOverlay = () => {
if( shouldDrawOverlay ){
r.drawNodeOverlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawUnderlay = () => {
if( shouldDrawOverlay ){
r.drawNodeUnderlay( context, node, pos, nodeWidth, nodeHeight );
}
};
let drawText = () => {
r.drawElementText( context, node, null, drawLabel );
};
let ghost = node.pstyle('ghost').value === 'yes';
if( ghost ){
let gx = node.pstyle('ghost-offset-x').pfValue;
let gy = node.pstyle('ghost-offset-y').pfValue;
let ghostOpacity = node.pstyle('ghost-opacity').value;
let effGhostOpacity = ghostOpacity * eleOpacity;
context.translate( gx, gy );
setupOutlineColor();
drawOutline();
setupShapeColor( ghostOpacity * bgOpacity );
drawShape();
drawImages( effGhostOpacity, true );
setupBorderColor( ghostOpacity * borderOpacity );
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawStripe( darkness !== 0 || borderWidth !== 0 );
drawImages( effGhostOpacity, false );
darken( effGhostOpacity );
context.translate( -gx, -gy );
}
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawUnderlay();
if( usePaths ){
context.translate( pos.x, pos.y );
}
setupOutlineColor();
drawOutline();
setupShapeColor();
drawShape();
drawImages(eleOpacity, true);
setupBorderColor();
drawBorder();
drawPie( darkness !== 0 || borderWidth !== 0 );
drawStripe( darkness !== 0 || borderWidth !== 0 );
drawImages(eleOpacity, false);
darken();
if( usePaths ){
context.translate( -pos.x, -pos.y );
}
drawText();
drawOverlay();
//
// clean up shift
if( shiftToOriginWithBb ){
context.translate( bb.x1, bb.y1 );
}
};
const drawNodeOverlayUnderlay = function( overlayOrUnderlay ) {
if (!['overlay', 'underlay'].includes(overlayOrUnderlay)) {
throw new Error('Invalid state');
}
return function( context, node, pos, nodeWidth, nodeHeight ){
let r = this;
if( !node.visible() ){ return; }
let padding = node.pstyle( `${overlayOrUnderlay}-padding` ).pfValue;
let opacity = node.pstyle( `${overlayOrUnderlay}-opacity` ).value;
let color = node.pstyle( `${overlayOrUnderlay}-color` ).value;
let shape = node.pstyle( `${overlayOrUnderlay}-shape` ).value;
let radius = node.pstyle( `${overlayOrUnderlay}-corner-radius` ).value;
if( opacity > 0 ){
pos = pos || node.position();
if( nodeWidth == null || nodeHeight == null ){
let padding = node.padding();
nodeWidth = node.width() + 2 * padding;
nodeHeight = node.height() + 2 * padding;
}
r.colorFillStyle( context, color[0], color[1], color[2], opacity );
r.nodeShapes[shape].draw(
context,
pos.x,
pos.y,
nodeWidth + padding * 2,
nodeHeight + padding * 2,
radius
);
context.fill();
}
};
};
CRp.drawNodeOverlay = drawNodeOverlayUnderlay('overlay');
CRp.drawNodeUnderlay = drawNodeOverlayUnderlay('underlay');
// does the node have at least one pie piece?
CRp.hasPie = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasPie;
};
CRp.hasStripe = function( node ){
node = node[0]; // ensure ele ref
return node._private.hasStripe;
};
CRp.drawPie = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let pieSize = node.pstyle( 'pie-size' );
let hole = node.pstyle('pie-hole');
let overallStartAngle = node.pstyle('pie-start-angle').pfValue;
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let radius = Math.min( nodeW, nodeH ) / 2; // must fit in node
let holeRadius;
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
if( usePaths ){
x = 0;
y = 0;
}
if( pieSize.units === '%' ){
radius = radius * pieSize.pfValue;
} else if( pieSize.pfValue !== undefined ){
radius = pieSize.pfValue / 2; // diameter in pixels => radius
}
if (hole.units === '%') {
holeRadius = radius * hole.pfValue;
} else if (hole.pfValue !== undefined) {
holeRadius = hole.pfValue / 2; // diameter in pixels => radius
}
if (holeRadius >= radius) {
return; // the pie would be invisible anyway
}
for( let i = 1; i <= cyStyle.pieBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'pie-' + i + '-background-size' ).value;
let color = node.pstyle( 'pie-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'pie-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
let angleStart = (1.5 * Math.PI) + (2 * Math.PI * lastPercent); // start at 12 o'clock and go clockwise
angleStart += overallStartAngle; // shift by the overall pie start angle
let angleDelta = 2 * Math.PI * percent;
let angleEnd = angleStart + angleDelta;
// ignore if
// - zero size
// - we're already beyond the full circle
// - adding the current slice would go beyond the full circle
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
if (holeRadius === 0) { // make a pie slice
context.beginPath();
context.moveTo( x, y );
context.arc( x, y, radius, angleStart, angleEnd );
context.closePath();
} else { // make a pie slice that's like the above but with a hole in the middle
context.beginPath();
context.arc(x, y, radius, angleStart, angleEnd);
context.arc(x, y, holeRadius, angleEnd, angleStart, true); // true for anticlockwise
context.closePath();
}
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
};
CRp.drawStripe = function( context, node, nodeOpacity, pos ){
node = node[0]; // ensure ele ref
pos = pos || node.position();
let cyStyle = node.cy().style();
let x = pos.x;
let y = pos.y;
let nodeW = node.width();
let nodeH = node.height();
let lastPercent = 0; // what % to continue drawing pie slices from on [0, 1]
let usePaths = this.usePaths();
context.save();
let direction = node.pstyle('stripe-direction').value;
let stripeSize = node.pstyle('stripe-size');
switch (direction) {
case 'vertical':
break; // default
case 'righward':
context.rotate(-Math.PI / 2);
break;
}
let stripeW = nodeW;
let stripeH = nodeH;
if( stripeSize.units === '%' ){
stripeW = stripeW * stripeSize.pfValue;
stripeH = stripeH * stripeSize.pfValue;
} else if( stripeSize.pfValue !== undefined ){
stripeW = stripeSize.pfValue;
stripeH = stripeSize.pfValue;
}
if( usePaths ){
x = 0;
y = 0;
}
// shift up from the centre of the node to the top-left corner
y -= stripeW / 2;
x -= stripeH / 2;
for( let i = 1; i <= cyStyle.stripeBackgroundN; i++ ){ // 1..N
let size = node.pstyle( 'stripe-' + i + '-background-size' ).value;
let color = node.pstyle( 'stripe-' + i + '-background-color' ).value;
let opacity = node.pstyle( 'stripe-' + i + '-background-opacity' ).value * nodeOpacity;
let percent = size / 100; // map integer range [0, 100] to [0, 1]
// percent can't push beyond 1
if( percent + lastPercent > 1 ){
percent = 1 - lastPercent;
}
// ignore if
// - zero size
// - we're already beyond the full chart
// - adding the current slice would go beyond the full chart
if( size === 0 || lastPercent >= 1 || lastPercent + percent > 1 ){
continue;
}
// draw rect for the current stripe
context.beginPath();
context.rect( x, y + stripeH * lastPercent, stripeW, stripeH * percent );
context.closePath();
this.colorFillStyle( context, color[0], color[1], color[2], opacity );
context.fill();
lastPercent += percent;
}
context.restore();
};
export default CRp;

View File

@@ -0,0 +1,555 @@
import * as math from '../../../math.mjs';
import { trueify, falsify, removeFromArray, clearArray, MAX_INT, assign, defaults } from '../../../util/index.mjs';
import Heap from '../../../heap.mjs';
import defs from './texture-cache-defs.mjs';
import ElementTextureCacheLookup from './ele-texture-cache-lookup.mjs';
const minTxrH = 25; // the size of the texture cache for small height eles (special case)
const txrStepH = 50; // the min size of the regular cache, and the size it increases with each step up
const minLvl = -4; // when scaling smaller than that we don't need to re-render
export const maxLvl = 3; // when larger than this scale just render directly (caching is not helpful)
export const maxZoom = 7.99; // beyond this zoom level, layered textures are not used
const eleTxrSpacing = 8; // spacing between elements on textures to avoid blitting overlaps
const defTxrWidth = 1024; // default/minimum texture width
const maxTxrW = 1024; // the maximum width of a texture
const maxTxrH = 1024; // the maximum height of a texture
const minUtility = 0.2; // if usage of texture is less than this, it is retired
const maxFullness = 0.8; // fullness of texture after which queue removal is checked
const maxFullnessChecks = 10; // dequeued after this many checks
const deqCost = 0.15; // % of add'l rendering cost allowed for dequeuing ele caches each frame
const deqAvgCost = 0.1; // % of add'l rendering cost compared to average overall redraw time
const deqNoDrawCost = 0.9; // % of avg frame time that can be used for dequeueing when not drawing
const deqFastCost = 0.9; // % of frame time to be used when >60fps
const deqRedrawThreshold = 100; // time to batch redraws together from dequeueing to allow more dequeueing calcs to happen in the meanwhile
const maxDeqSize = 1; // number of eles to dequeue and render at higher texture in each batch
const getTxrReasons = {
dequeue: 'dequeue',
downscale: 'downscale',
highQuality: 'highQuality'
};
const initDefaults = defaults({
getKey: null,
doesEleInvalidateKey: falsify,
drawElement: null,
getBoundingBox: null,
getRotationPoint: null,
getRotationOffset: null,
isVisible: trueify,
allowEdgeTxrCaching: true,
allowParentTxrCaching: true
});
const ElementTextureCache = function( renderer, initOptions ){
let self = this;
self.renderer = renderer;
self.onDequeues = [];
let opts = initDefaults(initOptions);
assign(self, opts);
self.lookup = new ElementTextureCacheLookup(opts.getKey, opts.doesEleInvalidateKey);
self.setupDequeueing();
};
const ETCp = ElementTextureCache.prototype;
ETCp.reasons = getTxrReasons;
// the list of textures in which new subtextures for elements can be placed
ETCp.getTextureQueue = function( txrH ){
let self = this;
self.eleImgCaches = self.eleImgCaches || {};
return ( self.eleImgCaches[ txrH ] = self.eleImgCaches[ txrH ] || [] );
};
// the list of usused textures which can be recycled (in use in texture queue)
ETCp.getRetiredTextureQueue = function( txrH ){
let self = this;
let rtxtrQs = self.eleImgCaches.retired = self.eleImgCaches.retired || {};
let rtxtrQ = rtxtrQs[ txrH ] = rtxtrQs[ txrH ] || [];
return rtxtrQ;
};
// queue of element draw requests at different scale levels
ETCp.getElementQueue = function(){
let self = this;
let q = self.eleCacheQueue = self.eleCacheQueue || new Heap(function( a, b ){
return b.reqs - a.reqs;
});
return q;
};
// queue of element draw requests at different scale levels (element id lookup)
ETCp.getElementKeyToQueue = function(){
let self = this;
let k2q = self.eleKeyToCacheQueue = self.eleKeyToCacheQueue || {};
return k2q;
};
ETCp.getElement = function( ele, bb, pxRatio, lvl, reason ){
let self = this;
let r = this.renderer;
let zoom = r.cy.zoom();
let lookup = this.lookup;
if( !bb || bb.w === 0 || bb.h === 0 || isNaN(bb.w) || isNaN(bb.h) || !ele.visible() || ele.removed() ){ return null; }
if(
( !self.allowEdgeTxrCaching && ele.isEdge() )
|| ( !self.allowParentTxrCaching && ele.isParent() )
){
return null;
}
if( lvl == null ){
lvl = Math.ceil( math.log2( zoom * pxRatio ) );
}
if( lvl < minLvl ){
lvl = minLvl;
} else if( zoom >= maxZoom || lvl > maxLvl ){
return null;
}
let scale = Math.pow( 2, lvl );
let eleScaledH = bb.h * scale;
let eleScaledW = bb.w * scale;
let scaledLabelShown = r.eleTextBiggerThanMin( ele, scale );
if( !this.isVisible(ele, scaledLabelShown) ){ return null; }
let eleCache = lookup.get( ele, lvl );
// if this get was on an unused/invalidated cache, then restore the texture usage metric
if( eleCache && eleCache.invalidated ){
eleCache.invalidated = false;
eleCache.texture.invalidatedWidth -= eleCache.width;
}
if( eleCache ){
return eleCache;
}
let txrH; // which texture height this ele belongs to
if( eleScaledH <= minTxrH ){
txrH = minTxrH;
} else if( eleScaledH <= txrStepH ){
txrH = txrStepH;
} else {
txrH = Math.ceil( eleScaledH / txrStepH ) * txrStepH;
}
if( eleScaledH > maxTxrH || eleScaledW > maxTxrW ){
return null; // caching large elements is not efficient
}
let txrQ = self.getTextureQueue( txrH );
// first try the second last one in case it has space at the end
let txr = txrQ[ txrQ.length - 2 ];
let addNewTxr = function(){
return self.recycleTexture( txrH, eleScaledW ) || self.addTexture( txrH, eleScaledW );
};
// try the last one if there is no second last one
if( !txr ){
txr = txrQ[ txrQ.length - 1 ];
}
// if the last one doesn't exist, we need a first one
if( !txr ){
txr = addNewTxr();
}
// if there's no room in the current texture, we need a new one
if( txr.width - txr.usedWidth < eleScaledW ){
txr = addNewTxr();
}
let scalableFrom = function( otherCache ){
return otherCache && otherCache.scaledLabelShown === scaledLabelShown;
};
let deqing = reason && reason === getTxrReasons.dequeue;
let highQualityReq = reason && reason === getTxrReasons.highQuality;
let downscaleReq = reason && reason === getTxrReasons.downscale;
let higherCache; // the nearest cache with a higher level
for( let l = lvl + 1; l <= maxLvl; l++ ){
let c = lookup.get( ele, l );
if( c ){ higherCache = c; break; }
}
let oneUpCache = higherCache && higherCache.level === lvl + 1 ? higherCache : null;
let downscale = function(){
txr.context.drawImage(
oneUpCache.texture.canvas,
oneUpCache.x, 0,
oneUpCache.width, oneUpCache.height,
txr.usedWidth, 0,
eleScaledW, eleScaledH
);
};
// reset ele area in texture
txr.context.setTransform( 1, 0, 0, 1, 0, 0 );
txr.context.clearRect( txr.usedWidth, 0, eleScaledW, txrH );
if( scalableFrom(oneUpCache) ){
// then we can relatively cheaply rescale the existing image w/o rerendering
downscale();
} else if( scalableFrom(higherCache) ){
// then use the higher cache for now and queue the next level down
// to cheaply scale towards the smaller level
if( highQualityReq ){
for( let l = higherCache.level; l > lvl; l-- ){
oneUpCache = self.getElement( ele, bb, pxRatio, l, getTxrReasons.downscale );
}
downscale();
} else {
self.queueElement( ele, higherCache.level - 1 );
return higherCache;
}
} else {
let lowerCache; // the nearest cache with a lower level
if( !deqing && !highQualityReq && !downscaleReq ){
for( let l = lvl - 1; l >= minLvl; l-- ){
let c = lookup.get( ele, l );
if( c ){ lowerCache = c; break; }
}
}
if( scalableFrom(lowerCache) ){
// then use the lower quality cache for now and queue the better one for later
self.queueElement( ele, lvl );
return lowerCache;
}
txr.context.translate( txr.usedWidth, 0 );
txr.context.scale( scale, scale );
this.drawElement( txr.context, ele, bb, scaledLabelShown, false );
txr.context.scale( 1/scale, 1/scale );
txr.context.translate( -txr.usedWidth, 0 );
}
eleCache = {
x: txr.usedWidth,
texture: txr,
level: lvl,
scale: scale,
width: eleScaledW,
height: eleScaledH,
scaledLabelShown: scaledLabelShown
};
txr.usedWidth += Math.ceil( eleScaledW + eleTxrSpacing );
txr.eleCaches.push( eleCache );
lookup.set( ele, lvl, eleCache );
self.checkTextureFullness( txr );
return eleCache;
};
ETCp.invalidateElements = function( eles ){
for( let i = 0; i < eles.length; i++ ){
this.invalidateElement(eles[i]);
}
};
ETCp.invalidateElement = function( ele ){
let self = this;
let lookup = self.lookup;
let caches = [];
let invalid = lookup.isInvalid(ele);
if( !invalid ){
return; // override the invalidation request if the element key has not changed
}
for( let lvl = minLvl; lvl <= maxLvl; lvl++ ){
let cache = lookup.getForCachedKey( ele, lvl );
if( cache ){
caches.push( cache );
}
}
let noOtherElesUseCache = lookup.invalidate(ele);
if( noOtherElesUseCache ){
for( let i = 0; i < caches.length; i++ ){
let cache = caches[i];
let txr = cache.texture;
// remove space from the texture it belongs to
txr.invalidatedWidth += cache.width;
// mark the cache as invalidated
cache.invalidated = true;
// retire the texture if its utility is low
self.checkTextureUtility( txr );
}
}
// remove from queue since the old req was for the old state
self.removeFromQueue( ele );
};
ETCp.checkTextureUtility = function( txr ){
// invalidate all entries in the cache if the cache size is small
if( txr.invalidatedWidth >= minUtility * txr.width ){
this.retireTexture( txr );
}
};
ETCp.checkTextureFullness = function( txr ){
// if texture has been mostly filled and passed over several times, remove
// it from the queue so we don't need to waste time looking at it to put new things
let self = this;
let txrQ = self.getTextureQueue( txr.height );
if( txr.usedWidth / txr.width > maxFullness && txr.fullnessChecks >= maxFullnessChecks ){
removeFromArray( txrQ, txr );
} else {
txr.fullnessChecks++;
}
};
ETCp.retireTexture = function( txr ){
let self = this;
let txrH = txr.height;
let txrQ = self.getTextureQueue( txrH );
let lookup = this.lookup;
// retire the texture from the active / searchable queue:
removeFromArray( txrQ, txr );
txr.retired = true;
// remove the refs from the eles to the caches:
let eleCaches = txr.eleCaches;
for( let i = 0; i < eleCaches.length; i++ ){
let eleCache = eleCaches[i];
lookup.deleteCache( eleCache.key, eleCache.level );
}
clearArray( eleCaches );
// add the texture to a retired queue so it can be recycled in future:
let rtxtrQ = self.getRetiredTextureQueue( txrH );
rtxtrQ.push( txr );
};
ETCp.addTexture = function( txrH, minW ){
let self = this;
let txrQ = self.getTextureQueue( txrH );
let txr = {};
txrQ.push( txr );
txr.eleCaches = [];
txr.height = txrH;
txr.width = Math.max( defTxrWidth, minW );
txr.usedWidth = 0;
txr.invalidatedWidth = 0;
txr.fullnessChecks = 0;
txr.canvas = self.renderer.makeOffscreenCanvas(txr.width, txr.height);
txr.context = txr.canvas.getContext('2d');
return txr;
};
ETCp.recycleTexture = function( txrH, minW ){
let self = this;
let txrQ = self.getTextureQueue( txrH );
let rtxtrQ = self.getRetiredTextureQueue( txrH );
for( let i = 0; i < rtxtrQ.length; i++ ){
let txr = rtxtrQ[i];
if( txr.width >= minW ){
txr.retired = false;
txr.usedWidth = 0;
txr.invalidatedWidth = 0;
txr.fullnessChecks = 0;
clearArray( txr.eleCaches );
txr.context.setTransform( 1, 0, 0, 1, 0, 0 );
txr.context.clearRect( 0, 0, txr.width, txr.height );
removeFromArray( rtxtrQ, txr );
txrQ.push( txr );
return txr;
}
}
};
ETCp.queueElement = function( ele, lvl ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let key = this.getKey(ele);
let existingReq = k2q[key];
if( existingReq ){
// use the max lvl b/c in between lvls are cheap to make
existingReq.level = Math.max( existingReq.level, lvl );
existingReq.eles.merge(ele);
existingReq.reqs++;
q.updateItem( existingReq );
} else {
let req = {
eles: ele.spawn().merge(ele),
level: lvl,
reqs: 1,
key
};
q.push( req );
k2q[key] = req;
}
};
ETCp.dequeue = function( pxRatio /*, extent*/ ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let dequeued = [];
let lookup = self.lookup;
for( let i = 0; i < maxDeqSize; i++ ){
if( q.size() > 0 ){
let req = q.pop();
let key = req.key;
let ele = req.eles[0]; // all eles have the same key
let cacheExists = lookup.hasCache(ele, req.level);
// clear out the key to req lookup
k2q[key] = null;
// dequeueing isn't necessary with an existing cache
if( cacheExists ){ continue; }
dequeued.push( req );
let bb = self.getBoundingBox( ele );
self.getElement( ele, bb, pxRatio, req.level, getTxrReasons.dequeue );
} else {
break;
}
}
return dequeued;
};
ETCp.removeFromQueue = function( ele ){
let self = this;
let q = self.getElementQueue();
let k2q = self.getElementKeyToQueue();
let key = this.getKey(ele);
let req = k2q[key];
if( req != null ){
if( req.eles.length === 1 ){ // remove if last ele in the req
// bring to front of queue
req.reqs = MAX_INT;
q.updateItem(req);
q.pop(); // remove from queue
k2q[key] = null; // remove from lookup map
} else { // otherwise just remove ele from req
req.eles.unmerge(ele);
}
}
};
ETCp.onDequeue = function( fn ){ this.onDequeues.push( fn ); };
ETCp.offDequeue = function( fn ){ removeFromArray( this.onDequeues, fn ); };
ETCp.setupDequeueing = defs.setupDequeueing({
deqRedrawThreshold: deqRedrawThreshold,
deqCost: deqCost,
deqAvgCost: deqAvgCost,
deqNoDrawCost: deqNoDrawCost,
deqFastCost: deqFastCost,
deq: function( self, pxRatio, extent ){
return self.dequeue( pxRatio, extent );
},
onDeqd: function( self, deqd ){
for( let i = 0; i < self.onDequeues.length; i++ ){
let fn = self.onDequeues[i];
fn( deqd );
}
},
shouldRedraw: function( self, deqd, pxRatio, extent ){
for( let i = 0; i < deqd.length; i++ ){
let eles = deqd[i].eles;
for( let j = 0; j < eles.length; j++ ){
let bb = eles[j].boundingBox();
if( math.boundingBoxesIntersect( bb, extent ) ){
return true;
}
}
}
return false;
},
priority: function( self ){
return self.renderer.beforeRenderPriorities.eleTxrDeq;
}
});
export default ElementTextureCache;

View File

@@ -0,0 +1,390 @@
/*
The canvas renderer was written by Yue Dong.
Modifications tracked on Github.
*/
/* global OffscreenCanvas */
import * as util from '../../../util/index.mjs';
import * as is from '../../../is.mjs';
import { makeBoundingBox } from '../../../math.mjs';
import ElementTextureCache from './ele-texture-cache.mjs';
import LayeredTextureCache from './layered-texture-cache.mjs';
import arrowShapes from './arrow-shapes.mjs';
import drawingElements from './drawing-elements.mjs';
import drawingEdges from './drawing-edges.mjs';
import drawingImages from './drawing-images.mjs';
import drawingLabelText from './drawing-label-text.mjs';
import drawingNodes from './drawing-nodes.mjs';
import drawingRedraw from './drawing-redraw.mjs';
import drawingRedrawWebGL from './webgl/drawing-redraw-webgl.mjs';
import drawingShapes from './drawing-shapes.mjs';
import exportImage from './export-image.mjs';
import nodeShapes from './node-shapes.mjs';
var CR = CanvasRenderer;
var CRp = CanvasRenderer.prototype;
CRp.CANVAS_LAYERS = 3;
//
CRp.SELECT_BOX = 0;
CRp.DRAG = 1;
CRp.NODE = 2;
CRp.WEBGL = 3;
CRp.CANVAS_TYPES = [ '2d', '2d', '2d', 'webgl2' ];
CRp.BUFFER_COUNT = 3;
//
CRp.TEXTURE_BUFFER = 0;
CRp.MOTIONBLUR_BUFFER_NODE = 1;
CRp.MOTIONBLUR_BUFFER_DRAG = 2;
function CanvasRenderer( options ){
var r = this;
var containerWindow = r.cy.window();
var document = containerWindow.document;
if( options.webgl ){
CRp.CANVAS_LAYERS = r.CANVAS_LAYERS = 4;
console.log('webgl rendering enabled');
}
r.data = {
canvases: new Array( CRp.CANVAS_LAYERS ),
contexts: new Array( CRp.CANVAS_LAYERS ),
canvasNeedsRedraw: new Array( CRp.CANVAS_LAYERS ),
bufferCanvases: new Array( CRp.BUFFER_COUNT ),
bufferContexts: new Array( CRp.CANVAS_LAYERS ),
};
var tapHlOffAttr = '-webkit-tap-highlight-color';
var tapHlOffStyle = 'rgba(0,0,0,0)';
r.data.canvasContainer = document.createElement( 'div' ); // eslint-disable-line no-undef
var containerStyle = r.data.canvasContainer.style;
r.data.canvasContainer.style[tapHlOffAttr] = tapHlOffStyle;
containerStyle.position = 'relative';
containerStyle.zIndex = '0';
containerStyle.overflow = 'hidden';
var container = options.cy.container();
container.appendChild( r.data.canvasContainer );
container.style[tapHlOffAttr] = tapHlOffStyle;
var styleMap = {
'-webkit-user-select': 'none',
'-moz-user-select': '-moz-none',
'user-select': 'none',
'-webkit-tap-highlight-color': 'rgba(0,0,0,0)',
'outline-style': 'none',
};
if(is.ms()) {
styleMap['-ms-touch-action'] = 'none';
styleMap['touch-action'] = 'none';
}
for( var i = 0; i < CRp.CANVAS_LAYERS; i++ ){
var canvas = r.data.canvases[ i ] = document.createElement( 'canvas' ); // eslint-disable-line no-undef
var type = CRp.CANVAS_TYPES[ i ];
r.data.contexts[ i ] = canvas.getContext( type );
if( !r.data.contexts[ i ] ) {
util.error( 'Could not create canvas of type ' + type );
}
Object.keys(styleMap).forEach((k) => {
canvas.style[k] = styleMap[k];
});
canvas.style.position = 'absolute';
canvas.setAttribute( 'data-id', 'layer' + i );
canvas.style.zIndex = String( CRp.CANVAS_LAYERS - i );
r.data.canvasContainer.appendChild( canvas );
r.data.canvasNeedsRedraw[ i ] = false;
}
r.data.topCanvas = r.data.canvases[0];
r.data.canvases[ CRp.NODE ].setAttribute( 'data-id', 'layer' + CRp.NODE + '-node' );
r.data.canvases[ CRp.SELECT_BOX ].setAttribute( 'data-id', 'layer' + CRp.SELECT_BOX + '-selectbox' );
r.data.canvases[ CRp.DRAG ].setAttribute( 'data-id', 'layer' + CRp.DRAG + '-drag' );
if( r.data.canvases[ CRp.WEBGL ] ) {
r.data.canvases[ CRp.WEBGL ].setAttribute( 'data-id', 'layer' + CRp.WEBGL + '-webgl' );
}
for( var i = 0; i < CRp.BUFFER_COUNT; i++ ){
r.data.bufferCanvases[ i ] = document.createElement( 'canvas' ); // eslint-disable-line no-undef
r.data.bufferContexts[ i ] = r.data.bufferCanvases[ i ].getContext( '2d' );
r.data.bufferCanvases[ i ].style.position = 'absolute';
r.data.bufferCanvases[ i ].setAttribute( 'data-id', 'buffer' + i );
r.data.bufferCanvases[ i ].style.zIndex = String( -i - 1 );
r.data.bufferCanvases[ i ].style.visibility = 'hidden';
//r.data.canvasContainer.appendChild(r.data.bufferCanvases[i]);
}
r.pathsEnabled = true;
let emptyBb = makeBoundingBox();
let getBoxCenter = bb => ({ x: (bb.x1 + bb.x2)/2, y: (bb.y1 + bb.y2)/2 });
let getCenterOffset = bb => ({ x: -bb.w/2, y: -bb.h/2 });
let backgroundTimestampHasChanged = ele => {
let _p = ele[0]._private;
let same = _p.oldBackgroundTimestamp === _p.backgroundTimestamp;
return !same;
};
let getStyleKey = ele => ele[0]._private.nodeKey;
let getLabelKey = ele => ele[0]._private.labelStyleKey;
let getSourceLabelKey = ele => ele[0]._private.sourceLabelStyleKey;
let getTargetLabelKey = ele => ele[0]._private.targetLabelStyleKey;
let drawElement = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElement( context, ele, bb, false, false, useEleOpacity );
let drawLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'main', useEleOpacity );
let drawSourceLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'source', useEleOpacity );
let drawTargetLabel = (context, ele, bb, scaledLabelShown, useEleOpacity) => r.drawElementText( context, ele, bb, scaledLabelShown, 'target', useEleOpacity );
let getElementBox = ele => { ele.boundingBox(); return ele[0]._private.bodyBounds; };
let getLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.main || emptyBb; };
let getSourceLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.source || emptyBb; };
let getTargetLabelBox = ele => { ele.boundingBox(); return ele[0]._private.labelBounds.target || emptyBb; };
let isLabelVisibleAtScale = (ele, scaledLabelShown) => scaledLabelShown;
let getElementRotationPoint = ele => getBoxCenter( getElementBox(ele) );
let addTextMargin = (prefix, pt, ele) => {
let pre = prefix ? prefix + '-' : '';
return {
x: pt.x + ele.pstyle(pre + 'text-margin-x').pfValue,
y: pt.y + ele.pstyle(pre + 'text-margin-y').pfValue
};
};
let getRsPt = (ele, x, y) => {
let rs = ele[0]._private.rscratch;
return { x: rs[x], y: rs[y] };
};
let getLabelRotationPoint = ele => addTextMargin('', getRsPt(ele, 'labelX', 'labelY'), ele);
let getSourceLabelRotationPoint = ele => addTextMargin('source', getRsPt(ele, 'sourceLabelX', 'sourceLabelY'), ele);
let getTargetLabelRotationPoint = ele => addTextMargin('target', getRsPt(ele, 'targetLabelX', 'targetLabelY'), ele);
let getElementRotationOffset = ele => getCenterOffset( getElementBox(ele) );
let getSourceLabelRotationOffset = ele => getCenterOffset( getSourceLabelBox(ele) );
let getTargetLabelRotationOffset = ele => getCenterOffset( getTargetLabelBox(ele) );
let getLabelRotationOffset = ele => {
let bb = getLabelBox(ele);
let p = getCenterOffset( getLabelBox(ele) );
if( ele.isNode() ){
switch( ele.pstyle('text-halign').value ){
case 'left':
p.x = -bb.w - (bb.leftPad || 0);
break;
case 'right':
p.x = -(bb.rightPad || 0);
break;
}
switch( ele.pstyle('text-valign').value ){
case 'top':
p.y = -bb.h - (bb.topPad || 0);
break;
case 'bottom':
p.y = -(bb.botPad || 0);
break;
}
}
return p;
};
let eleTxrCache = r.data.eleTxrCache = new ElementTextureCache( r, {
getKey: getStyleKey,
doesEleInvalidateKey: backgroundTimestampHasChanged,
drawElement: drawElement,
getBoundingBox: getElementBox,
getRotationPoint: getElementRotationPoint,
getRotationOffset: getElementRotationOffset,
allowEdgeTxrCaching: false,
allowParentTxrCaching: false
} );
let lblTxrCache = r.data.lblTxrCache = new ElementTextureCache( r, {
getKey: getLabelKey,
drawElement: drawLabel,
getBoundingBox: getLabelBox,
getRotationPoint: getLabelRotationPoint,
getRotationOffset: getLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let slbTxrCache = r.data.slbTxrCache = new ElementTextureCache( r, {
getKey: getSourceLabelKey,
drawElement: drawSourceLabel,
getBoundingBox: getSourceLabelBox,
getRotationPoint: getSourceLabelRotationPoint,
getRotationOffset: getSourceLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let tlbTxrCache = r.data.tlbTxrCache = new ElementTextureCache( r, {
getKey: getTargetLabelKey,
drawElement: drawTargetLabel,
getBoundingBox: getTargetLabelBox,
getRotationPoint: getTargetLabelRotationPoint,
getRotationOffset: getTargetLabelRotationOffset,
isVisible: isLabelVisibleAtScale
} );
let lyrTxrCache = r.data.lyrTxrCache = new LayeredTextureCache( r );
r.onUpdateEleCalcs(function invalidateTextureCaches( willDraw, eles ){
// each cache should check for sub-key diff to see that the update affects that cache particularly
eleTxrCache.invalidateElements( eles );
lblTxrCache.invalidateElements( eles );
slbTxrCache.invalidateElements( eles );
tlbTxrCache.invalidateElements( eles );
// any change invalidates the layers
lyrTxrCache.invalidateElements( eles );
// update the old bg timestamp so diffs can be done in the ele txr caches
for( let i = 0; i < eles.length; i++ ){
let _p = eles[i]._private;
_p.oldBackgroundTimestamp = _p.backgroundTimestamp;
}
});
let refineInLayers = reqs => {
for( var i = 0; i < reqs.length; i++ ){
lyrTxrCache.enqueueElementRefinement( reqs[i].ele );
}
};
eleTxrCache.onDequeue(refineInLayers);
lblTxrCache.onDequeue(refineInLayers);
slbTxrCache.onDequeue(refineInLayers);
tlbTxrCache.onDequeue(refineInLayers);
if( options.webgl ) {
r.initWebgl( options, {
getStyleKey,
getLabelKey,
getSourceLabelKey,
getTargetLabelKey,
drawElement,
drawLabel,
drawSourceLabel,
drawTargetLabel,
getElementBox,
getLabelBox,
getSourceLabelBox,
getTargetLabelBox,
getElementRotationPoint,
getElementRotationOffset,
getLabelRotationPoint,
getSourceLabelRotationPoint,
getTargetLabelRotationPoint,
getLabelRotationOffset,
getSourceLabelRotationOffset,
getTargetLabelRotationOffset
} );
}
}
CRp.redrawHint = function( group, bool ){
var r = this;
switch( group ){
case 'eles':
r.data.canvasNeedsRedraw[ CRp.NODE ] = bool;
break;
case 'drag':
r.data.canvasNeedsRedraw[ CRp.DRAG ] = bool;
break;
case 'select':
r.data.canvasNeedsRedraw[ CRp.SELECT_BOX ] = bool;
break;
case 'gc':
r.data.gc = true;
break;
}
};
// whether to use Path2D caching for drawing
var pathsImpld = typeof Path2D !== 'undefined';
CRp.path2dEnabled = function( on ){
if( on === undefined ){
return this.pathsEnabled;
}
this.pathsEnabled = on ? true : false;
};
CRp.usePaths = function(){
return pathsImpld && this.pathsEnabled;
};
CRp.setImgSmoothing = function( context, bool ){
if( context.imageSmoothingEnabled != null ){
context.imageSmoothingEnabled = bool;
} else {
context.webkitImageSmoothingEnabled = bool;
context.mozImageSmoothingEnabled = bool;
context.msImageSmoothingEnabled = bool;
}
};
CRp.getImgSmoothing = function( context ){
if( context.imageSmoothingEnabled != null ){
return context.imageSmoothingEnabled;
} else {
return context.webkitImageSmoothingEnabled || context.mozImageSmoothingEnabled || context.msImageSmoothingEnabled;
}
};
CRp.makeOffscreenCanvas = function(width, height){
let canvas;
if( typeof OffscreenCanvas !== typeof undefined ){
canvas = new OffscreenCanvas(width, height);
} else {
var containerWindow = this.cy.window();
var document = containerWindow.document;
canvas = document.createElement('canvas'); // eslint-disable-line no-undef
canvas.width = width;
canvas.height = height;
}
return canvas;
};
[
arrowShapes,
drawingElements,
drawingEdges,
drawingImages,
drawingLabelText,
drawingNodes,
drawingRedraw,
drawingRedrawWebGL,
drawingShapes,
exportImage,
nodeShapes
].forEach( function( props ){
util.extend( CRp, props );
} );
export default CR;

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,208 @@
// Canvas Upscaling Plugin
class MiscUpscaler {
constructor(options = {}) {
// Plugin Settings
this.options = Object.assign({
useEdgeDetection: true, // Edge Detection
scaleFactor: 2.0, // Upscaling Value
}, options);
this.gl = null;
this.program = null;
this.frameBuffer = null;
this.texture = null;
this.outputTexture = null;
}
init(inputCanvas, outputCanvas) {
this.inputCanvas = inputCanvas;
this.outputCanvas = outputCanvas;
// Set output canvas size based on scale factor
this.outputCanvas.width = this.inputCanvas.width * this.options.scaleFactor;
this.outputCanvas.height = this.inputCanvas.height * this.options.scaleFactor;
// Initialize WebGL context
this.gl = this.outputCanvas.getContext('webgl2');
if (!this.gl) {
console.error('WebGL not supported');
return false;
}
// Initialize shaders and buffers
this.initShaders();
this.initBuffers();
this.initTextures();
return true;
}
initShaders() {
// Vertex shader
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 vUv;
void main() {
vUv = a_texCoord;
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
// Fragment shader
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D tDiffuse;
uniform vec2 resolution;
varying vec2 vUv;
void main() {
vec2 texelSize = 1.0 / resolution;
// Get Neighbor Pixels
vec4 color = texture2D(tDiffuse, vUv);
vec4 colorUp = texture2D(tDiffuse, vUv + vec2(0.0, texelSize.y));
vec4 colorDown = texture2D(tDiffuse, vUv - vec2(0.0, texelSize.y));
vec4 colorLeft = texture2D(tDiffuse, vUv - vec2(texelSize.x, 0.0));
vec4 colorRight = texture2D(tDiffuse, vUv + vec2(texelSize.x, 0.0));
// Work with edges
float edgeStrength = 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorUp.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorDown.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorLeft.rgb));
edgeStrength += 1.0 - smoothstep(0.1, 0.3, length(color.rgb - colorRight.rgb));
edgeStrength = clamp(edgeStrength, 0.0, 1.0);
// Applying edges incresing and filtering
vec3 enhancedColor = mix(color.rgb, vec3(1.0) - (1.0 - color.rgb) * edgeStrength, 0.5);
gl_FragColor = vec4(enhancedColor, color.a);
}
`;
// Create shader program
const vertexShader = this.createShader(this.gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = this.createShader(this.gl.FRAGMENT_SHADER, fragmentShaderSource);
this.program = this.createProgram(vertexShader, fragmentShader);
// Get attribute and uniform locations
this.positionLocation = this.gl.getAttribLocation(this.program, 'a_position');
this.texCoordLocation = this.gl.getAttribLocation(this.program, 'a_texCoord');
this.resolutionLocation = this.gl.getUniformLocation(this.program, 'resolution');
this.textureLocation = this.gl.getUniformLocation(this.program, 'tDiffuse');
}
createShader(type, source) {
const shader = this.gl.createShader(type);
this.gl.shaderSource(shader, source);
this.gl.compileShader(shader);
if (!this.gl.getShaderParameter(shader, this.gl.COMPILE_STATUS)) {
console.error('Shader compile error:', this.gl.getShaderInfoLog(shader));
this.gl.deleteShader(shader);
return null;
}
return shader;
}
createProgram(vertexShader, fragmentShader) {
const program = this.gl.createProgram();
this.gl.attachShader(program, vertexShader);
this.gl.attachShader(program, fragmentShader);
this.gl.linkProgram(program);
if (!this.gl.getProgramParameter(program, this.gl.LINK_STATUS)) {
console.error('Program link error:', this.gl.getProgramInfoLog(program));
return null;
}
return program;
}
initBuffers() {
// Create a buffer for positions
this.positionBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
// Full screen quad (2 triangles)
const positions = [
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1
];
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(positions), this.gl.STATIC_DRAW);
// Create a buffer for texture coordinates
this.texCoordBuffer = this.gl.createBuffer();
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
// Texture coordinates for the quad with Y-flipped to match canvas coordinates
const texCoords = [
0, 1, // top-left (flipped from 0,0)
1, 1, // top-right (flipped from 1,0)
0, 0, // bottom-left (flipped from 0,1)
0, 0, // bottom-left (flipped from 0,1)
1, 1, // top-right (flipped from 1,0)
1, 0 // bottom-right (flipped from 1,1)
];
this.gl.bufferData(this.gl.ARRAY_BUFFER, new Float32Array(texCoords), this.gl.STATIC_DRAW);
}
initTextures() {
// Create texture for input canvas
this.texture = this.gl.createTexture();
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
// Set parameters for texture
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_S, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_WRAP_T, this.gl.CLAMP_TO_EDGE);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MIN_FILTER, this.gl.LINEAR);
this.gl.texParameteri(this.gl.TEXTURE_2D, this.gl.TEXTURE_MAG_FILTER, this.gl.LINEAR);
}
render() {
if (!this.gl) return;
// Update texture from input canvas
this.gl.bindTexture(this.gl.TEXTURE_2D, this.texture);
this.gl.texImage2D(this.gl.TEXTURE_2D, 0, this.gl.RGBA, this.gl.RGBA, this.gl.UNSIGNED_BYTE, this.inputCanvas);
// Set viewport and clear
this.gl.viewport(0, 0, this.outputCanvas.width, this.outputCanvas.height);
this.gl.clearColor(0, 0, 0, 0);
this.gl.clear(this.gl.COLOR_BUFFER_BIT);
// Use shader program
this.gl.useProgram(this.program);
// Set uniforms
this.gl.uniform2f(this.resolutionLocation, this.inputCanvas.width, this.inputCanvas.height);
this.gl.uniform1i(this.textureLocation, 0);
// Set position attribute
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.positionBuffer);
this.gl.enableVertexAttribArray(this.positionLocation);
this.gl.vertexAttribPointer(this.positionLocation, 2, this.gl.FLOAT, false, 0, 0);
// Set texture coordinate attribute
this.gl.bindBuffer(this.gl.ARRAY_BUFFER, this.texCoordBuffer);
this.gl.enableVertexAttribArray(this.texCoordLocation);
this.gl.vertexAttribPointer(this.texCoordLocation, 2, this.gl.FLOAT, false, 0, 0);
// Draw
this.gl.drawArrays(this.gl.TRIANGLES, 0, 6);
}
resize() {
if (this.inputCanvas && this.outputCanvas) {
this.outputCanvas.width = this.inputCanvas.width * this.options.scaleFactor;
this.outputCanvas.height = this.inputCanvas.height * this.options.scaleFactor;
}
}
}
export { MiscUpscaler as UpscalerPlugin };

View File

@@ -0,0 +1,9 @@
import nullRenderer from './null/index.mjs';
import baseRenderer from './base/index.mjs';
import canvasRenderer from './canvas/index.mjs';
export default [
{ name: 'null', impl: nullRenderer },
{ name: 'base', impl: baseRenderer },
{ name: 'canvas', impl: canvasRenderer },
];

145
frontend/node_modules/cytoscape/src/is.mjs generated vendored Normal file
View File

@@ -0,0 +1,145 @@
/*global HTMLElement DocumentTouch */
import window from './window.mjs';
let navigator = window ? window.navigator : null;
let document = window ? window.document : null;
let typeofstr = typeof '';
let typeofobj = typeof {};
let typeoffn = typeof function(){};
let typeofhtmlele = typeof HTMLElement;
let instanceStr = function( obj ){
return obj && obj.instanceString && fn( obj.instanceString ) ? obj.instanceString() : null;
};
export const defined = obj =>
obj != null; // not undefined or null
export const string = obj =>
obj != null && typeof obj == typeofstr;
export const fn = obj =>
obj != null && typeof obj === typeoffn;
export const array = obj =>
!(elementOrCollection(obj)) && (Array.isArray ? Array.isArray( obj ) : obj != null && obj instanceof Array);
export const plainObject = obj =>
obj != null && typeof obj === typeofobj && !array( obj ) && obj.constructor === Object;
export const object = obj =>
obj != null && typeof obj === typeofobj;
export const number = obj =>
obj != null && typeof obj === typeof 1 && !isNaN( obj );
export const integer = obj =>
number( obj ) && Math.floor( obj ) === obj;
export const bool = obj =>
obj != null && typeof obj === typeof true;
export const htmlElement = obj => {
if( 'undefined' === typeofhtmlele ){
return undefined;
} else {
return null != obj && obj instanceof HTMLElement;
}
};
export const elementOrCollection = obj =>
element( obj ) || collection( obj );
export const element = obj =>
instanceStr( obj ) === 'collection' && obj._private.single;
export const collection = obj =>
instanceStr( obj ) === 'collection' && !obj._private.single;
export const core = obj =>
instanceStr( obj ) === 'core';
export const style = obj =>
instanceStr( obj ) === 'style';
export const stylesheet = obj =>
instanceStr( obj ) === 'stylesheet';
export const event = obj =>
instanceStr( obj ) === 'event';
export const thread = obj =>
instanceStr( obj ) === 'thread';
export const fabric = obj =>
instanceStr( obj ) === 'fabric';
export const emptyString = obj => {
if( obj === undefined || obj === null ){ // null is empty
return true;
} else if( obj === '' || obj.match( /^\s+$/ ) ){
return true; // empty string is empty
}
return false; // otherwise, we don't know what we've got
};
export const nonemptyString = obj => {
if( obj && string( obj ) && obj !== '' && !obj.match( /^\s+$/ ) ){
return true;
}
return false;
};
export const domElement = obj => {
if( typeof HTMLElement === 'undefined' ){
return false; // we're not in a browser so it doesn't matter
} else {
return obj instanceof HTMLElement;
}
};
export const boundingBox = obj =>
plainObject( obj ) &&
number( obj.x1 ) && number( obj.x2 ) &&
number( obj.y1 ) && number( obj.y2 )
;
export const promise = obj =>
object( obj ) && fn( obj.then );
export const touch = () =>
window && ( ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch );
export const gecko = () =>
window && ( typeof InstallTrigger !== 'undefined' || ('MozAppearance' in document.documentElement.style) );
export const webkit = () =>
window && ( typeof webkitURL !== 'undefined' || ('WebkitAppearance' in document.documentElement.style) );
export const chromium = () =>
window && ( typeof chrome !== 'undefined' );
export const khtml = () =>
navigator && navigator.vendor.match( /kde/i ); // probably a better way to detect this...
export const khtmlEtc = () =>
khtml() || webkit() || chromium();
export const ms = () =>
navigator && navigator.userAgent.match( /msie|trident|edge/i ); // probably a better way to detect this...
export const windows = () =>
navigator && navigator.appVersion.match( /Win/i );
export const mac = () =>
navigator && navigator.appVersion.match( /Mac/i );
export const linux = () =>
navigator && navigator.appVersion.match( /Linux/i );
export const unix = () =>
navigator && navigator.appVersion.match( /X11/i );

View File

@@ -0,0 +1,13 @@
/**
* Make a new query object
*
* @prop type {Type} The type enum (int) of the query
* @prop checks List of checks to make against an ele to test for a match
*/
let newQuery = function(){
return {
checks: []
};
};
export default newQuery;

254
frontend/node_modules/cytoscape/src/selector/parse.mjs generated vendored Normal file
View File

@@ -0,0 +1,254 @@
import { warn } from '../util/index.mjs';
import * as is from '../is.mjs';
import exprs from './expressions.mjs';
import newQuery from './new-query.mjs';
import Type from './type.mjs';
/**
* Of all the expressions, find the first match in the remaining text.
* @param {string} remaining The remaining text to parse
* @returns The matched expression and the newly remaining text `{ expr, match, name, remaining }`
*/
const consumeExpr = ( remaining ) => {
let expr;
let match;
let name;
for( let j = 0; j < exprs.length; j++ ){
let e = exprs[ j ];
let n = e.name;
let m = remaining.match( e.regexObj );
if( m != null ){
match = m;
expr = e;
name = n;
let consumed = m[0];
remaining = remaining.substring( consumed.length );
break; // we've consumed one expr, so we can return now
}
}
return {
expr: expr,
match: match,
name: name,
remaining: remaining
};
};
/**
* Consume all the leading whitespace
* @param {string} remaining The text to consume
* @returns The text with the leading whitespace removed
*/
const consumeWhitespace = ( remaining ) => {
let match = remaining.match( /^\s+/ );
if( match ){
let consumed = match[0];
remaining = remaining.substring( consumed.length );
}
return remaining;
};
/**
* Parse the string and store the parsed representation in the Selector.
* @param {string} selector The selector string
* @returns `true` if the selector was successfully parsed, `false` otherwise
*/
const parse = function( selector ){
let self = this;
let remaining = self.inputText = selector;
let currentQuery = self[0] = newQuery();
self.length = 1;
remaining = consumeWhitespace( remaining ); // get rid of leading whitespace
for( ;; ){
let exprInfo = consumeExpr( remaining );
if( exprInfo.expr == null ){
warn( 'The selector `' + selector + '`is invalid' );
return false;
} else {
let args = exprInfo.match.slice( 1 );
// let the token populate the selector object in currentQuery
let ret = exprInfo.expr.populate( self, currentQuery, args );
if( ret === false ){
return false; // exit if population failed
} else if( ret != null ){
currentQuery = ret; // change the current query to be filled if the expr specifies
}
}
remaining = exprInfo.remaining;
// we're done when there's nothing left to parse
if( remaining.match( /^\s*$/ ) ){
break;
}
}
let lastQ = self[self.length - 1];
if( self.currentSubject != null ){
lastQ.subject = self.currentSubject;
}
lastQ.edgeCount = self.edgeCount;
lastQ.compoundCount = self.compoundCount;
for( let i = 0; i < self.length; i++ ){
let q = self[i];
// in future, this could potentially be allowed if there were operator precedence and detection of invalid combinations
if( q.compoundCount > 0 && q.edgeCount > 0 ){
warn( 'The selector `' + selector + '` is invalid because it uses both a compound selector and an edge selector' );
return false;
}
if( q.edgeCount > 1 ){
warn( 'The selector `' + selector + '` is invalid because it uses multiple edge selectors' );
return false;
} else if( q.edgeCount === 1 ){
warn( 'The selector `' + selector + '` is deprecated. Edge selectors do not take effect on changes to source and target nodes after an edge is added, for performance reasons. Use a class or data selector on edges instead, updating the class or data of an edge when your app detects a change in source or target nodes.' );
}
}
return true; // success
};
/**
* Get the selector represented as a string. This value uses default formatting,
* so things like spacing may differ from the input text passed to the constructor.
* @returns {string} The selector string
*/
export const toString = function(){
if( this.toStringCache != null ){
return this.toStringCache;
}
let clean = function( obj ){
if( obj == null ){
return '';
} else {
return obj;
}
};
let cleanVal = function( val ){
if( is.string( val ) ){
return '"' + val + '"';
} else {
return clean( val );
}
};
let space = ( val ) => {
return ' ' + val + ' ';
};
let checkToString = ( check, subject ) => {
let { type, value } = check;
switch( type ){
case Type.GROUP: {
let group = clean( value );
return group.substring( 0, group.length - 1 );
}
case Type.DATA_COMPARE: {
let { field, operator } = check;
return '[' + field + space( clean( operator ) ) + cleanVal( value ) + ']';
}
case Type.DATA_BOOL: {
let { operator, field } = check;
return '[' + clean( operator ) + field + ']';
}
case Type.DATA_EXIST: {
let { field } = check;
return '[' + field + ']';
}
case Type.META_COMPARE: {
let { operator, field } = check;
return '[[' + field + space( clean( operator ) ) + cleanVal( value ) + ']]';
}
case Type.STATE: {
return value;
}
case Type.ID: {
return '#' + value;
}
case Type.CLASS: {
return '.' + value;
}
case Type.PARENT:
case Type.CHILD: {
return queryToString(check.parent, subject) + space('>') + queryToString(check.child, subject);
}
case Type.ANCESTOR:
case Type.DESCENDANT: {
return queryToString(check.ancestor, subject) + ' ' + queryToString(check.descendant, subject);
}
case Type.COMPOUND_SPLIT: {
let lhs = queryToString(check.left, subject);
let sub = queryToString(check.subject, subject);
let rhs = queryToString(check.right, subject);
return lhs + (lhs.length > 0 ? ' ' : '') + sub + rhs;
}
case Type.TRUE: {
return '';
}
}
};
let queryToString = ( query, subject ) => {
return query.checks.reduce((str, chk, i) => {
return str + (subject === query && i === 0 ? '$' : '') + checkToString(chk, subject);
}, '');
};
let str = '';
for( let i = 0; i < this.length; i++ ){
let query = this[ i ];
str += queryToString( query, query.subject );
if( this.length > 1 && i < this.length - 1 ){
str += ', ';
}
}
this.toStringCache = str;
return str;
};
export default { parse, toString };

View File

@@ -0,0 +1,122 @@
import Type from './type.mjs';
import { stateSelectorMatches } from './state.mjs';
import { valCmp, boolCmp, existCmp, meta, data } from './data.mjs';
/** A lookup of `match(check, ele)` functions by `Type` int */
export const match = [];
/**
* Returns whether the query matches for the element
* @param query The `{ type, value, ... }` query object
* @param ele The element to compare against
*/
export const matches = (query, ele) => {
return query.checks.every( chk => match[chk.type](chk, ele) );
};
match[Type.GROUP] = (check, ele) => {
let group = check.value;
return group === '*' || group === ele.group();
};
match[Type.STATE] = (check, ele) => {
let stateSelector = check.value;
return stateSelectorMatches( stateSelector, ele );
};
match[Type.ID] = (check, ele) => {
let id = check.value;
return ele.id() === id;
};
match[Type.CLASS] = (check, ele) => {
let cls = check.value;
return ele.hasClass(cls);
};
match[Type.META_COMPARE] = (check, ele) => {
let { field, operator, value } = check;
return valCmp( meta(ele, field), operator, value );
};
match[Type.DATA_COMPARE] = (check, ele) => {
let { field, operator, value } = check;
return valCmp( data(ele, field), operator, value );
};
match[Type.DATA_BOOL] = (check, ele) => {
let { field, operator } = check;
return boolCmp( data(ele, field), operator );
};
match[Type.DATA_EXIST] = (check, ele) => {
let { field, operator } = check;
return existCmp( data(ele, field), operator );
};
match[Type.UNDIRECTED_EDGE] = (check, ele) => {
let qA = check.nodes[0];
let qB = check.nodes[1];
let src = ele.source();
let tgt = ele.target();
return ( matches(qA, src) && matches(qB, tgt) ) || ( matches(qB, src) && matches(qA, tgt) );
};
match[Type.NODE_NEIGHBOR] = (check, ele) => {
return matches(check.node, ele) && ele.neighborhood().some( n => n.isNode() && matches(check.neighbor, n) );
};
match[Type.DIRECTED_EDGE] = (check, ele) => {
return matches(check.source, ele.source()) && matches(check.target, ele.target());
};
match[Type.NODE_SOURCE] = (check, ele) => {
return matches(check.source, ele) && ele.outgoers().some( n => n.isNode() && matches(check.target, n) );
};
match[Type.NODE_TARGET] = (check, ele) => {
return matches(check.target, ele) && ele.incomers().some( n => n.isNode() && matches(check.source, n) );
};
match[Type.CHILD] = (check, ele) => {
return matches(check.child, ele) && matches(check.parent, ele.parent());
};
match[Type.PARENT] = (check, ele) => {
return matches(check.parent, ele) && ele.children().some( c => matches(check.child, c) );
};
match[Type.DESCENDANT] = (check, ele) => {
return matches(check.descendant, ele) && ele.ancestors().some( a => matches(check.ancestor, a) );
};
match[Type.ANCESTOR] = (check, ele) => {
return matches(check.ancestor, ele) && ele.descendants().some( d => matches(check.descendant, d) );
};
match[Type.COMPOUND_SPLIT] = (check, ele) => {
return matches(check.subject, ele) && matches(check.left, ele) && matches(check.right, ele);
};
match[Type.TRUE] = () => true;
match[Type.COLLECTION] = (check, ele) => {
let collection = check.value;
return collection.has(ele);
};
match[Type.FILTER] = (check, ele) => {
let filter = check.value;
return filter(ele);
};

849
frontend/node_modules/cytoscape/src/style/apply.mjs generated vendored Normal file
View File

@@ -0,0 +1,849 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import Promise from '../promise.mjs';
const styfn = {};
// keys for style blocks, e.g. ttfftt
const TRUE = 't';
const FALSE = 'f';
// (potentially expensive calculation)
// apply the style to the element based on
// - its bypass
// - what selectors match it
styfn.apply = function( eles ){
let self = this;
let _p = self._private;
let cy = _p.cy;
let updatedEles = cy.collection();
for( let ie = 0; ie < eles.length; ie++ ){
let ele = eles[ ie ];
let cxtMeta = self.getContextMeta( ele );
if( cxtMeta.empty ){
continue;
}
let cxtStyle = self.getContextStyle( cxtMeta );
let app = self.applyContextStyle( cxtMeta, cxtStyle, ele );
if( ele._private.appliedInitStyle ){
self.updateTransitions( ele, app.diffProps );
} else {
ele._private.appliedInitStyle = true;
}
let hintsDiff = self.updateStyleHints( ele );
if( hintsDiff ){
updatedEles.push( ele );
}
} // for elements
return updatedEles;
};
styfn.getPropertiesDiff = function( oldCxtKey, newCxtKey ){
let self = this;
let cache = self._private.propDiffs = self._private.propDiffs || {};
let dualCxtKey = oldCxtKey + '-' + newCxtKey;
let cachedVal = cache[ dualCxtKey ];
if( cachedVal ){
return cachedVal;
}
let diffProps = [];
let addedProp = {};
for( let i = 0; i < self.length; i++ ){
let cxt = self[ i ];
let oldHasCxt = oldCxtKey[ i ] === TRUE;
let newHasCxt = newCxtKey[ i ] === TRUE;
let cxtHasDiffed = oldHasCxt !== newHasCxt;
let cxtHasMappedProps = cxt.mappedProperties.length > 0;
if( cxtHasDiffed || ( newHasCxt && cxtHasMappedProps )){
let props;
if( cxtHasDiffed && cxtHasMappedProps ){
props = cxt.properties; // suffices b/c mappedProperties is a subset of properties
} else if( cxtHasDiffed ){
props = cxt.properties; // need to check them all
} else if( cxtHasMappedProps ){
props = cxt.mappedProperties; // only need to check mapped
}
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
let name = prop.name;
// if a later context overrides this property, then the fact that this context has switched/diffed doesn't matter
// (semi expensive check since it makes this function O(n^2) on context length, but worth it since overall result
// is cached)
let laterCxtOverrides = false;
for( let k = i + 1; k < self.length; k++ ){
let laterCxt = self[ k ];
let hasLaterCxt = newCxtKey[ k ] === TRUE;
if( !hasLaterCxt ){ continue; } // can't override unless the context is active
laterCxtOverrides = laterCxt.properties[ prop.name ] != null;
if( laterCxtOverrides ){ break; } // exit early as long as one later context overrides
}
if( !addedProp[ name ] && !laterCxtOverrides ){
addedProp[ name ] = true;
diffProps.push( name );
}
} // for props
} // if
} // for contexts
cache[ dualCxtKey ] = diffProps;
return diffProps;
};
styfn.getContextMeta = function( ele ){
let self = this;
let cxtKey = '';
let diffProps;
let prevKey = ele._private.styleCxtKey || '';
// get the cxt key
for( let i = 0; i < self.length; i++ ){
let context = self[ i ];
let contextSelectorMatches = context.selector && context.selector.matches( ele ); // NB: context.selector may be null for 'core'
if( contextSelectorMatches ){
cxtKey += TRUE;
} else {
cxtKey += FALSE;
}
} // for context
diffProps = self.getPropertiesDiff( prevKey, cxtKey );
ele._private.styleCxtKey = cxtKey;
return {
key: cxtKey,
diffPropNames: diffProps,
empty: diffProps.length === 0
};
};
// gets a computed ele style object based on matched contexts
styfn.getContextStyle = function( cxtMeta ){
let cxtKey = cxtMeta.key;
let self = this;
let cxtStyles = this._private.contextStyles = this._private.contextStyles || {};
// if already computed style, returned cached copy
if( cxtStyles[ cxtKey ] ){ return cxtStyles[ cxtKey ]; }
let style = {
_private: {
key: cxtKey
}
};
for( let i = 0; i < self.length; i++ ){
let cxt = self[ i ];
let hasCxt = cxtKey[ i ] === TRUE;
if( !hasCxt ){ continue; }
for( let j = 0; j < cxt.properties.length; j++ ){
let prop = cxt.properties[ j ];
style[ prop.name ] = prop;
}
}
cxtStyles[ cxtKey ] = style;
return style;
};
styfn.applyContextStyle = function( cxtMeta, cxtStyle, ele ){
let self = this;
let diffProps = cxtMeta.diffPropNames;
let retDiffProps = {};
let types = self.types;
for( let i = 0; i < diffProps.length; i++ ){
let diffPropName = diffProps[ i ];
let cxtProp = cxtStyle[ diffPropName ];
let eleProp = ele.pstyle( diffPropName );
if( !cxtProp ){ // no context prop means delete
if( !eleProp ){
continue; // no existing prop means nothing needs to be removed
// nb affects initial application on mapped values like control-point-distances
} else if( eleProp.bypass ){
cxtProp = { name: diffPropName, deleteBypassed: true };
} else {
cxtProp = { name: diffPropName, delete: true };
}
}
// save cycles when the context prop doesn't need to be applied
if( eleProp === cxtProp ){ continue; }
// save cycles when a mapped context prop doesn't need to be applied
if(
cxtProp.mapped === types.fn // context prop is function mapper
&& eleProp != null // some props can be null even by default (e.g. a prop that overrides another one)
&& eleProp.mapping != null // ele prop is a concrete value from from a mapper
&& eleProp.mapping.value === cxtProp.value // the current prop on the ele is a flat prop value for the function mapper
){ // NB don't write to cxtProp, as it's shared among eles (stored in stylesheet)
let mapping = eleProp.mapping; // can write to mapping, as it's a per-ele copy
let fnValue = mapping.fnValue = cxtProp.value( ele ); // temporarily cache the value in case of a miss
if( fnValue === mapping.prevFnValue ){ continue; }
}
let retDiffProp = retDiffProps[ diffPropName ] = {
prev: eleProp
};
self.applyParsedProperty( ele, cxtProp );
retDiffProp.next = ele.pstyle( diffPropName );
if( retDiffProp.next && retDiffProp.next.bypass ){
retDiffProp.next = retDiffProp.next.bypassed;
}
}
return {
diffProps: retDiffProps
};
};
styfn.updateStyleHints = function(ele){
let _p = ele._private;
let self = this;
let propNames = self.propertyGroupNames;
let propGrKeys = self.propertyGroupKeys;
let propHash = ( ele, propNames, seedKey ) => self.getPropertiesHash( ele, propNames, seedKey );
let oldStyleKey = _p.styleKey;
if( ele.removed() ){ return false; }
let isNode = _p.group === 'nodes';
// get the style key hashes per prop group
// but lazily -- only use non-default prop values to reduce the number of hashes
//
let overriddenStyles = ele._private.style;
propNames = Object.keys( overriddenStyles );
for( let i = 0; i < propGrKeys.length; i++ ){
let grKey = propGrKeys[i];
_p.styleKeys[ grKey ] = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
}
let updateGrKey1 = (val, grKey) => _p.styleKeys[ grKey ][0] = util.hashInt( val, _p.styleKeys[ grKey ][0] );
let updateGrKey2 = (val, grKey) => _p.styleKeys[ grKey ][1] = util.hashIntAlt( val, _p.styleKeys[ grKey ][1] );
let updateGrKey = (val, grKey) => {
updateGrKey1(val, grKey);
updateGrKey2(val, grKey);
};
let updateGrKeyWStr = (strVal, grKey) => {
for( let j = 0; j < strVal.length; j++ ){
let ch = strVal.charCodeAt(j);
updateGrKey1(ch, grKey);
updateGrKey2(ch, grKey);
}
};
// - hashing works on 32 bit ints b/c we use bitwise ops
// - small numbers get cut off (e.g. 0.123 is seen as 0 by the hashing function)
// - raise up small numbers so more significant digits are seen by hashing
// - make small numbers larger than a normal value to avoid collisions
// - works in practice and it's relatively cheap
let N = 2000000000;
let cleanNum = val => (-128 < val && val < 128) && Math.floor(val) !== val ? N - ((val * 1024) | 0) : val;
for( let i = 0; i < propNames.length; i++ ){
let name = propNames[i];
let parsedProp = overriddenStyles[ name ];
if( parsedProp == null ){ continue; }
let propInfo = this.properties[name];
let type = propInfo.type;
let grKey = propInfo.groupKey;
let normalizedNumberVal;
if( propInfo.hashOverride != null ){
normalizedNumberVal = propInfo.hashOverride(ele, parsedProp);
} else if( parsedProp.pfValue != null ){
normalizedNumberVal = parsedProp.pfValue;
}
// might not be a number if it allows enums
let numberVal = propInfo.enums == null ? parsedProp.value : null;
let haveNormNum = normalizedNumberVal != null;
let haveUnitedNum = numberVal != null;
let haveNum = haveNormNum || haveUnitedNum;
let units = parsedProp.units;
// numbers are cheaper to hash than strings
// 1 hash op vs n hash ops (for length n string)
if( type.number && haveNum && !type.multiple ){
let v = haveNormNum ? normalizedNumberVal : numberVal;
updateGrKey(cleanNum(v), grKey);
if( !haveNormNum && units != null ){
updateGrKeyWStr(units, grKey);
}
} else {
updateGrKeyWStr(parsedProp.strValue, grKey);
}
}
// overall style key
//
let hash = [ util.DEFAULT_HASH_SEED, util.DEFAULT_HASH_SEED_ALT ];
for( let i = 0; i < propGrKeys.length; i++ ){
let grKey = propGrKeys[i];
let grHash = _p.styleKeys[ grKey ];
hash[0] = util.hashInt( grHash[0], hash[0] );
hash[1] = util.hashIntAlt( grHash[1], hash[1] );
}
_p.styleKey = util.combineHashes(hash[0], hash[1]);
// label dims
//
let sk = _p.styleKeys;
_p.labelDimsKey = util.combineHashesArray(sk.labelDimensions);
let labelKeys = propHash( ele, ['label'], sk.labelDimensions );
_p.labelKey = util.combineHashesArray(labelKeys);
_p.labelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, labelKeys));
if( !isNode ){
let sourceLabelKeys = propHash( ele, ['source-label'], sk.labelDimensions );
_p.sourceLabelKey = util.combineHashesArray(sourceLabelKeys);
_p.sourceLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, sourceLabelKeys));
let targetLabelKeys = propHash( ele, ['target-label'], sk.labelDimensions );
_p.targetLabelKey = util.combineHashesArray(targetLabelKeys);
_p.targetLabelStyleKey = util.combineHashesArray(util.hashArrays(sk.commonLabel, targetLabelKeys));
}
// node
//
if( isNode ){
let { nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie, stripe } = _p.styleKeys;
let nodeKeys = [ nodeBody, nodeBorder, nodeOutline, backgroundImage, compound, pie, stripe ].filter(k => k != null).reduce(util.hashArrays, [
util.DEFAULT_HASH_SEED,
util.DEFAULT_HASH_SEED_ALT
]);
_p.nodeKey = util.combineHashesArray(nodeKeys);
_p.hasPie = pie != null && pie[0] !== util.DEFAULT_HASH_SEED && pie[1] !== util.DEFAULT_HASH_SEED_ALT;
_p.hasStripe = stripe != null && stripe[0] !== util.DEFAULT_HASH_SEED && stripe[1] !== util.DEFAULT_HASH_SEED_ALT;
}
return oldStyleKey !== _p.styleKey;
};
styfn.clearStyleHints = function(ele){
let _p = ele._private;
_p.styleCxtKey = '';
_p.styleKeys = {};
_p.styleKey = null;
_p.labelKey = null;
_p.labelStyleKey = null;
_p.sourceLabelKey = null;
_p.sourceLabelStyleKey = null;
_p.targetLabelKey = null;
_p.targetLabelStyleKey = null;
_p.nodeKey = null;
_p.hasPie = null;
_p.hasStripe = null;
};
// apply a property to the style (for internal use)
// returns whether application was successful
//
// now, this function flattens the property, and here's how:
//
// for parsedProp:{ bypass: true, deleteBypass: true }
// no property is generated, instead the bypass property in the
// element's style is replaced by what's pointed to by the `bypassed`
// field in the bypass property (i.e. restoring the property the
// bypass was overriding)
//
// for parsedProp:{ mapped: truthy }
// the generated flattenedProp:{ mapping: prop }
//
// for parsedProp:{ bypass: true }
// the generated flattenedProp:{ bypassed: parsedProp }
styfn.applyParsedProperty = function( ele, parsedProp ){
let self = this;
let prop = parsedProp;
let style = ele._private.style;
let flatProp;
let types = self.types;
let type = self.properties[ prop.name ].type;
let propIsBypass = prop.bypass;
let origProp = style[ prop.name ];
let origPropIsBypass = origProp && origProp.bypass;
let _p = ele._private;
let flatPropMapping = 'mapping';
let getVal = p => {
if( p == null ){
return null;
} else if( p.pfValue != null ){
return p.pfValue;
} else {
return p.value;
}
};
let checkTriggers = () => {
let fromVal = getVal(origProp);
let toVal = getVal(prop);
self.checkTriggers( ele, prop.name, fromVal, toVal );
};
// edge sanity checks to prevent the client from making serious mistakes
if(
parsedProp.name === 'curve-style'
&& ele.isEdge()
&& (
( // loops must be bundled beziers
parsedProp.value !== 'bezier'
&& ele.isLoop()
) || ( // edges connected to compound nodes can not be haystacks
parsedProp.value === 'haystack'
&& ( ele.source().isParent() || ele.target().isParent() )
)
)
){
prop = parsedProp = this.parse( parsedProp.name, 'bezier', propIsBypass );
}
if( prop.delete ){ // delete the property and use the default value on falsey value
style[ prop.name ] = undefined;
checkTriggers();
return true;
}
if( prop.deleteBypassed ){ // delete the property that the
if( !origProp ){
checkTriggers();
return true; // can't delete if no prop
} else if( origProp.bypass ){ // delete bypassed
origProp.bypassed = undefined;
checkTriggers();
return true;
} else {
return false; // we're unsuccessful deleting the bypassed
}
}
// check if we need to delete the current bypass
if( prop.deleteBypass ){ // then this property is just here to indicate we need to delete
if( !origProp ){
checkTriggers();
return true; // property is already not defined
} else if( origProp.bypass ){ // then replace the bypass property with the original
// because the bypassed property was already applied (and therefore parsed), we can just replace it (no reapplying necessary)
style[ prop.name ] = origProp.bypassed;
checkTriggers();
return true;
} else {
return false; // we're unsuccessful deleting the bypass
}
}
let printMappingErr = function(){
util.warn( 'Do not assign mappings to elements without corresponding data (i.e. ele `' + ele.id() + '` has no mapping for property `' + prop.name + '` with data field `' + prop.field + '`); try a `[' + prop.field + ']` selector to limit scope to elements with `' + prop.field + '` defined' );
};
// put the property in the style objects
switch( prop.mapped ){ // flatten the property if mapped
case types.mapData: {
// flatten the field (e.g. data.foo.bar)
let fields = prop.field.split( '.' );
let fieldVal = _p.data;
for( let i = 0; i < fields.length && fieldVal; i++ ){
let field = fields[ i ];
fieldVal = fieldVal[ field ];
}
if( fieldVal == null ){
printMappingErr();
return false;
}
let percent;
if( !is.number( fieldVal ) ){ // then don't apply and fall back on the existing style
util.warn('Do not use continuous mappers without specifying numeric data (i.e. `' + prop.field + ': ' + fieldVal + '` for `' + ele.id() + '` is non-numeric)');
return false;
} else {
let fieldWidth = prop.fieldMax - prop.fieldMin;
if( fieldWidth === 0 ){ // safety check -- not strictly necessary as no props of zero range should be passed here
percent = 0;
} else {
percent = (fieldVal - prop.fieldMin) / fieldWidth;
}
}
// make sure to bound percent value
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
if( type.color ){
let r1 = prop.valueMin[0];
let r2 = prop.valueMax[0];
let g1 = prop.valueMin[1];
let g2 = prop.valueMax[1];
let b1 = prop.valueMin[2];
let b2 = prop.valueMax[2];
let a1 = prop.valueMin[3] == null ? 1 : prop.valueMin[3];
let a2 = prop.valueMax[3] == null ? 1 : prop.valueMax[3];
let clr = [
Math.round( r1 + (r2 - r1) * percent ),
Math.round( g1 + (g2 - g1) * percent ),
Math.round( b1 + (b2 - b1) * percent ),
Math.round( a1 + (a2 - a1) * percent )
];
flatProp = { // colours are simple, so just create the flat property instead of expensive string parsing
bypass: prop.bypass, // we're a bypass if the mapping property is a bypass
name: prop.name,
value: clr,
strValue: 'rgb(' + clr[0] + ', ' + clr[1] + ', ' + clr[2] + ')'
};
} else if( type.number ){
let calcValue = prop.valueMin + (prop.valueMax - prop.valueMin) * percent;
flatProp = this.parse( prop.name, calcValue, prop.bypass, flatPropMapping );
} else {
return false; // can only map to colours and numbers
}
if( !flatProp ){ // if we can't flatten the property, then don't apply the property and fall back on the existing style
printMappingErr();
return false;
}
flatProp.mapping = prop; // keep a reference to the mapping
prop = flatProp; // the flattened (mapped) property is the one we want
break;
}
// direct mapping
case types.data: {
// flatten the field (e.g. data.foo.bar)
let fields = prop.field.split( '.' );
let fieldVal = _p.data;
for( let i = 0; i < fields.length && fieldVal; i++ ){
let field = fields[ i ];
fieldVal = fieldVal[ field ];
}
if( fieldVal != null ){
flatProp = this.parse( prop.name, fieldVal, prop.bypass, flatPropMapping );
}
if( !flatProp ){ // if we can't flatten the property, then don't apply and fall back on the existing style
printMappingErr();
return false;
}
flatProp.mapping = prop; // keep a reference to the mapping
prop = flatProp; // the flattened (mapped) property is the one we want
break;
}
case types.fn: {
let fn = prop.value;
let fnRetVal = prop.fnValue != null ? prop.fnValue : fn( ele ); // check for cached value before calling function
prop.prevFnValue = fnRetVal;
if( fnRetVal == null ){
util.warn('Custom function mappers may not return null (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is null)');
return false;
}
flatProp = this.parse( prop.name, fnRetVal, prop.bypass, flatPropMapping );
if( !flatProp ){
util.warn('Custom function mappers may not return invalid values for the property type (i.e. `' + prop.name + '` for ele `' + ele.id() + '` is invalid)');
return false;
}
flatProp.mapping = util.copy( prop ); // keep a reference to the mapping
prop = flatProp; // the flattened (mapped) property is the one we want
break;
}
case undefined:
break; // just set the property
default:
return false; // not a valid mapping
}
// if the property is a bypass property, then link the resultant property to the original one
if( propIsBypass ){
if( origPropIsBypass ){ // then this bypass overrides the existing one
prop.bypassed = origProp.bypassed; // steal bypassed prop from old bypass
} else { // then link the orig prop to the new bypass
prop.bypassed = origProp;
}
style[ prop.name ] = prop; // and set
} else { // prop is not bypass
if( origPropIsBypass ){ // then keep the orig prop (since it's a bypass) and link to the new prop
origProp.bypassed = prop;
} else { // then just replace the old prop with the new one
style[ prop.name ] = prop;
}
}
checkTriggers();
return true;
};
styfn.cleanElements = function( eles, keepBypasses ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
this.clearStyleHints(ele);
ele.dirtyCompoundBoundsCache();
ele.dirtyBoundingBoxCache();
if( !keepBypasses ){
ele._private.style = {};
} else {
let style = ele._private.style;
let propNames = Object.keys(style);
for( let j = 0; j < propNames.length; j++ ){
let propName = propNames[j];
let eleProp = style[ propName ];
if( eleProp != null ){
if( eleProp.bypass ){
eleProp.bypassed = null;
} else {
style[ propName ] = null;
}
}
}
}
}
};
// updates the visual style for all elements (useful for manual style modification after init)
styfn.update = function(){
let cy = this._private.cy;
let eles = cy.mutableElements();
eles.updateStyle();
};
// diffProps : { name => { prev, next } }
styfn.updateTransitions = function( ele, diffProps ){
let self = this;
let _p = ele._private;
let props = ele.pstyle( 'transition-property' ).value;
let duration = ele.pstyle( 'transition-duration' ).pfValue;
let delay = ele.pstyle( 'transition-delay' ).pfValue;
if( props.length > 0 && duration > 0 ){
let style = {};
// build up the style to animate towards
let anyPrev = false;
for( let i = 0; i < props.length; i++ ){
let prop = props[ i ];
let styProp = ele.pstyle( prop );
let diffProp = diffProps[ prop ];
if( !diffProp ){ continue; }
let prevProp = diffProp.prev;
let fromProp = prevProp;
let toProp = diffProp.next != null ? diffProp.next : styProp;
let diff = false;
let initVal;
let initDt = 0.000001; // delta time % value for initVal (allows animating out of init zero opacity)
if( !fromProp ){ continue; }
// consider px values
if( is.number( fromProp.pfValue ) && is.number( toProp.pfValue ) ){
diff = toProp.pfValue - fromProp.pfValue; // nonzero is truthy
initVal = fromProp.pfValue + initDt * diff;
// consider numerical values
} else if( is.number( fromProp.value ) && is.number( toProp.value ) ){
diff = toProp.value - fromProp.value; // nonzero is truthy
initVal = fromProp.value + initDt * diff;
// consider colour values
} else if( is.array( fromProp.value ) && is.array( toProp.value ) ){
diff = fromProp.value[0] !== toProp.value[0]
|| fromProp.value[1] !== toProp.value[1]
|| fromProp.value[2] !== toProp.value[2]
;
initVal = fromProp.strValue;
}
// the previous value is good for an animation only if it's different
if( diff ){
style[ prop ] = toProp.strValue; // to val
this.applyBypass( ele, prop, initVal ); // from val
anyPrev = true;
}
} // end if props allow ani
// can't transition if there's nothing previous to transition from
if( !anyPrev ){ return; }
_p.transitioning = true;
( new Promise(function( resolve ){
if( delay > 0 ){
ele.delayAnimation( delay ).play().promise().then( resolve );
} else {
resolve();
}
}) ).then(function(){
return ele.animation( {
style: style,
duration: duration,
easing: ele.pstyle( 'transition-timing-function' ).value,
queue: false
} ).play().promise();
}).then(function(){
// if( !isBypass ){
self.removeBypasses( ele, props );
ele.emitAndNotify('style');
// }
_p.transitioning = false;
});
} else if( _p.transitioning ){
this.removeBypasses( ele, props );
ele.emitAndNotify('style');
_p.transitioning = false;
}
};
styfn.checkTrigger = function( ele, name, fromValue, toValue, getTrigger, onTrigger ){
let prop = this.properties[ name ];
let triggerCheck = getTrigger( prop );
if (ele.removed()) { return; }
if( triggerCheck != null && triggerCheck( fromValue, toValue, ele ) ){
onTrigger(prop);
}
};
styfn.checkZOrderTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersZOrder, () => {
this._private.cy.notify('zorder', ele);
});
};
styfn.checkBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBounds, prop => {
ele.dirtyCompoundBoundsCache();
ele.dirtyBoundingBoxCache();
});
};
styfn.checkConnectedEdgesBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBoundsOfConnectedEdges, prop => {
ele.connectedEdges().forEach(edge => {
edge.dirtyBoundingBoxCache();
});
});
};
styfn.checkParallelEdgesBoundsTrigger = function( ele, name, fromValue, toValue ){
this.checkTrigger( ele, name, fromValue, toValue, prop => prop.triggersBoundsOfParallelEdges, prop => {
ele.parallelEdges().forEach(pllEdge => {
pllEdge.dirtyBoundingBoxCache();
});
});
};
styfn.checkTriggers = function( ele, name, fromValue, toValue ){
ele.dirtyStyleCache();
this.checkZOrderTrigger( ele, name, fromValue, toValue );
this.checkBoundsTrigger( ele, name, fromValue, toValue );
this.checkConnectedEdgesBoundsTrigger( ele, name, fromValue, toValue );
this.checkParallelEdgesBoundsTrigger( ele, name, fromValue, toValue );
};
export default styfn;

204
frontend/node_modules/cytoscape/src/style/index.mjs generated vendored Normal file
View File

@@ -0,0 +1,204 @@
import * as is from '../is.mjs';
import * as util from '../util/index.mjs';
import Selector from '../selector/index.mjs';
import apply from './apply.mjs';
import bypass from './bypass.mjs';
import container from './container.mjs';
import getForEle from './get-for-ele.mjs';
import json from './json.mjs';
import stringSheet from './string-sheet.mjs';
import properties from './properties.mjs';
import parse from './parse.mjs';
let Style = function( cy ){
if( !(this instanceof Style) ){
return new Style( cy );
}
if( !is.core( cy ) ){
util.error( 'A style must have a core reference' );
return;
}
this._private = {
cy: cy,
coreStyle: {}
};
this.length = 0;
this.resetToDefault();
};
let styfn = Style.prototype;
styfn.instanceString = function(){
return 'style';
};
// remove all contexts
styfn.clear = function(){
let _p = this._private;
let cy = _p.cy;
let eles = cy.elements();
for( let i = 0; i < this.length; i++ ){
this[ i ] = undefined;
}
this.length = 0;
_p.contextStyles = {};
_p.propDiffs = {};
this.cleanElements( eles, true );
eles.forEach(ele => {
let ele_p = ele[0]._private;
ele_p.styleDirty = true;
ele_p.appliedInitStyle = false;
});
return this; // chaining
};
styfn.resetToDefault = function(){
this.clear();
this.addDefaultStylesheet();
return this;
};
// builds a style object for the 'core' selector
styfn.core = function( propName ){
return this._private.coreStyle[ propName ] || this.getDefaultProperty( propName );
};
// create a new context from the specified selector string and switch to that context
styfn.selector = function( selectorStr ){
// 'core' is a special case and does not need a selector
let selector = selectorStr === 'core' ? null : new Selector( selectorStr );
let i = this.length++; // new context means new index
this[ i ] = {
selector: selector,
properties: [],
mappedProperties: [],
index: i
};
return this; // chaining
};
// add one or many css rules to the current context
styfn.css = function(){
let self = this;
let args = arguments;
if( args.length === 1 ){
let map = args[0];
for( let i = 0; i < self.properties.length; i++ ){
let prop = self.properties[ i ];
let mapVal = map[ prop.name ];
if( mapVal === undefined ){
mapVal = map[ util.dash2camel( prop.name ) ];
}
if( mapVal !== undefined ){
this.cssRule( prop.name, mapVal );
}
}
} else if( args.length === 2 ){
this.cssRule( args[0], args[1] );
}
// do nothing if args are invalid
return this; // chaining
};
styfn.style = styfn.css;
// add a single css rule to the current context
styfn.cssRule = function( name, value ){
// name-value pair
let property = this.parse( name, value );
// add property to current context if valid
if( property ){
let i = this.length - 1;
this[ i ].properties.push( property );
this[ i ].properties[ property.name ] = property; // allow access by name as well
if( property.name.match( /pie-(\d+)-background-size/ ) && property.value ){
this._private.hasPie = true;
}
if( property.name.match( /stripe-(\d+)-background-size/ ) && property.value ){
this._private.hasStripe = true;
}
if( property.mapped ){
this[ i ].mappedProperties.push( property );
}
// add to core style if necessary
let currentSelectorIsCore = !this[ i ].selector;
if( currentSelectorIsCore ){
this._private.coreStyle[ property.name ] = property;
}
}
return this; // chaining
};
styfn.append = function( style ){
if( is.stylesheet( style ) ){
style.appendToStyle( this );
} else if( is.array( style ) ){
this.appendFromJson( style );
} else if( is.string( style ) ){
this.appendFromString( style );
} // you probably wouldn't want to append a Style, since you'd duplicate the default parts
return this;
};
// static function
Style.fromJson = function( cy, json ){
let style = new Style( cy );
style.fromJson( json );
return style;
};
Style.fromString = function( cy, string ){
return new Style( cy ).fromString( string );
};
[
apply,
bypass,
container,
getForEle,
json,
stringSheet,
properties,
parse
].forEach( function( props ){
util.extend( styfn, props );
} );
Style.types = styfn.types;
Style.properties = styfn.properties;
Style.propertyGroups = styfn.propertyGroups;
Style.propertyGroupNames = styfn.propertyGroupNames;
Style.propertyGroupKeys = styfn.propertyGroupKeys;
export default Style;

59
frontend/node_modules/cytoscape/src/style/json.mjs generated vendored Normal file
View File

@@ -0,0 +1,59 @@
let styfn = {};
styfn.appendFromJson = function( json ){
let style = this;
for( let i = 0; i < json.length; i++ ){
let context = json[ i ];
let selector = context.selector;
let props = context.style || context.css;
let names = Object.keys( props );
style.selector( selector ); // apply selector
for( let j = 0; j < names.length; j++ ){
let name = names[j];
let value = props[ name ];
style.css( name, value ); // apply property
}
}
return style;
};
// accessible cy.style() function
styfn.fromJson = function( json ){
let style = this;
style.resetToDefault();
style.appendFromJson( json );
return style;
};
// get json from cy.style() api
styfn.json = function(){
let json = [];
for( let i = this.defaultLength; i < this.length; i++ ){
let cxt = this[ i ];
let selector = cxt.selector;
let props = cxt.properties;
let css = {};
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
css[ prop.name ] = prop.strValue;
}
json.push( {
selector: !selector ? 'core' : selector.toString(),
style: css
} );
}
return json;
};
export default styfn;

435
frontend/node_modules/cytoscape/src/style/parse.mjs generated vendored Normal file
View File

@@ -0,0 +1,435 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import * as math from '../math.mjs';
let styfn = {};
// a caching layer for property parsing
styfn.parse = function( name, value, propIsBypass, propIsFlat ){
let self = this;
// function values can't be cached in all cases, and there isn't much benefit of caching them anyway
if( is.fn( value ) ){
return self.parseImplWarn( name, value, propIsBypass, propIsFlat );
}
let flatKey = ( propIsFlat === 'mapping' || propIsFlat === true || propIsFlat === false || propIsFlat == null ) ? 'dontcare' : propIsFlat;
let bypassKey = propIsBypass ? 't' : 'f';
let valueKey = '' + value;
let argHash = util.hashStrings( name, valueKey, bypassKey, flatKey );
let propCache = self.propCache = self.propCache || [];
let ret;
if( !(ret = propCache[ argHash ]) ){
ret = propCache[ argHash ] = self.parseImplWarn( name, value, propIsBypass, propIsFlat );
}
// - bypasses can't be shared b/c the value can be changed by animations or otherwise overridden
// - mappings can't be shared b/c mappings are per-element
if( propIsBypass || propIsFlat === 'mapping' ){
// need a copy since props are mutated later in their lifecycles
ret = util.copy( ret );
if( ret ){
ret.value = util.copy( ret.value ); // because it could be an array, e.g. colour
}
}
return ret;
};
styfn.parseImplWarn = function( name, value, propIsBypass, propIsFlat ){
let prop = this.parseImpl( name, value, propIsBypass, propIsFlat );
if( !prop && value != null ){
util.warn(`The style property \`${name}: ${value}\` is invalid`);
}
if( prop && (prop.name === 'width' || prop.name === 'height') && value === 'label' ){
util.warn('The style value of `label` is deprecated for `' + prop.name + '`');
}
return prop;
};
// parse a property; return null on invalid; return parsed property otherwise
// fields :
// - name : the name of the property
// - value : the parsed, native-typed value of the property
// - strValue : a string value that represents the property value in valid css
// - bypass : true iff the property is a bypass property
styfn.parseImpl = function( name, value, propIsBypass, propIsFlat ){
let self = this;
name = util.camel2dash( name ); // make sure the property name is in dash form (e.g. 'property-name' not 'propertyName')
let property = self.properties[ name ];
let passedValue = value;
let types = self.types;
if( !property ){ return null; } // return null on property of unknown name
if( value === undefined ){ return null; } // can't assign undefined
// the property may be an alias
if( property.alias ){
property = property.pointsTo;
name = property.name;
}
let valueIsString = is.string( value );
if( valueIsString ){ // trim the value to make parsing easier
value = value.trim();
}
let type = property.type;
if( !type ){ return null; } // no type, no luck
// check if bypass is null or empty string (i.e. indication to delete bypass property)
if( propIsBypass && (value === '' || value === null) ){
return {
name: name,
value: value,
bypass: true,
deleteBypass: true
};
}
// check if value is a function used as a mapper
if( is.fn( value ) ){
return {
name: name,
value: value,
strValue: 'fn',
mapped: types.fn,
bypass: propIsBypass
};
}
// check if value is mapped
let data, mapData;
if( !valueIsString || propIsFlat || value.length < 7 || value[1] !== 'a' ){
// then don't bother to do the expensive regex checks
} else if(value.length >= 7 && value[0] === 'd' && ( data = new RegExp( types.data.regex ).exec( value ) )){
if( propIsBypass ){ return false; } // mappers not allowed in bypass
let mapped = types.data;
return {
name: name,
value: data,
strValue: '' + value,
mapped: mapped,
field: data[1],
bypass: propIsBypass
};
} else if(value.length >= 10 && value[0] === 'm' && ( mapData = new RegExp( types.mapData.regex ).exec( value ) )){
if( propIsBypass ){ return false; } // mappers not allowed in bypass
if( type.multiple ){ return false; } // impossible to map to num
let mapped = types.mapData;
// we can map only if the type is a colour or a number
if( !(type.color || type.number) ){ return false; }
let valueMin = this.parse( name, mapData[4] ); // parse to validate
if( !valueMin || valueMin.mapped ){ return false; } // can't be invalid or mapped
let valueMax = this.parse( name, mapData[5] ); // parse to validate
if( !valueMax || valueMax.mapped ){ return false; } // can't be invalid or mapped
// check if valueMin and valueMax are the same
if( valueMin.pfValue === valueMax.pfValue || valueMin.strValue === valueMax.strValue ){
util.warn('`' + name + ': ' + value + '` is not a valid mapper because the output range is zero; converting to `' + name + ': ' + valueMin.strValue + '`');
return this.parse(name, valueMin.strValue); // can't make much of a mapper without a range
} else if( type.color ){
let c1 = valueMin.value;
let c2 = valueMax.value;
let same = c1[0] === c2[0] // red
&& c1[1] === c2[1] // green
&& c1[2] === c2[2] // blue
&& ( // optional alpha
c1[3] === c2[3] // same alpha outright
|| (
(c1[3] == null || c1[3] === 1) // full opacity for colour 1?
&&
(c2[3] == null || c2[3] === 1) // full opacity for colour 2?
)
)
;
if( same ){ return false; } // can't make a mapper without a range
}
return {
name: name,
value: mapData,
strValue: '' + value,
mapped: mapped,
field: mapData[1],
fieldMin: parseFloat( mapData[2] ), // min & max are numeric
fieldMax: parseFloat( mapData[3] ),
valueMin: valueMin.value,
valueMax: valueMax.value,
bypass: propIsBypass
};
}
if( type.multiple && propIsFlat !== 'multiple' ){
let vals;
if( valueIsString ){
vals = value.split( /\s+/ );
} else if( is.array( value ) ){
vals = value;
} else {
vals = [ value ];
}
if( type.evenMultiple && vals.length % 2 !== 0 ){ return null; }
let valArr = [];
let unitsArr = [];
let pfValArr = [];
let strVal = '';
let hasEnum = false;
for( let i = 0; i < vals.length; i++ ){
let p = self.parse( name, vals[i], propIsBypass, 'multiple' );
hasEnum = hasEnum || is.string( p.value );
valArr.push( p.value );
pfValArr.push( p.pfValue != null ? p.pfValue : p.value );
unitsArr.push( p.units );
strVal += (i > 0 ? ' ' : '') + p.strValue;
}
if( type.validate && !type.validate( valArr, unitsArr ) ){
return null;
}
if( type.singleEnum && hasEnum ){
if( valArr.length === 1 && is.string( valArr[0] ) ){
return {
name: name,
value: valArr[0],
strValue: valArr[0],
bypass: propIsBypass
};
} else {
return null;
}
}
return {
name: name,
value: valArr,
pfValue: pfValArr,
strValue: strVal,
bypass: propIsBypass,
units: unitsArr
};
}
// several types also allow enums
let checkEnums = function(){
for( let i = 0; i < type.enums.length; i++ ){
let en = type.enums[ i ];
if( en === value ){
return {
name: name,
value: value,
strValue: '' + value,
bypass: propIsBypass
};
}
}
return null;
};
// check the type and return the appropriate object
if( type.number ){
let units;
let implicitUnits = 'px'; // not set => px
if( type.units ){ // use specified units if set
units = type.units;
}
if( type.implicitUnits ){
implicitUnits = type.implicitUnits;
}
if( !type.unitless ){
if( valueIsString ){
let unitsRegex = 'px|em' + (type.allowPercent ? '|\\%' : '');
if( units ){ unitsRegex = units; } // only allow explicit units if so set
let match = value.match( '^(' + util.regex.number + ')(' + unitsRegex + ')?' + '$' );
if( match ){
value = match[1];
units = match[2] || implicitUnits;
}
} else if( !units || type.implicitUnits ){
units = implicitUnits; // implicitly px if unspecified
}
}
value = parseFloat( value );
// if not a number and enums not allowed, then the value is invalid
if( isNaN( value ) && type.enums === undefined ){
return null;
}
// check if this number type also accepts special keywords in place of numbers
// (i.e. `left`, `auto`, etc)
if( isNaN( value ) && type.enums !== undefined ){
value = passedValue;
return checkEnums();
}
// check if value must be an integer
if( type.integer && !is.integer( value ) ){
return null;
}
// check value is within range
if( ( type.min !== undefined && ( value < type.min || (type.strictMin && value === type.min) ) )
|| ( type.max !== undefined && ( value > type.max || (type.strictMax && value === type.max) ) )
){
return null;
}
let ret = {
name: name,
value: value,
strValue: '' + value + (units ? units : ''),
units: units,
bypass: propIsBypass
};
// normalise value in pixels
if( type.unitless || (units !== 'px' && units !== 'em') ){
ret.pfValue = value;
} else {
ret.pfValue = ( units === 'px' || !units ? (value) : (this.getEmSizeInPixels() * value) );
}
// normalise value in ms
if( units === 'ms' || units === 's' ){
ret.pfValue = units === 'ms' ? value : 1000 * value;
}
// normalise value in rad
if( units === 'deg' || units === 'rad' ){
ret.pfValue = units === 'rad' ? value : math.deg2rad( value );
}
// normalize value in %
if( units === '%' ){
ret.pfValue = value / 100;
}
return ret;
} else if( type.propList ){
let props = [];
let propsStr = '' + value;
if( propsStr === 'none' ){
// leave empty
} else { // go over each prop
let propsSplit = propsStr.split( /\s*,\s*|\s+/ );
for( let i = 0; i < propsSplit.length; i++ ){
let propName = propsSplit[ i ].trim();
if( self.properties[ propName ] ){
props.push( propName );
} else {
util.warn('`' + propName + '` is not a valid property name');
}
}
if( props.length === 0 ){ return null; }
}
return {
name: name,
value: props,
strValue: props.length === 0 ? 'none' : props.join(' '),
bypass: propIsBypass
};
} else if( type.color ){
let tuple = util.color2tuple( value );
if( !tuple ){ return null; }
return {
name: name,
value: tuple,
pfValue: tuple,
strValue: 'rgb(' + tuple[0] + ',' + tuple[1] + ',' + tuple[2] + ')', // n.b. no spaces b/c of multiple support
bypass: propIsBypass
};
} else if( type.regex || type.regexes ){
// first check enums
if( type.enums ){
let enumProp = checkEnums();
if( enumProp ){ return enumProp; }
}
let regexes = type.regexes ? type.regexes : [ type.regex ];
for( let i = 0; i < regexes.length; i++ ){
let regex = new RegExp( regexes[ i ] ); // make a regex from the type string
let m = regex.exec( value );
if( m ){ // regex matches
return {
name: name,
value: type.singleRegexMatchValue ? m[1] : m,
strValue: '' + value,
bypass: propIsBypass
};
}
}
return null; // didn't match any
} else if( type.string ){
// just return
return {
name: name,
value: '' + value,
strValue: '' + value,
bypass: propIsBypass
};
} else if( type.enums ){ // check enums last because it's a combo type in others
return checkEnums();
} else {
return null; // not a type we can handle
}
};
export default styfn;

97
frontend/node_modules/cytoscape/src/stylesheet.mjs generated vendored Normal file
View File

@@ -0,0 +1,97 @@
import * as is from './is.mjs';
import Style from './style/index.mjs';
import { dash2camel } from './util/index.mjs';
// a dummy stylesheet object that doesn't need a reference to the core
// (useful for init)
let Stylesheet = function(){
if( !(this instanceof Stylesheet) ){
return new Stylesheet();
}
this.length = 0;
};
let sheetfn = Stylesheet.prototype;
sheetfn.instanceString = function(){
return 'stylesheet';
};
// just store the selector to be parsed later
sheetfn.selector = function( selector ){
let i = this.length++;
this[ i ] = {
selector: selector,
properties: []
};
return this; // chaining
};
// just store the property to be parsed later
sheetfn.css = function( name, value ){
let i = this.length - 1;
if( is.string( name ) ){
this[ i ].properties.push( {
name: name,
value: value
} );
} else if( is.plainObject( name ) ){
let map = name;
let propNames = Object.keys( map );
for( let j = 0; j < propNames.length; j++ ){
let key = propNames[ j ];
let mapVal = map[ key ];
if( mapVal == null ){ continue; }
let prop = Style.properties[key] || Style.properties[dash2camel(key)];
if( prop == null ){ continue; }
let name = prop.name;
let value = mapVal;
this[ i ].properties.push( {
name: name,
value: value
} );
}
}
return this; // chaining
};
sheetfn.style = sheetfn.css;
// generate a real style object from the dummy stylesheet
sheetfn.generateStyle = function( cy ){
let style = new Style( cy );
return this.appendToStyle( style );
};
// append a dummy stylesheet object on a real style object
sheetfn.appendToStyle = function( style ){
for( let i = 0; i < this.length; i++ ){
let context = this[ i ];
let selector = context.selector;
let props = context.properties;
style.selector( selector ); // apply selector
for( let j = 0; j < props.length; j++ ){
let prop = props[ j ];
style.css( prop.name, prop.value ); // apply property
}
}
return style;
};
export default Stylesheet;

18
frontend/node_modules/cytoscape/src/test.mjs generated vendored Normal file
View File

@@ -0,0 +1,18 @@
/*
This file tells the Mocha tests what build of Cytoscape to use.
*/
// For manual build tests, use the ESM build
// NB : Must do `npm run build` before `npm test`
let cytoscape;
if (process.env.TEST_BUILD) {
// Dynamically import the ESM build
cytoscape = await import('../build/cytoscape.esm.mjs'); // Assuming the ESM build uses `.esm.js`
} else {
// Dynamically import the unbundled, unbabelified raw source
cytoscape = await import('./index.mjs');
}
// Export the module (adjust based on whether you need default or named exports)
export default cytoscape.default || cytoscape;

290
frontend/node_modules/cytoscape/src/util/colors.mjs generated vendored Normal file
View File

@@ -0,0 +1,290 @@
import * as is from '../is.mjs';
import * as regex from './regex.mjs';
// get [r, g, b] from #abc or #aabbcc
export const hex2tuple = hex => {
if( !(hex.length === 4 || hex.length === 7) || hex[0] !== '#' ){ return; }
let shortHex = hex.length === 4;
let r, g, b;
let base = 16;
if( shortHex ){
r = parseInt( hex[1] + hex[1], base );
g = parseInt( hex[2] + hex[2], base );
b = parseInt( hex[3] + hex[3], base );
} else {
r = parseInt( hex[1] + hex[2], base );
g = parseInt( hex[3] + hex[4], base );
b = parseInt( hex[5] + hex[6], base );
}
return [ r, g, b ];
};
// get [r, g, b, a] from hsl(0, 0, 0) or hsla(0, 0, 0, 0)
export const hsl2tuple = hsl => {
let ret;
let h, s, l, a, r, g, b;
function hue2rgb( p, q, t ){
if( t < 0 ) t += 1;
if( t > 1 ) t -= 1;
if( t < 1 / 6 ) return p + (q - p) * 6 * t;
if( t < 1 / 2 ) return q;
if( t < 2 / 3 ) return p + (q - p) * (2 / 3 - t) * 6;
return p;
}
let m = new RegExp( '^' + regex.hsla + '$' ).exec( hsl );
if( m ){
// get hue
h = parseInt( m[1] );
if( h < 0 ){
h = ( 360 - (-1 * h % 360) ) % 360;
} else if( h > 360 ){
h = h % 360;
}
h /= 360; // normalise on [0, 1]
s = parseFloat( m[2] );
if( s < 0 || s > 100 ){ return; } // saturation is [0, 100]
s = s / 100; // normalise on [0, 1]
l = parseFloat( m[3] );
if( l < 0 || l > 100 ){ return; } // lightness is [0, 100]
l = l / 100; // normalise on [0, 1]
a = m[4];
if( a !== undefined ){
a = parseFloat( a );
if( a < 0 || a > 1 ){ return; } // alpha is [0, 1]
}
// now, convert to rgb
// code from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript
if( s === 0 ){
r = g = b = Math.round( l * 255 ); // achromatic
} else {
let q = l < 0.5 ? l * (1 + s) : l + s - l * s;
let p = 2 * l - q;
r = Math.round( 255 * hue2rgb( p, q, h + 1 / 3 ) );
g = Math.round( 255 * hue2rgb( p, q, h ) );
b = Math.round( 255 * hue2rgb( p, q, h - 1 / 3 ) );
}
ret = [ r, g, b, a ];
}
return ret;
};
// get [r, g, b, a] from rgb(0, 0, 0) or rgba(0, 0, 0, 0)
export const rgb2tuple = rgb => {
let ret;
let m = new RegExp( '^' + regex.rgba + '$' ).exec( rgb );
if( m ){
ret = [];
let isPct = [];
for( let i = 1; i <= 3; i++ ){
let channel = m[ i ];
if( channel[ channel.length - 1 ] === '%' ){
isPct[ i ] = true;
}
channel = parseFloat( channel );
if( isPct[ i ] ){
channel = channel / 100 * 255; // normalise to [0, 255]
}
if( channel < 0 || channel > 255 ){ return; } // invalid channel value
ret.push( Math.floor( channel ) );
}
let atLeastOneIsPct = isPct[1] || isPct[2] || isPct[3];
let allArePct = isPct[1] && isPct[2] && isPct[3];
if( atLeastOneIsPct && !allArePct ){ return; } // must all be percent values if one is
let alpha = m[4];
if( alpha !== undefined ){
alpha = parseFloat( alpha );
if( alpha < 0 || alpha > 1 ){ return; } // invalid alpha value
ret.push( alpha );
}
}
return ret;
};
export const colorname2tuple = color => {
return colors[ color.toLowerCase() ];
};
export const color2tuple = color => {
return ( is.array( color ) ? color : null )
|| colorname2tuple( color )
|| hex2tuple( color )
|| rgb2tuple( color )
|| hsl2tuple( color );
};
export const colors = {
// special colour names
transparent: [0, 0, 0, 0], // NB alpha === 0
// regular colours
aliceblue: [ 240, 248, 255 ],
antiquewhite: [ 250, 235, 215 ],
aqua: [0, 255, 255 ],
aquamarine: [ 127, 255, 212 ],
azure: [ 240, 255, 255 ],
beige: [ 245, 245, 220 ],
bisque: [ 255, 228, 196 ],
black: [0, 0, 0],
blanchedalmond: [ 255, 235, 205 ],
blue: [0, 0, 255 ],
blueviolet: [ 138, 43, 226 ],
brown: [ 165, 42, 42 ],
burlywood: [ 222, 184, 135 ],
cadetblue: [ 95, 158, 160 ],
chartreuse: [ 127, 255, 0],
chocolate: [ 210, 105, 30 ],
coral: [ 255, 127, 80 ],
cornflowerblue: [ 100, 149, 237 ],
cornsilk: [ 255, 248, 220 ],
crimson: [ 220, 20, 60 ],
cyan: [0, 255, 255 ],
darkblue: [0, 0, 139 ],
darkcyan: [0, 139, 139 ],
darkgoldenrod: [ 184, 134, 11 ],
darkgray: [ 169, 169, 169 ],
darkgreen: [0, 100, 0],
darkgrey: [ 169, 169, 169 ],
darkkhaki: [ 189, 183, 107 ],
darkmagenta: [ 139, 0, 139 ],
darkolivegreen: [ 85, 107, 47 ],
darkorange: [ 255, 140, 0],
darkorchid: [ 153, 50, 204 ],
darkred: [ 139, 0, 0],
darksalmon: [ 233, 150, 122 ],
darkseagreen: [ 143, 188, 143 ],
darkslateblue: [ 72, 61, 139 ],
darkslategray: [ 47, 79, 79 ],
darkslategrey: [ 47, 79, 79 ],
darkturquoise: [0, 206, 209 ],
darkviolet: [ 148, 0, 211 ],
deeppink: [ 255, 20, 147 ],
deepskyblue: [0, 191, 255 ],
dimgray: [ 105, 105, 105 ],
dimgrey: [ 105, 105, 105 ],
dodgerblue: [ 30, 144, 255 ],
firebrick: [ 178, 34, 34 ],
floralwhite: [ 255, 250, 240 ],
forestgreen: [ 34, 139, 34 ],
fuchsia: [ 255, 0, 255 ],
gainsboro: [ 220, 220, 220 ],
ghostwhite: [ 248, 248, 255 ],
gold: [ 255, 215, 0],
goldenrod: [ 218, 165, 32 ],
gray: [ 128, 128, 128 ],
grey: [ 128, 128, 128 ],
green: [0, 128, 0],
greenyellow: [ 173, 255, 47 ],
honeydew: [ 240, 255, 240 ],
hotpink: [ 255, 105, 180 ],
indianred: [ 205, 92, 92 ],
indigo: [ 75, 0, 130 ],
ivory: [ 255, 255, 240 ],
khaki: [ 240, 230, 140 ],
lavender: [ 230, 230, 250 ],
lavenderblush: [ 255, 240, 245 ],
lawngreen: [ 124, 252, 0],
lemonchiffon: [ 255, 250, 205 ],
lightblue: [ 173, 216, 230 ],
lightcoral: [ 240, 128, 128 ],
lightcyan: [ 224, 255, 255 ],
lightgoldenrodyellow: [ 250, 250, 210 ],
lightgray: [ 211, 211, 211 ],
lightgreen: [ 144, 238, 144 ],
lightgrey: [ 211, 211, 211 ],
lightpink: [ 255, 182, 193 ],
lightsalmon: [ 255, 160, 122 ],
lightseagreen: [ 32, 178, 170 ],
lightskyblue: [ 135, 206, 250 ],
lightslategray: [ 119, 136, 153 ],
lightslategrey: [ 119, 136, 153 ],
lightsteelblue: [ 176, 196, 222 ],
lightyellow: [ 255, 255, 224 ],
lime: [0, 255, 0],
limegreen: [ 50, 205, 50 ],
linen: [ 250, 240, 230 ],
magenta: [ 255, 0, 255 ],
maroon: [ 128, 0, 0],
mediumaquamarine: [ 102, 205, 170 ],
mediumblue: [0, 0, 205 ],
mediumorchid: [ 186, 85, 211 ],
mediumpurple: [ 147, 112, 219 ],
mediumseagreen: [ 60, 179, 113 ],
mediumslateblue: [ 123, 104, 238 ],
mediumspringgreen: [0, 250, 154 ],
mediumturquoise: [ 72, 209, 204 ],
mediumvioletred: [ 199, 21, 133 ],
midnightblue: [ 25, 25, 112 ],
mintcream: [ 245, 255, 250 ],
mistyrose: [ 255, 228, 225 ],
moccasin: [ 255, 228, 181 ],
navajowhite: [ 255, 222, 173 ],
navy: [0, 0, 128 ],
oldlace: [ 253, 245, 230 ],
olive: [ 128, 128, 0],
olivedrab: [ 107, 142, 35 ],
orange: [ 255, 165, 0],
orangered: [ 255, 69, 0],
orchid: [ 218, 112, 214 ],
palegoldenrod: [ 238, 232, 170 ],
palegreen: [ 152, 251, 152 ],
paleturquoise: [ 175, 238, 238 ],
palevioletred: [ 219, 112, 147 ],
papayawhip: [ 255, 239, 213 ],
peachpuff: [ 255, 218, 185 ],
peru: [ 205, 133, 63 ],
pink: [ 255, 192, 203 ],
plum: [ 221, 160, 221 ],
powderblue: [ 176, 224, 230 ],
purple: [ 128, 0, 128 ],
red: [ 255, 0, 0],
rosybrown: [ 188, 143, 143 ],
royalblue: [ 65, 105, 225 ],
saddlebrown: [ 139, 69, 19 ],
salmon: [ 250, 128, 114 ],
sandybrown: [ 244, 164, 96 ],
seagreen: [ 46, 139, 87 ],
seashell: [ 255, 245, 238 ],
sienna: [ 160, 82, 45 ],
silver: [ 192, 192, 192 ],
skyblue: [ 135, 206, 235 ],
slateblue: [ 106, 90, 205 ],
slategray: [ 112, 128, 144 ],
slategrey: [ 112, 128, 144 ],
snow: [ 255, 250, 250 ],
springgreen: [0, 255, 127 ],
steelblue: [ 70, 130, 180 ],
tan: [ 210, 180, 140 ],
teal: [0, 128, 128 ],
thistle: [ 216, 191, 216 ],
tomato: [ 255, 99, 71 ],
turquoise: [ 64, 224, 208 ],
violet: [ 238, 130, 238 ],
wheat: [ 245, 222, 179 ],
white: [ 255, 255, 255 ],
whitesmoke: [ 245, 245, 245 ],
yellow: [ 255, 255, 0],
yellowgreen: [ 154, 205, 50 ]
};