+7
@@ -0,0 +1,7 @@
|
||||
ie 9-11
|
||||
firefox >= 4
|
||||
chrome >= 23
|
||||
> 1%
|
||||
node >= 0.10
|
||||
last 2 major versions
|
||||
since 2012
|
||||
+64
@@ -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.
|
||||
|
||||
+30
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||
+108
@@ -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`.
|
||||
+27
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Generated
Vendored
+151
@@ -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
|
||||
+37
@@ -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"
|
||||
+38
@@ -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"
|
||||
+85
@@ -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
@@ -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
@@ -0,0 +1 @@
|
||||
22
|
||||
+78
@@ -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
@@ -0,0 +1,6 @@
|
||||
language: node_js
|
||||
node_js:
|
||||
- "10"
|
||||
- "12"
|
||||
sudo: false
|
||||
script: npm run travis
|
||||
+128
@@ -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
@@ -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
@@ -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">
|
||||
|
||||
[](https://github.com/cytoscape/cytoscape.js)
|
||||
[](https://blog.js.cytoscape.org)
|
||||
[](https://raw.githubusercontent.com/cytoscape/cytoscape.js/master/LICENSE)
|
||||
[](https://www.npmjs.com/package/cytoscape)
|
||||
[](https://zenodo.org/badge/latestdoi/2255947)
|
||||
[](https://www.npmjs.com/package/cytoscape)
|
||||
[](https://github.com/cytoscape/cytoscape.js/actions/workflows/tests.yml)
|
||||
[](https://js.cytoscape.org/#extensions)
|
||||
[](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`).
|
||||
+35604
File diff suppressed because one or more lines are too long
+9
File diff suppressed because one or more lines are too long
+35602
File diff suppressed because one or more lines are too long
+31
File diff suppressed because one or more lines are too long
+35610
File diff suppressed because one or more lines are too long
+14
@@ -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
File diff suppressed because it is too large
Load Diff
+32
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+329
@@ -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 };
|
||||
+202
@@ -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;
|
||||
Generated
Vendored
+174
@@ -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
@@ -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;
|
||||
+101
@@ -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;
|
||||
+64
@@ -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 );
|
||||
}
|
||||
}
|
||||
+159
@@ -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
@@ -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;
|
||||
+140
@@ -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;
|
||||
Generated
Vendored
+316
@@ -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
@@ -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;
|
||||
Generated
Vendored
+118
@@ -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
@@ -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;
|
||||
+468
@@ -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
|
||||
};
|
||||
+198
@@ -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
@@ -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;
|
||||
+232
@@ -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
@@ -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;
|
||||
Generated
Vendored
+75
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
+1045
File diff suppressed because it is too large
Load Diff
+56
@@ -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
@@ -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
@@ -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;
|
||||
+130
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
Reference in New Issue
Block a user