First commit.

Signed-off-by: Chen Xiao <abigwc@gmail.com>
This commit is contained in:
Chen Xiao
2026-05-08 14:43:16 +08:00
commit 0b64e2de94
10989 changed files with 2253791 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
ie 9-11
firefox >= 4
chrome >= 23
> 1%
node >= 0.10
last 2 major versions
since 2012
+64
View File
@@ -0,0 +1,64 @@
---
name: Bug report
about: Let us know about a bug
title: ''
labels: bug
assignees: ''
---
**Before you post**
A request for help or a request for a how-to should be directed to [Phind]([url](https://www.phind.com/search?c=I%27m%20using%20the%20Cytoscape.js%20graph%20theory%20JS%20library.&q=How%20do%20I%20create%20a%20graph%20in%20my%20HTML%20page)).
If your issue pertains to an extension, your issue should be filed in that extension's repository -- not here.
**Environment info**
- Cytoscape.js version :
- Browser/Node.js & version :
**Current (buggy) behaviour**
_What does the bug do?_
**Desired behaviour**
_What do you expect Cytoscape.js to do instead?_
**Minimum steps to reproduce**
_What do you need to do to reproduce the issue?_
_Fork/clone this JSBin demo and reproduce your issue so that your issue can be addressed quickly and effectively:_
1. Go to our base JSBin: http://jsbin.com/fiqugiq
2. Click the 'Edit in JSBin' button in the top-right corner.
3. Click File > Clone to make your own copy of the demo (with its own unique URL) that you can edit.
4. Click the yellow 'Login or Register' button at the top-right corner so that your JSBin persists longer-term.
5. Log in with your GitHub account.
6. Edit the JSBin to reproduce your issue.
7. When you're finished editing, click 'File > Save snapshot` (command + s) to save your edits.
8. Post the link to your JSBin here.
**For reviewers**
_Reviewers should ensure that the following tasks are carried out for incorporated issues:_
- [ ] Ensure that the reporter has included a reproducible demo. They can easily fork this JSBin demo: http://jsbin.com/fiqugiq
- [ ] The issue has been associated with a corresponding [milestone](https://github.com/cytoscape/cytoscape.js/milestones).
- [ ] The commits have been incorporated into the corresponding branches. Bug-fix patches go on
- [ ] `master` and
- [ ] `unstable`.
- [ ] The issue has been labelled as a [`bug`](https://github.com/cytoscape/cytoscape.js/labels/bug), if necessary.
@@ -0,0 +1,30 @@
---
name: Feature request
about: Suggest an idea for something new
title: ''
labels: ''
assignees: ''
---
**Description of new feature**
_What should the new feature do? For visual features, include an image/mockup of the expected output._
**Motivation for new feature**
_Describe your use case for this new feature._
**For reviewers**
_Reviewers should ensure that the following tasks are carried out for incorporated issues:_
- [ ] Ensure that the reporter has adequately described their idea. If not, elicit more information about the use case. You should iteratively build a spec together.
- [ ] Ensure that the issue is a good fit for the core library. Some things are best done in extensions (e.g. UI-related features that aren't style-related). Some things are best done by app authors themselves -- instead of in Cytoscape libraries.
- [ ] The issue has been associated with a corresponding [milestone](https://github.com/cytoscape/cytoscape.js/milestones).
- [ ] The commits have been incorporated into the `unstable` branch via pull request. The corresponding pull request is cross-referenced.
+29
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.
+9
View File
@@ -0,0 +1,9 @@
# Reporting security issues in Cytoscape.js
Please report security issues to at least one of the following people:
- Max Franz: maxkfranz@gmail.com
- Dylan Fong: dylanfong.ut@gmail.com
- Christian Lopes: chrtannus@gmail.com
- Mike Kucera: mikekucera@gmail.com
- Alex Pico: alex.pico@gladstone.ucsf.edu
+17
View File
@@ -0,0 +1,17 @@
# Number of days of inactivity before an issue becomes stale
daysUntilStale: 14
# Number of days of inactivity before a stale issue is closed
daysUntilClose: 7
# Issues with these labels will never be considered stale
exemptLabels:
- pinned
# Label to use when marking an issue as stale
staleLabel: stale
# Comment to post when marking an issue as stale. Set to `false` to disable
markComment: >
This issue has been automatically marked as stale, because it has not had
activity within the past 14 days. It will be closed if no further activity
occurs within the next 7 days. If a feature request is important to you,
please consider making a pull request. Thank you for your contributions.
# Comment to post when closing a stale issue. Set to `false` to disable
closeComment: false
+98
View File
@@ -0,0 +1,98 @@
name: Feature Release
on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to run the action on'
required: true
default: 'unstable'
jobs:
feature-release:
runs-on: ubuntu-latest
environment: prod
steps:
- name: checkout unstable branch
uses: actions/checkout@v3
with:
ref: 'unstable'
fetch-depth: 0
- 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: Determine version for the release
id: get_new_patch_version
run: |
pwd
chmod 777 -R ./* ./.[!.]*
. ./.github/workflows/scripts/new-feature-version.sh
shell: bash
- name: See new master version and jq version
run: |
echo VERSION ${{ env.VERSION }}
jq --version
shell: bash
- name: Checkout Master Branch
uses: actions/checkout@v3
with:
ref: master
fetch-depth: 0
- name: Update documentation and Merge unstable to master branch
run: |
. ./.github/workflows/scripts/merge_unstable_to_master.sh
- name: Build release and verify changes
id: release
run: |
. .github/workflows/scripts/pre_release_test.sh master
- name: Archive code coverage 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 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 🚀
uses: JamesIves/github-pages-deploy-action@v4
with:
folder: documentation
- name: Create Issue
run: |
TITLE="Create a blog post for release v$VERSION"
echo "Issue title: $TITLE"
sed ':a;N;$!ba;s/\n/\\n/g' ./.github/workflows/md/blog-issue-template.md > output_file.txt
BODY="$(cat output_file.txt)"
echo "Body: $BODY"
curl -X POST \
-H "Authorization: token ${{ secrets.CYTOSCAPE_JS_BLOG_TOKEN }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/cytoscape/cytoscape.js-blog/issues" \
-d "{\"title\":\"$TITLE\",\"body\":\"$BODY\"}"
npm_publish:
needs: feature-release
permissions:
contents: read
id-token: write
uses: ./.github/workflows/npm-publish.yml
+82
View File
@@ -0,0 +1,82 @@
# Feature Release GitHub Action - README
## Introduction
This GitHub Action, named "Feature Release Test", automates the process of releasing new features for the [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) repository. This action is triggered by a manual workflow_dispatch event, allowing you to specify the version of the new release. The action performs various tasks, including merging changes, running tests, publishing to npmjs and GitHub Releases, deploying to GitHub Pages, and creating a related issue on the repository's blog.
## Prerequisites
Before using this GitHub Action, ensure you have the following prerequisites in place:
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)
- `CYTOSCAPE_JS_BLOG_TOKEN`: Token for creating issues on the repository's blog Cytoscape/Cytoscape.js-blog repo.
## Usage
1. Navigate to the [Cytoscape.js](https://github.com/cytoscape/cytoscape.js) repository.
2. Go to the [Actions](https://github.com/cytoscape/cytoscape.js/actions) tab.
3. Click on [Feature-Release](https://github.com/cytoscape/cytoscape.js/actions/workflows/feature-release-test.yml) workflow.
4. Click the "Run workflow" button.
5. DO NOT CHANGE BRANCH FROM `unstable` FOR RELEASE. Provide the desired version for the new release when prompted if you want to do a feature release for a specific version. **Note: This version will used as the version of release.** Otherwise, the github will automatically determine a new version based upon package.json version
## Workflow Steps
Below are the steps performed by the "Feature Release Test" GitHub Action:
1. **Checkout Patch Branch**:
- Action: Checks out the `unstable` branch.
- Uses: `actions/checkout@v3`.
2. **Setup Node.js Environment**:
- Action: Sets up Node.js environment.
- Uses: `actions/setup-node@v3`.
- Node Version: 18.
- Caches npm packages.
3. **Get New Version String**:
- Action: Determines the new version for the release.
- Script: Determines the new version based on user input or a script.
- Uses the `github.event.inputs.version` input for the version if provided.
4. **Checkout Master Branch**:
- Action: Checks out the `master` branch.
- Uses: `actions/checkout@v3`.
5. **Merge Unstable to Master Branch**:
- Action: Merges changes from `unstable` to `master` branch.
- Custom script: Fetches and merges changes from the `unstable` branch.
6. **Install Dependencies**:
- Action: Installs project dependencies.
- Command: `npm install`.
7. **Run Tests**:
- Action: Runs tests for the project.
- Command: `npm test`.
8. **Pre Release Tests**:
- Action: Runs pre-release tests.
- Custom script: Executes pre-release tests with the `master` branch.
9. **Archive Code Coverage Results**:
- Action: Archives code coverage results in case of test failure.
- Uses: `actions/upload-artifact@v3`.
10. **Publish Package to npmjs**:
- Action: Publishes the package to npmjs.
- Command: `npm publish`.
11. **Publish Package to GitHub Releases**:
- Action: Publishes the package to GitHub Releases.
- Uses GitHub API to create a release with provided information.
12. **Deploy to Github Pages**:
- Action: Deploys documentation to GitHub Pages.
- Uses: `JamesIves/github-pages-deploy-action@v4`.
13. **Create Issue**:
- Action: Creates an issue on the repository's blog.
- Creates an issue with a title and body based on a template.
+36
View File
@@ -0,0 +1,36 @@
## Release instructions
1. Add the upcoming release version in [version.json](https://github.com/cytoscape/cytoscape.js/blob/unstable/documentation/versions.json) file.
2. Ensure that [milestones](https://github.com/cytoscape/cytoscape.js/milestones) exist for the releases that you would like to make. Each milestone should contain its corresponding issues and pull requests.
1. For patch releases, do the back-port patch release before the corresponding current release. This ensures that npm lists the current version as the latest one.
1. `git checkout 1.1.x`, e.g. if the previous feature release is 1.1
1. Follow the remaining ordinary release steps (step 5 and onward).
1. Current releases are based on the `master` branch: `git checkout master`
1. If you are making a patch release, you can just release `master` with its new patches.
1. If you are making a feature release, you need to merge `unstable` onto `master`. Since there can be conflicts, it's easiest to use the 'ours' strategy which will allow you to use the state of `unstable` as-is (i.e. no conflict resolution necessary):
1. Make sure your local `master` is up-to-date: `git checkout master && git pull`
1. Make sure your local `unstable` is up-to-date: `git checkout unstable && git pull`
1. Create a merge commit that selects the state of `unstable` and push it: `git merge -s ours master && git push`
1. Fast-forward `master` to the merge commit: `git checkout master && git merge unstable && git push`
1. Update the version number in `package.json` and `package-lock.json` on `unstable` to some provisional new version number, and push it.
1. Update the `VERSION` environment variable for the release number you want to make, e.g. `export VERSION=1.2.3`
1. Confirm all the tests are passing:
1. `npm run test`
1. See also `test/index.html` for browser testing (optional)
1. Confirm all the tests are passing in IE9 (for feature releases):
1. `npm run watch:umd`
1. Open an [IE9 VM](https://developer.microsoft.com/en-us/microsoft-edge/tools/vms/)
1. Open `http://yourip:8081/test/ie.html` in IE
1. Prepare a release: `npm run release`
1. Review the files that were just built in the previous step.
1. There should be a series of updated files in the `dist` directory and the `documentation` directory, identified with `git status`.
1. Try out the newly-built docs and demos in your browser.
1. Add the the release to git: `git add . && git commit -m "Build $VERSION"`
1. Update the package version and tag the release: `npm version $VERSION`
1. Push the release changes: `git push && git push --tags`
1. Publish the release to npm: `npm publish`
1. Run `npm run docs:push`
1. [Create a release](https://github.com/cytoscape/cytoscape.js/releases/new) for Zenodo from the latest tag. Make sure you wait at least 5 minutes since the last time that you made a release in order for Zenodo to work properly.
1. For feature releases: Create a release announcement on the [blog](https://github.com/cytoscape/cytoscape.js-blog). Share the announcement on mailing lists and social media.
@@ -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`.
@@ -0,0 +1,27 @@
## Create Issue for new release
- Create a file with name <i>YYYY-MM-DD-VERSION-release.md</i> in the [_posts](https://github.com/cytoscape/cytoscape.js-blog/tree/gh-pages/_posts) directory.
- Include these main points in the posts
- layout
- title
- subtitle
- tags
- New important features and who contributed to them.
- Full list of changes.
- Sample blog post:
```
---
layout: post
title: Cytoscape.js $VERSION released
subtitle: An overview of what's new in Cytoscape.js $VERSION
tags:
- news
---
Cytoscape.js $VERSION has been released. This version brings with it a [new right-slanting rhomboid shape](https://github.com/cytoscape/cytoscape.js/issues/3123) by [@somaniv](https://github.com/somaniv) and [trusted-types support](https://github.com/cytoscape/cytoscape.js/pull/3118) by [Jakub Vrána](https://github.com/vrana).
The full list of changes can be found in the [3.25.0 milestone on GitHub](https://github.com/cytoscape/cytoscape.js/milestone/236?closed=1).
```
+18
View File
@@ -0,0 +1,18 @@
## Instructions to setup repository for automated releases
### Tokens
- `NPM_TOKEN`: Token for npmjs package publishing. The token should a automation token with no expiration. 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. The token can be set to expire at 1 year (maximum limit of github PAT is 1 year). 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)
- Permissions Required:
- Actions: Read and Write
- Contents: Read and Write
- `CYTOSCAPE_JS_BLOG_TOKEN`: Token for creating issues on the repository's blog Cytoscape/Cytoscape.js-blog repo.
- Permissions Required:
- Issues: Read and Write
### Repository Setup
- Provide Github Actions permissions to read and write. Ref: [Managing Github Actions](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository)
+25
View File
@@ -0,0 +1,25 @@
name: npm publish
on:
workflow_call:
jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v3
with:
ref: master
- uses: actions/setup-node@v3
with:
node-version: 22
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm install
- name: Publish Package To npmjs
run: npm publish --provenance
+118
View File
@@ -0,0 +1,118 @@
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 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
npm_publish:
needs: patch-release
permissions:
contents: read
id-token: write
uses: ./.github/workflows/npm-publish.yml
@@ -0,0 +1,151 @@
#!/bin/bash
# Make script exit on first failure
set -e
# Check if VERSION variable is set
if [ -z "$VERSION" ]; then
echo "VERSION variable is not set."
exit 1
else
echo "VERSION is set to: $VERSION"
fi
# Check if NEXT_VERSION variable is set
if [ -z "$NEXT_VERSION" ]; then
echo "NEXT_VERSION variable is not set."
exit 1
else
echo "NEXT_VERSION is set to: $NEXT_VERSION"
fi
# Check if NEXT_BACK_PORT_VERSION variable is set
if [ -z "$NEXT_BACK_PORT_VERSION" ]; then
echo "NEXT_BACK_PORT_VERSION variable is not set."
exit 1
else
echo "NEXT_BACK_PORT_VERSION is set to: $NEXT_BACK_PORT_VERSION"
fi
# See current branch
echo "Current Branch: $(git branch --show-current)"
# See head of current branch
echo "Current Head: "
git log -n 1
# See current origin
echo "See remotes: "
git remote -v
# Set git configs
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@users.noreply.github.com"
# Create and push the new backport branch
git checkout master
git checkout -b "$NEXT_BACK_PORT_VERSION"
git push origin "$NEXT_BACK_PORT_VERSION"
# Step 2: Make sure local unstable is up-to-date
git checkout unstable
git pull
# Check if current Git branch is named "unstable"
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
if [ "$current_branch" = "unstable" ]; then
echo "Current Git branch is unstable."
else
echo "Current Git branch is not unstable."
exit 2
fi
echo "Updating documentation"
jq --arg ver "$VERSION" '.versions += [$ver]' ./documentation/versions.json > /tmp/temp.json
mv /tmp/temp.json ./documentation/versions.json
git add .
git commit -m "Documentation $VERSION: Append $VERSION to versions.json"
echo "Documentation committed"
# Step 3: Create a merge commit and push it
git merge -s ours master -m "Merge master to unstable"
echo "Master merged to unstable"
git push origin unstable
echo "Unstable pushed to remote"
# Step 4: Fast-forward master to the merge commit
git checkout master
git merge unstable
echo "Unstable merged in master"
git push
echo "Master pushed to remote"
# Update package.json
jq --arg ver "$VERSION" '.version = $ver' package.json > /tmp/temp.json
mv /tmp/temp.json package.json
# Update package-lock.json
jq --arg ver "$VERSION" '.version = $ver' package-lock.json > /tmp/temp.json
mv /tmp/temp.json package-lock.json
# Check if version is updated in package.json
version_check_package=$(jq -r '.version' package.json)
if [ "$version_check_package" != "$VERSION" ]; then
echo "Failed to update version in package.json"
exit 3
else
echo "Version updated in package.json"
fi
# Check if version is updated in package-lock.json
version_check_package_lock=$(jq -r '.version' package-lock.json)
if [ "$version_check_package_lock" != "$VERSION" ]; then
echo "Failed to update version in package-lock.json"
exit 4
else
echo "Version updated in package-lock.json"
fi
# Commit and push the updated version files
git add package.json package-lock.json
git commit -m "Update version to $VERSION"
git push
# Update new version in unstable
git checkout unstable
# Update package.json
jq --arg ver "$NEXT_VERSION" '.version = $ver' package.json > /tmp/temp.json
mv /tmp/temp.json package.json
# Update package-lock.json
jq --arg ver "$NEXT_VERSION" '.version = $ver' package-lock.json > /tmp/temp.json
mv /tmp/temp.json package-lock.json
# Check if version is updated in package.json for unstable
version_check_package_unstable=$(jq -r '.version' package.json)
if [ "$version_check_package_unstable" != "$NEXT_VERSION" ]; then
echo "Failed to update version in package.json for unstable"
exit 3
else
echo "Version updated in package.json for unstable"
fi
# Check if version is updated in package-lock.json for unstable
version_check_package_lock_unstable=$(jq -r '.version' package-lock.json)
if [ "$version_check_package_lock_unstable" != "$NEXT_VERSION" ]; then
echo "Failed to update version in package-lock.json for unstable"
exit 4
else
echo "Version updated in package-lock.json for unstable"
fi
# Commit and push the updated version files
git add package.json package-lock.json
git commit -m "Update version to $NEXT_VERSION"
git push
git checkout master
@@ -0,0 +1,37 @@
#!/bin/bash
# Get the current version from package.json
PREV_VERSION=$(jq -r '.version' package.json)
echo "Prev Feature Version $PREV_VERSION"
# Extract the version number by removing the "-unstable" suffix
VERSION="${PREV_VERSION%-unstable}"
echo "New Master Version $VERSION"
# Split the version number into major, minor, and patch components
IFS='.' read -ra VERSION_ARRAY <<< "$VERSION"
# Extract the minor and patch components
MINOR_VERSION="${VERSION_ARRAY[1]}"
PATCH_VERSION="${VERSION_ARRAY[2]}"
# Decrement the minor version for backport branch
((MINOR_VERSION--))
# Increment patch for new backport branch
NEXT_BACK_PORT_VERSION="${VERSION_ARRAY[0]}.${MINOR_VERSION}.x"
# Increment the minor component for the new unstable version
((MINOR_VERSION++))
((MINOR_VERSION++))
# Construct the new unstable version
NEXT_VERSION="${VERSION_ARRAY[0]}.${MINOR_VERSION}.0-unstable"
echo "Next Unstable Version: $NEXT_VERSION"
echo "Next Backport Version: $NEXT_BACK_PORT_VERSION"
# Export the versions to the GitHub Actions environment
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "NEXT_VERSION=$NEXT_VERSION" >> "$GITHUB_ENV"
echo "NEXT_BACK_PORT_VERSION=$NEXT_BACK_PORT_VERSION" >> "$GITHUB_ENV"
@@ -0,0 +1,38 @@
#!/bin/bash
# Get the current version from package.json
PREV_VERSION=$(jq -r '.version' package.json)
echo "Prev Patch Version $PREV_VERSION"
# Split the version number into major, minor, and patch components
IFS='.' read -a VERSION_ARRAY <<< "$PREV_VERSION"
echo "SPLITTING COMPLETED"
major="${VERSION_ARRAY[0]}"
minor="${VERSION_ARRAY[1]}"
patch="${VERSION_ARRAY[2]}"
echo "CURRENT PATCH VERSION $patch"
# Increment the patch version
patch=$((patch + 1))
echo "UPDATED PATCH VERSION $patch"
# Form the new version string
VERSION="$major.$minor.$patch"
# Split the new version number into major, minor, and patch components to validate
IFS='.' read -a VERSION_ARRAY_2 <<< "$VERSION"
if [[ ${#VERSION_ARRAY_2[@]} -lt 3 ]]; then
echo "Error: Invalid new version format"
exit 1
fi
# Set the branch name if it's not the master branch
if [ "$BRANCH" != "refs/heads/master" ]; then
BRANCH="${VERSION_ARRAY[0]}.${VERSION_ARRAY[1]}.x"
fi
echo "Version $VERSION"
# Export the new version to the GitHub Actions environment
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
@@ -0,0 +1,85 @@
#!/bin/bash
# Make script exit on first failure
set -e
# Check if VERSION variable is set
if [ -z "$VERSION" ]; then
echo "VERSION variable is not set."
exit 1
else
echo "VERSION is set to: $VERSION"
fi
# Check if current Git branch is named "master" or the provided branch name
current_branch=$(git symbolic-ref --short HEAD 2>/dev/null)
if [ "$current_branch" = "$1" ]; then
echo "Current Git branch is $1."
else
echo "Current Git branch is not $1."
exit 2
fi
FILE=./documentation/versions.json
if [ -f "$FILE" ]; then
echo "$FILE exists."
else
echo "$FILE doesn't exist. Exiting..."
exit 1
fi
npm install
npm run release
if [ "$current_branch" = "unstable" ] || [ "$current_branch" = "master" ]; then
echo "Starting to check changed files"
# List the files to check
files_to_check=("documentation/index.html" "documentation/js/cytoscape.min.js" "dist/cytoscape.umd.js")
echo "Files initialized"
git status
# Loop through the files
for file in "${files_to_check[@]}"
do
echo "Checking $file"
# Check if the file exists in the local FS
if [ -e "$file" ]; then
echo "The file $file exists in the locally-built files."
else
echo "The file $file does not exist in the locally-built files."
exit 1
fi
# Check if the file has changed
output="$(git status -s $file)"
echo "For $file, $output"
# Check if the file has changed
if [ -z "$output" ]; then
echo "The file $file has not changed."
exit 1
else
echo "The file $file has changed."
fi
done
fi
git add . && git commit -m "Build $VERSION"
git log -n 1
npm version "$VERSION" --allow-same-version
git push && git push --tags
git remote -v
git remote set-url origin git@github.com:cytoscape/cytoscape.js.git
exit 0
+18
View File
@@ -0,0 +1,18 @@
name: Automated tests
on: [push, pull_request]
permissions:
contents: read
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Run tests
run: npm test
+1
View File
@@ -0,0 +1 @@
22
+78
View File
@@ -0,0 +1,78 @@
{
"build/cytoscape.umd.js": {
"bundled": 1021871,
"minified": 365007,
"gzipped": 112477
},
"build/cytoscape.cjs.js": {
"bundled": 871227,
"minified": 371337,
"gzipped": 111837
},
"build/cytoscape.esm.js": {
"bundled": 870994,
"minified": 371149,
"gzipped": 111794,
"treeshaked": {
"rollup": {
"code": 346486,
"import_statements": 132
},
"webpack": {
"code": 347998
}
}
},
"build/cytoscape.esm.min.js": {
"bundled": 364812,
"minified": 364287,
"gzipped": 112306,
"treeshaked": {
"rollup": {
"code": 363750,
"import_statements": 0
},
"webpack": {
"code": 365038
}
}
},
"build\\cytoscape.umd.js": {
"bundled": 942118,
"minified": 351104,
"gzipped": 109191
},
"build\\cytoscape.esm.min.js": {
"bundled": 350909,
"minified": 350411,
"gzipped": 109012,
"treeshaked": {
"rollup": {
"code": 349903,
"import_statements": 0
},
"webpack": {
"code": 351193
}
}
},
"build\\cytoscape.cjs.js": {
"bundled": 868703,
"minified": 370553,
"gzipped": 111528
},
"build\\cytoscape.esm.js": {
"bundled": 868530,
"minified": 370410,
"gzipped": 111491,
"treeshaked": {
"rollup": {
"code": 345754,
"import_statements": 51
},
"webpack": {
"code": 347140
}
}
}
}
+6
View File
@@ -0,0 +1,6 @@
language: node_js
node_js:
- "10"
- "12"
sudo: false
script: npm run travis
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
gary.bader@utoronto.ca (Gary Bader) and max.franz@utoronto.ca (Max Franz).
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+19
View File
@@ -0,0 +1,19 @@
Copyright (c) 2016-2026, The Cytoscape Consortium.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+172
View File
@@ -0,0 +1,172 @@
<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)
[![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, cjs, 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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
import globals from "globals";
import pluginJs from "@eslint/js";
/** @type {import('eslint').Linter.Config[]} */
export default [
{languageOptions: { globals: globals.browser }},
pluginJs.configs.recommended,
{
rules: {
"no-unused-vars": "warn"
}
}
];
+6637
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -0,0 +1,32 @@
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const year = (new Date()).getFullYear();
const license = `Copyright (c) 2016-${year}, The Cytoscape Consortium.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the “Software”), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.`;
fs.writeFileSync(path.join(__dirname, 'LICENSE'), license);
+143
View File
@@ -0,0 +1,143 @@
{
"name": "cytoscape",
"version": "3.33.3",
"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, debug/webgl\" -w 500 --extraExts 'json'",
"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"
}
}
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1, maximum-scale=1">
<style>
body {
font-family: helvetica, arial, sans-serif;
}
#cytoscape {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
}
</style>
<script src="../build/cytoscape.umd.js"></script>
<title>cytoscape.js playwright page</title>
</head>
<body>
<div id="cytoscape"></div>
<script>
window.cy = cytoscape({
container: document.getElementById('cytoscape')
});
</script>
</body>
</html>
+678
View File
@@ -0,0 +1,678 @@
import { test, expect } from '@playwright/test';
const copy = obj => JSON.parse(JSON.stringify(obj));
const delay = async ms => new Promise(resolve => setTimeout(resolve, ms));
const expectUniquePoints = pts => {
const toStringPt = pt => `(${pt.x},${pt.y})`;
const toStringPts = pts => pts.map(toStringPt).join(' ');
const str = toStringPts(pts);
const strUnique = Array.from(new Set(pts.map(toStringPt))).join(' ');
expect(str).toBe(strUnique);
};
const dist = (pt1, pt2) => Math.sqrt((pt1.x - pt2.x) ** 2 + (pt1.y - pt2.y) ** 2);
test.describe('Renderer', () => {
test.beforeEach(async ({ page }) => {
page.on('console', (msg) => console.log(`[browser] ${msg.text()}`));
await page.setViewportSize({ width: 1280, height: 720 });
await page.goto('http://127.0.0.1:3333/playwright-page/index.html');
});
test('starts with no nodes', async ({ page }) => {
const numNodes = await page.evaluate(() => {
const cy = window.cy;
return cy.nodes().length;
});
expect(numNodes).toBe(0);
});
test('adds a node', async ({ page }) => {
const numNodes = await page.evaluate(() => {
const cy = window.cy;
cy.add({
data: { id: 'foo' }
});
return cy.nodes().length;
});
expect(numNodes).toBe(1);
});
test.describe('node style', () => {
test.beforeEach(async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.add({ data: { id: 'a' } });
});
}); // beforeEach
test('node bounding box extends beyond width and height for bordered triangle', async ({ page }) => {
const bb = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'node',
style: {
'shape': 'triangle',
'width': 100,
'height': 100,
'border-width': 10,
'border-color': 'black',
}
}
]).update();
return cy.$('#a').boundingBox();
});
expect(bb.w).toBeGreaterThan(105);
expect(bb.h).toBeGreaterThan(105);
} ); // node bounding box extends beyond width and height for triangle
test('boundingBox() does not throw for polygon node with border and edge', async ({ page }) => {
const bb = await page.evaluate(() => {
const cy = window.cy;
cy.add([
{
data: { id: 'p' },
position: { x: 100, y: 100 }
},
{
data: { id: 'q' },
position: { x: 200, y: 200 }
},
{
data: { id: 'pq', source: 'p', target: 'q' }
}
]);
cy.style().fromJson([
{
selector: '#p',
style: {
'shape': 'polygon',
'shape-polygon-points': '-1 -1 1 -1 1 1 -1 1',
'width': 50,
'height': 50,
'border-width': 5,
'border-color': 'black',
}
}
]).update();
return cy.$('#p').boundingBox();
});
expect(bb.w).toBeGreaterThan(0);
expect(bb.h).toBeGreaterThan(0);
expect(bb.x1).toBeDefined();
expect(bb.x2).toBeDefined();
expect(bb.y1).toBeDefined();
expect(bb.y2).toBeDefined();
}); // boundingBox() does not throw for polygon node with border and edge
test('polygon node bounding box extends beyond width and height for border', async ({ page }) => {
const bb = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'node',
style: {
'shape': 'polygon',
'shape-polygon-points': '-1 -1 1 -1 1 1 -1 1',
'width': 100,
'height': 100,
'border-width': 10,
'border-color': 'black',
}
}
]).update();
return cy.$('#a').boundingBox();
});
expect(bb.w).toBeGreaterThan(105);
expect(bb.h).toBeGreaterThan(105);
}); // polygon node bounding box extends beyond width and height for border
});
test.describe('straight edges', () => {
test.beforeEach(async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'edge',
style: {
'curve-style': 'straight'
}
}
]).update();
cy.add([
{
data: { id: 'a' },
position: { x: 0, y: 0 }
},
{
data: { id: 'b' },
position: { x: 0, y: 0 }
},
{
data: { id: 'ab1', source: 'a', target: 'b' }
},
{
data: { id: 'ab2', source: 'a', target: 'b' }
}
]);
});
}); // beforeEach
test('initial bounding box is zero', async ({ page }) => {
let bb1 = await page.evaluate(() => cy.$('#ab1').boundingBox());
let bb2 = await page.evaluate(() => cy.$('#ab2').boundingBox());
expect(bb1.w).toEqual(0);
expect(bb1.h).toEqual(0);
expect(bb2.w).toEqual(0);
expect(bb2.h).toEqual(0);
}); // initial bounding box is zero
test('moved bounding box is nonzero', async ({ page }) => {
await delay(500);
await page.evaluate(() => {
cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
});
await delay(500);
let bb1 = await page.evaluate(() => cy.$('#ab1').boundingBox());
let bb2 = await page.evaluate(() => cy.$('#ab2').boundingBox());
expect(bb1.w).not.toEqual(0);
expect(bb1.h).not.toEqual(0);
expect(bb2.w).not.toEqual(0);
expect(bb2.h).not.toEqual(0);
}); // initial bounding box is nonzero
test('manual endpoints correct', async ({ page }) => {
// Set the endpoints manually
const {
ab1Source, ab1Target, ab2Source, ab2Target,
ab1Midpoint, ab2Midpoint
} = await page.evaluate(() => {
const cy = window.cy;
cy.$('#1').position({ x: 0, y: 0 });
cy.$('#b').position({ x: 100, y: 0 });
// Set the endpoints for the fist edge via style
cy.$('#ab1').style('source-endpoint', '10px 10px');
cy.$('#ab1').style('target-endpoint', '20px 10px');
// Set the endpoints for the second edge via style
cy.$('#ab2').style('source-endpoint', '30px 20px');
cy.$('#ab2').style('target-endpoint', '40px 20px');
// Return the endpoints for verification
return {
ab1Source: cy.$('#ab1').sourceEndpoint(),
ab1Target: cy.$('#ab1').targetEndpoint(),
ab2Source: cy.$('#ab2').sourceEndpoint(),
ab2Target: cy.$('#ab2').targetEndpoint(),
ab1Midpoint: cy.$('#ab1').midpoint(),
ab2Midpoint: cy.$('#ab2').midpoint()
};
});
// Verify the endpoints
expect(ab1Source.x).toBe(10);
expect(ab1Source.y).toBe(10);
expect(ab1Target.x).toBe(20 + 100);
expect(ab1Target.y).toBe(10);
expect(ab2Source.x).toBe(30);
expect(ab2Source.y).toBe(20);
expect(ab2Target.x).toBe(40 + 100);
expect(ab2Target.y).toBe(20);
// Verify the midpoint y values only
expect(ab1Midpoint.y).toEqual(10);
expect(ab2Midpoint.y).toEqual(20);
}); // manual endpoints correct
});
test.describe('bundled beziers', () => {
const stepSize = 40;
let ctrlpts1;
let pt_ab1_1, pt_ab2_1;
test.beforeEach(async ({ page }) => {
ctrlpts1 = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'edge',
style: {
'curve-style': 'bezier',
'control-point-step-size': 40
}
}
]).update();
cy.add([
{
data: { id: 'a' }
},
{
data: { id: 'b' }
},
{
data: { id: 'ab1', source: 'a', target: 'b' }
},
{
data: { id: 'ab2', source: 'a', target: 'b' }
}
]);
cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
return cy.edges().map(edge => edge.controlPoints()[0]);
});
pt_ab1_1 = await page.evaluate(() => {
return window.cy.$('#ab1').controlPoints()[0];
});
pt_ab2_1 = await page.evaluate(() => {
return window.cy.$('#ab2').controlPoints()[0];
});
}); // beforeEach
test('move when adding to the bundle', async ({ page }) => {
expect(ctrlpts1.length).toBe(2);
expectUniquePoints(ctrlpts1);
// distance between the two control points
let d_1_01 = dist(ctrlpts1[0], ctrlpts1[1]);
expect(d_1_01).toBe(stepSize);
let ctrlpts2 = await page.evaluate(() => {
const cy = window.cy;
cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
}
]);
return cy.edges().map(edge => edge.controlPoints()[0]);
});
// console.log(ctrlpts2);
expect(ctrlpts2.length).toBe(4);
expectUniquePoints(ctrlpts2);
let d_2_01 = dist(ctrlpts2[0], ctrlpts2[1]);
let d_2_12 = dist(ctrlpts2[1], ctrlpts2[2]);
let d_2_23 = dist(ctrlpts2[2], ctrlpts2[3]);
expect(d_2_01).toBeCloseTo(stepSize);
expect(d_2_12).toBeCloseTo(stepSize);
expect(d_2_23).toBeCloseTo(stepSize);
let pt_ab1_2 = await page.evaluate(() => {
return window.cy.$('#ab1').controlPoints()[0];
});
let pt_ab2_2 = await page.evaluate(() => {
return window.cy.$('#ab2').controlPoints()[0];
});
// ctrl pts for ab1 and ab2 should have changed
expect(pt_ab1_1).not.toEqual(pt_ab1_2);
expect(pt_ab2_1).not.toEqual(pt_ab2_2);
}); // move when adding to the bundle
test('move when removing from the bundle', async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
},
{
data: { id: 'ab5', source: 'a', target: 'b' }
}
]);
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
let ctrlpts3 = await page.evaluate(() => {
const cy = window.cy;
cy.$('#ab3').remove(); // only ab1,2,4,5 left
return cy.edges().map(edge => edge.controlPoints()[0]);
});
let pt_ab1_3 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
let pt_ab2_3 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
expectUniquePoints(ctrlpts3);
expect(pt_ab1_2).not.toEqual(pt_ab1_3);
expect(pt_ab2_2).not.toEqual(pt_ab2_3);
let d_3_01 = dist(ctrlpts3[0], ctrlpts3[1]);
let d_3_12 = dist(ctrlpts3[1], ctrlpts3[2]);
let d_3_23 = dist(ctrlpts3[2], ctrlpts3[3]);
expect(d_3_01).toBeCloseTo(stepSize);
expect(d_3_12).toBeCloseTo(stepSize);
expect(d_3_23).toBeCloseTo(stepSize);
}); // move when removing from the bundle
test('do not move when setting one edge to visibility:hidden', async ({ page }) => {
await page.evaluate(() => {
window.cy.$('#ab1').style('visibility', 'hidden');
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints()[0]);
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
expect(pt_ab1_1).toEqual(pt_ab1_2);
expect(pt_ab2_1).toEqual(pt_ab2_2);
}); // do not move when setting one edge to visibility:hidden
test('move when setting one edge to display:none', async ({ page }) => {
await page.evaluate(() => {
window.cy.$('#ab1').style('display', 'none');
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints());
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints());
expect(pt_ab1_2).toBeUndefined(); // because display: none
expect(pt_ab2_2).toBeUndefined(); // because only one edge left => straight
}); // move when setting one edge to display:none
test('move when setting one edge to display:none (bigger bundle)', async ({ page }) => {
await page.evaluate(() => {
window.cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
},
{
data: { id: 'ab5', source: 'a', target: 'b' }
}
]);
cy.$('#ab1').style('display', 'none');
});
let pt_ab1_2 = await page.evaluate(() => cy.$('#ab1').controlPoints());
let pt_ab2_2 = await page.evaluate(() => cy.$('#ab2').controlPoints()[0]);
expect(pt_ab1_2).toBeUndefined(); // because display: none
expect(pt_ab2_2).toBeDefined();
}); // move when setting one edge to display:none
test('move when setting one edge to curve-style:straight', async ({ page }) => {
await page.evaluate(() => {
window.cy.add([
{
data: { id: 'ab3', source: 'a', target: 'b' }
},
{
data: { id: 'ab4', source: 'a', target: 'b' }
},
{
data: { id: 'ab5', source: 'a', target: 'b' }
}
]);
cy.$('#ab1').style('curve-style', 'straight');
});
let pt_ab1_2 = await page.evaluate(() => window.cy.$('#ab1').controlPoints());
let pt_ab2_2 = await page.evaluate(() => window.cy.$('#ab2').controlPoints()[0]);
expect(pt_ab1_2).toBeUndefined(); // because curve-style:straight
expect(pt_ab2_2).toBeDefined();
}); // move when setting one edge to curve-style:straight
test('diagonal bottom left to top right', async ({page}) => {
let controlPoints = await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([{
selector: 'edge',
style: {
'curve-style': 'bezier',
'control-point-step-size': Math.sqrt(200)
}
}]).update();
cy.add([{
data: {id: 'a'}
}, {
data: {id: 'b'}
}, {
data: {id: 'ab1', source: 'a', target: 'b'}
}, {
data: {id: 'ab2', source: 'a', target: 'b'}
}]);
var presetOptions = {
name: 'preset',
positions: {
a: {x: 0, y: 100},
b: {x: 100, y: 0}
}
};
cy.layout(presetOptions).run();
let pt_ab1_1 = cy.$('#ab1').controlPoints()[0];
let pt_ab2_1 = cy.$('#ab2').controlPoints()[0];
return [pt_ab1_1, pt_ab2_1]
});
// Step size 14.14, sqrt(10*10+10*10).
// Gives a 5 pixel translation away from the mid-point,
// if the vector between the nodes are at a 45 deg angle.
// Mid-point is at (50,50)
expect(controlPoints[0].x).toBeCloseTo(45, 5);
expect(controlPoints[0].y).toBeCloseTo(45, 5);
expect(controlPoints[1].x).toBeCloseTo(55, 5);
expect(controlPoints[1].y).toBeCloseTo(55, 5);
});
// test('do not move when straight edge added', async ({ page }) => {
// await page.evaluate(() => {
// window.cy.add([
// {
// data: { id: 'ab3', source: 'a', target: 'b' },
// style: {
// 'curve-style': 'straight'
// }
// }
// ]);
// });
// // TODO...
// }); // move when setting one edge to curve-style:straight
}); // bundled beziers
test.describe('rounded-edges', () => {
let singleBentEdge;
let midCollinearEdge;
test.beforeEach(async ({ page }) => {
await page.evaluate(() => {
const cy = window.cy;
cy.style().fromJson([
{
selector: 'edge',
style: {
'curve-style': 'round-segments',
'segment-radii': 50,
'radius-type': 'influence-radius'
}
},
{
selector: '#ab1',
style: {
'segment-weights': [0.5],
'segment-distances': [50]
}
},
{
selector: '#ab2',
style: {
'segment-weights': [0.25 , 0.5, 0.75],
'segment-distances': [50, 50, 50]
}
}
]).update();
cy.add([
{
data: { id: 'a' }
},
{
data: { id: 'b' }
},
{
data: { id: 'ab1', source: 'a', target: 'b' }
},
{
data: { id: 'ab2', source: 'a', target: 'b' }
},
]);
cy.layout({ name: 'grid', rows: 1, cols: 2 }).run();
singleBentEdge = cy.$('#ab1');
midCollinearEdge = cy.$('#ab2');
});
}) // Before Each
test('collinear mid point correctly defined', async ({page}) => {
const midpoint = await page.evaluate(() => midCollinearEdge.midpoint());
const points = await page.evaluate(() => midCollinearEdge.segmentPoints());
expect(midpoint.x).not.toBeNaN();
expect(midpoint.y).not.toBeNaN();
expect(midpoint).toMatchObject(points[1])
});
test('mid point correctly defined', async ({page}) => {
const {midpoint, points, a,b} = await page.evaluate(() => ({
midpoint: singleBentEdge.midpoint(),
points: singleBentEdge.segmentPoints(),
a: singleBentEdge.source().position(),
b: singleBentEdge.target().position(),
}));
expect(midpoint.x).not.toBeNaN();
expect(midpoint.y).not.toBeNaN();
const controlPoint = points[0];
expect(midpoint).not.toMatchObject(controlPoint); // The mid point is supposed to be on the curve and not on the control point
const isInsideTriangle = (point, {a,b,c}) => {
const dX = point.x - a.x, dY = point.y - a.y;
const dX21 = b.x - a.x, dY21 = b.y - a.y;
const dX31 = c.x - a.x, dY31 = c.y - a.y;
const denom = dY21 * dX31 - dX21 * dY31;
const alpha = (dY21 * dX - dX21 * dY) / denom;
const beta = (dX31 * dY - dY31 * dX) / denom;
const gamma = 1 - alpha - beta;
return alpha >= 0 && beta >= 0 && gamma >= 0;
};
// The actual midpoint should be within a triangle between the source, the target and the defined control point
expect(isInsideTriangle(midpoint, {a,b,c: controlPoint})).toBeTruthy()
})
}); // Rounded edges
test.describe('with layout', () => {
test('single node cose layout with bounding box', async ({ page }) => {
const pos = await page.evaluate(async () => {
const cy = window.cy;
// remove all eles
cy.elements().remove();
// add one node
let node = cy.add({ data: { id: 'a' } });
// run layout
let layout = cy.layout({
name: 'cose',
boundingBox: {
x1: 0,
y1: 0,
x2: 100,
y2: 100
},
});
let layoutstop = layout.promiseOn('layoutstop');
layout.run();
await layoutstop;
return node.position();
});
expect(pos.x).not.toBeNaN();
expect(pos.y).not.toBeNaN();
}); // single node cose layout
}); // with layout
}); // renderer
+81
View File
@@ -0,0 +1,81 @@
// @ts-check
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* @see https://playwright.dev/docs/test-configuration
*/
export default defineConfig({
testDir: './playwright-tests',
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'list',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000/playwright-page',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
// {
// name: 'firefox',
// use: { ...devices['Desktop Firefox'] },
// },
// {
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
// },
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
webServer: {
command: 'npm run test:playwright:setup',
url: 'http://127.0.0.1:3333',
reuseExistingServer: !process.env.CI,
},
});
+130
View File
@@ -0,0 +1,130 @@
import nodeResolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import replace from '@rollup/plugin-replace';
import terser from '@rollup/plugin-terser';
import license from 'rollup-plugin-license';
import path from 'path';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const VERSION = process.env.VERSION || 'snapshot'; // default snapshot
const FILE = process.env.FILE;
const SOURCEMAPS = process.env.SOURCEMAPS === 'true'; // default false
const BABEL = process.env.BABEL !== 'false'; // default true
const NODE_ENV = process.env.NODE_ENV === 'development' ? 'development' : 'production'; // default prod
const input = './src/index.mjs';
const name = 'cytoscape';
const envVariables = {
'process.env.VERSION': JSON.stringify(VERSION),
'process.env.NODE_ENV': JSON.stringify(NODE_ENV)
};
const replaceOptions = {
values: envVariables,
preventAssignment: true
};
const getBabelOptions = () => ({
exclude: '**/node_modules/**',
babelHelpers: 'bundled'
});
// Ignore all node_modules dependencies
const isExternal = id => !id.startsWith('\0') && !id.startsWith('.') && !id.startsWith('/');
const licenseHeaderOptions = {
sourcemap: true,
banner: {
content: {
file: path.join(__dirname, 'LICENSE')
}
}
};
const configs = [
{
input,
output: {
file: 'build/cytoscape.umd.js',
format: 'umd',
name,
sourcemap: SOURCEMAPS ? 'inline' : false
},
plugins: [
nodeResolve(),
commonjs({ include: '**/node_modules/**' }),
BABEL ? babel(getBabelOptions()) : {},
replace(replaceOptions),
license(licenseHeaderOptions)
]
},
{
input,
output: {
file: 'build/cytoscape.min.js',
format: 'umd',
name
},
plugins: [
nodeResolve(),
commonjs({ include: '**/node_modules/**' }),
BABEL ? babel(getBabelOptions()) : {},
replace(replaceOptions),
terser(),
license(licenseHeaderOptions)
]
},
{
input,
output: {
file: 'build/cytoscape.esm.min.mjs',
format: 'es'
},
plugins: [
nodeResolve(),
commonjs({ include: '**/node_modules/**' }),
BABEL ? babel(getBabelOptions()) : {},
replace(replaceOptions),
license(licenseHeaderOptions),
terser()
]
},
{
input,
output: { file: 'build/cytoscape.cjs.js', format: 'cjs' },
plugins: [
nodeResolve(),
commonjs({ include: '**/node_modules/**' }),
BABEL ? babel(getBabelOptions()) : {},
replace(replaceOptions),
license(licenseHeaderOptions)
]
},
{
input,
output: { file: 'build/cytoscape.esm.mjs', format: 'es' },
plugins: [
nodeResolve(),
commonjs({ include: '**/node_modules/**' }),
BABEL ? babel(getBabelOptions()) : {},
replace(replaceOptions),
license(licenseHeaderOptions)
]
}
];
export default FILE
? configs.filter(config => config.output.file.endsWith(FILE + '.js') || config.output.file.endsWith(FILE + '.mjs'))
: configs;
+260
View File
@@ -0,0 +1,260 @@
import * as util from './util/index.mjs';
import * as is from './is.mjs';
import Promise from './promise.mjs';
let Animation = function( target, opts, opts2 ){
let isCore = is.core(target);
let isEle = !isCore;
let _p = this._private = util.extend( {
duration: 1000
}, opts, opts2 );
_p.target = target;
_p.style = _p.style || _p.css;
_p.started = false;
_p.playing = false;
_p.hooked = false;
_p.applying = false;
_p.progress = 0;
_p.completes = [];
_p.frames = [];
if( _p.complete && is.fn( _p.complete ) ){
_p.completes.push( _p.complete );
}
if( isEle ){
let pos = target.position();
_p.startPosition = _p.startPosition || {
x: pos.x,
y: pos.y
};
_p.startStyle = _p.startStyle || target.cy().style().getAnimationStartStyle( target, _p.style );
}
if( isCore ){
let pan = target.pan();
_p.startPan = {
x: pan.x,
y: pan.y
};
_p.startZoom = target.zoom();
}
// for future timeline/animations impl
this.length = 1;
this[0] = this;
};
let anifn = Animation.prototype;
util.extend( anifn, {
instanceString: function(){ return 'animation'; },
hook: function(){
let _p = this._private;
if( !_p.hooked ){
// add to target's animation queue
let q;
let tAni = _p.target._private.animation;
if( _p.queue ){
q = tAni.queue;
} else {
q = tAni.current;
}
q.push( this );
// add to the animation loop pool
if( is.elementOrCollection( _p.target ) ){
_p.target.cy().addToAnimationPool( _p.target );
}
_p.hooked = true;
}
return this;
},
play: function(){
let _p = this._private;
// autorewind
if( _p.progress === 1 ){
_p.progress = 0;
}
_p.playing = true;
_p.started = false; // needs to be started by animation loop
_p.stopped = false;
this.hook();
// the animation loop will start the animation...
return this;
},
playing: function(){
return this._private.playing;
},
apply: function(){
let _p = this._private;
_p.applying = true;
_p.started = false; // needs to be started by animation loop
_p.stopped = false;
this.hook();
// the animation loop will apply the animation at this progress
return this;
},
applying: function(){
return this._private.applying;
},
pause: function(){
let _p = this._private;
_p.playing = false;
_p.started = false;
return this;
},
stop: function(){
let _p = this._private;
_p.playing = false;
_p.started = false;
_p.stopped = true; // to be removed from animation queues
return this;
},
rewind: function(){
return this.progress( 0 );
},
fastforward: function(){
return this.progress( 1 );
},
time: function( t ){
let _p = this._private;
if( t === undefined ){
return _p.progress * _p.duration;
} else {
return this.progress( t / _p.duration );
}
},
progress: function( p ){
let _p = this._private;
let wasPlaying = _p.playing;
if( p === undefined ){
return _p.progress;
} else {
if( wasPlaying ){
this.pause();
}
_p.progress = p;
_p.started = false;
if( wasPlaying ){
this.play();
}
}
return this;
},
completed: function(){
return this._private.progress === 1;
},
reverse: function(){
let _p = this._private;
let wasPlaying = _p.playing;
if( wasPlaying ){
this.pause();
}
_p.progress = 1 - _p.progress;
_p.started = false;
let swap = function( a, b ){
let _pa = _p[ a ];
if( _pa == null ){ return; }
_p[ a ] = _p[ b ];
_p[ b ] = _pa;
};
swap( 'zoom', 'startZoom' );
swap( 'pan', 'startPan' );
swap( 'position', 'startPosition' );
// swap styles
if( _p.style ){
for( let i = 0; i < _p.style.length; i++ ){
let prop = _p.style[ i ];
let name = prop.name;
let startStyleProp = _p.startStyle[ name ];
_p.startStyle[ name ] = prop;
_p.style[ i ] = startStyleProp;
}
}
if( wasPlaying ){
this.play();
}
return this;
},
promise: function( type ){
let _p = this._private;
let arr;
switch( type ){
case 'frame':
arr = _p.frames;
break;
default:
case 'complete':
case 'completed':
arr = _p.completes;
}
return new Promise( function( resolve, reject ){
arr.push( function(){
resolve();
} );
} );
}
} );
anifn.complete = anifn.completed;
anifn.run = anifn.play;
anifn.running = anifn.playing;
export default Animation;
+4
View File
@@ -0,0 +1,4 @@
// an entrypoint to use the raw source in cjs projects
// e.g. require('cytoscape/src/cjs') or setting an alias in your build tool of 'cytoscape':'cytoscape/src/cjs'
module.exports = require('./index.js').default;
+165
View File
@@ -0,0 +1,165 @@
import Heap from '../../heap.mjs';
import Set from '../../set.mjs';
import { defaults } from '../../util/index.mjs';
const aStarDefaults = defaults({
root: null,
goal: null,
weight: edge => 1,
heuristic: edge => 0,
directed: false
});
let elesfn = ({
// Implemented from pseudocode from wikipedia
aStar: function( options ){
let cy = this.cy();
let { root, goal, heuristic, directed, weight } = aStarDefaults(options);
root = cy.collection(root)[0];
goal = cy.collection(goal)[0];
let sid = root.id();
let tid = goal.id();
let gScore = {};
let fScore = {};
let closedSetIds = {};
let openSet = new Heap( (a, b) => fScore[a.id()] - fScore[b.id()] );
let openSetIds = new Set();
let cameFrom = {};
let cameFromEdge = {};
let addToOpenSet = (ele, id) => {
openSet.push(ele);
openSetIds.add(id);
};
let cMin, cMinId;
let popFromOpenSet = () => {
cMin = openSet.pop();
cMinId = cMin.id();
openSetIds.delete(cMinId);
};
let isInOpenSet = id => openSetIds.has(id);
addToOpenSet(root, sid);
gScore[ sid ] = 0;
fScore[ sid ] = heuristic( root );
// Counter
let steps = 0;
// Main loop
while( openSet.size() > 0 ){
popFromOpenSet();
steps++;
// If we've found our goal, then we are done
if( cMinId === tid ){
let path = [];
let pathNode = goal;
let pathNodeId = tid;
let pathEdge = cameFromEdge[pathNodeId];
for( ;; ){
path.unshift(pathNode);
if( pathEdge != null ){
path.unshift(pathEdge);
}
pathNode = cameFrom[pathNodeId];
if( pathNode == null ){ break; }
pathNodeId = pathNode.id();
pathEdge = cameFromEdge[pathNodeId];
}
return {
found: true,
distance: gScore[ cMinId ],
path: this.spawn( path ),
steps
};
}
// Add cMin to processed nodes
closedSetIds[ cMinId ] = true;
// Update scores for neighbors of cMin
// Take into account if graph is directed or not
let vwEdges = cMin._private.edges;
for( let i = 0; i < vwEdges.length; i++ ){
let e = vwEdges[ i ];
// edge must be in set of calling eles
if( !this.hasElementWithId( e.id() ) ){ continue; }
// cMin must be the source of edge if directed
if( directed && e.data('source') !== cMinId ){ continue; }
let wSrc = e.source();
let wTgt = e.target();
let w = wSrc.id() !== cMinId ? wSrc : wTgt;
let wid = w.id();
// node must be in set of calling eles
if( !this.hasElementWithId( wid ) ){ continue; }
// if node is in closedSet, ignore it
if( closedSetIds[ wid ] ){
continue;
}
// New tentative score for node w
let tempScore = gScore[ cMinId ] + weight( e );
// Update gScore for node w if:
// w not present in openSet
// OR
// tentative gScore is less than previous value
// w not in openSet
if( !isInOpenSet(wid) ){
gScore[ wid ] = tempScore;
fScore[ wid ] = tempScore + heuristic( w );
addToOpenSet( w, wid );
cameFrom[ wid ] = cMin;
cameFromEdge[ wid ] = e;
continue;
}
// w already in openSet, but with greater gScore
if( tempScore < gScore[ wid ] ){
gScore[ wid ] = tempScore;
fScore[ wid ] = tempScore + heuristic( w );
cameFrom[ wid ] = cMin;
cameFromEdge[ wid ] = e;
}
} // End of neighbors update
} // End of main loop
// If we've reached here, then we've not reached our goal
return {
found: false,
distance: undefined,
path: undefined,
steps: steps
};
}
}); // elesfn
export default elesfn;
@@ -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 };
@@ -0,0 +1,202 @@
import * as is from '../../is.mjs';
import { warn, defaults } from '../../util/index.mjs';
import Map from '../../map.mjs';
const bellmanFordDefaults = defaults({
weight: edge => 1,
directed: false,
root: null
});
let elesfn = ({
// Implemented from pseudocode from wikipedia
bellmanFord: function( options ){
let { weight, directed, root } = bellmanFordDefaults(options);
let weightFn = weight;
let eles = this;
let cy = this.cy();
let { edges, nodes } = this.byGroup();
let numNodes = nodes.length;
let infoMap = new Map();
let hasNegativeWeightCycle = false;
let negativeWeightCycles = [];
root = cy.collection(root)[0]; // in case selector passed
edges.unmergeBy( edge => edge.isLoop() );
let numEdges = edges.length;
let getInfo = node => {
let obj = infoMap.get( node.id() );
if( !obj ){
obj = {};
infoMap.set( node.id(), obj );
}
return obj;
};
let getNodeFromTo = to => (is.string(to) ? cy.$(to) : to)[0];
let distanceTo = to => getInfo( getNodeFromTo(to) ).dist;
let pathTo = (to, thisStart = root) => {
let end = getNodeFromTo(to);
let path = [];
let node = end;
for( ;; ){
if( node == null ){ return this.spawn(); }
let { edge, pred } = getInfo( node );
path.unshift( node[0] );
if( node.same(thisStart) && path.length > 0 ){ break; }
if( edge != null ){
path.unshift( edge );
}
node = pred;
}
return eles.spawn( path );
};
// Initializations { dist, pred, edge }
for( let i = 0; i < numNodes; i++ ){
let node = nodes[i];
let info = getInfo( node );
if( node.same(root) ){
info.dist = 0;
} else {
info.dist = Infinity;
}
info.pred = null;
info.edge = null;
}
// Edges relaxation
let replacedEdge = false;
let checkForEdgeReplacement = (node1, node2, edge, info1, info2, weight) => {
let dist = info1.dist + weight;
if( dist < info2.dist && !edge.same(info1.edge) ){
info2.dist = dist;
info2.pred = node1;
info2.edge = edge;
replacedEdge = true;
}
};
for( let i = 1; i < numNodes; i++ ){
replacedEdge = false;
for( let e = 0; e < numEdges; e++ ){
let edge = edges[e];
let src = edge.source();
let tgt = edge.target();
let weight = weightFn(edge);
let srcInfo = getInfo(src);
let tgtInfo = getInfo(tgt);
checkForEdgeReplacement(src, tgt, edge, srcInfo, tgtInfo, weight);
// If undirected graph, we need to take into account the 'reverse' edge
if( !directed ){
checkForEdgeReplacement(tgt, src, edge, tgtInfo, srcInfo, weight);
}
}
if( !replacedEdge ){ break; }
}
if( replacedEdge ){
// Check for negative weight cycles
const negativeWeightCycleIds = [];
for( let e = 0; e < numEdges; e++ ){
let edge = edges[e];
let src = edge.source();
let tgt = edge.target();
let weight = weightFn(edge);
let srcDist = getInfo(src).dist;
let tgtDist = getInfo(tgt).dist;
if( srcDist + weight < tgtDist || (!directed && tgtDist + weight < srcDist) ){
if( !hasNegativeWeightCycle ){
warn('Graph contains a negative weight cycle for Bellman-Ford');
hasNegativeWeightCycle = true;
}
if( options.findNegativeWeightCycles !== false ){
const negativeNodes = [];
if( srcDist + weight < tgtDist ){
negativeNodes.push(src);
}
if( !directed && tgtDist + weight < srcDist ) {
negativeNodes.push(tgt);
}
const numNegativeNodes = negativeNodes.length;
for( let n = 0; n < numNegativeNodes; n++ ){
const start = negativeNodes[n];
let cycle = [start];
cycle.push(getInfo(start).edge);
let node = getInfo(start).pred;
while( cycle.indexOf(node) === -1 ){
cycle.push(node);
cycle.push(getInfo(node).edge);
node = getInfo(node).pred;
}
cycle = cycle.slice(cycle.indexOf(node));
let smallestId = cycle[0].id();
let smallestIndex = 0;
for( let c = 2; c < cycle.length; c+=2 ){
if( cycle[c].id() < smallestId ){
smallestId = cycle[c].id();
smallestIndex = c;
}
}
cycle = cycle.slice(smallestIndex)
.concat(cycle.slice(0, smallestIndex));
cycle.push(cycle[0]);
const cycleId = cycle.map(el => el.id()).join(",");
if( negativeWeightCycleIds.indexOf(cycleId) === -1 ){
negativeWeightCycles.push(eles.spawn(cycle));
negativeWeightCycleIds.push(cycleId);
}
}
} else {
break;
}
}
}
}
return {
distanceTo,
pathTo,
hasNegativeWeightCycle,
negativeWeightCycles
};
} // bellmanFord
}); // elesfn
export default elesfn;
@@ -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;
+136
View File
@@ -0,0 +1,136 @@
import * as is from '../../is.mjs';
let defineSearch = function( params ){
params = {
bfs: params.bfs || !params.dfs,
dfs: params.dfs || !params.bfs
};
// from pseudocode on wikipedia
return function searchFn( roots, fn, directed ){
let options;
if( is.plainObject( roots ) && !is.elementOrCollection( roots ) ){
options = roots;
roots = options.roots || options.root;
fn = options.visit;
directed = options.directed;
}
directed = arguments.length === 2 && !is.fn( fn ) ? fn : directed;
fn = is.fn( fn ) ? fn : function(){};
let cy = this._private.cy;
let v = roots = is.string( roots ) ? this.filter( roots ) : roots;
let Q = [];
let connectedNodes = [];
let connectedBy = {};
let id2depth = {};
let V = {};
let j = 0;
let found;
let { nodes, edges } = this.byGroup();
// enqueue v
for( let i = 0; i < v.length; i++ ){
let vi = v[i];
let viId = vi.id();
if( vi.isNode() ){
Q.unshift( vi );
if( params.bfs ){
V[ viId ] = true;
connectedNodes.push( vi );
}
id2depth[ viId ] = 0;
}
}
while( Q.length !== 0 ){
let v = params.bfs ? Q.shift() : Q.pop();
let vId = v.id();
if( params.dfs ){
if( V[ vId ] ){ continue; }
V[ vId ] = true;
connectedNodes.push( v );
}
let depth = id2depth[ vId ];
let prevEdge = connectedBy[ vId ];
let src = prevEdge != null ? prevEdge.source() : null;
let tgt = prevEdge != null ? prevEdge.target() : null;
let prevNode = prevEdge == null ? undefined : ( v.same(src) ? tgt[0] : src[0] );
let ret;
ret = fn( v, prevEdge, prevNode, j++, depth );
if( ret === true ){
found = v;
break;
}
if( ret === false ){
break;
}
let vwEdges = v.connectedEdges().filter(e => (!directed || e.source().same(v)) && edges.has(e));
for( let i = 0; i < vwEdges.length; i++ ){
let e = vwEdges[ i ];
let w = e.connectedNodes().filter(n => !n.same(v) && nodes.has(n));
let wId = w.id();
if( w.length !== 0 && !V[ wId ] ){
w = w[0];
Q.push( w );
if( params.bfs ){
V[ wId ] = true;
connectedNodes.push( w );
}
connectedBy[ wId ] = e;
id2depth[ wId ] = id2depth[ vId ] + 1;
}
}
}
let connectedEles = cy.collection();
for( let i = 0; i < connectedNodes.length; i++ ){
let node = connectedNodes[ i ];
let edge = connectedBy[ node.id() ];
if( edge != null ){
connectedEles.push( edge );
}
connectedEles.push( node );
}
return {
path: cy.collection( connectedEles ),
found: cy.collection( found )
};
};
};
// search, spanning trees, etc
let elesfn = ({
breadthFirstSearch: defineSearch( { bfs: true } ),
depthFirstSearch: defineSearch( { dfs: true } )
});
// nice, short mathematical alias
elesfn.bfs = elesfn.breadthFirstSearch;
elesfn.dfs = elesfn.depthFirstSearch;
export default elesfn;
@@ -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;
@@ -0,0 +1,64 @@
// Common distance metrics for clustering algorithms
// https://en.wikipedia.org/wiki/Hierarchical_clustering#Metric
import * as is from '../../is.mjs';
let identity = x => x;
let absDiff = ( p, q ) => Math.abs( q - p );
let addAbsDiff = ( total, p, q ) => total + absDiff(p, q);
let addSquaredDiff = ( total, p, q ) => total + Math.pow( q - p, 2 );
let sqrt = x => Math.sqrt(x);
let maxAbsDiff = ( currentMax, p, q ) => Math.max( currentMax, absDiff(p, q) );
let getDistance = function( length, getP, getQ, init, visit, post = identity ){
let ret = init;
let p, q;
for ( let dim = 0; dim < length; dim++ ) {
p = getP(dim);
q = getQ(dim);
ret = visit( ret, p, q );
}
return post( ret );
};
let distances = {
euclidean: function ( length, getP, getQ ) {
if( length >= 2 ){
return getDistance( length, getP, getQ, 0, addSquaredDiff, sqrt );
} else { // for single attr case, more efficient to avoid sqrt
return getDistance( length, getP, getQ, 0, addAbsDiff );
}
},
squaredEuclidean: function ( length, getP, getQ ) {
return getDistance( length, getP, getQ, 0, addSquaredDiff );
},
manhattan: function ( length, getP, getQ ) {
return getDistance( length, getP, getQ, 0, addAbsDiff );
},
max: function ( length, getP, getQ ) {
return getDistance( length, getP, getQ, -Infinity, maxAbsDiff );
}
};
// in case the user accidentally doesn't use camel case
distances['squared-euclidean'] = distances['squaredEuclidean'];
distances['squaredeuclidean'] = distances['squaredEuclidean'];
export default function( method, length, getP, getQ, nodeP, nodeQ ){
let impl;
if( is.fn( method ) ){
impl = method;
} else {
impl = distances[ method ] || distances.euclidean;
}
if( length === 0 && is.fn( method ) ){
return impl( nodeP, nodeQ );
} else {
return impl( length, getP, getQ, nodeP, nodeQ );
}
}
@@ -0,0 +1,159 @@
import * as is from '../../is.mjs';
import * as util from '../../util/index.mjs';
const defaults = util.defaults({
root: null,
weight: edge => 1,
directed: false,
alpha: 0
});
let elesfn = ({
degreeCentralityNormalized: function( options ){
options = defaults( options );
let cy = this.cy();
let nodes = this.nodes();
let numNodes = nodes.length;
if( !options.directed ){
let degrees = {};
let maxDegree = 0;
for( let i = 0; i < numNodes; i++ ){
let node = nodes[ i ];
// add current node to the current options object and call degreeCentrality
options.root = node;
let currDegree = this.degreeCentrality( options );
if( maxDegree < currDegree.degree ){
maxDegree = currDegree.degree;
}
degrees[ node.id() ] = currDegree.degree;
}
return {
degree: function( node ){
if( maxDegree === 0 ){ return 0; }
if( is.string( node ) ){
// from is a selector string
node = cy.filter( node );
}
return degrees[ node.id() ] / maxDegree;
}
};
} else {
let indegrees = {};
let outdegrees = {};
let maxIndegree = 0;
let maxOutdegree = 0;
for( let i = 0; i < numNodes; i++ ){
let node = nodes[ i ];
let id = node.id();
// add current node to the current options object and call degreeCentrality
options.root = node;
let currDegree = this.degreeCentrality( options );
if( maxIndegree < currDegree.indegree )
maxIndegree = currDegree.indegree;
if( maxOutdegree < currDegree.outdegree )
maxOutdegree = currDegree.outdegree;
indegrees[ id ] = currDegree.indegree;
outdegrees[ id ] = currDegree.outdegree;
}
return {
indegree: function( node ){
if ( maxIndegree == 0 ){ return 0; }
if( is.string( node ) ){
// from is a selector string
node = cy.filter( node );
}
return indegrees[ node.id() ] / maxIndegree;
},
outdegree: function( node ){
if ( maxOutdegree === 0 ){ return 0; }
if( is.string( node ) ){
// from is a selector string
node = cy.filter( node );
}
return outdegrees[ node.id() ] / maxOutdegree;
}
};
}
}, // degreeCentralityNormalized
// Implemented from the algorithm in Opsahl's paper
// "Node centrality in weighted networks: Generalizing degree and shortest paths"
// check the heading 2 "Degree"
degreeCentrality: function( options ){
options = defaults( options );
let cy = this.cy();
let callingEles = this;
let { root, weight, directed, alpha } = options;
root = cy.collection(root)[0];
if( !directed ){
let connEdges = root.connectedEdges().intersection( callingEles );
let k = connEdges.length;
let s = 0;
// Now, sum edge weights
for( let i = 0; i < connEdges.length; i++ ){
s += weight( connEdges[i] );
}
return {
degree: Math.pow( k, 1 - alpha ) * Math.pow( s, alpha )
};
} else {
let edges = root.connectedEdges();
let incoming = edges.filter( edge => edge.target().same(root) && callingEles.has(edge) );
let outgoing = edges.filter( edge => edge.source().same(root) && callingEles.has(edge) );
let k_in = incoming.length;
let k_out = outgoing.length;
let s_in = 0;
let s_out = 0;
// Now, sum incoming edge weights
for( let i = 0; i < incoming.length; i++ ){
s_in += weight( incoming[i] );
}
// Now, sum outgoing edge weights
for( let i = 0; i < outgoing.length; i++ ){
s_out += weight( outgoing[i] );
}
return {
indegree: Math.pow( k_in, 1 - alpha ) * Math.pow( s_in, alpha ),
outdegree: Math.pow( k_out, 1 - alpha ) * Math.pow( s_out, alpha )
};
}
} // degreeCentrality
}); // elesfn
// nice, short mathematical alias
elesfn.dc = elesfn.degreeCentrality;
elesfn.dcn = elesfn.degreeCentralityNormalised = elesfn.degreeCentralityNormalized;
export default elesfn;
+133
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;
@@ -0,0 +1,140 @@
import * as is from '../../is.mjs';
import { defaults } from '../../util/index.mjs';
const floydWarshallDefaults = defaults({
weight: edge => 1,
directed: false
});
let elesfn = ({
// Implemented from pseudocode from wikipedia
floydWarshall: function( options ){
let cy = this.cy();
let { weight, directed } = floydWarshallDefaults(options);
let weightFn = weight;
let { nodes, edges } = this.byGroup();
let N = nodes.length;
let Nsq = N * N;
let indexOf = node => nodes.indexOf(node);
let atIndex = i => nodes[i];
// Initialize distance matrix
let dist = new Array(Nsq);
for( let n = 0; n < Nsq; n++ ){
let j = n % N;
let i = (n - j) / N;
if( i === j ){
dist[n] = 0;
} else {
dist[n] = Infinity;
}
}
// Initialize matrix used for path reconstruction
// Initialize distance matrix
let next = new Array(Nsq);
let edgeNext = new Array(Nsq);
// Process edges
for( let i = 0; i < edges.length; i++ ){
let edge = edges[i];
let src = edge.source()[0];
let tgt = edge.target()[0];
if( src === tgt ){ continue; } // exclude loops
let s = indexOf( src );
let t = indexOf( tgt );
let st = s * N + t; // source to target index
let weight = weightFn( edge );
// Check if already process another edge between same 2 nodes
if( dist[st] > weight ){
dist[st] = weight;
next[st] = t;
edgeNext[st] = edge;
}
// If undirected graph, process 'reversed' edge
if( !directed ){
let ts = t * N + s; // target to source index
if( !directed && dist[ts] > weight ){
dist[ts] = weight;
next[ts] = s;
edgeNext[ts] = edge;
}
}
}
// Main loop
for( let k = 0; k < N; k++ ){
for( let i = 0; i < N; i++ ){
let ik = i * N + k;
for( let j = 0; j < N; j++ ){
let ij = i * N + j;
let kj = k * N + j;
if( dist[ik] + dist[kj] < dist[ij] ){
dist[ij] = dist[ik] + dist[kj];
next[ij] = next[ik];
}
}
}
}
let getArgEle = ele => ( is.string(ele) ? cy.filter(ele) : ele )[0];
let indexOfArgEle = ele => indexOf(getArgEle(ele));
let res = {
distance: function( from, to ){
let i = indexOfArgEle(from);
let j = indexOfArgEle(to);
return dist[ i * N + j ];
},
path: function( from, to ){
let i = indexOfArgEle(from);
let j = indexOfArgEle(to);
let fromNode = atIndex(i);
if( i === j ){ return fromNode.collection(); }
if( next[i * N + j] == null ){ return cy.collection(); }
let path = cy.collection();
let prev = i;
let edge;
path.merge( fromNode );
while( i !== j ){
prev = i;
i = next[i * N + j];
edge = edgeNext[prev * N + i];
path.merge( edge );
path.merge( atIndex(i) );
}
return path;
}
};
return res;
} // floydWarshall
}); // elesfn
export default elesfn;
@@ -0,0 +1,316 @@
// Implemented by Zoe Xi @zoexi for GSOC 2016
// https://github.com/cytoscape/cytoscape.js-hierarchical
// Implemented from the reference library: https://harthur.github.io/clusterfck/
import * as util from '../../util/index.mjs';
import clusteringDistance from './clustering-distances.mjs';
const defaults = util.defaults({
distance: 'euclidean', // distance metric to compare nodes
linkage: 'min', // linkage criterion : how to determine the distance between clusters of nodes
mode: 'threshold',
// mode:'threshold' => clusters must be threshold distance apart
threshold: Infinity, // the distance threshold
// mode:'dendrogram' => the nodes are organised as leaves in a tree (siblings are close), merging makes clusters
addDendrogram: false, // whether to add the dendrogram to the graph for viz
dendrogramDepth: 0, // depth at which dendrogram branches are merged into the returned clusters
attributes: [] // array of attr functions
});
const linkageAliases = {
'single': 'min',
'complete': 'max'
};
let setOptions = ( options ) => {
let opts = defaults( options );
let preferredAlias = linkageAliases[ opts.linkage ];
if( preferredAlias != null ){
opts.linkage = preferredAlias;
}
return opts;
};
let mergeClosest = function( clusters, index, dists, mins, opts ) {
// Find two closest clusters from cached mins
let minKey = 0;
let min = Infinity;
let dist;
let attrs = opts.attributes;
let getDist = (n1, n2) => clusteringDistance( opts.distance, attrs.length, i => attrs[i](n1), i => attrs[i](n2), n1, n2 );
for ( let i = 0; i < clusters.length; i++ ) {
let key = clusters[i].key;
let dist = dists[key][mins[key]];
if ( dist < min ) {
minKey = key;
min = dist;
}
}
if ( (opts.mode === 'threshold' && min >= opts.threshold) ||
(opts.mode === 'dendrogram' && clusters.length === 1) ) {
return false;
}
let c1 = index[minKey];
let c2 = index[mins[minKey]];
let merged;
// Merge two closest clusters
if ( opts.mode === 'dendrogram' ) {
merged = {
left: c1,
right: c2,
key: c1.key
};
}
else {
merged = {
value: c1.value.concat(c2.value),
key: c1.key
};
}
clusters[c1.index] = merged;
clusters.splice(c2.index, 1);
index[c1.key] = merged;
// Update distances with new merged cluster
for ( let i = 0; i < clusters.length; i++ ) {
let cur = clusters[i];
if ( c1.key === cur.key ) {
dist = Infinity;
}
else if ( opts.linkage === 'min' ) {
dist = dists[c1.key][cur.key];
if ( dists[c1.key][cur.key] > dists[c2.key][cur.key] ) {
dist = dists[c2.key][cur.key];
}
}
else if ( opts.linkage === 'max' ) {
dist = dists[c1.key][cur.key];
if ( dists[c1.key][cur.key] < dists[c2.key][cur.key] ) {
dist = dists[c2.key][cur.key];
}
}
else if ( opts.linkage === 'mean' ) {
dist = (dists[c1.key][cur.key] * c1.size + dists[c2.key][cur.key] * c2.size) / (c1.size + c2.size);
}
else {
if ( opts.mode === 'dendrogram' )
dist = getDist( cur.value, c1.value );
else
dist = getDist( cur.value[0], c1.value[0] );
}
dists[c1.key][cur.key] = dists[cur.key][c1.key] = dist; // distance matrix is symmetric
}
// Update cached mins
for ( let i = 0; i < clusters.length; i++ ) {
let key1 = clusters[i].key;
if ( mins[key1] === c1.key || mins[key1] === c2.key ) {
let min = key1;
for ( let j = 0; j < clusters.length; j++ ) {
let key2 = clusters[j].key;
if ( dists[key1][key2] < dists[key1][min] ) {
min = key2;
}
}
mins[key1] = min;
}
clusters[i].index = i;
}
// Clean up meta data used for clustering
c1.key = c2.key = c1.index = c2.index = null;
return true;
};
let getAllChildren = function( root, arr, cy ) {
if ( !root )
return;
if ( root.value ) {
arr.push( root.value );
}
else {
if ( root.left )
getAllChildren( root.left, arr, cy );
if ( root.right )
getAllChildren( root.right, arr, cy );
}
};
let buildDendrogram = function ( root, cy ) {
if ( !root )
return '';
if ( root.left && root.right ) {
let leftStr = buildDendrogram( root.left, cy );
let rightStr = buildDendrogram( root.right, cy );
let node = cy.add({group:'nodes', data: {id: leftStr + ',' + rightStr}});
cy.add({group:'edges', data: { source: leftStr, target: node.id() }});
cy.add({group:'edges', data: { source: rightStr, target: node.id() }});
return node.id();
}
else if ( root.value ) {
return root.value.id();
}
};
let buildClustersFromTree = function( root, k, cy ) {
if ( !root )
return [];
let left = [], right = [], leaves = [];
if ( k === 0 ) { // don't cut tree, simply return all nodes as 1 single cluster
if ( root.left )
getAllChildren( root.left, left, cy );
if ( root.right )
getAllChildren( root.right, right, cy );
leaves = left.concat(right);
return [ cy.collection(leaves) ];
}
else if ( k === 1 ) { // cut at root
if ( root.value ) { // leaf node
return [ cy.collection( root.value ) ];
}
else {
if ( root.left )
getAllChildren( root.left, left, cy );
if ( root.right )
getAllChildren( root.right, right, cy );
return [ cy.collection(left), cy.collection(right) ];
}
}
else {
if ( root.value ) {
return [ cy.collection(root.value) ];
}
else {
if ( root.left )
left = buildClustersFromTree( root.left, k - 1, cy );
if ( root.right )
right = buildClustersFromTree( root.right, k - 1, cy );
return left.concat(right);
}
}
};
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
let printMatrix = function( M ) { // used for debugging purposes only
let n = M.length;
for(let i = 0; i < n; i++ ) {
let row = '';
for ( let j = 0; j < n; j++ ) {
row += Math.round(M[i][j]*100)/100 + ' ';
}
console.log(row);
}
console.log('');
};
} /* eslint-enable */
let hierarchicalClustering = function( options ){
let cy = this.cy();
let nodes = this.nodes();
// Set parameters of algorithm: linkage type, distance metric, etc.
let opts = setOptions( options );
let attrs = opts.attributes;
let getDist = (n1, n2) => clusteringDistance( opts.distance, attrs.length, i => attrs[i](n1), i => attrs[i](n2), n1, n2 );
// Begin hierarchical algorithm
let clusters = [];
let dists = []; // distances between each pair of clusters
let mins = []; // closest cluster for each cluster
let index = []; // hash of all clusters by key
// In agglomerative (bottom-up) clustering, each node starts as its own cluster
for ( let n = 0; n < nodes.length; n++ ) {
let cluster = {
value: (opts.mode === 'dendrogram') ? nodes[n] : [ nodes[n] ],
key: n,
index: n
};
clusters[n] = cluster;
index[n] = cluster;
dists[n] = [];
mins[n] = 0;
}
// Calculate the distance between each pair of clusters
for ( let i = 0; i < clusters.length; i++ ) {
for ( let j = 0; j <= i; j++ ) {
let dist;
if ( opts.mode === 'dendrogram' ){ // modes store cluster values differently
dist = (i === j) ? Infinity : getDist( clusters[i].value, clusters[j].value );
} else {
dist = (i === j) ? Infinity : getDist( clusters[i].value[0], clusters[j].value[0] );
}
dists[i][j] = dist;
dists[j][i] = dist;
if ( dist < dists[i][mins[i]] ) {
mins[i] = j; // Cache mins: closest cluster to cluster i is cluster j
}
}
}
// Find the closest pair of clusters and merge them into a single cluster.
// Update distances between new cluster and each of the old clusters, and loop until threshold reached.
let merged = mergeClosest( clusters, index, dists, mins, opts );
while ( merged ) {
merged = mergeClosest( clusters, index, dists, mins, opts );
}
let retClusters;
// Dendrogram mode builds the hierarchy and adds intermediary nodes + edges
// in addition to returning the clusters.
if ( opts.mode === 'dendrogram') {
retClusters = buildClustersFromTree( clusters[0], opts.dendrogramDepth, cy );
if ( opts.addDendrogram )
buildDendrogram( clusters[0], cy );
}
else { // Regular mode simply returns the clusters
retClusters = new Array(clusters.length);
clusters.forEach( function( cluster, i ) {
// Clean up meta data used for clustering
cluster.key = cluster.index = null;
retClusters[i] = cy.collection( cluster.value );
});
}
return retClusters;
};
export default { hierarchicalClustering, hca: hierarchicalClustering };
+136
View File
@@ -0,0 +1,136 @@
import * as is from '../../is.mjs';
import { defaults } from '../../util/index.mjs';
const hierholzerDefaults = defaults({
root: undefined,
directed: false
});
let elesfn = ({
hierholzer: function( options ){
if (!is.plainObject(options)) {
let args = arguments;
options = { root: args[0], directed: args[1] };
}
let { root, directed } = hierholzerDefaults(options);
let eles = this;
let dflag = false;
let oddIn;
let oddOut;
let startVertex;
if (root) startVertex = is.string(root) ? this.filter(root)[0].id() : root[0].id();
let nodes = {};
let edges = {};
if (directed) {
eles.forEach(function(ele){
let id = ele.id();
if(ele.isNode()) {
let ind = ele.indegree(true);
let outd = ele.outdegree(true);
let d1 = ind - outd;
let d2 = outd - ind;
if (d1 == 1) {
if (oddIn) dflag = true;
else oddIn = id;
} else if (d2 == 1) {
if (oddOut) dflag = true;
else oddOut = id;
} else if ((d2 > 1) || (d1 > 1)) {
dflag = true;
}
nodes[id] = [];
ele.outgoers().forEach(e => {
if (e.isEdge()) nodes[id].push(e.id());
});
} else {
edges[id] = [undefined, ele.target().id()];
}
});
} else {
eles.forEach(function(ele){
let id = ele.id();
if(ele.isNode()) {
let d = ele.degree(true);
if (d%2) {
if (!oddIn) oddIn = id;
else if (!oddOut) oddOut = id;
else dflag = true;
}
nodes[id] = [];
ele.connectedEdges().forEach(e => nodes[id].push(e.id()));
} else {
edges[id] = [ele.source().id(), ele.target().id()];
}
});
}
let result = {
found: false,
trail: undefined
};
if (dflag) return result;
else if (oddOut && oddIn) {
if (directed) {
if (startVertex && (oddOut != startVertex)) {
return result;
}
startVertex = oddOut;
} else {
if (startVertex && (oddOut != startVertex) && (oddIn != startVertex)) {
return result;
} else if (!startVertex) {
startVertex = oddOut;
}
}
} else {
if (!startVertex) startVertex = eles[0].id();
}
const walk = (v) => {
let currentNode = v;
let subtour = [v];
let adj, adjTail, adjHead;
while (nodes[currentNode].length) {
adj = nodes[currentNode].shift();
adjTail = edges[adj][0];
adjHead = edges[adj][1];
if (currentNode != adjHead) {
nodes[adjHead] = nodes[adjHead].filter(e => e != adj);
currentNode = adjHead;
} else if (!directed && (currentNode != adjTail)) {
nodes[adjTail] = nodes[adjTail].filter(e => e != adj);
currentNode = adjTail;
}
subtour.unshift(adj);
subtour.unshift(currentNode);
}
return subtour;
};
let trail = [];
let subtour = [];
subtour = walk(startVertex);
while (subtour.length != 1) {
if (nodes[subtour[0]].length == 0) {
trail.unshift(eles.getElementById(subtour.shift()));
trail.unshift(eles.getElementById(subtour.shift()));
} else {
subtour = walk(subtour.shift()).concat(subtour);
}
}
trail.unshift(eles.getElementById(subtour.shift())); // final node
for (let d in nodes) {
if (nodes[d].length) {
return result;
}
}
result.found = true;
result.trail = this.spawn( trail, true );
return result;
},
});
export default elesfn;
@@ -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
};
+46
View File
@@ -0,0 +1,46 @@
import * as util from '../../util/index.mjs';
import bfsDfs from './bfs-dfs.mjs';
import dijkstra from './dijkstra.mjs';
import kruskal from './kruskal.mjs';
import aStar from './a-star.mjs';
import floydWarshall from './floyd-warshall.mjs';
import bellmanFord from './bellman-ford.mjs';
import kargerStein from './karger-stein.mjs';
import pageRank from './page-rank.mjs';
import degreeCentrality from './degree-centrality.mjs';
import closenessCentrality from './closeness-centrality.mjs';
import betweennessCentrality from './betweenness-centrality.mjs';
import markovClustering from './markov-clustering.mjs';
import kClustering from './k-clustering.mjs';
import hierarchicalClustering from './hierarchical-clustering.mjs';
import affinityPropagation from './affinity-propagation.mjs';
import hierholzer from './hierholzer.mjs';
import hopcroftTarjanBiconnected from './hopcroft-tarjan-biconnected.mjs';
import tarjanStronglyConnected from './tarjan-strongly-connected.mjs';
var elesfn = {};
[
bfsDfs,
dijkstra,
kruskal,
aStar,
floydWarshall,
bellmanFord,
kargerStein,
pageRank,
degreeCentrality,
closenessCentrality,
betweennessCentrality,
markovClustering,
kClustering,
hierarchicalClustering,
affinityPropagation,
hierholzer,
hopcroftTarjanBiconnected,
tarjanStronglyConnected
].forEach(function(props) {
util.extend(elesfn, props);
});
export default elesfn;
@@ -0,0 +1,468 @@
// Implemented by Zoe Xi @zoexi for GSOC 2016
// https://github.com/cytoscape/cytoscape.js-k-means
// References for k-means: https://harthur.github.io/clusterfck/
// References for k-medoids: http://www.math.le.ac.uk/people/ag153/homepage/KmeansKmedoids/Kmeans_Kmedoids.html
// References for fuzzy c-means: Ross, Fuzzy Logic w/Engineering Applications (2010), pages 352-353
// http://yaikhom.com/2013/03/16/implementing-the-fuzzy-c-means-algorithm.html
import clusteringDistance from './clustering-distances.mjs';
import * as util from '../../util/index.mjs';
let defaults = util.defaults({
k: 2,
m: 2,
sensitivityThreshold: 0.0001,
distance: 'euclidean',
maxIterations: 10,
attributes: [],
testMode: false,
testCentroids: null
});
var setOptions = ( options ) => defaults( options );
if( process.env.NODE_ENV !== 'production' ){ /* eslint-disable no-console, no-unused-vars */
var printMatrix = function( M ) { // used for debugging purposes only
for ( let i = 0; i < M.length; i++ ) {
let row = '';
for ( let j = 0; j < M[0].length; j++ ) {
row += Number(M[i][j]).toFixed(3) + ' ';
}
console.log(row);
}
console.log('');
};
} /* eslint-enable */
let getDist = function(type, node, centroid, attributes, mode){
let noNodeP = mode !== 'kMedoids';
let getP = noNodeP ? ( i => centroid[i] ) : ( i => attributes[i](centroid) );
let getQ = i => attributes[i](node);
let nodeP = centroid;
let nodeQ = node;
return clusteringDistance( type, attributes.length, getP, getQ, nodeP, nodeQ );
};
let randomCentroids = function( nodes, k, attributes ) {
let ndim = attributes.length;
let min = new Array(ndim);
let max = new Array(ndim);
let centroids = new Array(k);
let centroid = null;
// Find min, max values for each attribute dimension
for ( let i = 0; i < ndim; i++ ) {
min[i] = nodes.min( attributes[i] ).value;
max[i] = nodes.max( attributes[i] ).value;
}
// Build k centroids, each represented as an n-dim feature vector
for ( let c = 0; c < k; c++ ) {
centroid = [];
for ( let i = 0; i < ndim; i++ ) {
centroid[i] = Math.random() * (max[i] - min[i]) + min[i]; // random initial value
}
centroids[c] = centroid;
}
return centroids;
};
let classify = function( node, centroids, distance, attributes, type ) {
let min = Infinity;
let index = 0;
for ( let i = 0; i < centroids.length; i++ ) {
let dist = getDist( distance, node, centroids[i], attributes, type );
if (dist < min) {
min = dist;
index = i;
}
}
return index;
};
let buildCluster = function( centroid, nodes, assignment ) {
let cluster = [];
let node = null;
for ( let n = 0; n < nodes.length; n++ ) {
node = nodes[n];
if ( assignment[ node.id() ] === centroid ) {
//console.log("Node " + node.id() + " is associated with medoid #: " + m);
cluster.push( node );
}
}
return cluster;
};
let haveValuesConverged = function( v1, v2, sensitivityThreshold ){
return Math.abs( v2 - v1 ) <= sensitivityThreshold;
};
let haveMatricesConverged = function( v1, v2, sensitivityThreshold ) {
for ( let i = 0; i < v1.length; i++ ) {
for (let j = 0; j < v1[i].length; j++ ) {
let diff = Math.abs( v1[i][j] - v2[i][j] );
if( diff > sensitivityThreshold ){ return false; }
}
}
return true;
};
let seenBefore = function ( node, medoids, n ) {
for ( let i = 0; i < n; i++ ) {
if ( node === medoids[i] )
return true;
}
return false;
};
let randomMedoids = function( nodes, k ) {
let medoids = new Array(k);
// For small data sets, the probability of medoid conflict is greater,
// so we need to check to see if we've already seen or chose this node before.
if (nodes.length < 50) {
// Randomly select k medoids from the n nodes
for (let i = 0; i < k; i++) {
let node = nodes[ Math.floor( Math.random() * nodes.length ) ];
// If we've already chosen this node to be a medoid, don't choose it again (for small data sets).
// Instead choose a different random node.
while ( seenBefore( node, medoids, i ) ) {
node = nodes[ Math.floor( Math.random() * nodes.length ) ];
}
medoids[i] = node;
}
}
else { // Relatively large data set, so pretty safe to not check and just select random nodes
for (let i = 0; i < k; i++) {
medoids[i] = nodes[ Math.floor( Math.random() * nodes.length ) ];
}
}
return medoids;
};
let findCost = function( potentialNewMedoid, cluster, attributes ) {
let cost = 0;
for ( let n = 0; n < cluster.length; n++ ) {
cost += getDist( 'manhattan', cluster[n], potentialNewMedoid, attributes, 'kMedoids' );
}
return cost;
};
let kMeans = function( options ){
let cy = this.cy();
let nodes = this.nodes();
let node = null;
// Set parameters of algorithm: # of clusters, distance metric, etc.
let opts = setOptions( options );
// Begin k-means algorithm
let clusters = new Array(opts.k);
let assignment = {};
let centroids;
// Step 1: Initialize centroid positions
if ( opts.testMode ) {
if( typeof opts.testCentroids === 'number') {
// TODO: implement a seeded random number generator.
let seed = opts.testCentroids;
centroids = randomCentroids( nodes, opts.k, opts.attributes, seed );
}
else if ( typeof opts.testCentroids === 'object') {
centroids = opts.testCentroids;
}
else {
centroids = randomCentroids( nodes, opts.k, opts.attributes );
}
}
else {
centroids = randomCentroids( nodes, opts.k, opts.attributes );
}
let isStillMoving = true;
let iterations = 0;
while ( isStillMoving && iterations < opts.maxIterations ) {
// Step 2: Assign nodes to the nearest centroid
for ( let n = 0; n < nodes.length; n++ ) {
node = nodes[n];
// Determine which cluster this node belongs to: node id => cluster #
assignment[ node.id() ] = classify( node, centroids, opts.distance, opts.attributes, 'kMeans' );
}
// Step 3: For each of the k clusters, update its centroid
isStillMoving = false;
for ( let c = 0; c < opts.k; c++ ) {
// Get all nodes that belong to this cluster
let cluster = buildCluster( c, nodes, assignment );
if ( cluster.length === 0 ) { // If cluster is empty, break out early & move to next cluster
continue;
}
// Update centroids by calculating avg of all nodes within the cluster.
let ndim = opts.attributes.length;
let centroid = centroids[c]; // [ dim_1, dim_2, dim_3, ... , dim_n ]
let newCentroid = new Array(ndim);
let sum = new Array(ndim);
for ( let d = 0; d < ndim; d++ ) {
sum[d] = 0.0;
for ( let i = 0; i < cluster.length; i++ ) {
node = cluster[i];
sum[d] += opts.attributes[d](node);
}
newCentroid[d] = sum[d] / cluster.length;
// Check to see if algorithm has converged, i.e. when centroids no longer change
if ( !haveValuesConverged(newCentroid[d], centroid[d], opts.sensitivityThreshold) ) {
isStillMoving = true;
}
}
centroids[c] = newCentroid;
clusters[c] = cy.collection( cluster );
}
iterations++;
}
return clusters;
};
let kMedoids = function( options ) {
let cy = this.cy();
let nodes = this.nodes();
let node = null;
let opts = setOptions( options );
// Begin k-medoids algorithm
let clusters = new Array(opts.k);
let medoids;
let assignment = {};
let curCost;
let minCosts = new Array(opts.k); // minimum cost configuration for each cluster
// Step 1: Initialize k medoids
if ( opts.testMode ) {
if( typeof opts.testCentroids === 'number') {
// TODO: implement random generator so user can just input seed number
}
else if ( typeof opts.testCentroids === 'object') {
medoids = opts.testCentroids;
}
else {
medoids = randomMedoids(nodes, opts.k);
}
}
else {
medoids = randomMedoids(nodes, opts.k);
}
let isStillMoving = true;
let iterations = 0;
while ( isStillMoving && iterations < opts.maxIterations ) {
// Step 2: Assign nodes to the nearest medoid
for ( let n = 0; n < nodes.length; n++ ) {
node = nodes[n];
// Determine which cluster this node belongs to: node id => cluster #
assignment[ node.id() ] = classify( node, medoids, opts.distance, opts.attributes, 'kMedoids' );
}
isStillMoving = false;
// Step 3: For each medoid m, and for each node associated with mediod m,
// select the node with the lowest configuration cost as new medoid.
for ( let m = 0; m < medoids.length; m++ ) {
// Get all nodes that belong to this medoid
let cluster = buildCluster( m, nodes, assignment );
if ( cluster.length === 0 ) { // If cluster is empty, break out early & move to next cluster
continue;
}
minCosts[m] = findCost( medoids[m], cluster, opts.attributes ); // original cost
// Select different medoid if its configuration has the lowest cost
for ( let n = 0; n < cluster.length; n++ ) {
curCost = findCost( cluster[n], cluster, opts.attributes );
if ( curCost < minCosts[m] ) {
minCosts[m] = curCost;
medoids[m] = cluster[n];
isStillMoving = true;
}
}
clusters[m] = cy.collection( cluster );
}
iterations++;
}
return clusters;
};
let updateCentroids = function( centroids, nodes, U, weight, opts ) {
let numerator, denominator;
for ( let n = 0; n < nodes.length; n++ ) {
for ( let c = 0; c < centroids.length; c++ ) {
weight[n][c] = Math.pow( U[n][c], opts.m );
}
}
for ( let c = 0; c < centroids.length; c++ ) {
for ( let dim = 0; dim < opts.attributes.length; dim++ ) {
numerator = 0;
denominator = 0;
for ( let n = 0; n < nodes.length; n++ ) {
numerator += weight[n][c] * opts.attributes[dim](nodes[n]);
denominator += weight[n][c];
}
centroids[c][dim] = numerator / denominator;
}
}
};
let updateMembership = function( U, _U, centroids, nodes, opts ) {
// Save previous step
for (let i = 0; i < U.length; i++) {
_U[i] = U[i].slice();
}
let sum, numerator, denominator;
let pow = 2 / (opts.m - 1);
for ( let c = 0; c < centroids.length; c++ ) {
for ( let n = 0; n < nodes.length; n++ ) {
sum = 0;
for ( let k = 0; k < centroids.length; k++ ) { // against all other centroids
numerator = getDist( opts.distance, nodes[n], centroids[c], opts.attributes, 'cmeans' );
denominator = getDist( opts.distance, nodes[n], centroids[k], opts.attributes, 'cmeans' );
sum += Math.pow( numerator / denominator, pow );
}
U[n][c] = 1 / sum;
}
}
};
let assign = function( nodes, U, opts, cy ) {
let clusters = new Array(opts.k);
for ( let c = 0; c < clusters.length; c++ ) {
clusters[c] = [];
}
let max;
let index;
for ( let n = 0; n < U.length; n++ ) { // for each node (U is N x C matrix)
max = -Infinity;
index = -1;
// Determine which cluster the node is most likely to belong in
for ( let c = 0; c < U[0].length; c++ ) {
if ( U[n][c] > max ) {
max = U[n][c];
index = c;
}
}
clusters[index].push( nodes[n] );
}
// Turn every array into a collection of nodes
for ( let c = 0; c < clusters.length; c++ ) {
clusters[c] = cy.collection( clusters[c] );
}
return clusters;
};
let fuzzyCMeans = function( options ) {
let cy = this.cy();
let nodes = this.nodes();
let opts = setOptions( options );
// Begin fuzzy c-means algorithm
let clusters;
let centroids;
let U;
let _U;
let weight;
// Step 1: Initialize letiables.
_U = new Array(nodes.length);
for ( let i = 0; i < nodes.length; i++ ) { // N x C matrix
_U[i] = new Array(opts.k);
}
U = new Array(nodes.length);
for ( let i = 0; i < nodes.length; i++ ) { // N x C matrix
U[i] = new Array(opts.k);
}
for (let i = 0; i < nodes.length; i++) {
let total = 0;
for (let j = 0; j < opts.k; j++) {
U[i][j] = Math.random();
total += U[i][j];
}
for (let j = 0; j < opts.k; j++) {
U[i][j] = U[i][j] / total;
}
}
centroids = new Array(opts.k);
for ( let i = 0; i < opts.k; i++ ) {
centroids[i] = new Array(opts.attributes.length);
}
weight = new Array(nodes.length);
for ( let i = 0; i < nodes.length; i++ ) { // N x C matrix
weight[i] = new Array(opts.k);
}
// end init FCM
let isStillMoving = true;
let iterations = 0;
while ( isStillMoving && iterations < opts.maxIterations ) {
isStillMoving = false;
// Step 2: Calculate the centroids for each step.
updateCentroids( centroids, nodes, U, weight, opts );
// Step 3: Update the partition matrix U.
updateMembership( U, _U, centroids, nodes, opts );
// Step 4: Check for convergence.
if ( !haveMatricesConverged( U, _U, opts.sensitivityThreshold ) ) {
isStillMoving = true;
}
iterations++;
}
// Assign nodes to clusters with highest probability.
clusters = assign( nodes, U, opts, cy );
return {
clusters: clusters,
degreeOfMembership: U
};
};
export default {
kMeans, kMedoids, fuzzyCMeans, fcm: fuzzyCMeans
};
@@ -0,0 +1,198 @@
import { error } from '../../util/index.mjs';
const sqrt2 = Math.sqrt(2);
// Function which colapses 2 (meta) nodes into one
// Updates the remaining edge lists
// Receives as a paramater the edge which causes the collapse
const collapse = function( edgeIndex, nodeMap, remainingEdges ){
if( remainingEdges.length === 0 ){
error(`Karger-Stein must be run on a connected (sub)graph`);
}
let edgeInfo = remainingEdges[ edgeIndex ];
let sourceIn = edgeInfo[1];
let targetIn = edgeInfo[2];
let partition1 = nodeMap[ sourceIn ];
let partition2 = nodeMap[ targetIn ];
let newEdges = remainingEdges; // re-use array
// Delete all edges between partition1 and partition2
for( let i = newEdges.length - 1; i >=0; i-- ){
let edge = newEdges[i];
let src = edge[1];
let tgt = edge[2];
if(
( nodeMap[ src ] === partition1 && nodeMap[ tgt ] === partition2 ) ||
( nodeMap[ src ] === partition2 && nodeMap[ tgt ] === partition1 )
){
newEdges.splice(i, 1);
}
}
// All edges pointing to partition2 should now point to partition1
for( let i = 0; i < newEdges.length; i++ ){
let edge = newEdges[i];
if( edge[1] === partition2 ){ // Check source
newEdges[i] = edge.slice(); // copy
newEdges[i][1] = partition1;
} else if( edge[2] === partition2 ){ // Check target
newEdges[i] = edge.slice(); // copy
newEdges[i][2] = partition1;
}
}
// Move all nodes from partition2 to partition1
for( let i = 0; i < nodeMap.length; i++ ){
if( nodeMap[i] === partition2 ){
nodeMap[i] = partition1;
}
}
return newEdges;
};
// Contracts a graph until we reach a certain number of meta nodes
const contractUntil = function( metaNodeMap, remainingEdges, size, sizeLimit ){
while( size > sizeLimit ){
// Choose an edge randomly
let edgeIndex = Math.floor( (Math.random() * remainingEdges.length) );
// Collapse graph based on edge
remainingEdges = collapse( edgeIndex, metaNodeMap, remainingEdges );
size--;
}
return remainingEdges;
};
const elesfn = ({
// Computes the minimum cut of an undirected graph
// Returns the correct answer with high probability
kargerStein: function(){
let { nodes, edges } = this.byGroup();
edges.unmergeBy(edge => edge.isLoop());
let numNodes = nodes.length;
let numEdges = edges.length;
let numIter = Math.ceil( Math.pow( Math.log( numNodes ) / Math.LN2, 2 ) );
let stopSize = Math.floor( numNodes / sqrt2 );
if( numNodes < 2 ){
error( 'At least 2 nodes are required for Karger-Stein algorithm' );
return undefined;
}
// Now store edge destination as indexes
// Format for each edge (edge index, source node index, target node index)
let edgeIndexes = [];
for( let i = 0; i < numEdges; i++ ){
let e = edges[ i ];
edgeIndexes.push([ i, nodes.indexOf(e.source()), nodes.indexOf(e.target()) ]);
}
// We will store the best cut found here
let minCutSize = Infinity;
let minCutEdgeIndexes = [];
let minCutNodeMap = new Array(numNodes);
// Initial meta node partition
let metaNodeMap = new Array(numNodes);
let metaNodeMap2 = new Array(numNodes);
let copyNodesMap = (from, to) => {
for( let i = 0; i < numNodes; i++ ){
to[i] = from[i];
}
};
// Main loop
for( let iter = 0; iter <= numIter; iter++ ){
// Reset meta node partition
for( let i = 0; i < numNodes; i++ ){ metaNodeMap[i] = i; }
// Contract until stop point (stopSize nodes)
let edgesState = contractUntil( metaNodeMap, edgeIndexes.slice(), numNodes, stopSize );
let edgesState2 = edgesState.slice(); // copy
// Create a copy of the colapsed nodes state
copyNodesMap(metaNodeMap, metaNodeMap2);
// Run 2 iterations starting in the stop state
let res1 = contractUntil( metaNodeMap, edgesState, stopSize, 2 );
let res2 = contractUntil( metaNodeMap2, edgesState2, stopSize, 2 );
// Is any of the 2 results the best cut so far?
if( res1.length <= res2.length && res1.length < minCutSize ){
minCutSize = res1.length;
minCutEdgeIndexes = res1;
copyNodesMap(metaNodeMap, minCutNodeMap);
} else if( res2.length <= res1.length && res2.length < minCutSize ){
minCutSize = res2.length;
minCutEdgeIndexes = res2;
copyNodesMap(metaNodeMap2, minCutNodeMap);
}
} // end of main loop
// Construct result
let cut = this.spawn( minCutEdgeIndexes.map(e => edges[e[0]]) );
let partition1 = this.spawn();
let partition2 = this.spawn();
// traverse metaNodeMap for best cut
let witnessNodePartition = minCutNodeMap[0];
for( let i = 0; i < minCutNodeMap.length; i++ ){
let partitionId = minCutNodeMap[i];
let node = nodes[i];
if( partitionId === witnessNodePartition ){
partition1.merge( node );
} else {
partition2.merge( node );
}
}
// construct components corresponding to each disjoint subset of nodes
const constructComponent = (subset) => {
const component = this.spawn();
subset.forEach(node => {
component.merge(node);
node.connectedEdges().forEach(edge => {
// ensure edge is within calling collection and edge is not in cut
if (this.contains(edge) && !cut.contains(edge)) {
component.merge(edge);
}
});
});
return component;
};
const components = [
constructComponent(partition1),
constructComponent(partition2)
];
let ret = {
cut,
components,
// n.b. partitions are included to be compatible with the old api spec
// (could be removed in a future major version)
partition1,
partition2
};
return ret;
}
}); // elesfn
export default elesfn;
+50
View File
@@ -0,0 +1,50 @@
let elesfn = ({
// kruskal's algorithm (finds min spanning tree, assuming undirected graph)
// implemented from pseudocode from wikipedia
kruskal: function( weightFn ){
weightFn = weightFn || ( edge => 1 );
let { nodes, edges } = this.byGroup();
let numNodes = nodes.length;
let forest = new Array(numNodes);
let A = nodes; // assumes byGroup() creates new collections that can be safely mutated
let findSetIndex = ele => {
for( let i = 0; i < forest.length; i++ ){
let eles = forest[i];
if( eles.has(ele) ){ return i; }
}
};
// start with one forest per node
for( let i = 0; i < numNodes; i++ ){
forest[i] = this.spawn( nodes[i] );
}
let S = edges.sort( (a, b) => weightFn(a) - weightFn(b) );
for( let i = 0; i < S.length; i++ ){
let edge = S[i];
let u = edge.source()[0];
let v = edge.target()[0];
let setUIndex = findSetIndex(u);
let setVIndex = findSetIndex(v);
let setU = forest[ setUIndex ];
let setV = forest[ setVIndex ];
if( setUIndex !== setVIndex ){
A.merge( edge );
// combine forests for u and v
setU.merge( setV );
forest.splice( setVIndex, 1 );
}
}
return A;
}
});
export default elesfn;
@@ -0,0 +1,232 @@
// Implemented by Zoe Xi @zoexi for GSOC 2016
// https://github.com/cytoscape/cytoscape.js-markov-cluster
// Implemented from Stijn van Dongen's (author of MCL algorithm) documentation: http://micans.org/mcl/
// and lecture notes: https://www.cs.ucsb.edu/~xyan/classes/CS595D-2009winter/MCL_Presentation2.pdf
import * as util from '../../util/index.mjs';
/* eslint-disable no-unused-vars */
let defaults = util.defaults({
expandFactor: 2, // affects time of computation and cluster granularity to some extent: M * M
inflateFactor: 2, // affects cluster granularity (the greater the value, the more clusters): M(i,j) / E(j)
multFactor: 1, // optional self loops for each node. Use a neutral value to improve cluster computations.
maxIterations: 20, // maximum number of iterations of the MCL algorithm in a single run
attributes: [ // attributes/features used to group nodes, ie. similarity values between nodes
function(edge) {
return 1;
}
]
});
/* eslint-enable */
let setOptions = ( options ) => defaults( options );
/* eslint-disable no-unused-vars, no-console */
if( process.env.NODE_ENV !== 'production' ){
var printMatrix = function( M ) { // used for debugging purposes only
let n = Math.sqrt(M.length);
for ( let i = 0; i < n; i++ ) {
let row = '';
for ( let j = 0; j < n; j++ ) {
row += Number(M[i*n+j]).toFixed(3) + ' ';
}
console.log(row);
}
console.log('');
};
}
/* eslint-enable */
let getSimilarity = function( edge, attributes ) {
let total = 0;
for ( let i = 0; i < attributes.length; i++ ) {
total += attributes[i]( edge );
}
return total;
};
let addLoops = function( M, n, val ) {
for (let i = 0; i < n; i++) {
M[i * n + i] = val;
}
};
let normalize = function( M, n ) {
let sum;
for ( let col = 0; col < n; col++ ) {
sum = 0;
for ( let row = 0; row < n; row++ ) {
sum += M[row * n + col];
}
for ( let row = 0; row < n; row++ ) {
M[row * n + col] = M[row * n + col] / sum;
}
}
};
// TODO: blocked matrix multiplication?
let mmult = function( A, B, n ) {
let C = new Array( n * n );
for ( let i = 0; i < n; i++ ) {
for ( let j = 0; j < n; j++ ) {
C[i * n + j] = 0;
}
for ( let k = 0; k < n; k++ ) {
for ( let j = 0; j < n; j++ ) {
C[i * n + j] += A[i * n + k] * B[k * n + j];
}
}
}
return C;
};
let expand = function( M, n, expandFactor /** power **/ ) {
let _M = M.slice(0);
for ( let p = 1; p < expandFactor; p++ ) {
M = mmult( M, _M, n );
}
return M;
};
let inflate = function( M, n, inflateFactor /** r **/ ) {
let _M = new Array( n * n );
// M(i,j) ^ inflatePower
for ( let i = 0; i < n * n; i++ ) {
_M[i] = Math.pow( M[i], inflateFactor );
}
normalize( _M, n );
return _M;
};
let hasConverged = function( M, _M, n2, roundFactor ) {
// Check that both matrices have the same elements (i,j)
for ( let i = 0; i < n2; i++ ) {
let v1 = Math.round( M[i] * Math.pow(10, roundFactor) ) / Math.pow(10, roundFactor); // truncate to 'roundFactor' decimal places
let v2 = Math.round( _M[i] * Math.pow(10, roundFactor) ) / Math.pow(10, roundFactor);
if ( v1 !== v2 ) {
return false;
}
}
return true;
};
let assign = function( M, n, nodes, cy ) {
let clusters = [];
for ( let i = 0; i < n; i++ ) {
let cluster = [];
for ( let j = 0; j < n; j++ ) {
// Row-wise attractors and elements that they attract belong in same cluster
if ( Math.round( M[i * n + j] * 1000 ) / 1000 > 0 ) {
cluster.push( nodes[j] );
}
}
if ( cluster.length !== 0 ) {
clusters.push( cy.collection(cluster) );
}
}
return clusters;
};
let isDuplicate = function( c1, c2 ) {
for (let i = 0; i < c1.length; i++) {
if (!c2[i] || c1[i].id() !== c2[i].id()) {
return false;
}
}
return true;
};
let removeDuplicates = function( clusters ) {
for (let i = 0; i < clusters.length; i++) {
for (let j = 0; j < clusters.length; j++) {
if (i != j && isDuplicate(clusters[i], clusters[j])) {
clusters.splice(j, 1);
}
}
}
return clusters;
};
let markovClustering = function( options ) {
let nodes = this.nodes();
let edges = this.edges();
let cy = this.cy();
// Set parameters of algorithm:
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;
}
// Generate stochastic matrix M from input graph G (should be symmetric/undirected)
let n = nodes.length, n2 = n * n;
let M = new Array( n2 ), _M;
for ( let i = 0; i < n2; i++ ) {
M[i] = 0;
}
for ( let e = 0; e < edges.length; e++ ) {
let edge = edges[e];
let i = id2position[ edge.source().id() ];
let j = id2position[ edge.target().id() ];
let sim = getSimilarity( edge, opts.attributes );
M[i * n + j] += sim; // G should be symmetric and undirected
M[j * n + i] += sim;
}
// Begin Markov cluster algorithm
// Step 1: Add self loops to each node, ie. add multFactor to matrix diagonal
addLoops( M, n, opts.multFactor );
// Step 2: M = normalize( M );
normalize( M, n );
let isStillMoving = true;
let iterations = 0;
while ( isStillMoving && iterations < opts.maxIterations ) {
isStillMoving = false;
// Step 3:
_M = expand( M, n, opts.expandFactor );
// Step 4:
M = inflate( _M, n, opts.inflateFactor );
// Step 5: check to see if ~steady state has been reached
if ( ! hasConverged( M, _M, n2, 4 ) ) {
isStillMoving = true;
}
iterations++;
}
// Build clusters from matrix
let clusters = assign( M, n, nodes, cy );
// Remove duplicate clusters due to symmetry of graph and M matrix
clusters = removeDuplicates( clusters );
return clusters;
};
export default {
markovClustering,
mcl: markovClustering
};
+142
View File
@@ -0,0 +1,142 @@
import { defaults } from '../../util/index.mjs';
import { inPlaceSumNormalize } from '../../math.mjs';
const pageRankDefaults = defaults({
dampingFactor: 0.8,
precision: 0.000001,
iterations: 200,
weight: edge => 1
});
let elesfn = ({
pageRank: function( options ){
let { dampingFactor, precision, iterations, weight } = pageRankDefaults(options);
let cy = this._private.cy;
let { nodes, edges } = this.byGroup();
let numNodes = nodes.length;
let numNodesSqd = numNodes * numNodes;
let numEdges = edges.length;
// Construct transposed adjacency matrix
// First lets have a zeroed matrix of the right size
// We'll also keep track of the sum of each column
let matrix = new Array(numNodesSqd);
let columnSum = new Array(numNodes);
let additionalProb = (1 - dampingFactor) / numNodes;
// Create null matrix
for( let i = 0; i < numNodes; i++ ){
for( let j = 0; j < numNodes; j++ ){
let n = i * numNodes + j;
matrix[n] = 0;
}
columnSum[i] = 0;
}
// Now, process edges
for( let i = 0; i < numEdges; i++ ){
let edge = edges[ i ];
let srcId = edge.data('source');
let tgtId = edge.data('target');
// Don't include loops in the matrix
if( srcId === tgtId ){ continue; }
let s = nodes.indexOfId( srcId );
let t = nodes.indexOfId( tgtId );
let w = weight( edge );
let n = t * numNodes + s;
// Update matrix
matrix[n] += w;
// Update column sum
columnSum[s] += w;
}
// Add additional probability based on damping factor
// Also, take into account columns that have sum = 0
let p = 1.0 / numNodes + additionalProb; // Shorthand
// Traverse matrix, column by column
for( let j = 0; j < numNodes; j++ ){
if( columnSum[j] === 0 ){
// No 'links' out from node jth, assume equal probability for each possible node
for( let i = 0; i < numNodes; i++ ){
let n = i * numNodes + j;
matrix[n] = p;
}
} else {
// Node jth has outgoing link, compute normalized probabilities
for( let i = 0; i < numNodes; i++ ){
let n = i * numNodes + j;
matrix[n] = matrix[n] / columnSum[j] + additionalProb;
}
}
}
// Compute dominant eigenvector using power method
let eigenvector = new Array(numNodes);
let temp = new Array(numNodes);
let previous;
// Start with a vector of all 1's
// Also, initialize a null vector which will be used as shorthand
for( let i = 0; i < numNodes; i++ ){
eigenvector[i] = 1;
}
for( let iter = 0; iter < iterations; iter++ ){
// Temp array with all 0's
for( let i = 0; i < numNodes; i++ ){
temp[i] = 0;
}
// Multiply matrix with previous result
for( let i = 0; i < numNodes; i++ ){
for( let j = 0; j < numNodes; j++ ){
let n = i * numNodes + j;
temp[i] += matrix[n] * eigenvector[j];
}
}
inPlaceSumNormalize( temp );
previous = eigenvector;
eigenvector = temp;
temp = previous;
let diff = 0;
// Compute difference (squared module) of both vectors
for( let i = 0; i < numNodes; i++ ){
let delta = previous[i] - eigenvector[i];
diff += delta * delta;
}
// If difference is less than the desired threshold, stop iterating
if( diff < precision ){
break;
}
}
// Construct result
let res = {
rank: function( node ){
node = cy.collection(node)[0];
return eigenvector[ nodes.indexOf(node) ];
}
};
return res;
} // pageRank
}); // elesfn
export default elesfn;
@@ -0,0 +1,75 @@
let tarjanStronglyConnected = function() {
let eles = this;
let nodes = {};
let index = 0;
let components = [];
let stack = [];
let cut = eles.spawn(eles);
const stronglyConnectedSearch = sourceNodeId => {
stack.push(sourceNodeId);
nodes[sourceNodeId] = {
index : index,
low : index++,
explored : false
};
let connectedEdges = eles.getElementById(sourceNodeId)
.connectedEdges()
.intersection(eles);
connectedEdges.forEach(edge => {
let targetNodeId = edge.target().id();
if (targetNodeId !== sourceNodeId) {
if (!(targetNodeId in nodes)) {
stronglyConnectedSearch(targetNodeId);
}
if (!(nodes[targetNodeId].explored)) {
nodes[sourceNodeId].low = Math.min(nodes[sourceNodeId].low,
nodes[targetNodeId].low);
}
}
});
if (nodes[sourceNodeId].index === nodes[sourceNodeId].low) {
let componentNodes = eles.spawn();
for (;;) {
const nodeId = stack.pop();
componentNodes.merge(eles.getElementById(nodeId));
nodes[nodeId].low = nodes[sourceNodeId].index;
nodes[nodeId].explored = true;
if (nodeId === sourceNodeId) {
break;
}
}
let componentEdges = componentNodes.edgesWith(componentNodes);
let component = componentNodes.merge(componentEdges);
components.push(component);
cut = cut.difference(component);
}
};
eles.forEach(ele => {
if (ele.isNode()) {
let nodeId = ele.id();
if (!(nodeId in nodes)) {
stronglyConnectedSearch(nodeId);
}
}
});
return {
cut,
components
};
};
export default {
tarjanStronglyConnected,
tsc: tarjanStronglyConnected,
tscc: tarjanStronglyConnected,
tarjanStronglyConnectedComponents: tarjanStronglyConnected
};
+13
View File
@@ -0,0 +1,13 @@
import define from '../define/index.mjs';
let elesfn = ({
animate: define.animate(),
animation: define.animation(),
animated: define.animated(),
clearQueue: define.clearQueue(),
delay: define.delay(),
delayAnimation: define.delayAnimation(),
stop: define.stop()
});
export default elesfn;
+34
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;
+142
View File
@@ -0,0 +1,142 @@
import Set from '../set.mjs';
import * as is from '../is.mjs';
let elesfn = ({
classes: function( classes ){
let self = this;
if( classes === undefined ){
let ret = [];
self[0]._private.classes.forEach(cls => ret.push(cls));
return ret;
} else if( !is.array( classes ) ){
// extract classes from string
classes = ( classes || '' ).match( /\S+/g ) || [];
}
let changed = [];
let classesSet = new Set( classes );
// check and update each ele
for( let j = 0; j < self.length; j++ ){
let ele = self[ j ];
let _p = ele._private;
let eleClasses = _p.classes;
let changedEle = false;
// check if ele has all of the passed classes
for( let i = 0; i < classes.length; i++ ){
let cls = classes[i];
let eleHasClass = eleClasses.has(cls);
if( !eleHasClass ){
changedEle = true;
break;
}
}
// check if ele has classes outside of those passed
if( !changedEle ){
changedEle = eleClasses.size !== classes.length;
}
if( changedEle ){
_p.classes = classesSet;
changed.push( ele );
}
}
// trigger update style on those eles that had class changes
if( changed.length > 0 ){
this.spawn( changed )
.updateStyle()
.emit( 'class' )
;
}
return self;
},
addClass: function( classes ){
return this.toggleClass( classes, true );
},
hasClass: function( className ){
let ele = this[0];
return ( ele != null && ele._private.classes.has(className) );
},
toggleClass: function( classes, toggle ){
if( !is.array( classes ) ){
// extract classes from string
classes = classes.match( /\S+/g ) || [];
}
let self = this;
let toggleUndefd = toggle === undefined;
let changed = []; // eles who had classes changed
for( let i = 0, il = self.length; i < il; i++ ){
let ele = self[ i ];
let eleClasses = ele._private.classes;
let changedEle = false;
for( let j = 0; j < classes.length; j++ ){
let cls = classes[ j ];
let hasClass = eleClasses.has(cls);
let changedNow = false;
if( toggle || (toggleUndefd && !hasClass) ){
eleClasses.add(cls);
changedNow = true;
} else if( !toggle || (toggleUndefd && hasClass) ){
eleClasses.delete(cls);
changedNow = true;
}
if( !changedEle && changedNow ){
changed.push( ele );
changedEle = true;
}
} // for j classes
} // for i eles
// trigger update style on those eles that had class changes
if( changed.length > 0 ){
this.spawn( changed )
.updateStyle()
.emit( 'class' )
;
}
return self;
},
removeClass: function( classes ){
return this.toggleClass( classes, false );
},
flashClass: function( classes, duration ){
let self = this;
if( duration == null ){
duration = 250;
} else if( duration === 0 ){
return self; // nothing to do really
}
self.addClass( classes );
setTimeout( function(){
self.removeClass( classes );
}, duration );
return self;
}
});
elesfn.className = elesfn.classNames = elesfn.classes;
export default elesfn;
+97
View File
@@ -0,0 +1,97 @@
import Selector from '../selector/index.mjs';
let elesfn = ({
allAre: function( selector ){
let selObj = new Selector( selector );
return this.every(function( ele ){
return selObj.matches( ele );
});
},
is: function( selector ){
let selObj = new Selector( selector );
return this.some(function( ele ){
return selObj.matches( ele );
});
},
some: function( fn, thisArg ){
for( let i = 0; i < this.length; i++ ){
let ret = !thisArg ? fn( this[ i ], i, this ) : fn.apply( thisArg, [ this[ i ], i, this ] );
if( ret ){
return true;
}
}
return false;
},
every: function( fn, thisArg ){
for( let i = 0; i < this.length; i++ ){
let ret = !thisArg ? fn( this[ i ], i, this ) : fn.apply( thisArg, [ this[ i ], i, this ] );
if( !ret ){
return false;
}
}
return true;
},
same: function( collection ){
// cheap collection ref check
if( this === collection ){ return true; }
collection = this.cy().collection( collection );
let thisLength = this.length;
let collectionLength = collection.length;
// cheap length check
if( thisLength !== collectionLength ){ return false; }
// cheap element ref check
if( thisLength === 1 ){ return this[0] === collection[0]; }
return this.every(function( ele ){
return collection.hasElementWithId( ele.id() );
});
},
anySame: function( collection ){
collection = this.cy().collection( collection );
return this.some(function( ele ){
return collection.hasElementWithId( ele.id() );
});
},
allAreNeighbors: function( collection ){
collection = this.cy().collection( collection );
let nhood = this.neighborhood();
return collection.every(function( ele ){
return nhood.hasElementWithId( ele.id() );
});
},
contains: function( collection ){
collection = this.cy().collection( collection );
let self = this;
return collection.every(function( ele ){
return self.hasElementWithId( ele.id() );
});
}
});
elesfn.allAreNeighbours = elesfn.allAreNeighbors;
elesfn.has = elesfn.contains;
elesfn.equal = elesfn.equals = elesfn.same;
export default elesfn;
+219
View File
@@ -0,0 +1,219 @@
import Set from '../set.mjs';
import cache from './cache-traversal-call.mjs';
let elesfn = ({
parent: function( selector ){
let parents = [];
// optimisation for single ele call
if( this.length === 1 ){
let parent = this[0]._private.parent;
if( parent ){ return parent; }
}
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let parent = ele._private.parent;
if( parent ){
parents.push( parent );
}
}
return this.spawn( parents, true ).filter( selector );
},
parents: function( selector ){
let parents = [];
let eles = this.parent();
while( eles.nonempty() ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
parents.push( ele );
}
eles = eles.parent();
}
return this.spawn( parents, true ).filter( selector );
},
commonAncestors: function( selector ){
let ancestors;
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let parents = ele.parents();
ancestors = ancestors || parents;
ancestors = ancestors.intersect( parents ); // current list must be common with current ele parents set
}
return ancestors.filter( selector );
},
orphans: function( selector ){
return this.stdFilter( function( ele ){
return ele.isOrphan();
} ).filter( selector );
},
nonorphans: function( selector ){
return this.stdFilter( function( ele ){
return ele.isChild();
} ).filter( selector );
},
children: cache( function( selector ){
let children = [];
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let eleChildren = ele._private.children;
for( let j = 0; j < eleChildren.length; j++ ){
children.push( eleChildren[j] );
}
}
return this.spawn( children, true ).filter( selector );
}, 'children' ),
siblings: function( selector ){
return this.parent().children().not( this ).filter( selector );
},
isParent: function(){
let ele = this[0];
if( ele ){
return ele.isNode() && ele._private.children.length !== 0;
}
},
isChildless: function(){
let ele = this[0];
if( ele ){
return ele.isNode() && ele._private.children.length === 0;
}
},
isChild: function(){
let ele = this[0];
if( ele ){
return ele.isNode() && ele._private.parent != null;
}
},
isOrphan: function(){
let ele = this[0];
if( ele ){
return ele.isNode() && ele._private.parent == null;
}
},
descendants: function( selector ){
let elements = [];
function add( eles ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
elements.push( ele );
if( ele.children().nonempty() ){
add( ele.children() );
}
}
}
add( this.children() );
return this.spawn( elements, true ).filter( selector );
}
});
function forEachCompound( eles, fn, includeSelf, recursiveStep ){
let q = [];
let did = new Set();
let cy = eles.cy();
let hasCompounds = cy.hasCompoundNodes();
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
if( includeSelf ){
q.push( ele );
} else if( hasCompounds ){
recursiveStep( q, did, ele );
}
}
while( q.length > 0 ){
let ele = q.shift();
fn( ele );
did.add( ele.id() );
if( hasCompounds ){
recursiveStep( q, did, ele );
}
}
return eles;
}
function addChildren( q, did, ele ){
if( ele.isParent() ){
let children = ele._private.children;
for( let i = 0; i < children.length; i++ ){
let child = children[i];
if( !did.has( child.id() ) ){
q.push( child );
}
}
}
}
// very efficient version of eles.add( eles.descendants() ).forEach()
// for internal use
elesfn.forEachDown = function( fn, includeSelf = true ){
return forEachCompound( this, fn, includeSelf, addChildren );
};
function addParent( q, did, ele ){
if( ele.isChild() ){
let parent = ele._private.parent;
if( !did.has( parent.id() ) ){
q.push( parent );
}
}
}
elesfn.forEachUp = function( fn, includeSelf = true ){
return forEachCompound( this, fn, includeSelf, addParent );
};
function addParentAndChildren( q, did, ele ){
addParent( q, did, ele );
addChildren( q, did, ele );
}
elesfn.forEachUpAndDown = function( fn, includeSelf = true ){
return forEachCompound( this, fn, includeSelf, addParentAndChildren );
};
// aliases
elesfn.ancestors = elesfn.parents;
export default elesfn;
+86
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;
+119
View File
@@ -0,0 +1,119 @@
import * as util from '../util/index.mjs';
let elesfn = {};
function defineDegreeFunction( callback ){
return function( includeLoops ){
let self = this;
if( includeLoops === undefined ){
includeLoops = true;
}
if( self.length === 0 ){ return; }
if( self.isNode() && !self.removed() ){
let degree = 0;
let node = self[0];
let connectedEdges = node._private.edges;
for( let i = 0; i < connectedEdges.length; i++ ){
let edge = connectedEdges[ i ];
if( !includeLoops && edge.isLoop() ){
continue;
}
degree += callback( node, edge );
}
return degree;
} else {
return;
}
};
}
util.extend( elesfn, {
degree: defineDegreeFunction( function( node, edge ){
if( edge.source().same( edge.target() ) ){
return 2;
} else {
return 1;
}
} ),
indegree: defineDegreeFunction( function( node, edge ){
if( edge.target().same( node ) ){
return 1;
} else {
return 0;
}
} ),
outdegree: defineDegreeFunction( function( node, edge ){
if( edge.source().same( node ) ){
return 1;
} else {
return 0;
}
} )
} );
function defineDegreeBoundsFunction( degreeFn, callback ){
return function( includeLoops ){
let ret;
let nodes = this.nodes();
for( let i = 0; i < nodes.length; i++ ){
let ele = nodes[ i ];
let degree = ele[ degreeFn ]( includeLoops );
if( degree !== undefined && (ret === undefined || callback( degree, ret )) ){
ret = degree;
}
}
return ret;
};
}
util.extend( elesfn, {
minDegree: defineDegreeBoundsFunction( 'degree', function( degree, min ){
return degree < min;
} ),
maxDegree: defineDegreeBoundsFunction( 'degree', function( degree, max ){
return degree > max;
} ),
minIndegree: defineDegreeBoundsFunction( 'indegree', function( degree, min ){
return degree < min;
} ),
maxIndegree: defineDegreeBoundsFunction( 'indegree', function( degree, max ){
return degree > max;
} ),
minOutdegree: defineDegreeBoundsFunction( 'outdegree', function( degree, min ){
return degree < min;
} ),
maxOutdegree: defineDegreeBoundsFunction( 'outdegree', function( degree, max ){
return degree > max;
} )
} );
util.extend( elesfn, {
totalDegree: function( includeLoops ){
let total = 0;
let nodes = this.nodes();
for( let i = 0; i < nodes.length; i++ ){
total += nodes[ i ].degree( includeLoops );
}
return total;
}
} );
export default elesfn;
File diff suppressed because it is too large Load Diff
@@ -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;
}, {} );
+7
View File
@@ -0,0 +1,7 @@
import * as util from '../../util/index.mjs';
import position from './position.mjs';
import bounds from './bounds.mjs';
import widthHeight from './width-height.mjs';
import edgePoints from './edge-points.mjs';
export default util.assign( {}, position, bounds, widthHeight, edgePoints );
+272
View File
@@ -0,0 +1,272 @@
import define from '../../define/index.mjs';
import * as is from '../../is.mjs';
import * as math from '../../math.mjs';
import * as util from '../../util/index.mjs';
let fn, elesfn;
let beforePositionSet = function( eles, newPos, silent ){
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
if( !ele.locked() ){
let oldPos = ele._private.position;
let delta = {
x: newPos.x != null ? newPos.x - oldPos.x : 0,
y: newPos.y != null ? newPos.y - oldPos.y : 0
};
if( ele.isParent() && !(delta.x === 0 && delta.y === 0) ){
ele.children().shift( delta, silent );
}
ele.dirtyBoundingBoxCache();
}
}
};
let positionDef = {
field: 'position',
bindingEvent: 'position',
allowBinding: true,
allowSetting: true,
settingEvent: 'position',
settingTriggersEvent: true,
triggerFnName: 'emitAndNotify',
allowGetting: true,
validKeys: [ 'x', 'y' ],
beforeGet: function( ele ){
ele.updateCompoundBounds();
},
beforeSet: function( eles, newPos ){
beforePositionSet( eles, newPos, false );
},
onSet: function( eles ){
eles.dirtyCompoundBoundsCache();
},
canSet: function( ele ){
return !ele.locked();
}
};
fn = elesfn = ({
position: define.data( positionDef ),
// position but no notification to renderer
silentPosition: define.data( util.assign( {}, positionDef, {
allowBinding: false,
allowSetting: true,
settingTriggersEvent: false,
allowGetting: false,
beforeSet: function( eles, newPos ){
beforePositionSet( eles, newPos, true );
},
onSet: function( eles ){
eles.dirtyCompoundBoundsCache();
}
} ) ),
positions: function( pos, silent ){
if( is.plainObject( pos ) ){
if( silent ){
this.silentPosition( pos );
} else {
this.position( pos );
}
} else if( is.fn( pos ) ){
let fn = pos;
let cy = this.cy();
cy.startBatch();
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let pos;
if( ( pos = fn(ele, i) ) ){
if( silent ){
ele.silentPosition( pos );
} else {
ele.position( pos );
}
}
}
cy.endBatch();
}
return this; // chaining
},
silentPositions: function( pos ){
return this.positions( pos, true );
},
shift: function( dim, val, silent ){
let delta;
if( is.plainObject( dim ) ){
delta = {
x: is.number(dim.x) ? dim.x : 0,
y: is.number(dim.y) ? dim.y : 0
};
silent = val;
} else if( is.string( dim ) && is.number( val ) ){
delta = { x: 0, y: 0 };
delta[ dim ] = val;
}
if( delta != null ){
let cy = this.cy();
cy.startBatch();
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
// exclude any node that is a descendant of the calling collection
if (cy.hasCompoundNodes() && ele.isChild() && ele.ancestors().anySame(this)) {
continue;
}
let pos = ele.position();
let newPos = {
x: pos.x + delta.x,
y: pos.y + delta.y
};
if( silent ){
ele.silentPosition( newPos );
} else {
ele.position( newPos );
}
}
cy.endBatch();
}
return this;
},
silentShift: function( dim, val ){
if( is.plainObject( dim ) ){
this.shift( dim, true );
} else if( is.string( dim ) && is.number( val ) ){
this.shift( dim, val, true );
}
return this;
},
// get/set the rendered (i.e. on screen) positon of the element
renderedPosition: function( dim, val ){
let ele = this[0];
let cy = this.cy();
let zoom = cy.zoom();
let pan = cy.pan();
let rpos = is.plainObject( dim ) ? dim : undefined;
let setting = rpos !== undefined || ( val !== undefined && is.string( dim ) );
if( ele && ele.isNode() ){ // must have an element and must be a node to return position
if( setting ){
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
if( val !== undefined ){ // set one dimension
ele.position( dim, ( val - pan[ dim ] ) / zoom );
} else if( rpos !== undefined ){ // set whole position
ele.position( math.renderedToModelPosition( rpos, zoom, pan ) );
}
}
} else { // getting
let pos = ele.position();
rpos = math.modelToRenderedPosition( pos, zoom, pan );
if( dim === undefined ){ // then return the whole rendered position
return rpos;
} else { // then return the specified dimension
return rpos[ dim ];
}
}
} else if( !setting ){
return undefined; // for empty collection case
}
return this; // chaining
},
// get/set the position relative to the parent
relativePosition: function( dim, val ){
let ele = this[0];
let cy = this.cy();
let ppos = is.plainObject( dim ) ? dim : undefined;
let setting = ppos !== undefined || ( val !== undefined && is.string( dim ) );
let hasCompoundNodes = cy.hasCompoundNodes();
if( ele && ele.isNode() ){ // must have an element and must be a node to return position
if( setting ){
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let parent = hasCompoundNodes ? ele.parent() : null;
let hasParent = parent && parent.length > 0;
let relativeToParent = hasParent;
if( hasParent ){
parent = parent[0];
}
let origin = relativeToParent ? parent.position() : { x: 0, y: 0 };
if( val !== undefined ){ // set one dimension
ele.position( dim, val + origin[ dim ] );
} else if( ppos !== undefined ){ // set whole position
ele.position({
x: ppos.x + origin.x,
y: ppos.y + origin.y
});
}
}
} else { // getting
let pos = ele.position();
let parent = hasCompoundNodes ? ele.parent() : null;
let hasParent = parent && parent.length > 0;
let relativeToParent = hasParent;
if( hasParent ){
parent = parent[0];
}
let origin = relativeToParent ? parent.position() : { x: 0, y: 0 };
ppos = {
x: pos.x - origin.x,
y: pos.y - origin.y
};
if( dim === undefined ){ // then return the whole rendered position
return ppos;
} else { // then return the specified dimension
return ppos[ dim ];
}
}
} else if( !setting ){
return undefined; // for empty collection case
}
return this; // chaining
}
});
// aliases
fn.modelPosition = fn.point = fn.position;
fn.modelPositions = fn.points = fn.positions;
fn.renderedPoint = fn.renderedPosition;
fn.relativePoint = fn.relativePosition;
export default elesfn;
@@ -0,0 +1,130 @@
import * as util from '../../util/index.mjs';
let fn, elesfn;
fn = elesfn = {};
let defineDimFns = function( opts ){
opts.uppercaseName = util.capitalize( opts.name );
opts.autoName = 'auto' + opts.uppercaseName;
opts.labelName = 'label' + opts.uppercaseName;
opts.outerName = 'outer' + opts.uppercaseName;
opts.uppercaseOuterName = util.capitalize( opts.outerName );
fn[ opts.name ] = function dimImpl(){
let ele = this[0];
let _p = ele._private;
let cy = _p.cy;
let styleEnabled = cy._private.styleEnabled;
if( ele ){
if( styleEnabled ){
if( ele.isParent() ){
ele.updateCompoundBounds();
return _p[ opts.autoName ] || 0;
}
let d = ele.pstyle( opts.name );
switch( d.strValue ){
case 'label':
ele.recalculateRenderedStyle();
return _p.rstyle[ opts.labelName ] || 0;
default:
return d.pfValue;
}
} else {
return 1;
}
}
};
fn[ 'outer' + opts.uppercaseName ] = function outerDimImpl(){
let ele = this[0];
let _p = ele._private;
let cy = _p.cy;
let styleEnabled = cy._private.styleEnabled;
if( ele ){
if( styleEnabled ){
let dim = ele[ opts.name ]();
let borderPos = ele.pstyle( 'border-position' ).value;
let border;
if(borderPos === 'center') {
border = ele.pstyle( 'border-width' ).pfValue; // n.b. 1/2 each side
} else if(borderPos === 'outside') {
border = 2 * ele.pstyle( 'border-width' ).pfValue;
} else { // 'inside'
border = 0;
}
let padding = 2 * ele.padding();
return dim + border + padding;
} else {
return 1;
}
}
};
fn[ 'rendered' + opts.uppercaseName ] = function renderedDimImpl(){
let ele = this[0];
if( ele ){
let d = ele[ opts.name ]();
return d * this.cy().zoom();
}
};
fn[ 'rendered' + opts.uppercaseOuterName ] = function renderedOuterDimImpl(){
let ele = this[0];
if( ele ){
let od = ele[ opts.outerName ]();
return od * this.cy().zoom();
}
};
};
defineDimFns( {
name: 'width'
} );
defineDimFns( {
name: 'height'
} );
elesfn.padding = function(){
let ele = this[0];
let _p = ele._private;
if( ele.isParent() ){
ele.updateCompoundBounds();
if( _p.autoPadding !== undefined ){
return _p.autoPadding;
} else {
return ele.pstyle('padding').pfValue;
}
} else {
return ele.pstyle('padding').pfValue;
}
};
elesfn.paddedHeight = function(){
let ele = this[0];
return ele.height() + (2 * ele.padding());
};
elesfn.paddedWidth = function(){
let ele = this[0];
return ele.width() + (2 * ele.padding());
};
export default elesfn;
+130
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;
+152
View File
@@ -0,0 +1,152 @@
import Emitter from '../emitter.mjs';
import define from '../define/index.mjs';
import * as is from '../is.mjs';
import Selector from '../selector/index.mjs';
let emitterOptions = {
qualifierCompare: function( selector1, selector2 ){
if( selector1 == null || selector2 == null ){
return selector1 == null && selector2 == null;
} else {
return selector1.sameText( selector2 );
}
},
eventMatches: function( ele, listener, eventObj ){
let selector = listener.qualifier;
if( selector != null ){
return ele !== eventObj.target && is.element( eventObj.target ) && selector.matches( eventObj.target );
}
return true;
},
addEventFields: function( ele, evt ){
evt.cy = ele.cy();
evt.target = ele;
},
callbackContext: function( ele, listener, eventObj ){
return listener.qualifier != null ? eventObj.target : ele;
},
beforeEmit: function( context, listener/*, eventObj*/ ){
if( listener.conf && listener.conf.once ){
listener.conf.onceCollection.removeListener( listener.event, listener.qualifier, listener.callback );
}
},
bubble: function(){
return true;
},
parent: function( ele ){
return ele.isChild() ? ele.parent() : ele.cy();
}
};
let argSelector = function( arg ){
if( is.string(arg) ){
return new Selector( arg );
} else {
return arg;
}
};
let elesfn = ({
createEmitter: function(){
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
let _p = ele._private;
if( !_p.emitter ){
_p.emitter = new Emitter( emitterOptions, ele );
}
}
return this;
},
emitter: function(){
return this._private.emitter;
},
on: function( events, selector, callback ){
let argSel = argSelector(selector);
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
ele.emitter().on( events, argSel, callback );
}
return this;
},
removeListener: function( events, selector, callback ){
let argSel = argSelector(selector);
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
ele.emitter().removeListener( events, argSel, callback );
}
return this;
},
removeAllListeners: function(){
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
ele.emitter().removeAllListeners();
}
return this;
},
one: function( events, selector, callback ){
let argSel = argSelector(selector);
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
ele.emitter().one( events, argSel, callback );
}
return this;
},
once: function( events, selector, callback ){
let argSel = argSelector(selector);
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
ele.emitter().on( events, argSel, callback, {
once: true,
onceCollection: this
} );
}
},
emit: function( events, extraParams ){
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
ele.emitter().emit( events, extraParams );
}
return this;
},
emitAndNotify: function( event, extraParams ){ // for internal use only
if( this.length === 0 ){ return; } // empty collections don't need to notify anything
// notify renderer
this.cy().notify( event, this );
this.emit( event, extraParams );
return this;
}
});
define.eventAliasesOn( elesfn );
export default elesfn;
+393
View File
@@ -0,0 +1,393 @@
import * as is from '../is.mjs';
import Selector from '../selector/index.mjs';
let elesfn = ({
nodes: function( selector ){
return this.filter( ele => ele.isNode() ).filter( selector );
},
edges: function( selector ){
return this.filter( ele => ele.isEdge() ).filter( selector );
},
// internal helper to get nodes and edges as separate collections with single iteration over elements
byGroup: function(){
let nodes = this.spawn();
let edges = this.spawn();
for( let i = 0; i < this.length; i++ ){
let ele = this[i];
if( ele.isNode() ){
nodes.push(ele);
} else {
edges.push(ele);
}
}
return { nodes, edges };
},
filter: function( filter, thisArg ){
if( filter === undefined ){ // check this first b/c it's the most common/performant case
return this;
} else if( is.string( filter ) || is.elementOrCollection( filter ) ){
return new Selector( filter ).filter( this );
} else if( is.fn( filter ) ){
let filterEles = this.spawn();
let eles = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
let include = thisArg ? filter.apply( thisArg, [ ele, i, eles ] ) : filter( ele, i, eles );
if( include ){
filterEles.push( ele );
}
}
return filterEles;
}
return this.spawn(); // if not handled by above, give 'em an empty collection
},
not: function( toRemove ){
if( !toRemove ){
return this;
} else {
if( is.string( toRemove ) ){
toRemove = this.filter( toRemove );
}
let elements = this.spawn();
for( let i = 0; i < this.length; i++ ){
let element = this[ i ];
let remove = toRemove.has(element);
if( !remove ){
elements.push( element );
}
}
return elements;
}
},
absoluteComplement: function(){
let cy = this.cy();
return cy.mutableElements().not( this );
},
intersect: function( other ){
// if a selector is specified, then filter by it instead
if( is.string( other ) ){
let selector = other;
return this.filter( selector );
}
let elements = this.spawn();
let col1 = this;
let col2 = other;
let col1Smaller = this.length < other.length;
let colS = col1Smaller ? col1 : col2;
let colL = col1Smaller ? col2 : col1;
for( let i = 0; i < colS.length; i++ ){
let ele = colS[i];
if( colL.has(ele) ){
elements.push(ele);
}
}
return elements;
},
xor: function( other ){
let cy = this._private.cy;
if( is.string( other ) ){
other = cy.$( other );
}
let elements = this.spawn();
let col1 = this;
let col2 = other;
let add = function( col, other ){
for( let i = 0; i < col.length; i++ ){
let ele = col[ i ];
let id = ele._private.data.id;
let inOther = other.hasElementWithId( id );
if( !inOther ){
elements.push( ele );
}
}
};
add( col1, col2 );
add( col2, col1 );
return elements;
},
diff: function( other ){
let cy = this._private.cy;
if( is.string( other ) ){
other = cy.$( other );
}
let left = this.spawn();
let right = this.spawn();
let both = this.spawn();
let col1 = this;
let col2 = other;
let add = function( col, other, retEles ){
for( let i = 0; i < col.length; i++ ){
let ele = col[ i ];
let id = ele._private.data.id;
let inOther = other.hasElementWithId( id );
if( inOther ){
both.merge( ele );
} else {
retEles.push( ele );
}
}
};
add( col1, col2, left );
add( col2, col1, right );
return { left, right, both };
},
add: function( toAdd ){
let cy = this._private.cy;
if( !toAdd ){
return this;
}
if( is.string( toAdd ) ){
let selector = toAdd;
toAdd = cy.mutableElements().filter( selector );
}
let elements = this.spawnSelf();
for( let i = 0; i < toAdd.length; i++ ){
let ele = toAdd[i];
let add = !this.has(ele);
if( add ){
elements.push(ele);
}
}
return elements;
},
// in place merge on calling collection
merge: function( toAdd ){
let _p = this._private;
let cy = _p.cy;
if( !toAdd ){
return this;
}
if( toAdd && is.string( toAdd ) ){
let selector = toAdd;
toAdd = cy.mutableElements().filter( selector );
}
let map = _p.map;
for( let i = 0; i < toAdd.length; i++ ){
let toAddEle = toAdd[ i ];
let id = toAddEle._private.data.id;
let add = !map.has( id );
if( add ){
let index = this.length++;
this[ index ] = toAddEle;
map.set( id, { ele: toAddEle, index: index } );
}
}
return this; // chaining
},
unmergeAt: function( i ){
let ele = this[i];
let id = ele.id();
let _p = this._private;
let map = _p.map;
// remove ele
this[ i ] = undefined;
map.delete( id );
let unmergedLastEle = i === this.length - 1;
// replace empty spot with last ele in collection
if( this.length > 1 && !unmergedLastEle ){
let lastEleI = this.length - 1;
let lastEle = this[ lastEleI ];
let lastEleId = lastEle._private.data.id;
this[ lastEleI ] = undefined;
this[ i ] = lastEle;
map.set( lastEleId, { ele: lastEle, index: i } );
}
// the collection is now 1 ele smaller
this.length--;
return this;
},
// remove single ele in place in calling collection
unmergeOne: function( ele ){
ele = ele[0];
let _p = this._private;
let id = ele._private.data.id;
let map = _p.map;
let entry = map.get( id );
if( !entry ){
return this; // no need to remove
}
let i = entry.index;
this.unmergeAt(i);
return this;
},
// remove eles in place on calling collection
unmerge: function( toRemove ){
let cy = this._private.cy;
if( !toRemove ){
return this;
}
if( toRemove && is.string( toRemove ) ){
let selector = toRemove;
toRemove = cy.mutableElements().filter( selector );
}
for( let i = 0; i < toRemove.length; i++ ){
this.unmergeOne( toRemove[ i ] );
}
return this; // chaining
},
unmergeBy: function( toRmFn ){
for( let i = this.length - 1; i >= 0; i-- ){
let ele = this[i];
if( toRmFn(ele) ){
this.unmergeAt(i);
}
}
return this;
},
map: function( mapFn, thisArg ){
let arr = [];
let eles = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
let ret = thisArg ? mapFn.apply( thisArg, [ ele, i, eles ] ) : mapFn( ele, i, eles );
arr.push( ret );
}
return arr;
},
reduce: function( fn, initialValue ){
let val = initialValue;
let eles = this;
for( let i = 0; i < eles.length; i++ ){
val = fn( val, eles[i], i, eles );
}
return val;
},
max: function( valFn, thisArg ){
let max = -Infinity;
let maxEle;
let eles = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
let val = thisArg ? valFn.apply( thisArg, [ ele, i, eles ] ) : valFn( ele, i, eles );
if( val > max ){
max = val;
maxEle = ele;
}
}
return {
value: max,
ele: maxEle
};
},
min: function( valFn, thisArg ){
let min = Infinity;
let minEle;
let eles = this;
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
let val = thisArg ? valFn.apply( thisArg, [ ele, i, eles ] ) : valFn( ele, i, eles );
if( val < min ){
min = val;
minEle = ele;
}
}
return {
value: min,
ele: minEle
};
}
});
// aliases
let fn = elesfn;
fn[ 'u' ] = fn[ '|' ] = fn[ '+' ] = fn.union = fn.or = fn.add;
fn[ '\\' ] = fn[ '!' ] = fn[ '-' ] = fn.difference = fn.relativeComplement = fn.subtract = fn.not;
fn[ 'n' ] = fn[ '&' ] = fn[ '.' ] = fn.and = fn.intersection = fn.intersect;
fn[ '^' ] = fn[ '(+)' ] = fn[ '(-)' ] = fn.symmetricDifference = fn.symdiff = fn.xor;
fn.fnFilter = fn.filterFn = fn.stdFilter = fn.filter;
fn.complement = fn.abscomp = fn.absoluteComplement;
export default elesfn;
+28
View File
@@ -0,0 +1,28 @@
let elesfn = ({
isNode: function(){
return this.group() === 'nodes';
},
isEdge: function(){
return this.group() === 'edges';
},
isLoop: function(){
return this.isEdge() && this.source()[0] === this.target()[0];
},
isSimple: function(){
return this.isEdge() && this.source()[0] !== this.target()[0];
},
group: function(){
let ele = this[0];
if( ele ){
return ele._private.group;
}
}
});
export default elesfn;
+838
View File
@@ -0,0 +1,838 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import Map from '../map.mjs';
import Set from '../set.mjs';
import Element from './element.mjs';
import algorithms from './algorithms/index.mjs';
import animation from './animation.mjs';
import classNames from './class.mjs';
import comparators from './comparators.mjs';
import compounds from './compounds.mjs';
import data from './data.mjs';
import degree from './degree.mjs';
import dimensions from './dimensions/index.mjs';
import events from './events.mjs';
import filter from './filter.mjs';
import group from './group.mjs';
import iteration from './iteration.mjs';
import layout from './layout.mjs';
import style from './style.mjs';
import switchFunctions from './switch-functions.mjs';
import traversing from './traversing.mjs';
// represents a set of nodes, edges, or both together
let Collection = function( cy, elements, unique = false, removed = false ){
if( cy === undefined ){
util.error( 'A collection must have a reference to the core' );
return;
}
let map = new Map();
let createdElements = false;
if( !elements ){
elements = [];
} else if( elements.length > 0 && is.plainObject( elements[0] ) && !is.element( elements[0] ) ){
createdElements = true;
// make elements from json and restore all at once later
let eles = [];
let elesIds = new Set();
for( let i = 0, l = elements.length; i < l; i++ ){
let json = elements[ i ];
if( json.data == null ){
json.data = {};
}
let data = json.data;
// make sure newly created elements have valid ids
if( data.id == null ){
data.id = util.uuid();
} else if( cy.hasElementWithId( data.id ) || elesIds.has( data.id ) ){
continue; // can't create element if prior id already exists
}
let ele = new Element( cy, json, false );
eles.push( ele );
elesIds.add( data.id );
}
elements = eles;
}
this.length = 0;
for( let i = 0, l = elements.length; i < l; i++ ){
let element = elements[i][0]; // [0] in case elements is an array of collections, rather than array of elements
if( element == null ){ continue; }
let id = element._private.data.id;
if( !unique || !map.has(id) ){
if( unique ){
map.set( id, {
index: this.length,
ele: element
} );
}
this[ this.length ] = element;
this.length++;
}
}
this._private = {
eles: this,
cy: cy,
get map(){
if( this.lazyMap == null ){
this.rebuildMap();
}
return this.lazyMap;
},
set map(m){
this.lazyMap = m;
},
rebuildMap(){
const m = this.lazyMap = new Map();
const eles = this.eles;
for( let i = 0; i < eles.length; i++ ){
const ele = eles[i];
m.set(ele.id(), { index: i, ele });
}
}
};
if( unique ){
this._private.map = map;
}
// restore the elements if we created them from json
if( createdElements && !removed ){
this.restore();
}
};
// Functions
////////////////////////////////////////////////////////////////////////////////////////////////////
// keep the prototypes in sync (an element has the same functions as a collection)
// and use elefn and elesfn as shorthands to the prototypes
let elesfn = Element.prototype = Collection.prototype = Object.create(Array.prototype);
elesfn.instanceString = function(){
return 'collection';
};
elesfn.spawn = function( eles, unique ){
return new Collection( this.cy(), eles, unique );
};
elesfn.spawnSelf = function(){
return this.spawn( this );
};
elesfn.cy = function(){
return this._private.cy;
};
elesfn.renderer = function(){
return this._private.cy.renderer();
};
elesfn.element = function(){
return this[0];
};
elesfn.collection = function(){
if( is.collection( this ) ){
return this;
} else { // an element
return new Collection( this._private.cy, [ this ] );
}
};
elesfn.unique = function(){
return new Collection( this._private.cy, this, true );
};
elesfn.hasElementWithId = function( id ){
id = '' + id; // id must be string
return this._private.map.has( id );
};
elesfn.getElementById = function( id ){
id = '' + id; // id must be string
let cy = this._private.cy;
let entry = this._private.map.get( id );
return entry ? entry.ele : new Collection( cy ); // get ele or empty collection
};
elesfn.$id = elesfn.getElementById;
elesfn.poolIndex = function(){
let cy = this._private.cy;
let eles = cy._private.elements;
let id = this[0]._private.data.id;
return eles._private.map.get( id ).index;
};
elesfn.indexOf = function( ele ){
let id = ele[0]._private.data.id;
return this._private.map.get( id ).index;
};
elesfn.indexOfId = function( id ){
id = '' + id; // id must be string
return this._private.map.get( id ).index;
};
elesfn.json = function( obj ){
let ele = this.element();
let cy = this.cy();
if( ele == null && obj ){ return this; } // can't set to no eles
if( ele == null ){ return undefined; } // can't get from no eles
let p = ele._private;
if( is.plainObject( obj ) ){ // set
cy.startBatch();
if( obj.data ){
ele.data( obj.data );
let data = p.data;
if( ele.isEdge() ){ // source and target are immutable via data()
let move = false;
let spec = {};
let src = obj.data.source;
let tgt = obj.data.target;
if( src != null && src != data.source ){
spec.source = '' + src; // id must be string
move = true;
}
if( tgt != null && tgt != data.target ){
spec.target = '' + tgt; // id must be string
move = true;
}
if( move ){
ele = ele.move(spec);
}
} else { // parent is immutable via data()
let newParentValSpecd = 'parent' in obj.data;
let parent = obj.data.parent;
if( newParentValSpecd && (parent != null || data.parent != null) && parent != data.parent ){
if( parent === undefined ){ // can't set undefined imperatively, so use null
parent = null;
}
if( parent != null ){
parent = '' + parent; // id must be string
}
ele = ele.move({ parent });
}
}
}
if( obj.position ){
ele.position( obj.position );
}
// ignore group -- immutable
let checkSwitch = function( k, trueFnName, falseFnName ){
let obj_k = obj[ k ];
if( obj_k != null && obj_k !== p[ k ] ){
if( obj_k ){
ele[ trueFnName ]();
} else {
ele[ falseFnName ]();
}
}
};
checkSwitch( 'removed', 'remove', 'restore' );
checkSwitch( 'selected', 'select', 'unselect' );
checkSwitch( 'selectable', 'selectify', 'unselectify' );
checkSwitch( 'locked', 'lock', 'unlock' );
checkSwitch( 'grabbable', 'grabify', 'ungrabify' );
checkSwitch( 'pannable', 'panify', 'unpanify' );
if( obj.classes != null ){
ele.classes( obj.classes );
}
cy.endBatch();
return this;
} else if( obj === undefined ){ // get
let json = {
data: util.copy( p.data ),
position: util.copy( p.position ),
group: p.group,
removed: p.removed,
selected: p.selected,
selectable: p.selectable,
locked: p.locked,
grabbable: p.grabbable,
pannable: p.pannable,
classes: null
};
json.classes = '';
let i = 0;
p.classes.forEach( cls => json.classes += ( i++ === 0 ? cls : ' ' + cls ) );
return json;
}
};
elesfn.jsons = function(){
let jsons = [];
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let json = ele.json();
jsons.push( json );
}
return jsons;
};
elesfn.clone = function(){
let cy = this.cy();
let elesArr = [];
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let json = ele.json();
let clone = new Element( cy, json, false ); // NB no restore
elesArr.push( clone );
}
return new Collection( cy, elesArr );
};
elesfn.copy = elesfn.clone;
elesfn.restore = function( notifyRenderer = true, addToPool = true ){
let self = this;
let cy = self.cy();
let cy_p = cy._private;
// create arrays of nodes and edges, since we need to
// restore the nodes first
let nodes = [];
let edges = [];
let elements;
for( let i = 0, l = self.length; i < l; i++ ){
let ele = self[ i ];
if( addToPool && !ele.removed() ){
// don't need to handle this ele
continue;
}
// keep nodes first in the array and edges after
if( ele.isNode() ){ // put to front of array if node
nodes.push( ele );
} else { // put to end of array if edge
edges.push( ele );
}
}
elements = nodes.concat( edges );
let i;
let removeFromElements = function(){
elements.splice( i, 1 );
i--;
};
// now, restore each element
for( i = 0; i < elements.length; i++ ){
let ele = elements[ i ];
let _private = ele._private;
let data = _private.data;
// the traversal cache should start fresh when ele is added
ele.clearTraversalCache();
// set id and validate
if( !addToPool && !_private.removed ){
// already in graph, so nothing required
} else if( data.id === undefined ){
data.id = util.uuid();
} else if( is.number( data.id ) ){
data.id = '' + data.id; // now it's a string
} else if( is.emptyString( data.id ) || !is.string( data.id ) ){
util.error( 'Can not create element with invalid string ID `' + data.id + '`' );
// can't create element if it has empty string as id or non-string id
removeFromElements();
continue;
} else if( cy.hasElementWithId( data.id ) ){
util.error( 'Can not create second element with ID `' + data.id + '`' );
// can't create element if one already has that id
removeFromElements();
continue;
}
let id = data.id; // id is finalised, now let's keep a ref
if( ele.isNode() ){ // extra checks for nodes
let pos = _private.position;
// make sure the nodes have a defined position
if( pos.x == null ){
pos.x = 0;
}
if( pos.y == null ){
pos.y = 0;
}
}
if( ele.isEdge() ){ // extra checks for edges
let edge = ele;
let fields = [ 'source', 'target' ];
let fieldsLength = fields.length;
let badSourceOrTarget = false;
for( let j = 0; j < fieldsLength; j++ ){
let field = fields[ j ];
let val = data[ field ];
if( is.number( val ) ){
val = data[ field ] = '' + data[ field ]; // now string
}
if( val == null || val === '' ){
// can't create if source or target is not defined properly
util.error( 'Can not create edge `' + id + '` with unspecified ' + field );
badSourceOrTarget = true;
} else if( !cy.hasElementWithId( val ) ){
// can't create edge if one of its nodes doesn't exist
util.error( 'Can not create edge `' + id + '` with nonexistent ' + field + ' `' + val + '`' );
badSourceOrTarget = true;
}
}
if( badSourceOrTarget ){ removeFromElements(); continue; } // can't create this
let src = cy.getElementById( data.source );
let tgt = cy.getElementById( data.target );
// only one edge in node if loop
if (src.same(tgt)) {
src._private.edges.push( edge );
} else {
src._private.edges.push( edge );
tgt._private.edges.push( edge );
}
edge._private.source = src;
edge._private.target = tgt;
} // if is edge
// create mock ids / indexes maps for element so it can be used like collections
_private.map = new Map();
_private.map.set( id, { ele: ele, index: 0 } );
_private.removed = false;
if( addToPool ){
cy.addToPool( ele );
}
} // for each element
// do compound node sanity checks
for( let i = 0; i < nodes.length; i++ ){ // each node
let node = nodes[ i ];
let data = node._private.data;
if( is.number( data.parent ) ){ // then automake string
data.parent = '' + data.parent;
}
let parentId = data.parent;
let specifiedParent = parentId != null;
if( specifiedParent || node._private.parent ){
let parent = node._private.parent ? cy.collection().merge(node._private.parent) : cy.getElementById( parentId );
if( parent.empty() ){
// non-existant parent; just remove it
data.parent = undefined;
} else if( parent[0].removed() ) {
util.warn('Node added with missing parent, reference to parent removed');
data.parent = undefined;
node._private.parent = null;
} else {
let selfAsParent = false;
let ancestor = parent;
while( !ancestor.empty() ){
if( node.same( ancestor ) ){
// mark self as parent and remove from data
selfAsParent = true;
data.parent = undefined; // remove parent reference
// exit or we loop forever
break;
}
ancestor = ancestor.parent();
}
if( !selfAsParent ){
// connect with children
parent[0]._private.children.push( node );
node._private.parent = parent[0];
// let the core know we have a compound graph
cy_p.hasCompoundNodes = true;
}
} // else
} // if specified parent
} // for each node
if( elements.length > 0 ){
let restored = elements.length === self.length ? self : new Collection( cy, elements );
for( let i = 0; i < restored.length; i++ ){
let ele = restored[i];
if( ele.isNode() ){ continue; }
// adding an edge invalidates the traversal caches for the parallel edges
ele.parallelEdges().clearTraversalCache();
// adding an edge invalidates the traversal cache for the connected nodes
ele.source().clearTraversalCache();
ele.target().clearTraversalCache();
}
let toUpdateStyle;
if( cy_p.hasCompoundNodes ){
toUpdateStyle = cy.collection().merge( restored ).merge( restored.connectedNodes() ).merge( restored.parent() );
} else {
toUpdateStyle = restored;
}
toUpdateStyle.dirtyCompoundBoundsCache().dirtyBoundingBoxCache().updateStyle( notifyRenderer );
if( notifyRenderer ){
restored.emitAndNotify( 'add' );
} else if( addToPool ){
restored.emit( 'add' );
}
}
return self; // chainability
};
elesfn.removed = function(){
let ele = this[0];
return ele && ele._private.removed;
};
elesfn.inside = function(){
let ele = this[0];
return ele && !ele._private.removed;
};
elesfn.remove = function( notifyRenderer = true, removeFromPool = true ){
let self = this;
let elesToRemove = [];
let elesToRemoveIds = {};
let cy = self._private.cy;
// add connected edges
function addConnectedEdges( node ){
let edges = node._private.edges;
for( let i = 0; i < edges.length; i++ ){
add( edges[ i ] );
}
}
// add descendant nodes
function addChildren( node ){
let children = node._private.children;
for( let i = 0; i < children.length; i++ ){
add( children[ i ] );
}
}
function add( ele ){
let alreadyAdded = elesToRemoveIds[ ele.id() ];
if( (removeFromPool && ele.removed()) || alreadyAdded ){
return;
} else {
elesToRemoveIds[ ele.id() ] = true;
}
if( ele.isNode() ){
elesToRemove.push( ele ); // nodes are removed last
addConnectedEdges( ele );
addChildren( ele );
} else {
elesToRemove.unshift( ele ); // edges are removed first
}
}
// make the list of elements to remove
// (may be removing more than specified due to connected edges etc)
for( let i = 0, l = self.length; i < l; i++ ){
let ele = self[ i ];
add( ele );
}
function removeEdgeRef( node, edge ){
let connectedEdges = node._private.edges;
util.removeFromArray( connectedEdges, edge );
// removing an edges invalidates the traversal cache for its nodes
node.clearTraversalCache();
}
function removeParallelRef( pllEdge ){
// removing an edge invalidates the traversal caches for the parallel edges
pllEdge.clearTraversalCache();
}
let alteredParents = [];
alteredParents.ids = {};
function removeChildRef( parent, ele ){
ele = ele[0];
parent = parent[0];
let children = parent._private.children;
let pid = parent.id();
util.removeFromArray( children, ele ); // remove parent => child ref
ele._private.parent = null; // remove child => parent ref
if( !alteredParents.ids[ pid ] ){
alteredParents.ids[ pid ] = true;
alteredParents.push( parent );
}
}
self.dirtyCompoundBoundsCache();
if( removeFromPool ){
cy.removeFromPool( elesToRemove ); // remove from core pool
}
for( let i = 0; i < elesToRemove.length; i++ ){
let ele = elesToRemove[ i ];
if( ele.isEdge() ){ // remove references to this edge in its connected nodes
let src = ele.source()[0];
let tgt = ele.target()[0];
removeEdgeRef( src, ele );
removeEdgeRef( tgt, ele );
let pllEdges = ele.parallelEdges();
for( let j = 0; j < pllEdges.length; j++ ){
let pllEdge = pllEdges[j];
removeParallelRef(pllEdge);
if( pllEdge.isBundledBezier() ){
pllEdge.dirtyBoundingBoxCache();
}
}
} else { // remove reference to parent
let parent = ele.parent();
if( parent.length !== 0 ){
removeChildRef( parent, ele );
}
}
if( removeFromPool ){
// mark as removed
ele._private.removed = true;
}
}
// check to see if we have a compound graph or not
let elesStillInside = cy._private.elements;
cy._private.hasCompoundNodes = false;
for( let i = 0; i < elesStillInside.length; i++ ){
let ele = elesStillInside[ i ];
if( ele.isParent() ){
cy._private.hasCompoundNodes = true;
break;
}
}
let removedElements = new Collection( this.cy(), elesToRemove );
if( removedElements.size() > 0 ){
// must manually notify since trigger won't do this automatically once removed
if( notifyRenderer ){
removedElements.emitAndNotify('remove');
} else if( removeFromPool ){
removedElements.emit('remove');
}
}
// the parents who were modified by the removal need their style updated
for( let i = 0; i < alteredParents.length; i++ ){
let ele = alteredParents[ i ];
if( !removeFromPool || !ele.removed() ){
ele.updateStyle();
}
}
return removedElements;
};
elesfn.move = function( struct ){
let cy = this._private.cy;
let eles = this;
// just clean up refs, caches, etc. in the same way as when removing and then restoring
// (our calls to remove/restore do not remove from the graph or make events)
let notifyRenderer = false;
let modifyPool = false;
let toString = id => id == null ? id : '' + id; // id must be string
if( struct.source !== undefined || struct.target !== undefined ){
let srcId = toString(struct.source);
let tgtId = toString(struct.target);
let srcExists = srcId != null && cy.hasElementWithId( srcId );
let tgtExists = tgtId != null && cy.hasElementWithId( tgtId );
if( srcExists || tgtExists ){
cy.batch(() => { // avoid duplicate style updates
eles.remove( notifyRenderer, modifyPool ); // clean up refs etc.
eles.emitAndNotify('moveout');
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
let data = ele._private.data;
if( ele.isEdge() ){
if( srcExists ){ data.source = srcId; }
if( tgtExists ){ data.target = tgtId; }
}
}
eles.restore( notifyRenderer, modifyPool ); // make new refs, style, etc.
});
eles.emitAndNotify('move');
}
} else if( struct.parent !== undefined ){ // move node to new parent
let parentId = toString(struct.parent);
let parentExists = parentId === null || cy.hasElementWithId( parentId );
if( parentExists ){
let pidToAssign = parentId === null ? undefined : parentId;
cy.batch(() => { // avoid duplicate style updates
let updated = eles.remove( notifyRenderer, modifyPool ); // clean up refs etc.
updated.emitAndNotify('moveout');
for( let i = 0; i < eles.length; i++ ){
let ele = eles[i];
let data = ele._private.data;
if( ele.isNode() ){
data.parent = pidToAssign;
}
}
updated.restore( notifyRenderer, modifyPool ); // make new refs, style, etc.
});
eles.emitAndNotify('move');
}
}
return this;
};
[
algorithms,
animation,
classNames,
comparators,
compounds,
data,
degree,
dimensions,
events,
filter,
group,
iteration,
layout,
style,
switchFunctions,
traversing
].forEach( function( props ){
util.extend( elesfn, props );
} );
export default Collection;
+155
View File
@@ -0,0 +1,155 @@
import * as is from '../is.mjs' ;
import zIndexSort from './zsort.mjs' ;
import * as util from '../util/index.mjs';
let elesfn = ({
forEach: function( fn, thisArg ){
if( is.fn( fn ) ){
let N = this.length;
for( let i = 0; i < N; i++ ){
let ele = this[ i ];
let ret = thisArg ? fn.apply( thisArg, [ ele, i, this ] ) : fn( ele, i, this );
if( ret === false ){ break; } // exit each early on return false
}
}
return this;
},
toArray: function(){
let array = [];
for( let i = 0; i < this.length; i++ ){
array.push( this[ i ] );
}
return array;
},
slice: function( start, end ){
let array = [];
let thisSize = this.length;
if( end == null ){
end = thisSize;
}
if( start == null ){
start = 0;
}
if( start < 0 ){
start = thisSize + start;
}
if( end < 0 ){
end = thisSize + end;
}
for( let i = start; i >= 0 && i < end && i < thisSize; i++ ){
array.push( this[ i ] );
}
return this.spawn( array );
},
size: function(){
return this.length;
},
eq: function( i ){
return this[ i ] || this.spawn();
},
first: function(){
return this[0] || this.spawn();
},
last: function(){
return this[ this.length - 1 ] || this.spawn();
},
empty: function(){
return this.length === 0;
},
nonempty: function(){
return !this.empty();
},
sort: function( sortFn ){
if( !is.fn( sortFn ) ){
return this;
}
let sorted = this.toArray().sort( sortFn );
return this.spawn( sorted );
},
sortByZIndex: function(){
return this.sort( zIndexSort );
},
zDepth: function(){
let ele = this[0];
if( !ele ){ return undefined; }
// let cy = ele.cy();
let _p = ele._private;
let group = _p.group;
if( group === 'nodes' ){
let depth = _p.data.parent ? ele.parents().size() : 0;
if( !ele.isParent() ){
return util.MAX_INT - 1; // childless nodes always on top
}
return depth;
} else {
let src = _p.source;
let tgt = _p.target;
let srcDepth = src.zDepth();
let tgtDepth = tgt.zDepth();
return Math.max( srcDepth, tgtDepth, 0 ); // depth of deepest parent
}
}
});
elesfn.each = elesfn.forEach;
const defineSymbolIterator = () => {
const typeofUndef = typeof undefined;
const isIteratorSupported = typeof Symbol != typeofUndef && typeof Symbol.iterator != typeofUndef;
if (isIteratorSupported) {
elesfn[Symbol.iterator] = function() {
let entry = { value: undefined, done: false };
let i = 0;
let length = this.length;
return {
next: () => {
if ( i < length ) {
entry.value = this[i++];
} else {
entry.value = undefined;
entry.done = true;
}
return entry;
},
[Symbol.iterator]: function() {
return this;
}
};
};
}
};
defineSymbolIterator();
export default elesfn;
+196
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;
+457
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;
+164
View File
@@ -0,0 +1,164 @@
import * as is from '../is.mjs';
const elesfn = {};
function defineSwitchFunction( params ){
return function(){
let args = arguments;
let changedEles = [];
// e.g. cy.nodes().select( data, handler )
if( args.length === 2 ){
let data = args[0];
let handler = args[1];
this.on( params.event, data, handler );
}
// e.g. cy.nodes().select( handler )
else if( args.length === 1 && is.fn(args[0]) ){
let handler = args[0];
this.on( params.event, handler );
}
// e.g. cy.nodes().select()
// e.g. (private) cy.nodes().select(['tapselect'])
else if( args.length === 0 || (args.length === 1 && is.array(args[0])) ){
let addlEvents = args.length === 1 ? args[0] : null;
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let able = !params.ableField || ele._private[ params.ableField ];
let changed = ele._private[ params.field ] != params.value;
if( params.overrideAble ){
let overrideAble = params.overrideAble( ele );
if( overrideAble !== undefined ){
able = overrideAble;
if( !overrideAble ){ return this; } // to save cycles assume not able for all on override
}
}
if( able ){
ele._private[ params.field ] = params.value;
if( changed ){
changedEles.push( ele );
}
}
}
let changedColl = this.spawn( changedEles );
changedColl.updateStyle(); // change of state => possible change of style
changedColl.emit( params.event );
if( addlEvents ){
changedColl.emit( addlEvents );
}
}
return this;
};
}
function defineSwitchSet( params ){
elesfn[ params.field ] = function(){
let ele = this[0];
if( ele ){
if( params.overrideField ){
let val = params.overrideField( ele );
if( val !== undefined ){
return val;
}
}
return ele._private[ params.field ];
}
};
elesfn[ params.on ] = defineSwitchFunction( {
event: params.on,
field: params.field,
ableField: params.ableField,
overrideAble: params.overrideAble,
value: true
} );
elesfn[ params.off ] = defineSwitchFunction( {
event: params.off,
field: params.field,
ableField: params.ableField,
overrideAble: params.overrideAble,
value: false
} );
}
defineSwitchSet( {
field: 'locked',
overrideField: function( ele ){
return ele.cy().autolock() ? true : undefined;
},
on: 'lock',
off: 'unlock'
} );
defineSwitchSet( {
field: 'grabbable',
overrideField: function( ele ){
return ele.cy().autoungrabify() || ele.pannable() ? false : undefined;
},
on: 'grabify',
off: 'ungrabify'
} );
defineSwitchSet( {
field: 'selected',
ableField: 'selectable',
overrideAble: function( ele ){
return ele.cy().autounselectify() ? false : undefined;
},
on: 'select',
off: 'unselect'
} );
defineSwitchSet( {
field: 'selectable',
overrideField: function( ele ){
return ele.cy().autounselectify() ? false : undefined;
},
on: 'selectify',
off: 'unselectify'
} );
elesfn.deselect = elesfn.unselect;
elesfn.grabbed = function(){
let ele = this[0];
if( ele ){
return ele._private.grabbed;
}
};
defineSwitchSet( {
field: 'active',
on: 'activate',
off: 'unactivate'
} );
defineSwitchSet( {
field: 'pannable',
on: 'panify',
off: 'unpanify'
} );
elesfn.inactive = function(){
let ele = this[0];
if( ele ){
return !ele._private.active;
}
};
export default elesfn;
+424
View File
@@ -0,0 +1,424 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
import cache from './cache-traversal-call.mjs';
let elesfn = {};
// DAG functions
////////////////
let defineDagExtremity = function( params ){
return function dagExtremityImpl( selector ){
let eles = this;
let ret = [];
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
if( !ele.isNode() ){
continue;
}
let disqualified = false;
let edges = ele.connectedEdges();
for( let j = 0; j < edges.length; j++ ){
let edge = edges[j];
let src = edge.source();
let tgt = edge.target();
if(
( params.noIncomingEdges && tgt === ele && src !== ele )
|| ( params.noOutgoingEdges && src === ele && tgt !== ele )
){
disqualified = true;
break;
}
}
if( !disqualified ){
ret.push( ele );
}
}
return this.spawn( ret, true ).filter( selector );
};
};
let defineDagOneHop = function( params ){
return function( selector ){
let eles = this;
let oEles = [];
for( let i = 0; i < eles.length; i++ ){
let ele = eles[ i ];
if( !ele.isNode() ){ continue; }
let edges = ele.connectedEdges();
for( let j = 0; j < edges.length; j++ ){
let edge = edges[ j ];
let src = edge.source();
let tgt = edge.target();
if( params.outgoing && src === ele ){
oEles.push( edge );
oEles.push( tgt );
} else if( params.incoming && tgt === ele ){
oEles.push( edge );
oEles.push( src );
}
}
}
return this.spawn( oEles, true ).filter( selector );
};
};
let defineDagAllHops = function( params ){
return function( selector ){
let eles = this;
let sEles = [];
let sElesIds = {};
for( ;; ){
let next = params.outgoing ? eles.outgoers() : eles.incomers();
if( next.length === 0 ){ break; } // done if none left
let newNext = false;
for( let i = 0; i < next.length; i++ ){
let n = next[ i ];
let nid = n.id();
if( !sElesIds[ nid ] ){
sElesIds[ nid ] = true;
sEles.push( n );
newNext = true;
}
}
if( !newNext ){ break; } // done if touched all outgoers already
eles = next;
}
return this.spawn( sEles, true ).filter( selector );
};
};
elesfn.clearTraversalCache = function( ){
for( let i = 0; i < this.length; i++ ){
this[i]._private.traversalCache = null;
}
};
util.extend( elesfn, {
// get the root nodes in the DAG
roots: defineDagExtremity({ noIncomingEdges: true }),
// get the leaf nodes in the DAG
leaves: defineDagExtremity({ noOutgoingEdges: true }),
// normally called children in graph theory
// these nodes =edges=> outgoing nodes
outgoers: cache( defineDagOneHop({ outgoing: true }) , 'outgoers' ),
// aka DAG descendants
successors: defineDagAllHops({ outgoing: true }),
// normally called parents in graph theory
// these nodes <=edges= incoming nodes
incomers: cache( defineDagOneHop({ incoming: true }), 'incomers' ),
// aka DAG ancestors
predecessors: defineDagAllHops({ incoming: true })
} );
// Neighbourhood functions
//////////////////////////
util.extend( elesfn, {
neighborhood: cache(function( selector ){
let elements = [];
let nodes = this.nodes();
for( let i = 0; i < nodes.length; i++ ){ // for all nodes
let node = nodes[ i ];
let connectedEdges = node.connectedEdges();
// for each connected edge, add the edge and the other node
for( let j = 0; j < connectedEdges.length; j++ ){
let edge = connectedEdges[ j ];
let src = edge.source();
let tgt = edge.target();
let otherNode = node === src ? tgt : src;
// need check in case of loop
if( otherNode.length > 0 ){
elements.push( otherNode[0] ); // add node 1 hop away
}
// add connected edge
elements.push( edge[0] );
}
}
return ( this.spawn( elements, true ) ).filter( selector );
}, 'neighborhood'),
closedNeighborhood: function( selector ){
return this.neighborhood().add( this ).filter( selector );
},
openNeighborhood: function( selector ){
return this.neighborhood( selector );
}
} );
// aliases
elesfn.neighbourhood = elesfn.neighborhood;
elesfn.closedNeighbourhood = elesfn.closedNeighborhood;
elesfn.openNeighbourhood = elesfn.openNeighborhood;
// Edge functions
/////////////////
util.extend( elesfn, {
source: cache(function sourceImpl( selector ){
let ele = this[0];
let src;
if( ele ){
src = ele._private.source || ele.cy().collection();
}
return src && selector ? src.filter( selector ) : src;
}, 'source'),
target: cache(function targetImpl( selector ){
let ele = this[0];
let tgt;
if( ele ){
tgt = ele._private.target || ele.cy().collection();
}
return tgt && selector ? tgt.filter( selector ) : tgt;
}, 'target'),
sources: defineSourceFunction( {
attr: 'source'
} ),
targets: defineSourceFunction( {
attr: 'target'
} )
} );
function defineSourceFunction( params ){
return function sourceImpl( selector ){
let sources = [];
for( let i = 0; i < this.length; i++ ){
let ele = this[ i ];
let src = ele._private[ params.attr ];
if( src ){
sources.push( src );
}
}
return this.spawn( sources, true ).filter( selector );
};
}
util.extend( elesfn, {
edgesWith: cache( defineEdgesWithFunction(), 'edgesWith' ),
edgesTo: cache( defineEdgesWithFunction( {
thisIsSrc: true
} ), 'edgesTo' )
} );
function defineEdgesWithFunction( params ){
return function edgesWithImpl( otherNodes ){
let elements = [];
let cy = this._private.cy;
let p = params || {};
// get elements if a selector is specified
if( is.string( otherNodes ) ){
otherNodes = cy.$( otherNodes );
}
for( let h = 0; h < otherNodes.length; h++ ){
let edges = otherNodes[ h ]._private.edges;
for( let i = 0; i < edges.length; i++ ){
let edge = edges[ i ];
let edgeData = edge._private.data;
let thisToOther = this.hasElementWithId( edgeData.source ) && otherNodes.hasElementWithId( edgeData.target );
let otherToThis = otherNodes.hasElementWithId( edgeData.source ) && this.hasElementWithId( edgeData.target );
let edgeConnectsThisAndOther = thisToOther || otherToThis;
if( !edgeConnectsThisAndOther ){ continue; }
if( p.thisIsSrc || p.thisIsTgt ){
if( p.thisIsSrc && !thisToOther ){ continue; }
if( p.thisIsTgt && !otherToThis ){ continue; }
}
elements.push( edge );
}
}
return this.spawn( elements, true );
};
}
util.extend( elesfn, {
connectedEdges: cache(function( selector ){
let retEles = [];
let eles = this;
for( let i = 0; i < eles.length; i++ ){
let node = eles[ i ];
if( !node.isNode() ){ continue; }
let edges = node._private.edges;
for( let j = 0; j < edges.length; j++ ){
let edge = edges[ j ];
retEles.push( edge );
}
}
return this.spawn( retEles, true ).filter( selector );
}, 'connectedEdges'),
connectedNodes: cache(function( selector ){
let retEles = [];
let eles = this;
for( let i = 0; i < eles.length; i++ ){
let edge = eles[ i ];
if( !edge.isEdge() ){ continue; }
retEles.push( edge.source()[0] );
retEles.push( edge.target()[0] );
}
return this.spawn( retEles, true ).filter( selector );
}, 'connectedNodes'),
parallelEdges: cache( defineParallelEdgesFunction(), 'parallelEdges' ),
codirectedEdges: cache( defineParallelEdgesFunction( {
codirected: true
} ), 'codirectedEdges' )
} );
function defineParallelEdgesFunction( params ){
let defaults = {
codirected: false
};
params = util.extend( {}, defaults, params );
return function parallelEdgesImpl( selector ){ // micro-optimised for renderer
let elements = [];
let edges = this.edges();
let p = params;
// look at all the edges in the collection
for( let i = 0; i < edges.length; i++ ){
let edge1 = edges[ i ];
let edge1_p = edge1._private;
let src1 = edge1_p.source;
let srcid1 = src1._private.data.id;
let tgtid1 = edge1_p.data.target;
let srcEdges1 = src1._private.edges;
// look at edges connected to the src node of this edge
for( let j = 0; j < srcEdges1.length; j++ ){
let edge2 = srcEdges1[ j ];
let edge2data = edge2._private.data;
let tgtid2 = edge2data.target;
let srcid2 = edge2data.source;
let codirected = tgtid2 === tgtid1 && srcid2 === srcid1;
let oppdirected = srcid1 === tgtid2 && tgtid1 === srcid2;
if( (p.codirected && codirected) || (!p.codirected && (codirected || oppdirected)) ){
elements.push( edge2 );
}
}
}
return this.spawn( elements, true ).filter( selector );
};
}
// Misc functions
/////////////////
util.extend( elesfn, {
components: function(root){
let self = this;
let cy = self.cy();
let visited = cy.collection();
let unvisited = root == null ? self.nodes() : root.nodes();
let components = [];
if( root != null && unvisited.empty() ){ // root may contain only edges
unvisited = root.sources(); // doesn't matter which node to use (undirected), so just use the source sides
}
let visitInComponent = ( node, component ) => {
visited.merge( node );
unvisited.unmerge( node );
component.merge( node );
};
if( unvisited.empty() ){ return self.spawn(); }
do { // each iteration yields a component
let cmpt = cy.collection();
components.push( cmpt );
let root = unvisited[0];
visitInComponent( root, cmpt );
self.bfs({
directed: false,
roots: root,
visit: v => visitInComponent( v, cmpt )
} );
cmpt.forEach(node => {
node.connectedEdges().forEach(e => { // connectedEdges() usually cached
if( self.has(e) && cmpt.has(e.source()) && cmpt.has(e.target()) ){ // has() is cheap
cmpt.merge(e); // forEach() only considers nodes -- sets N at call time
}
});
});
} while( unvisited.length > 0 );
return components;
},
component: function(){
let ele = this[0];
return ele.cy().mutableElements().components( ele )[0];
}
} );
elesfn.componentsOf = elesfn.components;
export default elesfn;
+57
View File
@@ -0,0 +1,57 @@
/**
* Elements are drawn in a specific order based on compound depth (low to high), the element type (nodes above edges),
* and z-index (low to high). These styles affect how this applies:
*
* z-compound-depth: May be `bottom | orphan | auto | top`. The first drawn is `bottom`, then `orphan` which is the
* same depth as the root of the compound graph, followed by the default value `auto` which draws in order from
* root to leaves of the compound graph. The last drawn is `top`.
* z-index-compare: May be `auto | manual`. The default value is `auto` which always draws edges under nodes.
* `manual` ignores this convention and draws based on the `z-index` value setting.
* z-index: An integer value that affects the relative draw order of elements. In general, an element with a higher
* `z-index` will be drawn on top of an element with a lower `z-index`.
*/
import * as util from '../util/index.mjs';
let zIndexSort = function( a, b ){
let cy = a.cy();
let hasCompoundNodes = cy.hasCompoundNodes();
function getDepth(ele){
let style = ele.pstyle( 'z-compound-depth' );
if ( style.value === 'auto' ){
return hasCompoundNodes ? ele.zDepth() : 0;
} else if ( style.value === 'bottom' ){
return -1;
} else if ( style.value === 'top' ){
return util.MAX_INT;
}
// 'orphan'
return 0;
}
let depthDiff = getDepth(a) - getDepth(b);
if ( depthDiff !== 0 ){
return depthDiff;
}
function getEleDepth(ele){
let style = ele.pstyle( 'z-index-compare' );
if ( style.value === 'auto' ){
return ele.isNode() ? 1 : 0;
}
// 'manual'
return 0;
}
let eleDiff = getEleDepth(a) - getEleDepth(b);
if ( eleDiff !== 0 ){
return eleDiff;
}
let zDiff = a.pstyle( 'z-index' ).value - b.pstyle( 'z-index' ).value;
if ( zDiff !== 0 ){
return zDiff;
}
// compare indices in the core (order added to graph w/ last on top)
return a.poolIndex() - b.poolIndex();
};
export default zIndexSort;
+82
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;
+158
View File
@@ -0,0 +1,158 @@
/* global Float32Array */
/*! Bezier curve function generator. Copyright Gaetan Renaudeau. MIT License: http://en.wikipedia.org/wiki/MIT_License */
function generateCubicBezier(mX1, mY1, mX2, mY2) {
let NEWTON_ITERATIONS = 4,
NEWTON_MIN_SLOPE = 0.001,
SUBDIVISION_PRECISION = 0.0000001,
SUBDIVISION_MAX_ITERATIONS = 10,
kSplineTableSize = 11,
kSampleStepSize = 1.0 / (kSplineTableSize - 1.0),
float32ArraySupported = typeof Float32Array !== 'undefined';
/* Must contain four arguments. */
if (arguments.length !== 4) {
return false;
}
/* Arguments must be numbers. */
for (let i = 0; i < 4; ++i) {
if (typeof arguments[i] !== "number" || isNaN(arguments[i]) || !isFinite(arguments[i])) {
return false;
}
}
/* X values must be in the [0, 1] range. */
mX1 = Math.min(mX1, 1);
mX2 = Math.min(mX2, 1);
mX1 = Math.max(mX1, 0);
mX2 = Math.max(mX2, 0);
let mSampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize);
function A(aA1, aA2) {
return 1.0 - 3.0 * aA2 + 3.0 * aA1;
}
function B(aA1, aA2) {
return 3.0 * aA2 - 6.0 * aA1;
}
function C(aA1) {
return 3.0 * aA1;
}
function calcBezier(aT, aA1, aA2) {
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT;
}
function getSlope(aT, aA1, aA2) {
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
}
function newtonRaphsonIterate(aX, aGuessT) {
for (let i = 0; i < NEWTON_ITERATIONS; ++i) {
let currentSlope = getSlope(aGuessT, mX1, mX2);
if (currentSlope === 0.0) {
return aGuessT;
}
let currentX = calcBezier(aGuessT, mX1, mX2) - aX;
aGuessT -= currentX / currentSlope;
}
return aGuessT;
}
function calcSampleValues() {
for (let i = 0; i < kSplineTableSize; ++i) {
mSampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2);
}
}
function binarySubdivide(aX, aA, aB) {
let currentX, currentT, i = 0;
do {
currentT = aA + (aB - aA) / 2.0;
currentX = calcBezier(currentT, mX1, mX2) - aX;
if (currentX > 0.0) {
aB = currentT;
} else {
aA = currentT;
}
} while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS);
return currentT;
}
function getTForX(aX) {
let intervalStart = 0.0,
currentSample = 1,
lastSample = kSplineTableSize - 1;
for (; currentSample !== lastSample && mSampleValues[currentSample] <= aX; ++currentSample) {
intervalStart += kSampleStepSize;
}
--currentSample;
let dist = (aX - mSampleValues[currentSample]) / (mSampleValues[currentSample + 1] - mSampleValues[currentSample]),
guessForT = intervalStart + dist * kSampleStepSize,
initialSlope = getSlope(guessForT, mX1, mX2);
if (initialSlope >= NEWTON_MIN_SLOPE) {
return newtonRaphsonIterate(aX, guessForT);
} else if (initialSlope === 0.0) {
return guessForT;
} else {
return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize);
}
}
let _precomputed = false;
function precompute() {
_precomputed = true;
if (mX1 !== mY1 || mX2 !== mY2) {
calcSampleValues();
}
}
let f = function(aX) {
if (!_precomputed) {
precompute();
}
if (mX1 === mY1 && mX2 === mY2) {
return aX;
}
if (aX === 0) {
return 0;
}
if (aX === 1) {
return 1;
}
return calcBezier(getTForX(aX), mY1, mY2);
};
f.getControlPoints = function() {
return [{
x: mX1,
y: mY1
}, {
x: mX2,
y: mY2
}];
};
let str = "generateBezier(" + [mX1, mY1, mX2, mY2] + ")";
f.toString = function() {
return str;
};
return f;
}
export default generateCubicBezier;
+82
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;
+76
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;
+60
View File
@@ -0,0 +1,60 @@
import define from '../../define/index.mjs';
import * as util from '../../util/index.mjs';
import stepAll from './step-all.mjs';
let corefn = ({
// pull in animation functions
animate: define.animate(),
animation: define.animation(),
animated: define.animated(),
clearQueue: define.clearQueue(),
delay: define.delay(),
delayAnimation: define.delayAnimation(),
stop: define.stop(),
addToAnimationPool: function( eles ){
let cy = this;
if( !cy.styleEnabled() ){ return; } // save cycles when no style used
cy._private.aniEles.merge( eles );
},
stopAnimationLoop: function(){
this._private.animationsRunning = false;
},
startAnimationLoop: function(){
let cy = this;
cy._private.animationsRunning = true;
if( !cy.styleEnabled() ){ return; } // save cycles when no style used
// NB the animation loop will exec in headless environments if style enabled
// and explicit cy.destroy() is necessary to stop the loop
function headlessStep(){
if( !cy._private.animationsRunning ){ return; }
util.requestAnimationFrame( function animationStep( now ){
stepAll( now, cy );
headlessStep();
} );
}
let renderer = cy.renderer();
if( renderer && renderer.beforeRender ){ // let the renderer schedule animations
renderer.beforeRender( function rendererAnimationStep( willDraw, now ){
stepAll( now, cy );
}, renderer.beforeRenderPriorities.animations );
} else { // manage the animation loop ourselves
headlessStep(); // first call
}
}
});
export default corefn;
+88
View File
@@ -0,0 +1,88 @@
/*! Runge-Kutta spring physics function generator. Adapted from Framer.js, copyright Koen Bok. MIT License: http://en.wikipedia.org/wiki/MIT_License */
/* Given a tension, friction, and duration, a simulation at 60FPS will first run without a defined duration in order to calculate the full path. A second pass
then adjusts the time delta -- using the relation between actual time and duration -- to calculate the path for the duration-constrained animation. */
let generateSpringRK4 = (function(){
function springAccelerationForState( state ){
return (-state.tension * state.x) - (state.friction * state.v);
}
function springEvaluateStateWithDerivative( initialState, dt, derivative ){
let state = {
x: initialState.x + derivative.dx * dt,
v: initialState.v + derivative.dv * dt,
tension: initialState.tension,
friction: initialState.friction
};
return { dx: state.v, dv: springAccelerationForState( state ) };
}
function springIntegrateState( state, dt ){
let a = {
dx: state.v,
dv: springAccelerationForState( state )
},
b = springEvaluateStateWithDerivative( state, dt * 0.5, a ),
c = springEvaluateStateWithDerivative( state, dt * 0.5, b ),
d = springEvaluateStateWithDerivative( state, dt, c ),
dxdt = 1.0 / 6.0 * (a.dx + 2.0 * (b.dx + c.dx) + d.dx),
dvdt = 1.0 / 6.0 * (a.dv + 2.0 * (b.dv + c.dv) + d.dv);
state.x = state.x + dxdt * dt;
state.v = state.v + dvdt * dt;
return state;
}
return function springRK4Factory( tension, friction, duration ){
let initState = {
x: -1,
v: 0,
tension: null,
friction: null
},
path = [0],
time_lapsed = 0,
tolerance = 1 / 10000,
DT = 16 / 1000,
have_duration, dt, last_state;
tension = parseFloat( tension ) || 500;
friction = parseFloat( friction ) || 20;
duration = duration || null;
initState.tension = tension;
initState.friction = friction;
have_duration = duration !== null;
/* Calculate the actual time it takes for this animation to complete with the provided conditions. */
if( have_duration ){
/* Run the simulation without a duration. */
time_lapsed = springRK4Factory( tension, friction );
/* Compute the adjusted time delta. */
dt = time_lapsed / duration * DT;
} else {
dt = DT;
}
for(;;){
/* Next/step function .*/
last_state = springIntegrateState( last_state || initState, dt );
/* Store the position. */
path.push( 1 + last_state.x );
time_lapsed += 16;
/* If the change threshold is reached, break. */
if( !(Math.abs( last_state.x ) > tolerance && Math.abs( last_state.v ) > tolerance) ){
break;
}
}
/* If duration is not defined, return the actual time required for completing this animation. Otherwise, return a closure that holds the
computed path and returns a snapshot of the position according to a given percentComplete. */
return !have_duration ? time_lapsed : function( percentComplete ){ return path[ (percentComplete * (path.length - 1)) | 0 ]; };
};
}());
export default generateSpringRK4;
+8
View File
@@ -0,0 +1,8 @@
function startAnimation( self, ani, now, isCore ){
let ani_p = ani._private;
ani_p.started = true;
ani_p.startTime = now - ani_p.progress * ani_p.duration;
}
export default startAnimation;
+120
View File
@@ -0,0 +1,120 @@
import step from './step.mjs';
import startAnimation from './start.mjs';
function stepAll( now, cy ){
let eles = cy._private.aniEles;
let doneEles = [];
function stepOne( ele, isCore ){
let _p = ele._private;
let current = _p.animation.current;
let queue = _p.animation.queue;
let ranAnis = false;
// if nothing currently animating, get something from the queue
if( current.length === 0 ){
let next = queue.shift();
if( next ){
current.push( next );
}
}
let callbacks = function( callbacks ){
for( let j = callbacks.length - 1; j >= 0; j-- ){
let cb = callbacks[ j ];
cb();
}
callbacks.splice( 0, callbacks.length );
};
// step and remove if done
for( let i = current.length - 1; i >= 0; i-- ){
let ani = current[ i ];
let ani_p = ani._private;
if( ani_p.stopped ){
current.splice( i, 1 );
ani_p.hooked = false;
ani_p.playing = false;
ani_p.started = false;
callbacks( ani_p.frames );
continue;
}
if( !ani_p.playing && !ani_p.applying ){ continue; }
// an apply() while playing shouldn't do anything
if( ani_p.playing && ani_p.applying ){
ani_p.applying = false;
}
if( !ani_p.started ){
startAnimation( ele, ani, now, isCore );
}
step( ele, ani, now, isCore );
if( ani_p.applying ){
ani_p.applying = false;
}
callbacks( ani_p.frames );
if( ani_p.step != null ){
ani_p.step(now);
}
if( ani.completed() ){
current.splice( i, 1 );
ani_p.hooked = false;
ani_p.playing = false;
ani_p.started = false;
callbacks( ani_p.completes );
}
ranAnis = true;
}
if( !isCore && current.length === 0 && queue.length === 0 ){
doneEles.push( ele );
}
return ranAnis;
} // stepElement
// handle all eles
let ranEleAni = false;
for( let e = 0; e < eles.length; e++ ){
let ele = eles[ e ];
let handledThisEle = stepOne( ele );
ranEleAni = ranEleAni || handledThisEle;
} // each element
let ranCoreAni = stepOne( cy, true );
// notify renderer
if( ranEleAni || ranCoreAni ){
if( eles.length > 0 ){
cy.notify('draw', eles);
} else {
cy.notify('draw');
}
}
// remove elements from list of currently animating if its queues are empty
eles.unmerge( doneEles );
cy.emit('step');
} // stepAll
export default stepAll;
+162
View File
@@ -0,0 +1,162 @@
import easings from './easings.mjs';
import ease from './ease.mjs';
import * as is from '../../is.mjs';
import {bound} from '../../math.mjs';
function step( self, ani, now, isCore ){
let isEles = !isCore;
let _p = self._private;
let ani_p = ani._private;
let pEasing = ani_p.easing;
let startTime = ani_p.startTime;
let cy = isCore ? self : self.cy();
let style = cy.style();
if( !ani_p.easingImpl ){
if( pEasing == null ){ // use default
ani_p.easingImpl = easings[ 'linear' ];
} else { // then define w/ name
let easingVals;
if( is.string( pEasing ) ){
let easingProp = style.parse( 'transition-timing-function', pEasing );
easingVals = easingProp.value;
} else { // then assume preparsed array
easingVals = pEasing;
}
let name, args;
if( is.string( easingVals ) ){
name = easingVals;
args = [];
} else {
name = easingVals[1];
args = easingVals.slice( 2 ).map( function( n ){ return +n; } );
}
if( args.length > 0 ){ // create with args
if( name === 'spring' ){
args.push( ani_p.duration ); // need duration to generate spring
}
ani_p.easingImpl = easings[ name ].apply( null, args );
} else { // static impl by name
ani_p.easingImpl = easings[ name ];
}
}
}
let easing = ani_p.easingImpl;
let percent;
if( ani_p.duration === 0 ){
percent = 1;
} else {
percent = (now - startTime) / ani_p.duration;
}
if( ani_p.applying ){
percent = ani_p.progress;
}
if( percent < 0 ){
percent = 0;
} else if( percent > 1 ){
percent = 1;
}
if( ani_p.delay == null ){ // then update
let startPos = ani_p.startPosition;
let endPos = ani_p.position;
if( endPos && isEles && !self.locked() ){
let newPos = {};
if( valid( startPos.x, endPos.x ) ){
newPos.x = ease( startPos.x, endPos.x, percent, easing );
}
if( valid( startPos.y, endPos.y ) ){
newPos.y = ease( startPos.y, endPos.y, percent, easing );
}
self.position( newPos );
}
let startPan = ani_p.startPan;
let endPan = ani_p.pan;
let pan = _p.pan;
let animatingPan = endPan != null && isCore;
if( animatingPan ){
if( valid( startPan.x, endPan.x ) ){
pan.x = ease( startPan.x, endPan.x, percent, easing );
}
if( valid( startPan.y, endPan.y ) ){
pan.y = ease( startPan.y, endPan.y, percent, easing );
}
self.emit( 'pan' );
}
let startZoom = ani_p.startZoom;
let endZoom = ani_p.zoom;
let animatingZoom = endZoom != null && isCore;
if( animatingZoom ){
if( valid( startZoom, endZoom ) ){
_p.zoom = bound( _p.minZoom, ease( startZoom, endZoom, percent, easing ), _p.maxZoom );
}
self.emit( 'zoom' );
}
if( animatingPan || animatingZoom ){
self.emit( 'viewport' );
}
let props = ani_p.style;
if( props && props.length > 0 && isEles ){
for( let i = 0; i < props.length; i++ ){
let prop = props[ i ];
let name = prop.name;
let end = prop;
let start = ani_p.startStyle[ name ];
let propSpec = style.properties[ start.name ];
let easedVal = ease( start, end, percent, easing, propSpec );
style.overrideBypass( self, name, easedVal );
} // for props
self.emit('style');
} // if
}
ani_p.progress = percent;
return percent;
}
function valid( start, end ){
if( start == null || end == null ){
return false;
}
if( is.number( start ) && is.number( end ) ){
return true;
} else if( (start) && (end) ){
return true;
}
return false;
}
export default step;
+49
View File
@@ -0,0 +1,49 @@
import define from '../define/index.mjs';
const fn = {
data: define.data( {
field: 'data',
bindingEvent: 'data',
allowBinding: true,
allowSetting: true,
settingEvent: 'data',
settingTriggersEvent: true,
triggerFnName: 'trigger',
allowGetting: true,
updateStyle: true
} ),
removeData: define.removeData( {
field: 'data',
event: 'data',
triggerFnName: 'trigger',
triggerEvent: 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
} )
};
// aliases
fn.attr = fn.data;
fn.removeAttr = fn.removeData;
export default fn;
+102
View File
@@ -0,0 +1,102 @@
import Emitter from '../emitter.mjs';
import define from '../define/index.mjs';
import * as is from '../is.mjs';
import Selector from '../selector/index.mjs';
let emitterOptions = {
qualifierCompare: function( selector1, selector2 ){
if( selector1 == null || selector2 == null ){
return selector1 == null && selector2 == null;
} else {
return selector1.sameText( selector2 );
}
},
eventMatches: function( cy, listener, eventObj ){
let selector = listener.qualifier;
if( selector != null ){
return cy !== eventObj.target && is.element( eventObj.target ) && selector.matches( eventObj.target );
}
return true;
},
addEventFields: function( cy, evt ){
evt.cy = cy;
evt.target = cy;
},
callbackContext: function( cy, listener, eventObj ){
return listener.qualifier != null ? eventObj.target : cy;
}
};
let argSelector = function( arg ){
if( is.string(arg) ){
return new Selector( arg );
} else {
return arg;
}
};
let elesfn = ({
createEmitter: function(){
let _p = this._private;
if( !_p.emitter ){
_p.emitter = new Emitter( emitterOptions, this );
}
return this;
},
emitter: function(){
return this._private.emitter;
},
on: function( events, selector, callback ){
this.emitter().on( events, argSelector(selector), callback );
return this;
},
removeListener: function( events, selector, callback ){
this.emitter().removeListener( events, argSelector(selector), callback );
return this;
},
removeAllListeners: function(){
this.emitter().removeAllListeners();
return this;
},
one: function( events, selector, callback ){
this.emitter().one( events, argSelector(selector), callback );
return this;
},
once: function( events, selector, callback ){
this.emitter().one( events, argSelector(selector), callback );
return this;
},
emit: function( events, extraParams ){
this.emitter().emit( events, extraParams );
return this;
},
emitAndNotify: function( event, eles ){
this.emit( event );
this.notify( event, eles );
return this;
}
});
define.eventAliasesOn( elesfn );
export default elesfn;
+23
View File
@@ -0,0 +1,23 @@
let corefn = ({
png: function( options ){
let renderer = this._private.renderer;
options = options || {};
return renderer.png( options );
},
jpg: function( options ){
let renderer = this._private.renderer;
options = options || {};
options.bg = options.bg || '#fff';
return renderer.jpg( options );
}
});
corefn.jpeg = corefn.jpg;
export default corefn;
+522
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;
+46
View File
@@ -0,0 +1,46 @@
import * as util from '../util/index.mjs';
import * as is from '../is.mjs';
let corefn = ({
layout: function( options ){
let cy = this;
if( options == null ){
util.error( 'Layout options must be specified to make a layout' );
return;
}
if( options.name == null ){
util.error( 'A `name` must be specified to make a layout' );
return;
}
let name = options.name;
let Layout = cy.extension( 'layout', name );
if( Layout == null ){
util.error( 'No such layout `' + name + '` found. Did you forget to import it and `cytoscape.use()` it?' );
return;
}
let eles;
if( is.string( options.eles ) ){
eles = cy.$( options.eles );
} else {
eles = options.eles != null ? options.eles : cy.$();
}
let layout = new Layout( util.extend( {}, options, {
cy: cy,
eles: eles
} ) );
return layout;
}
});
corefn.createLayout = corefn.makeLayout = corefn.layout;
export default corefn;
+120
View File
@@ -0,0 +1,120 @@
let corefn = ({
notify: function( eventName, eventEles ){
let _p = this._private;
if( this.batching() ){
_p.batchNotifications = _p.batchNotifications || {};
let eles = _p.batchNotifications[ eventName ] = _p.batchNotifications[ eventName ] || this.collection();
if( eventEles != null ){
eles.merge( eventEles );
}
return; // notifications are disabled during batching
}
if( !_p.notificationsEnabled ){ return; } // exit on disabled
let renderer = this.renderer();
// exit if destroy() called on core or renderer in between frames #1499 #1528
if( this.destroyed() || !renderer ){ return; }
renderer.notify( eventName, eventEles );
},
notifications: function( bool ){
let p = this._private;
if( bool === undefined ){
return p.notificationsEnabled;
} else {
p.notificationsEnabled = bool ? true : false;
}
return this;
},
noNotifications: function( callback ){
this.notifications( false );
callback();
this.notifications( true );
},
batching: function(){
return this._private.batchCount > 0;
},
startBatch: function(){
let _p = this._private;
if( _p.batchCount == null ){
_p.batchCount = 0;
}
if( _p.batchCount === 0 ){
_p.batchStyleEles = this.collection();
_p.batchNotifications = {};
}
_p.batchCount++;
return this;
},
endBatch: function(){
let _p = this._private;
if( _p.batchCount === 0 ){ return this; }
_p.batchCount--;
if( _p.batchCount === 0 ){
// update style for dirty eles
_p.batchStyleEles.updateStyle();
let renderer = this.renderer();
// notify the renderer of queued eles and event types
Object.keys( _p.batchNotifications ).forEach( eventName => {
let eles = _p.batchNotifications[eventName];
if( eles.empty() ){
renderer.notify( eventName );
} else {
renderer.notify( eventName, eles );
}
} );
}
return this;
},
batch: function( callback ){
this.startBatch();
callback();
this.endBatch();
return this;
},
// for backwards compatibility
batchData: function( map ){
let cy = this;
return this.batch( function(){
let ids = Object.keys( map );
for( let i = 0; i < ids.length; i++ ){
let id = ids[i];
let data = map[ id ];
let ele = cy.getElementById( id );
ele.data( data );
}
} );
}
});
export default corefn;
+113
View File
@@ -0,0 +1,113 @@
import * as util from '../util/index.mjs';
let rendererDefaults = util.defaults({
hideEdgesOnViewport: false,
textureOnViewport: false,
motionBlur: false,
motionBlurOpacity: 0.05,
pixelRatio: undefined,
desktopTapThreshold: 4,
touchTapThreshold: 8,
wheelSensitivity: 1,
debug: false,
showFps: false,
// webgl options
webgl: false,
webglDebug: false,
webglDebugShowAtlases: false,
// defaults good for mobile
webglTexSize: 2048,
webglTexRows: 36,
webglTexRowsNodes: 18,
webglBatchSize: 2048,
webglTexPerBatch: 14,
webglBgColor: [255, 255, 255]
});
let corefn = ({
renderTo: function( context, zoom, pan, pxRatio ){
let r = this._private.renderer;
r.renderTo( context, zoom, pan, pxRatio );
return this;
},
renderer: function(){
return this._private.renderer;
},
forceRender: function(){
this.notify('draw');
return this;
},
resize: function(){
this.invalidateSize();
this.emitAndNotify('resize');
return this;
},
initRenderer: function( options ){
let cy = this;
let RendererProto = cy.extension( 'renderer', options.name );
if( RendererProto == null ){
util.error( `Can not initialise: No such renderer \`${options.name}\` found. Did you forget to import it and \`cytoscape.use()\` it?` );
return;
}
if( options.wheelSensitivity !== undefined ){
util.warn(`You have set a custom wheel sensitivity. This will make your app zoom unnaturally when using mainstream mice. You should change this value from the default only if you can guarantee that all your users will use the same hardware and OS configuration as your current machine.`);
}
let rOpts = rendererDefaults(options);
rOpts.cy = cy;
cy._private.renderer = new RendererProto( rOpts );
this.notify('init');
},
destroyRenderer: function(){
let cy = this;
cy.notify('destroy'); // destroy the renderer
let domEle = cy.container();
if( domEle ){
domEle._cyreg = null;
while( domEle.childNodes.length > 0 ){
domEle.removeChild( domEle.childNodes[0] );
}
}
cy._private.renderer = null; // to be extra safe, remove the ref
cy.mutableElements().forEach(function( ele ){
let _p = ele._private;
_p.rscratch = {};
_p.rstyle = {};
_p.animation.current = [];
_p.animation.queue = [];
});
},
onRender: function( fn ){
return this.on('render', fn);
},
offRender: function( fn ){
return this.off('render', fn);
}
});
corefn.invalidateDimensions = corefn.resize;
export default corefn;
+72
View File
@@ -0,0 +1,72 @@
import * as is from '../is.mjs';
import Collection from '../collection/index.mjs';
let corefn = ({
// get a collection
// - empty collection on no args
// - collection of elements in the graph on selector arg
// - guarantee a returned collection when elements or collection specified
collection: function( eles, opts ){
if( is.string( eles ) ){
return this.$( eles );
} else if( is.elementOrCollection( eles ) ){
return eles.collection();
} else if( is.array( eles ) ){
if (!opts) {
opts = {};
}
return new Collection( this, eles, opts.unique, opts.removed );
}
return new Collection( this );
},
nodes: function( selector ){
let nodes = this.$( function( ele ){
return ele.isNode();
} );
if( selector ){
return nodes.filter( selector );
}
return nodes;
},
edges: function( selector ){
let edges = this.$( function( ele ){
return ele.isEdge();
} );
if( selector ){
return edges.filter( selector );
}
return edges;
},
// search the graph like jQuery
$: function( selector ){
let eles = this._private.elements;
if( selector ){
return eles.filter( selector );
} else {
return eles.spawnSelf();
}
},
mutableElements: function(){
return this._private.elements;
}
});
// aliases
corefn.elements = corefn.filter = corefn.$;
export default corefn;

Some files were not shown because too many files have changed in this diff Show More