├── .babelrc ├── .editorconfig ├── .github └── workflows │ ├── add-to-backlog.yaml │ └── node.js.yaml ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── STYLEGUIDE.md ├── dts-bundle.json ├── eslint.config.mjs ├── examples ├── common.ts ├── composite.ts.old ├── dbpedia.ts ├── demo.ts ├── edit.ts ├── envendpoint.ts ├── rdf.ts.old ├── resources │ ├── classes.json │ ├── elements.json │ ├── exampleMetadataApi.ts │ ├── linkTypes.json │ ├── links.json │ └── orgOntology.ttl ├── template.ejs ├── toolbarCustomization.tsx ├── wikidata.ts └── wikidataGraph.ts ├── images ├── add-to-filter.png ├── arrow-bottom.png ├── arrow-bottom1.png ├── arrow-left.png ├── arrow-left1.png ├── arrow-right.png ├── arrow-right1.png ├── arrow-top.png ├── arrow-top1.png ├── close-connections.svg ├── collapse-properties.png ├── connections.svg ├── delete.svg ├── delete_source.svg ├── direction-in.png ├── direction-out.png ├── expand-link.png ├── expand-properties.png ├── expand-properties_100x100.png ├── font-awesome │ ├── certificate-solid.svg │ ├── cog-solid.svg │ ├── edit.svg │ ├── exclamation-triangle.svg │ ├── minus-square-regular.svg │ ├── pen-square-solid.svg │ ├── plug.svg │ ├── plus-square-regular.svg │ ├── times-circle-regular.svg │ ├── trash-alt.svg │ └── undo-solid.svg ├── icons │ ├── class.svg │ ├── country.svg │ ├── datatypeProperty.svg │ ├── event.svg │ ├── location.svg │ ├── object.svg │ ├── objectProperty.svg │ ├── organization.svg │ └── person.svg ├── left-panel-active.svg ├── left-panel.svg ├── link.svg ├── remove.svg ├── resizable-column-handle.png ├── right-panel-active.svg ├── right-panel.svg └── tree │ ├── collapse-toggle.svg │ ├── expand-toggle.svg │ ├── leaf-default.svg │ └── leaf-folder.svg ├── package-lock.json ├── package.json ├── schema └── context-v1.json ├── src └── graph-explorer │ ├── customization │ ├── defaultLinkStyles.ts │ ├── defaultTypeStyles.ts │ ├── props.ts │ └── templates │ │ ├── default.tsx │ │ ├── group.tsx │ │ ├── index.ts │ │ ├── standard.tsx │ │ └── utils.ts │ ├── data │ ├── composite │ │ ├── composite.ts │ │ └── mergeUtils.ts │ ├── demo │ │ └── provider.ts │ ├── metadataApi.ts │ ├── model.ts │ ├── provider.ts │ ├── schema.ts │ ├── sparql │ │ ├── blankNodes.ts │ │ ├── graphBuilder.ts │ │ ├── responseHandler.ts │ │ ├── sparqlDataProvider.ts │ │ ├── sparqlDataProviderSettings.ts │ │ ├── sparqlGraphBuilder.ts │ │ ├── sparqlModels.ts │ │ └── turtle.ts │ ├── utils.ts │ └── validationApi.ts │ ├── diagram │ ├── commands.ts │ ├── elementLayer.tsx │ ├── elements.ts │ ├── embeddedLayer.tsx │ ├── geometry.ts │ ├── graph.ts │ ├── history.ts │ ├── linkLayer.tsx │ ├── linkRouter.ts │ ├── model.ts │ ├── paper.tsx │ ├── paperArea.tsx │ └── view.ts │ ├── editor │ ├── asyncModel.ts │ ├── authoredEntity.tsx │ ├── authoringState.ts │ ├── dataFetcher.ts │ ├── editLayer.tsx │ ├── editorController.tsx │ ├── elementDecorator.tsx │ ├── linkStateWidget.tsx │ ├── serializedDiagram.ts │ ├── temporaryState.ts │ └── validation.ts │ ├── emptyModule.ts │ ├── forms │ ├── editElementTypeForm.tsx │ ├── editEntityForm.tsx │ ├── editLinkForm.tsx │ ├── editLinkLabelForm.tsx │ ├── elementTypeSelector.tsx │ └── linkTypeSelector.tsx │ ├── index.ts │ ├── internalApi.ts │ ├── viewUtils │ ├── async.ts │ ├── collections.ts │ ├── events.ts │ ├── keyedObserver.ts │ ├── layout.ts │ ├── react.ts │ ├── spinner.tsx │ └── toSvg.ts │ ├── widgets │ ├── classTree │ │ ├── classTree.tsx │ │ ├── index.ts │ │ ├── leaf.tsx │ │ └── treeModel.ts │ ├── connectionsMenu.tsx │ ├── dialog.tsx │ ├── halo.tsx │ ├── haloLink.tsx │ ├── instancesSearch.tsx │ ├── linksToolbox.tsx │ ├── listElementView.tsx │ ├── navigator.tsx │ ├── progressBar.tsx │ └── searchResults.tsx │ └── workspace │ ├── accordion.tsx │ ├── accordionItem.tsx │ ├── draggableHandle.tsx │ ├── layout │ └── layout.tsx │ ├── resizableSidebar.tsx │ ├── toolbar.tsx │ ├── workspace.ts │ ├── workspaceContext.ts │ └── workspaceMarkup.tsx ├── styles ├── diagram │ ├── _elementLayer.scss │ ├── _linkLayer.scss │ ├── _paperArea.scss │ └── animation.scss ├── editor │ ├── _authoringState.scss │ └── _loadingWidget.scss ├── main.scss ├── templates │ ├── _defaultElement.scss │ ├── _group.scss │ ├── _icons.scss │ └── _standard.scss ├── viewUtils │ ├── _badge.scss │ ├── _btn-group.scss │ ├── _btn.scss │ ├── _clearfix.scss │ ├── _form-control.scss │ ├── _input-group.scss │ ├── _label.scss │ ├── _list-group.scss │ └── _spinner.scss ├── widgets │ ├── _authoringTools.scss │ ├── _classTree.scss │ ├── _connectionsMenu.scss │ ├── _dialog.scss │ ├── _editForm.scss │ ├── _halo.scss │ ├── _haloLink.scss │ ├── _instancesSearch.scss │ ├── _linksToolbox.scss │ ├── _listElementView.scss │ ├── _navigator.scss │ ├── _progressBar.scss │ └── _searchResults.scss └── workspace │ ├── _accordion.scss │ ├── _resizableSidebar.scss │ ├── _toolbar.scss │ ├── _tutorial.scss │ └── _workspace.scss ├── thirdparty.txt ├── tsconfig.json ├── tsconfig.typings.json ├── typings ├── local │ └── file-saverjs │ │ └── index.d.ts └── typings.d.ts ├── webpack.config.js └── webpack.demo.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env", 4 | "@babel/preset-react", 5 | "@babel/preset-typescript" 6 | ], 7 | "plugins": [ 8 | [ 9 | "@babel/plugin-transform-runtime", 10 | { 11 | "regenerator": true 12 | } 13 | ] 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/workflows/add-to-backlog.yaml: -------------------------------------------------------------------------------- 1 | name: Add issues to shared backlog 2 | 3 | on: 4 | issues: 5 | types: 6 | - opened 7 | pull_request: 8 | types: 9 | - opened 10 | 11 | jobs: 12 | add-to-project: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/add-to-project@v0.4.0 16 | with: 17 | project-url: https://github.com/orgs/zazuko/projects/23 18 | github-token: ${{ secrets.BACKLOG_PAT }} 19 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - name: Use Node.Js 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: lts/* 17 | - name: Install dependencies 18 | run: npm ci 19 | - name: Build the app 20 | run: npm run build-all 21 | - name: Run the linter 22 | run: npm run lint 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | /*.sh 4 | /*.bat 5 | !travis-dist-deploy.sh 6 | .idea/ 7 | .vscode/ 8 | 9 | *.iml 10 | *.log 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist/temp 3 | /heroku-app 4 | /*.sh 5 | /*.bat 6 | 7 | .circleci/ 8 | .idea/ 9 | .vscode/ 10 | 11 | *.iml 12 | *.log 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Graph Explorer 2 | Copyright (C) Netage B.V. 3 | Copyright (C) Zazuko GmbH. 4 | 5 | Based on Ontodia data diagramming library 6 | Copyright (C) 2019 metaphacts GmbH. 7 | 8 | This library is free software; you can redistribute it and/or 9 | modify it under the terms of the GNU Lesser General Public 10 | License as published by the Free Software Foundation; either 11 | version 2.1 of the License, or (at your option) any later version. 12 | 13 | This library is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 16 | Lesser General Public License for more details. 17 | 18 | You should have received a copy of the GNU Lesser General Public 19 | License along with this library; if not, you can receive a copy 20 | of the GNU Lesser General Public License from http://www.gnu.org/ 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Graph Explorer 2 | [![npm version](https://badge.fury.io/js/graph-explorer.svg)](https://www.npmjs.com/package/graph-explorer) 3 | ![CI status](https://github.com/zazuko/graph-explorer/workflows/Node.js%20CI/badge.svg) 4 | 5 | Working with RDF graph-based data and struggling to grasp the underlying model? 6 | 7 | Experiencing rapid graph growth and seeking an exploratory tool to better comprehend your knowledge graph's connections? 8 | 9 | Need a solution for documenting your schema, ontology, or data for others? 10 | 11 | Graph Explorer is the perfect tool for you! 12 | 13 | Graph Explorer is a JavaScript application and library designed to visualize, navigate, and examine RDF-based knowledge graphs and data sources. It's configurable to operate with single or multiple SPARQL endpoints and can load RDF resources from the web. 14 | 15 | In essence, Graph Explorer empowers you, your team, and the global community to access and understand linked data more effectively. 16 | 17 | Graph Explorer is a fork of [Ontodia](https://github.com/metaphacts/ontodia), now incorporated into [Metaphacts](https://metaphacts.com/). To advance the open-source version, we chose to fork, maintain, and expand the codebase as necessary. Contributions from partners are highly encouraged! 18 | 19 | ## Features 20 | 21 | - Visual navigation and diagramming for extensive graph data sets 22 | - Engaging graph visualization and context-aware navigation capabilities 23 | - Diagram storage and retrieval functionality 24 | - User-friendly design, no graph query language or schema knowledge needed 25 | - Customizable UI (by adjusting node and link templates) and data storage back-end 26 | 27 | ## Examples 28 | 29 | `npm run demo` and open 30 | 31 | or 32 | 33 | `SPARQL_ENDPOINT=http://localhost:7200/repositories/foobar npm run demo` and open 34 | 35 | ## Installation 36 | 37 | `npm install graph-explorer` 38 | 39 | ## Building / Publishing 40 | 41 | ``` 42 | npm run build-all 43 | npm run typings 44 | 45 | npm publish 46 | ``` 47 | -------------------------------------------------------------------------------- /STYLEGUIDE.md: -------------------------------------------------------------------------------- 1 | * Pay attention to TSLint warnings. 2 | 3 | * Module files and directories should be named in camelCase; if module has single important entity like 4 | class or function then this file should be named after it: 5 | 6 | ``` 7 | // DO 8 | (src/foo/bar/bazBazBaz.ts) 9 | export class BazBazBaz { ... } 10 | export function barBaz(baz: BazBazBaz) { ... } 11 | export default BazBazBaz; 12 | 13 | // DON'T 14 | (src/foo/bar/BazBazBaz.ts) 15 | (src/foo/bar/baz-baz-baz.ts) 16 | ``` 17 | 18 | * Inline interface declarations and module imports block should have spaces inside braces, 19 | object literals should not: 20 | 21 | ``` 22 | // DO 23 | import { barBaz, BazBazBaz } from '../bar/bazBazBaz'; 24 | const point: { x: number; y: number; } = {x: 42, y: 10}; 25 | export { point }; 26 | 27 | // DON'T 28 | import {barBaz, BazBazBaz} from '../bar/bazBazBaz'; 29 | const point: {x: number; y: number;} = { x: 42, y: 10; }; 30 | export {point}; 31 | ``` 32 | 33 | * Don't use parenthesis around lambda function with single type inferenced argument: 34 | 35 | ``` 36 | // DO 37 | items.map(item => ...) 38 | 39 | // DON'T 40 | items.map((item) => ...) 41 | ``` 42 | 43 | * Use const keyword to declare variables by default instead of let if you are not intended to modify it. 44 | 45 | * Declare imports from libraries first, then imports from project other than current module directory, 46 | then modules from current directory: 47 | 48 | ``` 49 | // DO 50 | import * as $ from 'jquery'; 51 | import { keyBy } from 'lodash'; 52 | 53 | import { BazBazBaz } from '../bar/bazBazBaz'; 54 | 55 | import { Foo } from './foo'; 56 | import { frob } from './frob'; 57 | 58 | // DON'T 59 | import { Foo } from './foo'; 60 | import * as $ from 'jquery'; 61 | import { BazBazBaz } from '../bar/bazBazBaz'; 62 | import { keyBy } from 'lodash'; 63 | import { Foo } from './frob'; 64 | ``` 65 | -------------------------------------------------------------------------------- /dts-bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-explorer", 3 | "main": "dist/temp/dts/index.d.ts", 4 | "out": "../../graph-explorer.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from "@eslint/js"; 2 | import prettierConfig from "eslint-config-prettier"; 3 | import stylisticTs from "@stylistic/eslint-plugin-ts"; 4 | import tseslint from "typescript-eslint"; 5 | 6 | export default tseslint.config( 7 | eslint.configs.recommended, 8 | tseslint.configs.strict, 9 | tseslint.configs.stylistic, 10 | { 11 | plugins: { 12 | "@stylistic/ts": stylisticTs 13 | }, 14 | ignores: [ 15 | "node_modules", 16 | "dist", 17 | "webpack.config.js", 18 | ], 19 | rules: { 20 | "@stylistic/ts/indent": ["error", 2], 21 | "@stylistic/ts/semi": ["error", "always"], 22 | "@typescript-eslint/no-unused-vars": ["error", { 23 | argsIgnorePattern: "^_", 24 | varsIgnorePattern: "^_", 25 | caughtErrorsIgnorePattern: "^_", 26 | destructuredArrayIgnorePattern: "^_", 27 | }], 28 | "@typescript-eslint/no-empty-object-type": "off", 29 | "@typescript-eslint/no-explicit-any": "off", 30 | "@typescript-eslint/prefer-literal-enum-member": "off", 31 | } 32 | }, 33 | prettierConfig, 34 | ); 35 | -------------------------------------------------------------------------------- /examples/common.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SerializedDiagram, 3 | convertToSerializedDiagram, 4 | } from "../src/graph-explorer/index"; 5 | 6 | export function onPageLoad(callback: (container: HTMLDivElement) => void) { 7 | document.addEventListener("DOMContentLoaded", () => { 8 | const container = document.createElement("div"); 9 | container.id = "root"; 10 | document.body.appendChild(container); 11 | callback(container); 12 | }); 13 | } 14 | 15 | export function tryLoadLayoutFromLocalStorage(): SerializedDiagram | undefined { 16 | if (window.location.hash.length > 1) { 17 | try { 18 | const key = window.location.hash.substring(1); 19 | const unparsedLayout = localStorage.getItem(key); 20 | const entry = unparsedLayout && JSON.parse(unparsedLayout); 21 | 22 | // backward compatibility test. If we encounder old diagram, 23 | // wrap it into Diagram interface, jsonld - pass through 24 | if (entry["@context"]) { 25 | return entry; 26 | } else { 27 | return convertToSerializedDiagram({ 28 | layoutData: entry, 29 | linkTypeOptions: [], 30 | }); 31 | } 32 | } catch (_e) { 33 | /* ignore */ 34 | } 35 | } 36 | return undefined; 37 | } 38 | 39 | export function saveLayoutToLocalStorage(diagram: SerializedDiagram): string { 40 | const randomKey = Math.floor((1 + Math.random()) * 0x10000000000) 41 | .toString(16) 42 | .substring(1); 43 | localStorage.setItem(randomKey, JSON.stringify(diagram)); 44 | return randomKey; 45 | } 46 | -------------------------------------------------------------------------------- /examples/dbpedia.ts: -------------------------------------------------------------------------------- 1 | import { createElement, ClassAttributes } from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import { 5 | Workspace, 6 | WorkspaceProps, 7 | SparqlDataProvider, 8 | SparqlQueryMethod, 9 | DBPediaSettings, 10 | } from "../src/graph-explorer/index"; 11 | 12 | import { 13 | onPageLoad, 14 | tryLoadLayoutFromLocalStorage, 15 | saveLayoutToLocalStorage, 16 | } from "./common"; 17 | 18 | function onWorkspaceMounted(workspace: Workspace) { 19 | if (!workspace) { 20 | return; 21 | } 22 | 23 | const diagram = tryLoadLayoutFromLocalStorage(); 24 | workspace.getModel().importLayout({ 25 | diagram, 26 | validateLinks: true, 27 | dataProvider: new SparqlDataProvider( 28 | { 29 | endpointUrl: "https://dbpedia.org/sparql", 30 | imagePropertyUris: [ 31 | "http://xmlns.com/foaf/0.1/depiction", 32 | "http://xmlns.com/foaf/0.1/img", 33 | ], 34 | queryMethod: SparqlQueryMethod.GET, 35 | }, 36 | { 37 | ...DBPediaSettings, 38 | ...{ 39 | classTreeQuery: ` 40 | PREFIX rdfs: 41 | PREFIX rdf: 42 | PREFIX owl: 43 | 44 | SELECT distinct ?class ?label ?parent WHERE { 45 | ?class a owl:Class. 46 | ?class rdfs:label ?label. 47 | OPTIONAL {?class rdfs:subClassOf ?parent} 48 | }`, 49 | }, 50 | } 51 | ), 52 | }); 53 | } 54 | 55 | const props: WorkspaceProps & ClassAttributes = { 56 | ref: onWorkspaceMounted, 57 | onSaveDiagram: (workspace) => { 58 | const diagram = workspace.getModel().exportLayout(); 59 | window.location.hash = saveLayoutToLocalStorage(diagram); 60 | window.location.reload(); 61 | }, 62 | viewOptions: { 63 | onIriClick: ({ iri }) => window.open(iri), 64 | }, 65 | }; 66 | 67 | onPageLoad((container) => { 68 | ReactDOM.render(createElement(Workspace, props), container); 69 | }); 70 | -------------------------------------------------------------------------------- /examples/demo.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | import { createElement, ClassAttributes } from "react"; 4 | import * as ReactDOM from "react-dom"; 5 | 6 | import { 7 | Workspace, 8 | WorkspaceProps, 9 | DemoDataProvider, 10 | } from "../src/graph-explorer/index"; 11 | 12 | import { 13 | onPageLoad, 14 | tryLoadLayoutFromLocalStorage, 15 | saveLayoutToLocalStorage, 16 | } from "./common"; 17 | 18 | const CLASSES = require("./resources/classes.json"); 19 | const LINK_TYPES = require("./resources/linkTypes.json"); 20 | const ELEMENTS = require("./resources/elements.json"); 21 | const LINKS = require("./resources/links.json"); 22 | 23 | function onWorkspaceMounted(workspace: Workspace) { 24 | if (!workspace) { 25 | return; 26 | } 27 | 28 | const diagram = tryLoadLayoutFromLocalStorage(); 29 | workspace.getModel().importLayout({ 30 | diagram, 31 | dataProvider: new DemoDataProvider(CLASSES, LINK_TYPES, ELEMENTS, LINKS), 32 | validateLinks: true, 33 | }); 34 | } 35 | 36 | const props: WorkspaceProps & ClassAttributes = { 37 | ref: onWorkspaceMounted, 38 | onSaveDiagram: (workspace) => { 39 | const diagram = workspace.getModel().exportLayout(); 40 | window.location.hash = saveLayoutToLocalStorage(diagram); 41 | window.location.reload(); 42 | }, 43 | viewOptions: { 44 | onIriClick: ({ iri }) => window.open(iri), 45 | }, 46 | }; 47 | 48 | onPageLoad((container) => { 49 | ReactDOM.render(createElement(Workspace, props), container); 50 | }); 51 | -------------------------------------------------------------------------------- /examples/envendpoint.ts: -------------------------------------------------------------------------------- 1 | import { createElement, ClassAttributes } from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import { 5 | Workspace, 6 | WorkspaceProps, 7 | SparqlDataProvider, 8 | SparqlQueryMethod, 9 | OWLRDFSSettings, 10 | } from "../src/graph-explorer/index"; 11 | 12 | import { 13 | onPageLoad, 14 | tryLoadLayoutFromLocalStorage, 15 | saveLayoutToLocalStorage, 16 | } from "./common"; 17 | 18 | function onWorkspaceMounted(workspace: Workspace) { 19 | if (!workspace) { 20 | return; 21 | } 22 | 23 | const diagram = tryLoadLayoutFromLocalStorage(); 24 | workspace.getModel().importLayout({ 25 | diagram, 26 | validateLinks: true, 27 | dataProvider: new SparqlDataProvider( 28 | { 29 | // this goes to process.env.SPARQL_ENDPOINT via devServer proxy rule in webpack.demo.config.js 30 | endpointUrl: "../sparql", 31 | 32 | imagePropertyUris: [ 33 | "http://xmlns.com/foaf/0.1/depiction", 34 | "http://xmlns.com/foaf/0.1/img", 35 | ], 36 | queryMethod: SparqlQueryMethod.GET, 37 | }, 38 | { 39 | ...OWLRDFSSettings, 40 | ...{ 41 | classTreeQuery: ` 42 | SELECT distinct ?class ?label ?parent WHERE { 43 | ?class a owl:Class. 44 | OPTIONAL {?class rdfs:label ?label.} 45 | OPTIONAL {?class rdfs:subClassOf ?parent} 46 | }`, 47 | }, 48 | } 49 | ), 50 | }); 51 | } 52 | 53 | const props: WorkspaceProps & ClassAttributes = { 54 | ref: onWorkspaceMounted, 55 | onSaveDiagram: (workspace) => { 56 | const diagram = workspace.getModel().exportLayout(); 57 | window.location.hash = saveLayoutToLocalStorage(diagram); 58 | window.location.reload(); 59 | }, 60 | viewOptions: { 61 | onIriClick: ({ iri }) => window.open(iri), 62 | }, 63 | }; 64 | 65 | onPageLoad((container) => { 66 | ReactDOM.render(createElement(Workspace, props), container); 67 | }); 68 | -------------------------------------------------------------------------------- /examples/rdf.ts.old: -------------------------------------------------------------------------------- 1 | import { createElement, ClassAttributes } from 'react'; 2 | import * as ReactDOM from 'react-dom'; 3 | 4 | import { 5 | Workspace, 6 | WorkspaceProps, 7 | RDFDataProvider, 8 | GroupTemplate, 9 | } from '../src/graph-explorer/index'; 10 | 11 | import { 12 | ExampleMetadataApi, 13 | ExampleValidationApi, 14 | } from './resources/exampleMetadataApi'; 15 | import { 16 | onPageLoad, 17 | tryLoadLayoutFromLocalStorage, 18 | saveLayoutToLocalStorage, 19 | } from './common'; 20 | 21 | const N3Parser: any = require('rdf-parser-n3'); 22 | const RdfXmlParser: any = require('rdf-parser-rdfxml'); 23 | const JsonLdParser: any = require('rdf-parser-jsonld'); 24 | 25 | const data = require('./resources/orgOntology.ttl'); 26 | 27 | function onWorkspaceMounted(workspace: Workspace) { 28 | if (!workspace) { 29 | return; 30 | } 31 | 32 | const dataProvider = new RDFDataProvider({ 33 | data: [ 34 | { 35 | content: data, 36 | type: 'text/turtle', 37 | fileName: 'testData.ttl', 38 | }, 39 | ], 40 | acceptBlankNodes: false, 41 | dataFetching: false, 42 | parsers: { 43 | 'application/rdf+xml': new RdfXmlParser(), 44 | 'application/ld+json': new JsonLdParser(), 45 | 'text/turtle': new N3Parser(), 46 | }, 47 | }); 48 | 49 | const diagram = tryLoadLayoutFromLocalStorage(); 50 | workspace.getModel().importLayout({ 51 | diagram, 52 | validateLinks: true, 53 | dataProvider, 54 | }); 55 | } 56 | 57 | const props: WorkspaceProps & ClassAttributes = { 58 | ref: onWorkspaceMounted, 59 | onSaveDiagram: (workspace) => { 60 | const diagram = workspace.getModel().exportLayout(); 61 | window.location.hash = saveLayoutToLocalStorage(diagram); 62 | window.location.reload(); 63 | }, 64 | onPersistChanges: (workspace) => { 65 | const state = workspace.getEditor().authoringState; 66 | // tslint:disable-next-line:no-console 67 | console.log('Authoring state:', state); 68 | }, 69 | metadataApi: new ExampleMetadataApi(), 70 | validationApi: new ExampleValidationApi(), 71 | viewOptions: { 72 | onIriClick: ({ iri }) => window.open(iri), 73 | groupBy: [ 74 | { 75 | linkType: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', 76 | linkDirection: 'in', 77 | }, 78 | ], 79 | }, 80 | elementTemplateResolver: (types) => { 81 | if (types.length === 0) { 82 | // use group template only for classes 83 | return GroupTemplate; 84 | } 85 | return undefined; 86 | }, 87 | }; 88 | 89 | onPageLoad((container) => { 90 | ReactDOM.render(createElement(Workspace, props), container); 91 | }); 92 | -------------------------------------------------------------------------------- /examples/resources/linkTypes.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "http://ailab.ifmo.ru/dialog/tv/schema#own", 4 | "label": { 5 | "values": [ 6 | { 7 | "value": "own", 8 | "language": "" 9 | } 10 | ] 11 | }, 12 | "count": 0 13 | }, 14 | { 15 | "id": "http://www.w3.org/2002/07/owl#disjointWith", 16 | "label": { 17 | "values": [ 18 | { 19 | "value": "disjointWith", 20 | "language": "" 21 | } 22 | ] 23 | }, 24 | "count": 8 25 | }, 26 | { 27 | "id": "http://www.w3.org/1999/02/22-rdf-syntax-ns#type", 28 | "label": { 29 | "values": [ 30 | { 31 | "value": "type", 32 | "language": "" 33 | } 34 | ] 35 | }, 36 | "count": 93 37 | }, 38 | { 39 | "id": "http://ailab.ifmo.ru/dialog/tv/schema#isClientOf", 40 | "label": { 41 | "values": [ 42 | { 43 | "value": "isClientOf", 44 | "language": "" 45 | } 46 | ] 47 | }, 48 | "count": 0 49 | }, 50 | { 51 | "id": "http://www.w3.org/2000/01/rdf-schema#domain", 52 | "label": { 53 | "values": [ 54 | { 55 | "value": "domain", 56 | "language": "" 57 | } 58 | ] 59 | }, 60 | "count": 33 61 | }, 62 | { 63 | "id": "http://www.w3.org/2000/01/rdf-schema#subPropertyOf", 64 | "label": { 65 | "values": [ 66 | { 67 | "value": "subPropertyOf", 68 | "language": "" 69 | } 70 | ] 71 | }, 72 | "count": 2 73 | }, 74 | { 75 | "id": "http://ailab.ifmo.ru/dialog/tv/schema#rent", 76 | "label": { 77 | "values": [ 78 | { 79 | "value": "rent", 80 | "language": "" 81 | } 82 | ] 83 | }, 84 | "count": 0 85 | }, 86 | { 87 | "id": "http://www.w3.org/2000/01/rdf-schema#subClassOf", 88 | "label": { 89 | "values": [ 90 | { 91 | "value": "subClassOf", 92 | "language": "" 93 | } 94 | ] 95 | }, 96 | "count": 45 97 | }, 98 | { 99 | "id": "http://www.w3.org/2000/01/rdf-schema#range", 100 | "label": { 101 | "values": [ 102 | { 103 | "value": "range", 104 | "language": "" 105 | } 106 | ] 107 | }, 108 | "count": 33 109 | } 110 | ] 111 | -------------------------------------------------------------------------------- /examples/template.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= htmlWebpackPlugin.options.title %> 7 | 8 | 23 | 24 | 25 | 34 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /examples/toolbarCustomization.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | import * as React from "react"; 4 | import * as ReactDOM from "react-dom"; 5 | 6 | import { 7 | Workspace, 8 | WorkspaceProps, 9 | DemoDataProvider, 10 | ToolbarProps, 11 | } from "../src/graph-explorer/index"; 12 | import { 13 | onPageLoad, 14 | tryLoadLayoutFromLocalStorage, 15 | saveLayoutToLocalStorage, 16 | } from "./common"; 17 | 18 | const CLASSES = require("./resources/classes.json"); 19 | const LINK_TYPES = require("./resources/linkTypes.json"); 20 | const ELEMENTS = require("./resources/elements.json"); 21 | const LINKS = require("./resources/links.json"); 22 | 23 | export interface Props extends ToolbarProps { 24 | onExampleClick?: () => void; 25 | } 26 | 27 | const CLASS_NAME = "graph-explorer-toolbar"; 28 | 29 | export class Toolbar extends React.Component { 30 | render() { 31 | return ( 32 |
33 |
34 | 35 | 38 | 39 | 50 | 57 | 58 | 59 |
60 |
61 | ); 62 | } 63 | } 64 | 65 | function onWorkspaceMounted(workspace: Workspace) { 66 | if (!workspace) { 67 | return; 68 | } 69 | 70 | const model = workspace.getModel(); 71 | 72 | const diagram = tryLoadLayoutFromLocalStorage(); 73 | model.importLayout({ 74 | dataProvider: new DemoDataProvider(CLASSES, LINK_TYPES, ELEMENTS, LINKS), 75 | diagram, 76 | validateLinks: true, 77 | }); 78 | } 79 | 80 | const props: WorkspaceProps & React.ClassAttributes = { 81 | ref: onWorkspaceMounted, 82 | onSaveDiagram: (workspace) => { 83 | const diagram = workspace.getModel().exportLayout(); 84 | window.location.hash = saveLayoutToLocalStorage(diagram); 85 | window.location.reload(); 86 | }, 87 | toolbar: ( 88 | { 90 | alert("Example button has been pressed!"); 91 | }} 92 | /> 93 | ), 94 | }; 95 | 96 | onPageLoad((container) => { 97 | ReactDOM.render(React.createElement(Workspace, props), container); 98 | }); 99 | -------------------------------------------------------------------------------- /examples/wikidata.ts: -------------------------------------------------------------------------------- 1 | import { createElement, ClassAttributes } from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import { 5 | Workspace, 6 | WorkspaceProps, 7 | SparqlDataProvider, 8 | WikidataSettings, 9 | PropertySuggestionParams, 10 | PropertyScore, 11 | } from "../src/graph-explorer/index"; 12 | 13 | import { 14 | onPageLoad, 15 | tryLoadLayoutFromLocalStorage, 16 | saveLayoutToLocalStorage, 17 | } from "./common"; 18 | 19 | const WIKIDATA_PREFIX = "http://www.wikidata.org/prop/direct/"; 20 | 21 | let workspace: Workspace; 22 | 23 | function getElementLabel(id: string): string { 24 | const model = workspace.getModel(); 25 | const view = workspace.getDiagram(); 26 | const element = model.getElement(id); 27 | return element 28 | ? view.formatLabel(element.data.label.values, element.iri) 29 | : ""; 30 | } 31 | 32 | function wikidataSuggestProperties(params: PropertySuggestionParams) { 33 | const idMap: Record = {}; 34 | 35 | const properties = params.properties.map((id) => { 36 | let resultID; 37 | if (id.startsWith(WIKIDATA_PREFIX)) { 38 | resultID = id.substr(WIKIDATA_PREFIX.length, id.length); 39 | } else { 40 | resultID = id; 41 | } 42 | idMap[resultID] = id; 43 | return resultID; 44 | }); 45 | const term = params.token.toLowerCase() || getElementLabel(params.elementId); 46 | const requestBody = { 47 | threshold: 0.1, 48 | term, 49 | 50 | instance_properties: properties, 51 | }; 52 | return fetch("/wikidata-prop-suggest", { 53 | method: "POST", 54 | body: JSON.stringify(requestBody), 55 | credentials: "same-origin", 56 | mode: "cors", 57 | cache: "default", 58 | }) 59 | .then((response) => { 60 | if (response.ok) { 61 | return response.json(); 62 | } else { 63 | const error = new Error(response.statusText); 64 | (error as any).response = response; 65 | throw error; 66 | } 67 | }) 68 | .then((json) => { 69 | const dictionary: Record = {}; 70 | for (const scoredItem of json.data) { 71 | const propertyIri = idMap[scoredItem.id]; 72 | const item = dictionary[propertyIri]; 73 | 74 | if (item && item.score > scoredItem.value) { 75 | continue; 76 | } 77 | 78 | dictionary[propertyIri] = { propertyIri, score: scoredItem.value }; 79 | } 80 | 81 | Object.keys(idMap).forEach((key) => { 82 | const propertyIri = idMap[key]; 83 | 84 | if (dictionary[propertyIri]) { 85 | return; 86 | } 87 | 88 | dictionary[propertyIri] = { propertyIri, score: 0 }; 89 | }); 90 | 91 | return dictionary; 92 | }); 93 | } 94 | 95 | function onWorkspaceMounted(wspace: Workspace) { 96 | if (!wspace) { 97 | return; 98 | } 99 | 100 | workspace = wspace; 101 | 102 | const diagram = tryLoadLayoutFromLocalStorage(); 103 | const dataProvider = new SparqlDataProvider( 104 | { 105 | endpointUrl: "https://query.wikidata.org/bigdata/namespace/wdq/sparql", 106 | imagePropertyUris: [ 107 | "http://www.wikidata.org/prop/direct/P18", 108 | "http://www.wikidata.org/prop/direct/P154", 109 | ], 110 | }, 111 | WikidataSettings 112 | ); 113 | 114 | workspace 115 | .getModel() 116 | .importLayout({ diagram, dataProvider, validateLinks: true }); 117 | } 118 | 119 | const props: WorkspaceProps & ClassAttributes = { 120 | ref: onWorkspaceMounted, 121 | onSaveDiagram: (self) => { 122 | const diagram = self.getModel().exportLayout(); 123 | window.location.hash = saveLayoutToLocalStorage(diagram); 124 | window.location.reload(); 125 | }, 126 | viewOptions: { 127 | suggestProperties: wikidataSuggestProperties, 128 | onIriClick: ({ iri }) => window.open(iri), 129 | }, 130 | }; 131 | 132 | onPageLoad((container) => { 133 | ReactDOM.render(createElement(Workspace, props), container); 134 | }); 135 | -------------------------------------------------------------------------------- /examples/wikidataGraph.ts: -------------------------------------------------------------------------------- 1 | import { createElement, ClassAttributes } from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | 4 | import { 5 | Workspace, 6 | WorkspaceProps, 7 | SparqlDataProvider, 8 | SparqlGraphBuilder, 9 | WikidataSettings, 10 | } from "../src/graph-explorer/index"; 11 | 12 | import { onPageLoad } from "./common"; 13 | 14 | function onWorkspaceMounted(workspace: Workspace) { 15 | if (!workspace) { 16 | return; 17 | } 18 | 19 | const dataProvider = new SparqlDataProvider( 20 | { 21 | endpointUrl: "https://query.wikidata.org/bigdata/namespace/wdq/sparql", 22 | imagePropertyUris: [ 23 | "http://www.wikidata.org/prop/direct/P18", 24 | "http://www.wikidata.org/prop/direct/P154", 25 | ], 26 | }, 27 | WikidataSettings 28 | ); 29 | const graphBuilder = new SparqlGraphBuilder(dataProvider); 30 | 31 | const loadingGraph = graphBuilder.getGraphFromConstruct(` 32 | CONSTRUCT { ?current ?p ?o. } 33 | WHERE { 34 | { 35 | ?current ?p ?o. 36 | ?p ?label. 37 | FILTER(ISIRI(?o)) 38 | FILTER exists{?o ?p1 ?o2} 39 | } 40 | } 41 | LIMIT 20 42 | VALUES (?current) { 43 | () 44 | }`); 45 | workspace.showWaitIndicatorWhile(loadingGraph); 46 | 47 | loadingGraph 48 | .then(({ diagram, preloadedElements }) => 49 | workspace 50 | .getModel() 51 | .importLayout({ diagram, preloadedElements, dataProvider }) 52 | ) 53 | .then(() => { 54 | workspace.forceLayout(); 55 | workspace.zoomToFit(); 56 | }); 57 | } 58 | 59 | const props: WorkspaceProps & ClassAttributes = { 60 | ref: onWorkspaceMounted, 61 | viewOptions: { 62 | onIriClick: ({ iri }) => window.open(iri), 63 | }, 64 | }; 65 | 66 | onPageLoad((container) => { 67 | ReactDOM.render(createElement(Workspace, props), container); 68 | }); 69 | -------------------------------------------------------------------------------- /images/add-to-filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/add-to-filter.png -------------------------------------------------------------------------------- /images/arrow-bottom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-bottom.png -------------------------------------------------------------------------------- /images/arrow-bottom1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-bottom1.png -------------------------------------------------------------------------------- /images/arrow-left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-left.png -------------------------------------------------------------------------------- /images/arrow-left1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-left1.png -------------------------------------------------------------------------------- /images/arrow-right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-right.png -------------------------------------------------------------------------------- /images/arrow-right1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-right1.png -------------------------------------------------------------------------------- /images/arrow-top.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-top.png -------------------------------------------------------------------------------- /images/arrow-top1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/arrow-top1.png -------------------------------------------------------------------------------- /images/close-connections.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /images/collapse-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/collapse-properties.png -------------------------------------------------------------------------------- /images/connections.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 12 | 15 | 16 | -------------------------------------------------------------------------------- /images/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /images/delete_source.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 29 | 31 | 54 | 60 | 106 | 107 | -------------------------------------------------------------------------------- /images/direction-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/direction-in.png -------------------------------------------------------------------------------- /images/direction-out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/direction-out.png -------------------------------------------------------------------------------- /images/expand-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/expand-link.png -------------------------------------------------------------------------------- /images/expand-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/expand-properties.png -------------------------------------------------------------------------------- /images/expand-properties_100x100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/expand-properties_100x100.png -------------------------------------------------------------------------------- /images/font-awesome/certificate-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/cog-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/exclamation-triangle.svg: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /images/font-awesome/minus-square-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/pen-square-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/plug.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/plus-square-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/times-circle-regular.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/trash-alt.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/font-awesome/undo-solid.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /images/icons/class.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | C 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/icons/country.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /images/icons/datatypeProperty.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | DP 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/icons/event.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /images/icons/location.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /images/icons/object.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /images/icons/objectProperty.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | OP 5 | 6 | 7 | -------------------------------------------------------------------------------- /images/icons/organization.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | -------------------------------------------------------------------------------- /images/icons/person.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /images/left-panel-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/left-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/link.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 13 | 14 | -------------------------------------------------------------------------------- /images/remove.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /images/resizable-column-handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zazuko/graph-explorer/923161ca1a622ba2c23d60c1d4bcf3ae293e1a70/images/resizable-column-handle.png -------------------------------------------------------------------------------- /images/right-panel-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/right-panel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /images/tree/collapse-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /images/tree/expand-toggle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /images/tree/leaf-default.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 36 | 38 | 45 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /images/tree/leaf-folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 16 | 36 | 38 | 44 | 51 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-explorer", 3 | "version": "1.3.0", 4 | "description": "Graph Explorer can be used to explore and RDF graphs in SPARQL endpoints or on the web.", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+ssh://git@github.com/zazuko/graph-explorer.git" 8 | }, 9 | "keywords": [ 10 | "owl", 11 | "graph", 12 | "rdf", 13 | "diagram", 14 | "sparql" 15 | ], 16 | "author": { 17 | "name": "RDF Tools", 18 | "url": "https://github.com/zazuko/graph-explorer" 19 | }, 20 | "license": "LGPL-2.1", 21 | "bugs": { 22 | "url": "https://github.com/zazuko/graph-explorer/issues" 23 | }, 24 | "homepage": "https://github.com/zazuko/graph-explorer#readme", 25 | "scripts": { 26 | "demo": "./node_modules/.bin/webpack-dev-server --port 10444 --config webpack.demo.config.js", 27 | "build": "npm run _webpack && npm run typings", 28 | "build-all": "npm run _webpack && BUNDLE_PEERS=true npm run _webpack", 29 | "build-examples": "./node_modules/.bin/webpack --config webpack.demo.config.js", 30 | "typings": "npm run _typings-tsc && npm run _typings-dts-bundle", 31 | "lint": "eslint './{src,examples}/**/*.{ts,tsx}'", 32 | "_typings-tsc": "./node_modules/.bin/tsc --project tsconfig.typings.json", 33 | "_typings-dts-bundle": "./node_modules/.bin/dts-bundle --configJson dts-bundle.json", 34 | "_webpack": "./node_modules/.bin/webpack" 35 | }, 36 | "dependencies": { 37 | "@rdfjs/data-model": "^2.1.0", 38 | "@rdfjs/types": "^2.0.1", 39 | "@types/n3": "^1.21.1", 40 | "@types/rdfjs__data-model": "^2.0.9", 41 | "@types/sparqljs": "^3.1.3", 42 | "d3-color": "^3.1.0", 43 | "file-saverjs": "~1.3.6", 44 | "lodash": "~4.17.21", 45 | "n3": "^1.23.1", 46 | "sparql-http-client": "^3.0.1", 47 | "sparqljs": "^3.6.2", 48 | "webcola": "^3.4.0" 49 | }, 50 | "peerDependencies": { 51 | "react": "^16.0.0", 52 | "react-dom": "^16.0.0" 53 | }, 54 | "devDependencies": { 55 | "@babel/core": "^7.13.10", 56 | "@babel/plugin-transform-runtime": "^7.13.10", 57 | "@babel/preset-env": "^7.13.10", 58 | "@babel/preset-react": "^7.12.13", 59 | "@babel/preset-typescript": "^7.13.0", 60 | "@babel/runtime": "^7.13.10", 61 | "@eslint/js": "^9.21.0", 62 | "@stylistic/eslint-plugin": "^4.2.0", 63 | "@stylistic/eslint-plugin-ts": "^4.2.0", 64 | "@types/d3-color": "^1.2.2", 65 | "@types/lodash": "^4.14.191", 66 | "@types/node": "^22.13.9", 67 | "@types/react-dom": "^16.0.0", 68 | "@types/sparql-http-client": "^3.0.5", 69 | "@typescript-eslint/eslint-plugin": "^8.25.0", 70 | "css-loader": "^7.1.2", 71 | "dts-bundle": "^0.7.3", 72 | "eslint": "^9.21.0", 73 | "eslint-config-prettier": "^10.0.2", 74 | "html-webpack-plugin": "^5.6.3", 75 | "node-polyfill-webpack-plugin": "^4.1.0", 76 | "raw-loader": "0.5.1", 77 | "react": "^16.0.0", 78 | "react-dom": "^16.0.0", 79 | "sass": "^1.85.1", 80 | "sass-loader": "^16.0.5", 81 | "style-loader": "0.23.0", 82 | "ts-loader": "9.5.2", 83 | "tslib": "2.8.1", 84 | "typescript": "^5.8.2", 85 | "typescript-eslint": "^8.26.0", 86 | "url-loader": "4.1.1", 87 | "webpack": "^5.98.0", 88 | "webpack-cli": "^6.0.1", 89 | "webpack-dev-server": "^5.2.0" 90 | }, 91 | "resolutions": { 92 | "@types/react": "15.6.9" 93 | }, 94 | "main": "dist/graph-explorer.js", 95 | "typings": "dist/graph-explorer.d.ts" 96 | } 97 | -------------------------------------------------------------------------------- /schema/context-v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "@context": { 3 | "graph-explorer": "http://graph-explorer.org/schema/v1#", 4 | "xsd": "http://www.w3.org/2001/XMLSchema#", 5 | "Diagram": "graph-explorer:Diagram", 6 | "layoutData": "graph-explorer:layoutData", 7 | "linkTypeOptions": "graph-explorer:linkTypeOptions", 8 | "Layout": "graph-explorer:Layout", 9 | "elements": { 10 | "@id": "graph-explorer:hasElement", 11 | "@container": "@set" 12 | }, 13 | "links": { 14 | "@id": "graph-explorer:hasLink", 15 | "@container": "@set" 16 | }, 17 | "Element": "graph-explorer:Element", 18 | "iri": { 19 | "@id": "graph-explorer:resource", 20 | "@type": "@id" 21 | }, 22 | "position": "graph-explorer:position", 23 | "x": { 24 | "@id": "graph-explorer:xCoordValue" 25 | }, 26 | "y": { 27 | "@id": "graph-explorer:yCoordValue" 28 | }, 29 | "size": "graph-explorer:size", 30 | "height": { 31 | "@id": "graph-explorer:height" 32 | }, 33 | "width": { 34 | "@id": "graph-explorer:width" 35 | }, 36 | "isExpanded": { 37 | "@id": "graph-explorer:isExpanded" 38 | }, 39 | "Link": "graph-explorer:Link", 40 | "property": { 41 | "@id": "graph-explorer:property", 42 | "@type": "@id" 43 | }, 44 | "source": "graph-explorer:source", 45 | "target": "graph-explorer:target", 46 | "vertices": { 47 | "@id": "graph-explorer:vertex", 48 | "@container": "@list" 49 | }, 50 | "LinkTypeOptions": "graph-explorer:LinkTypeOptions", 51 | "visible": "graph-explorer:visible", 52 | "showLabel": "graph-explorer:showLabel", 53 | "elementState": "graph-explorer:elementState", 54 | "linkState": "graph-explorer:linkState" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/defaultLinkStyles.ts: -------------------------------------------------------------------------------- 1 | import { LinkTemplate, LinkTemplateResolver } from "./props"; 2 | import { PLACEHOLDER_LINK_TYPE } from "../data/schema"; 3 | 4 | export const LINK_SHOW_IRI: LinkTemplate = { 5 | renderLink: (link) => ({ 6 | properties: [ 7 | { 8 | position: 0.5, 9 | attrs: { 10 | text: { 11 | text: [ 12 | { 13 | value: link.typeId, 14 | language: "", 15 | }, 16 | ], 17 | fill: "gray", 18 | "font-size": 12, 19 | "font-weight": "lighter", 20 | }, 21 | }, 22 | }, 23 | ], 24 | }), 25 | }; 26 | 27 | const LINK_SUB_CLASS_OF: LinkTemplate = { 28 | markerTarget: { 29 | fill: "#f8a485", 30 | stroke: "#cf8e76", 31 | }, 32 | renderLink: () => ({ 33 | connection: { 34 | stroke: "#f8a485", 35 | "stroke-width": 2, 36 | }, 37 | }), 38 | }; 39 | 40 | const LINK_DOMAIN: LinkTemplate = { 41 | markerTarget: { 42 | fill: "#34c7f3", 43 | stroke: "#38b5db", 44 | }, 45 | renderLink: () => ({ 46 | connection: { 47 | stroke: "#34c7f3", 48 | "stroke-width": 2, 49 | }, 50 | }), 51 | }; 52 | 53 | const LINK_RANGE: LinkTemplate = { 54 | markerTarget: { 55 | fill: "#34c7f3", 56 | stroke: "#38b5db", 57 | }, 58 | renderLink: () => ({ 59 | connection: { 60 | stroke: "#34c7f3", 61 | "stroke-width": 2, 62 | }, 63 | }), 64 | }; 65 | 66 | const LINK_TYPE_OF: LinkTemplate = { 67 | markerTarget: { 68 | fill: "#8cd965", 69 | stroke: "#5b9a3b", 70 | }, 71 | renderLink: () => ({ 72 | connection: { 73 | stroke: "#8cd965", 74 | "stroke-width": 2, 75 | }, 76 | }), 77 | }; 78 | 79 | export const DefaultLinkTemplateBundle: LinkTemplateResolver = (type) => { 80 | if (type === "http://www.w3.org/2000/01/rdf-schema#subClassOf") { 81 | return LINK_SUB_CLASS_OF; 82 | } else if (type === "http://www.w3.org/2000/01/rdf-schema#domain") { 83 | return LINK_DOMAIN; 84 | } else if (type === "http://www.w3.org/2000/01/rdf-schema#range") { 85 | return LINK_RANGE; 86 | } else if (type === "http://www.w3.org/1999/02/22-rdf-syntax-ns#type") { 87 | return LINK_TYPE_OF; 88 | } else if (type === PLACEHOLDER_LINK_TYPE) { 89 | return { markerTarget: { fill: "none" } }; 90 | } else { 91 | return undefined; 92 | } 93 | }; 94 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/defaultTypeStyles.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-require-imports */ 2 | 3 | import { TypeStyleResolver } from "./props"; 4 | 5 | const classIcon = require("../../../images/icons/class.svg").default as string; 6 | const objectPropertyIcon = require("../../../images/icons/objectProperty.svg") 7 | .default as string; 8 | const datatypePropertyIcon = 9 | require("../../../images/icons/datatypeProperty.svg").default as string; 10 | const personIcon = require("../../../images/icons/person.svg") 11 | .default as string; 12 | const countryIcon = require("../../../images/icons/country.svg") 13 | .default as string; 14 | const organizationIcon = require("../../../images/icons/organization.svg") 15 | .default as string; 16 | const locationIcon = require("../../../images/icons/location.svg") 17 | .default as string; 18 | const eventIcon = require("../../../images/icons/event.svg").default as string; 19 | const objectIcon = require("../../../images/icons/object.svg") 20 | .default as string; 21 | 22 | export const DefaultTypeStyleBundle: TypeStyleResolver = (types) => { 23 | if ( 24 | types.indexOf("http://www.w3.org/2002/07/owl#Class") !== -1 || 25 | types.indexOf("http://www.w3.org/2000/01/rdf-schema#Class") !== -1 26 | ) { 27 | return { color: "#eaac77", icon: classIcon }; 28 | } else if ( 29 | types.indexOf("http://www.w3.org/2002/07/owl#ObjectProperty") !== -1 30 | ) { 31 | return { color: "#34c7f3", icon: objectPropertyIcon }; 32 | } else if ( 33 | types.indexOf("http://www.w3.org/2002/07/owl#DatatypeProperty") !== -1 34 | ) { 35 | return { color: "#34c7f3", icon: datatypePropertyIcon }; 36 | } else if ( 37 | types.indexOf("http://xmlns.com/foaf/0.1/Person") !== -1 || 38 | types.indexOf("http://www.wikidata.org/entity/Q5") !== -1 39 | ) { 40 | return { color: "#eb7777", icon: personIcon }; 41 | } else if (types.indexOf("http://www.wikidata.org/entity/Q6256") !== -1) { 42 | return { color: "#77ca98", icon: countryIcon }; 43 | } else if ( 44 | types.indexOf("http://schema.org/Organization") !== -1 || 45 | types.indexOf("http://dbpedia.org/ontology/Organisation") !== -1 || 46 | types.indexOf("http://xmlns.com/foaf/0.1/Organization") !== -1 || 47 | types.indexOf("http://www.wikidata.org/entity/Q43229") !== -1 48 | ) { 49 | return { color: "#77ca98", icon: organizationIcon }; 50 | } else if (types.indexOf("http://www.wikidata.org/entity/Q618123") !== -1) { 51 | return { color: "#bebc71", icon: locationIcon }; 52 | } else if (types.indexOf("http://www.wikidata.org/entity/Q1190554") !== -1) { 53 | return { color: "#b4b1fb", icon: eventIcon }; 54 | } else if (types.indexOf("http://www.wikidata.org/entity/Q488383") !== -1) { 55 | return { color: "#53ccb2", icon: objectIcon }; 56 | } else { 57 | return undefined; 58 | } 59 | }; 60 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/props.ts: -------------------------------------------------------------------------------- 1 | import { ComponentClass } from "react"; 2 | import { DiagramModel } from "../diagram/model"; 3 | 4 | import { 5 | ElementIri, 6 | ElementModel, 7 | Dictionary, 8 | LocalizedString, 9 | Property, 10 | } from "../data/model"; 11 | import { Link } from "../diagram/elements"; 12 | 13 | export type TypeStyleResolver = ( 14 | types: string[] 15 | ) => CustomTypeStyle | undefined; 16 | export type LinkTemplateResolver = ( 17 | linkType: string 18 | ) => LinkTemplate | undefined; 19 | export type TemplateResolver = (types: string[]) => ElementTemplate | undefined; 20 | 21 | export interface CustomTypeStyle { 22 | color?: string; 23 | icon?: string; 24 | } 25 | 26 | export type ElementTemplate = ComponentClass; 27 | 28 | export interface TemplateProps { 29 | elementId: string; 30 | data: ElementModel; 31 | iri: ElementIri; 32 | types: string; 33 | label: string; 34 | color: any; 35 | iconUrl: string; 36 | imgUrl?: string; 37 | isExpanded?: boolean; 38 | propsAsList?: PropArray; 39 | props?: Dictionary; 40 | } 41 | 42 | export type PropArray = { 43 | id: string; 44 | name: string; 45 | property: Property; 46 | }[]; 47 | 48 | export interface LinkTemplate { 49 | markerSource?: LinkMarkerStyle; 50 | markerTarget?: LinkMarkerStyle; 51 | renderLink?(link: Link): LinkStyle; 52 | setLinkLabel?: (link: Link, label: string) => void; 53 | } 54 | 55 | export interface LinkStyle { 56 | connection?: { 57 | fill?: string; 58 | stroke?: string; 59 | "stroke-width"?: number; 60 | "stroke-dasharray"?: string; 61 | }; 62 | label?: LinkLabel; 63 | properties?: LinkLabel[]; 64 | connector?: { name?: string; args?: {} }; 65 | } 66 | 67 | export interface LinkRouter { 68 | route(model: DiagramModel): RoutedLinks; 69 | } 70 | 71 | export type RoutedLinks = Map; 72 | 73 | export interface RoutedLink { 74 | linkId: string; 75 | vertices: readonly Vertex[]; 76 | labelTextAnchor?: "start" | "middle" | "end"; 77 | } 78 | 79 | export interface Vertex { 80 | x: number; 81 | y: number; 82 | } 83 | 84 | export interface LinkMarkerStyle { 85 | fill?: string; 86 | stroke?: string; 87 | strokeWidth?: string; 88 | d?: string; 89 | width?: number; 90 | height?: number; 91 | } 92 | 93 | export interface LinkLabel { 94 | position?: number; 95 | title?: string; 96 | attrs?: { 97 | rect?: { 98 | fill?: string; 99 | stroke?: string; 100 | "stroke-width"?: number; 101 | }; 102 | text?: { 103 | fill?: string; 104 | stroke?: string; 105 | "stroke-width"?: number; 106 | "font-family"?: string; 107 | "font-size"?: string | number; 108 | "font-weight"?: "normal" | "bold" | "lighter" | "bolder" | number; 109 | text?: LocalizedString[]; 110 | }; 111 | }; 112 | } 113 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/templates/default.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { TemplateProps } from "../props"; 4 | 5 | import { getPropertyValues } from "./utils"; 6 | 7 | const CLASS_NAME = "graph-explorer-default-template"; 8 | 9 | export class DefaultElementTemplate extends React.Component { 10 | render() { 11 | const props = this.props; 12 | 13 | const image = props.imgUrl ? ( 14 |
15 | 16 |
17 | ) : undefined; 18 | 19 | const expander = props.isExpanded ? ( 20 |
21 |
22 |
23 | IRI: 24 |
25 | 34 |
35 |
36 | {this.renderPropertyTable()} 37 |
38 | ) : null; 39 | 40 | return ( 41 |
49 |
53 | 59 |
63 |
64 | {props.types} 65 |
66 |
67 |
68 | {image} 69 |
73 | 77 | {props.label} 78 | 79 | {expander} 80 |
81 |
82 | ); 83 | } 84 | 85 | renderPropertyTable() { 86 | const { propsAsList } = this.props; 87 | if (propsAsList && propsAsList.length > 0) { 88 | return ( 89 |
90 | {propsAsList.map((prop) => { 91 | const propertyValues = getPropertyValues(prop.property); 92 | const values = propertyValues.map((text, index) => ( 93 |
98 | {text} 99 |
100 | )); 101 | return ( 102 |
106 |
110 | {prop.name} 111 |
112 |
113 | {values} 114 |
115 |
116 | ); 117 | })} 118 |
119 | ); 120 | } else { 121 | return
no properties
; 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/templates/group.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { TemplateProps } from "../props"; 4 | import { EmbeddedLayer } from "../../diagram/embeddedLayer"; 5 | 6 | const CLASS = "graph-explorer-group-template"; 7 | 8 | export class GroupTemplate extends React.Component { 9 | render() { 10 | const { label, iconUrl, types, color, isExpanded } = this.props; 11 | 12 | return ( 13 |
14 |
21 |
22 |
23 | 24 |
25 |
26 |
{types}
27 |
28 |
29 |
30 | 31 | {label} 32 | 33 | {isExpanded ? ( 34 |
35 | 36 |
37 | ) : null} 38 |
39 |
40 |
41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/templates/index.ts: -------------------------------------------------------------------------------- 1 | import { TemplateResolver } from "../props"; 2 | 3 | export * from "./default"; 4 | export * from "./group"; 5 | export * from "./standard"; 6 | 7 | export const DefaultElementTemplateBundle: TemplateResolver = (_types) => 8 | undefined; 9 | -------------------------------------------------------------------------------- /src/graph-explorer/customization/templates/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Dictionary, 3 | Property, 4 | isIriProperty, 5 | isLiteralProperty, 6 | } from "../../data/model"; 7 | 8 | export function getProperty(props: Dictionary, id: string) { 9 | if (props && props[id]) { 10 | return getPropertyValues(props[id]).join(", "); 11 | } else { 12 | return undefined; 13 | } 14 | } 15 | 16 | export function getPropertyValues(property: Property): string[] { 17 | if (isIriProperty(property)) { 18 | return property.values.map(({ value }) => value); 19 | } else if (isLiteralProperty(property)) { 20 | return property.values.map(({ value }) => value); 21 | } 22 | return []; 23 | } 24 | -------------------------------------------------------------------------------- /src/graph-explorer/data/metadataApi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElementModel, 3 | ElementTypeIri, 4 | LinkTypeIri, 5 | PropertyTypeIri, 6 | LinkModel, 7 | } from "./model"; 8 | import { LinkDirection } from "../diagram/elements"; 9 | import { CancellationToken } from "../viewUtils/async"; 10 | 11 | export interface MetadataApi { 12 | /** 13 | * Can user create element and link from this element? 14 | */ 15 | canDropOnCanvas( 16 | source: ElementModel, 17 | ct: CancellationToken 18 | ): Promise; 19 | 20 | /** 21 | * Can we create link between two elements? Maybe it's unnesesary. 22 | */ 23 | canDropOnElement( 24 | source: ElementModel, 25 | target: ElementModel, 26 | ct: CancellationToken 27 | ): Promise; 28 | 29 | /** 30 | * Links of which types can we create between elements? 31 | */ 32 | possibleLinkTypes( 33 | source: ElementModel, 34 | target: ElementModel, 35 | ct: CancellationToken 36 | ): Promise; 37 | 38 | /** 39 | * If new element is created by dragging link from existing element, this should return available element types. 40 | */ 41 | typesOfElementsDraggedFrom( 42 | source: ElementModel, 43 | ct: CancellationToken 44 | ): Promise; 45 | 46 | /** 47 | * List properties for type meant to be edited in-place. 48 | */ 49 | propertiesForType( 50 | type: ElementTypeIri, 51 | ct: CancellationToken 52 | ): Promise; 53 | 54 | filterConstructibleTypes( 55 | types: ReadonlySet, 56 | ct: CancellationToken 57 | ): Promise>; 58 | 59 | canDeleteElement( 60 | element: ElementModel, 61 | ct: CancellationToken 62 | ): Promise; 63 | 64 | canEditElement( 65 | element: ElementModel, 66 | ct: CancellationToken 67 | ): Promise; 68 | 69 | canLinkElement( 70 | element: ElementModel, 71 | ct: CancellationToken 72 | ): Promise; 73 | 74 | canDeleteLink( 75 | link: LinkModel, 76 | source: ElementModel, 77 | target: ElementModel, 78 | ct: CancellationToken 79 | ): Promise; 80 | 81 | canEditLink( 82 | link: LinkModel, 83 | source: ElementModel, 84 | target: ElementModel, 85 | ct: CancellationToken 86 | ): Promise; 87 | 88 | generateNewElement( 89 | types: readonly ElementTypeIri[], 90 | ct: CancellationToken 91 | ): Promise; 92 | } 93 | 94 | export interface DirectedLinkType { 95 | readonly linkTypeIri: LinkTypeIri; 96 | readonly direction: LinkDirection; 97 | } 98 | -------------------------------------------------------------------------------- /src/graph-explorer/data/schema.ts: -------------------------------------------------------------------------------- 1 | import { ElementTypeIri, LinkTypeIri } from "./model"; 2 | import { generate128BitID } from "./utils"; 3 | 4 | // context could be imported directly from NPM package, e.g. 5 | export const DIAGRAM_CONTEXT_URL_V1 = 6 | "https://graph-explorer.org/context/v1.json"; 7 | 8 | export const PLACEHOLDER_ELEMENT_TYPE = 9 | "http://graph-explorer.org/NewEntity" as ElementTypeIri; 10 | export const PLACEHOLDER_LINK_TYPE = 11 | "http://graph-explorer.org/NewLink" as LinkTypeIri; 12 | const GRAPH_EXPLORER_ID_URL_PREFIX = "http://graph-explorer.org/data/"; 13 | 14 | export const GenerateID = { 15 | forElement() { 16 | return `${GRAPH_EXPLORER_ID_URL_PREFIX}e_${generate128BitID()}`; 17 | }, 18 | forLink() { 19 | return `${GRAPH_EXPLORER_ID_URL_PREFIX}l_${generate128BitID()}`; 20 | }, 21 | }; 22 | 23 | export const TemplateProperties = { 24 | PinnedProperties: "graph-explorer:pinnedProperties", 25 | CustomLabel: "graph-explorer:customLabel", 26 | }; 27 | -------------------------------------------------------------------------------- /src/graph-explorer/data/sparql/graphBuilder.ts: -------------------------------------------------------------------------------- 1 | import { keyBy } from "lodash"; 2 | 3 | import { GenerateID } from "../schema"; 4 | import { 5 | LayoutElement, 6 | LayoutLink, 7 | SerializedDiagram, 8 | makeSerializedDiagram, 9 | } from "../../editor/serializedDiagram"; 10 | import { uniformGrid } from "../../viewUtils/layout"; 11 | 12 | import { 13 | Dictionary, 14 | ElementModel, 15 | LinkModel, 16 | ElementIri, 17 | LinkTypeIri, 18 | } from "../model"; 19 | import { DataProvider } from "../provider"; 20 | import { Triple } from "./sparqlModels"; 21 | import { parseTurtleText } from "./turtle"; 22 | 23 | const GREED_STEP = 150; 24 | 25 | export class GraphBuilder { 26 | constructor(public dataProvider: DataProvider) {} 27 | 28 | createGraph(graph: { 29 | elementIds: ElementIri[]; 30 | links: LinkModel[]; 31 | }): Promise<{ 32 | preloadedElements: Dictionary; 33 | diagram: SerializedDiagram; 34 | }> { 35 | return this.dataProvider 36 | .elementInfo({ elementIds: graph.elementIds }) 37 | .then((elementsInfo) => ({ 38 | preloadedElements: elementsInfo, 39 | diagram: makeLayout(graph.elementIds, graph.links), 40 | })); 41 | } 42 | 43 | getGraphFromRDFGraph(graph: Triple[]): Promise<{ 44 | preloadedElements: Dictionary; 45 | diagram: SerializedDiagram; 46 | }> { 47 | const { elementIds, links } = makeGraphItems(graph); 48 | return this.createGraph({ elementIds, links }); 49 | } 50 | 51 | getGraphFromTurtleGraph(graph: string): Promise<{ 52 | preloadedElements: Dictionary; 53 | diagram: SerializedDiagram; 54 | }> { 55 | return parseTurtleText(graph).then((triples) => 56 | this.getGraphFromRDFGraph(triples) 57 | ); 58 | } 59 | } 60 | 61 | export function makeGraphItems(response: readonly Triple[]): { 62 | elementIds: ElementIri[]; 63 | links: LinkModel[]; 64 | } { 65 | const elements: Dictionary = {}; 66 | const links: LinkModel[] = []; 67 | 68 | for (const { subject, predicate, object } of response) { 69 | if (subject.type === "uri" && !elements[subject.value]) { 70 | elements[subject.value] = true; 71 | } 72 | 73 | if (object.type === "uri" && !elements[object.value]) { 74 | elements[object.value] = true; 75 | } 76 | 77 | if (subject.type === "uri" && object.type === "uri") { 78 | links.push({ 79 | linkTypeId: predicate.value as LinkTypeIri, 80 | sourceId: subject.value as ElementIri, 81 | targetId: object.value as ElementIri, 82 | }); 83 | } 84 | } 85 | return { elementIds: Object.keys(elements) as ElementIri[], links }; 86 | } 87 | 88 | export function makeLayout( 89 | elementsIds: readonly ElementIri[], 90 | linksInfo: readonly LinkModel[] 91 | ): SerializedDiagram { 92 | const rows = Math.ceil(Math.sqrt(elementsIds.length)); 93 | const grid = uniformGrid({ 94 | rows, 95 | cellSize: { x: GREED_STEP, y: GREED_STEP }, 96 | }); 97 | 98 | const elements: LayoutElement[] = elementsIds.map( 99 | (id, index) => { 100 | const { x, y } = grid(index); 101 | return { 102 | "@type": "Element", 103 | "@id": GenerateID.forElement(), 104 | iri: id, 105 | position: { x, y }, 106 | }; 107 | } 108 | ); 109 | 110 | const layoutElementsMap: Record = keyBy( 111 | elements, 112 | "iri" 113 | ); 114 | const links: LayoutLink[] = []; 115 | 116 | linksInfo.forEach((link, _index) => { 117 | const source = layoutElementsMap[link.sourceId]; 118 | const target = layoutElementsMap[link.targetId]; 119 | 120 | if (!source || !target) { 121 | return; 122 | } 123 | 124 | links.push({ 125 | "@type": "Link", 126 | "@id": GenerateID.forLink(), 127 | property: link.linkTypeId, 128 | source: { "@id": source["@id"] }, 129 | target: { "@id": target["@id"] }, 130 | }); 131 | }); 132 | return makeSerializedDiagram({ 133 | layoutData: { "@type": "Layout", elements, links }, 134 | linkTypeOptions: [], 135 | }); 136 | } 137 | -------------------------------------------------------------------------------- /src/graph-explorer/data/sparql/sparqlGraphBuilder.ts: -------------------------------------------------------------------------------- 1 | import { SerializedDiagram } from "../../editor/serializedDiagram"; 2 | 3 | import { Dictionary, ElementModel } from "../model"; 4 | 5 | import { GraphBuilder } from "./graphBuilder"; 6 | import { SparqlDataProvider } from "./sparqlDataProvider"; 7 | 8 | const DEFAULT_PREFIX = 9 | `PREFIX rdfs: 10 | PREFIX rdf: 11 | PREFIX owl: ` + "\n\n"; 12 | 13 | export class SparqlGraphBuilder { 14 | graphBuilder: GraphBuilder; 15 | 16 | constructor(public dataProvider: SparqlDataProvider) { 17 | this.graphBuilder = new GraphBuilder(dataProvider); 18 | } 19 | 20 | getGraphFromConstruct(constructQuery: string): Promise<{ 21 | preloadedElements: Dictionary; 22 | diagram: SerializedDiagram; 23 | }> { 24 | const query = DEFAULT_PREFIX + constructQuery; 25 | return this.dataProvider 26 | .executeSparqlConstruct(query) 27 | .then((graph) => this.graphBuilder.getGraphFromRDFGraph(graph)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/graph-explorer/data/sparql/sparqlModels.ts: -------------------------------------------------------------------------------- 1 | export type RdfNode = RdfIri | RdfLiteral | RdfBlank; 2 | 3 | export interface RdfIri { 4 | type: "uri"; 5 | value: string; 6 | } 7 | 8 | export interface RdfBlank { 9 | type: "bnode"; 10 | value: string; 11 | } 12 | 13 | export interface RdfLiteral { 14 | type: "literal"; 15 | value: string; 16 | datatype?: string; 17 | "xml:lang": string; 18 | } 19 | 20 | export interface Triple { 21 | subject: RdfNode; 22 | predicate: RdfNode; 23 | object: RdfNode; 24 | } 25 | 26 | export function isRdfBlank(e: RdfNode): e is RdfBlank { 27 | return e && e.type === "bnode"; 28 | } 29 | 30 | export function isRdfIri(e: RdfNode): e is RdfIri { 31 | return e && e.type === "uri"; 32 | } 33 | 34 | export function isRdfLiteral(e: RdfNode): e is RdfLiteral { 35 | return e && e.type === "literal"; 36 | } 37 | 38 | export interface BlankBinding extends ElementBinding { 39 | blankType: { 40 | value: "listHead" | "blankNode"; 41 | }; 42 | blankTrgProp: RdfNode; 43 | blankTrg: RdfNode; 44 | blankSrc?: RdfNode; 45 | blankSrcProp?: RdfNode; 46 | newInst?: RdfIri | RdfBlank; 47 | } 48 | 49 | export function isBlankBinding( 50 | binding: ElementBinding | BlankBinding 51 | ): binding is BlankBinding { 52 | const blank = binding as BlankBinding; 53 | return ( 54 | blank.blankTrgProp !== undefined || 55 | blank.blankTrg !== undefined || 56 | blank.blankSrcProp !== undefined || 57 | blank.blankSrc !== undefined 58 | ); 59 | } 60 | 61 | export interface ElementBinding { 62 | inst: RdfIri | RdfBlank; 63 | class?: RdfIri; 64 | label?: RdfLiteral; 65 | propType?: RdfIri; 66 | propValue?: RdfIri | RdfLiteral; 67 | } 68 | 69 | export interface ClassBinding { 70 | class: RdfIri; 71 | instcount?: RdfLiteral; 72 | label?: RdfLiteral; 73 | parent?: RdfIri; 74 | } 75 | 76 | export interface PropertyBinding { 77 | property: RdfIri; 78 | label?: RdfLiteral; 79 | } 80 | 81 | export interface LinkBinding { 82 | source: RdfIri | RdfBlank; 83 | type: RdfIri; 84 | target: RdfIri | RdfBlank; 85 | propType?: RdfIri; 86 | propValue?: RdfLiteral; 87 | } 88 | 89 | export interface LinkCountBinding { 90 | link: RdfIri | RdfBlank; 91 | inCount: RdfLiteral; 92 | outCount: RdfLiteral; 93 | } 94 | 95 | export interface LinkTypeBinding { 96 | link: RdfIri; 97 | label?: RdfLiteral; 98 | instcount?: RdfLiteral; 99 | } 100 | 101 | export interface ElementImageBinding { 102 | inst: RdfIri; 103 | linkType: RdfIri; 104 | image: RdfIri; 105 | } 106 | 107 | export interface ElementTypeBinding { 108 | inst: RdfIri; 109 | class: RdfIri; 110 | } 111 | 112 | export interface FilterBinding { 113 | classAll?: RdfIri; 114 | link?: RdfIri; 115 | direction?: RdfLiteral; 116 | } 117 | 118 | export interface SparqlResponse { 119 | head: { vars: string[] }; 120 | results: { bindings: Binding[] }; 121 | } 122 | -------------------------------------------------------------------------------- /src/graph-explorer/data/sparql/turtle.ts: -------------------------------------------------------------------------------- 1 | import * as N3 from "n3"; 2 | import * as RDF from "@rdfjs/types"; 3 | 4 | import { RdfNode, Triple } from "./sparqlModels"; 5 | 6 | export function parseTurtleText(turtleText: string): Promise { 7 | return new Promise((resolve, reject) => { 8 | const triples: Triple[] = []; 9 | new N3.Parser().parse(turtleText, (error, triple, _hash) => { 10 | if (error) { 11 | reject(error); 12 | } else if (triple) { 13 | triples.push({ 14 | subject: n3toRdfNode(triple.subject), 15 | predicate: n3toRdfNode(triple.predicate), 16 | object: n3toRdfNode(triple.object), 17 | }); 18 | } else { 19 | resolve(triples); 20 | } 21 | }); 22 | }); 23 | } 24 | 25 | export function n3toRdfNode(entity: RDF.Term): RdfNode { 26 | if (N3.Util.isLiteral(entity)) { 27 | const literal = entity as RDF.Literal; 28 | return { 29 | type: "literal", 30 | value: literal.value, 31 | datatype: literal.datatype.value, 32 | "xml:lang": literal.language, 33 | }; 34 | } else { 35 | return { type: "uri", value: entity.value }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graph-explorer/data/utils.ts: -------------------------------------------------------------------------------- 1 | /** Generates random 32-digit hexadecimal string. */ 2 | export function generate128BitID() { 3 | function random32BitDigits() { 4 | return Math.floor((1 + Math.random()) * 0x100000000) 5 | .toString(16) 6 | .substring(1); 7 | } 8 | // generate by half because of restricted numerical precision 9 | return ( 10 | random32BitDigits() + 11 | random32BitDigits() + 12 | random32BitDigits() + 13 | random32BitDigits() 14 | ); 15 | } 16 | 17 | /** 18 | * Calculate a 32 bit FNV-1a hash 19 | * Found here: https://gist.github.com/vaiorabbit/5657561 20 | * Ref.: http://isthe.com/chongo/tech/comp/fnv/ 21 | * 22 | * @param {string} str the input value 23 | * @param {integer} [seed] optionally pass the hash of the previous chunk 24 | * @returns {integer} 25 | */ 26 | export function hashFnv32a(str: string, seed = 0x811c9dc5): number { 27 | let i: number, 28 | l: number, 29 | hval = seed & 0x7fffffff; 30 | 31 | for (i = 0, l = str.length; i < l; i++) { 32 | hval ^= str.charCodeAt(i); 33 | hval += 34 | (hval << 1) + (hval << 4) + (hval << 7) + (hval << 8) + (hval << 24); 35 | } 36 | return hval >>> 0; 37 | } 38 | 39 | /** 40 | * Extracts local name for URI the same way as it's done in RDF4J. 41 | */ 42 | export function getUriLocalName(uri: string): string | undefined { 43 | let index = uri.indexOf("#"); 44 | if (index < 0) { 45 | index = uri.lastIndexOf("/"); 46 | } 47 | if (index < 0) { 48 | index = uri.lastIndexOf(":"); 49 | } 50 | if (index < 0) { 51 | return undefined; 52 | } 53 | return uri.substring(index + 1); 54 | } 55 | -------------------------------------------------------------------------------- /src/graph-explorer/data/validationApi.ts: -------------------------------------------------------------------------------- 1 | import { DiagramModel } from "../diagram/model"; 2 | import { AuthoringState } from "../editor/authoringState"; 3 | import { CancellationToken } from "../viewUtils/async"; 4 | 5 | import { ElementModel, LinkModel, ElementIri, PropertyTypeIri } from "./model"; 6 | 7 | export interface ElementError { 8 | readonly type: "element"; 9 | readonly target: ElementIri; 10 | readonly message: string; 11 | readonly propertyType?: PropertyTypeIri; 12 | } 13 | 14 | export interface LinkError { 15 | readonly type: "link"; 16 | readonly target: LinkModel; 17 | readonly message: string; 18 | } 19 | 20 | export interface ValidationEvent { 21 | readonly target: ElementModel; 22 | readonly outboundLinks: readonly LinkModel[]; 23 | readonly model: DiagramModel; 24 | readonly state: AuthoringState; 25 | readonly cancellation: CancellationToken; 26 | } 27 | 28 | export interface ValidationApi { 29 | /** 30 | * Validate element and its outbound links. 31 | */ 32 | validate(e: ValidationEvent): Promise<(ElementError | LinkError)[]>; 33 | } 34 | -------------------------------------------------------------------------------- /src/graph-explorer/diagram/commands.ts: -------------------------------------------------------------------------------- 1 | import { ElementModel, ElementIri, LinkModel, sameLink } from "../data/model"; 2 | 3 | import { Element, Link, FatLinkType } from "./elements"; 4 | import { Vector, isPolylineEqual } from "./geometry"; 5 | import { Command } from "./history"; 6 | import { DiagramModel } from "./model"; 7 | 8 | export class RestoreGeometry implements Command { 9 | readonly title = "Move elements and links"; 10 | 11 | constructor( 12 | private elementState: readonly { element: Element; position: Vector }[], 13 | private linkState: readonly { 14 | link: Link; 15 | vertices: readonly Vector[]; 16 | }[] 17 | ) {} 18 | 19 | static capture(model: DiagramModel) { 20 | return RestoreGeometry.captureElementsAndLinks(model.elements, model.links); 21 | } 22 | 23 | private static captureElementsAndLinks( 24 | elements: readonly Element[], 25 | links: readonly Link[] 26 | ) { 27 | return new RestoreGeometry( 28 | elements.map((element) => ({ element, position: element.position })), 29 | links.map((link) => ({ link, vertices: link.vertices })) 30 | ); 31 | } 32 | 33 | hasChanges() { 34 | return this.elementState.length > 0 || this.linkState.length > 0; 35 | } 36 | 37 | filterOutUnchanged(): RestoreGeometry { 38 | return new RestoreGeometry( 39 | this.elementState.filter( 40 | ({ element, position }) => !Vector.equals(element.position, position) 41 | ), 42 | this.linkState.filter( 43 | ({ link, vertices }) => !isPolylineEqual(link.vertices, vertices) 44 | ) 45 | ); 46 | } 47 | 48 | invoke(): RestoreGeometry { 49 | const previous = RestoreGeometry.captureElementsAndLinks( 50 | this.elementState.map((state) => state.element), 51 | this.linkState.map((state) => state.link) 52 | ); 53 | // restore in reverse order to workaround position changed event 54 | // handling in EmbeddedLayer inside nested elements 55 | // (child's position change causes group to resize or move itself) 56 | for (const { element, position } of [...this.elementState].reverse()) { 57 | element.setPosition(position); 58 | } 59 | for (const { link, vertices } of this.linkState) { 60 | link.setVertices(vertices); 61 | } 62 | return previous; 63 | } 64 | } 65 | 66 | export function restoreCapturedLinkGeometry(link: Link): Command { 67 | const vertices = link.vertices; 68 | return Command.create("Change link vertices", () => { 69 | const capturedInverse = restoreCapturedLinkGeometry(link); 70 | link.setVertices(vertices); 71 | return capturedInverse; 72 | }); 73 | } 74 | 75 | export function setElementExpanded( 76 | element: Element, 77 | expanded: boolean 78 | ): Command { 79 | const title = expanded ? "Expand element" : "Collapse element"; 80 | return Command.create(title, () => { 81 | element.setExpanded(expanded); 82 | return setElementExpanded(element, !expanded); 83 | }); 84 | } 85 | 86 | export function changeLinkTypeVisibility(params: { 87 | linkType: FatLinkType; 88 | visible: boolean; 89 | showLabel: boolean; 90 | preventLoading?: boolean; 91 | }): Command { 92 | const { linkType, visible, showLabel, preventLoading } = params; 93 | return Command.create("Change link type visibility", () => { 94 | const previousVisible = linkType.visible; 95 | const previousShowLabel = linkType.showLabel; 96 | linkType.setVisibility({ visible, showLabel, preventLoading }); 97 | return changeLinkTypeVisibility({ 98 | linkType, 99 | visible: previousVisible, 100 | showLabel: previousShowLabel, 101 | preventLoading, 102 | }); 103 | }); 104 | } 105 | 106 | export function setElementData( 107 | model: DiagramModel, 108 | target: ElementIri, 109 | data: ElementModel 110 | ): Command { 111 | const elements = model.elements.filter((el) => el.iri === target); 112 | const previous = elements.length > 0 ? elements[0].data : undefined; 113 | return Command.create("Set element data", () => { 114 | for (const element of model.elements.filter((el) => el.iri === target)) { 115 | element.setData(data); 116 | } 117 | return setElementData(model, data.id, previous); 118 | }); 119 | } 120 | 121 | export function setLinkData( 122 | model: DiagramModel, 123 | oldData: LinkModel, 124 | newData: LinkModel 125 | ): Command { 126 | if (!sameLink(oldData, newData)) { 127 | throw new Error( 128 | "Cannot change typeId, sourceId or targetId when changing link data" 129 | ); 130 | } 131 | return Command.create("Set link data", () => { 132 | for (const link of model.links) { 133 | if (sameLink(link.data, oldData)) { 134 | link.setData(newData); 135 | } 136 | } 137 | return setLinkData(model, newData, oldData); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /src/graph-explorer/diagram/history.ts: -------------------------------------------------------------------------------- 1 | import { EventSource, Events } from "../viewUtils/events"; 2 | 3 | export interface Command { 4 | readonly title?: string; 5 | readonly invoke: CommandAction; 6 | } 7 | 8 | /** @returns Inverse command */ 9 | export type CommandAction = () => Command; 10 | 11 | function createAction(title: string, action: CommandAction): Command { 12 | return { title, invoke: action }; 13 | } 14 | 15 | function effectAction(title: string, body: () => void): Command { 16 | const perform = { 17 | title, 18 | invoke: () => { 19 | body(); 20 | return skip; 21 | }, 22 | }; 23 | const skip = { 24 | title: "Skipped effect: " + title, 25 | invoke: () => perform, 26 | }; 27 | return perform; 28 | } 29 | 30 | export const Command = { 31 | create: createAction, 32 | effect: effectAction, 33 | }; 34 | 35 | export interface CommandHistoryEvents { 36 | historyChanged: { hasChanges: boolean }; 37 | } 38 | 39 | export interface CommandHistory { 40 | readonly events: Events; 41 | readonly undoStack: readonly Command[]; 42 | readonly redoStack: readonly Command[]; 43 | reset(): void; 44 | undo(): void; 45 | redo(): void; 46 | execute(command: Command): void; 47 | registerToUndo(command: Command): void; 48 | startBatch(title?: string): Batch; 49 | } 50 | 51 | export interface Batch { 52 | readonly history: CommandHistory; 53 | store(): void; 54 | discard(): void; 55 | } 56 | 57 | export class NonRememberingHistory implements CommandHistory { 58 | private readonly source = new EventSource(); 59 | readonly events: Events = this.source; 60 | 61 | readonly undoStack: readonly Command[] = []; 62 | readonly redoStack: readonly Command[] = []; 63 | 64 | reset() { 65 | this.source.trigger("historyChanged", { hasChanges: false }); 66 | } 67 | undo() { 68 | throw new Error("Undo is unsupported"); 69 | } 70 | redo() { 71 | throw new Error("Redo is unsupported"); 72 | } 73 | 74 | execute(command: Command) { 75 | command.invoke(); 76 | this.source.trigger("historyChanged", { hasChanges: true }); 77 | } 78 | registerToUndo(_command: Command) { 79 | this.source.trigger("historyChanged", { hasChanges: true }); 80 | } 81 | startBatch(_title?: string): Batch { 82 | return { 83 | history: this, 84 | store: this.storeBatch, 85 | discard: this.discardBatch, 86 | }; 87 | } 88 | private storeBatch = () => { 89 | /* nothing */ 90 | }; 91 | private discardBatch = () => { 92 | /* nothing */ 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /src/graph-explorer/editor/dataFetcher.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElementModel, 3 | ClassModel, 4 | LinkType, 5 | PropertyModel, 6 | ElementIri, 7 | ElementTypeIri, 8 | LinkTypeIri, 9 | PropertyTypeIri, 10 | } from "../data/model"; 11 | import { DataProvider } from "../data/provider"; 12 | 13 | import { FatClassModel, FatLinkType, RichProperty } from "../diagram/elements"; 14 | import { Graph } from "../diagram/graph"; 15 | 16 | import { BufferingQueue } from "../viewUtils/async"; 17 | 18 | export class DataFetcher { 19 | private classQueue = new BufferingQueue((classIds) => { 20 | this.dataProvider.classInfo({ classIds }).then(this.onClassesLoaded); 21 | }); 22 | private linkTypeQueue = new BufferingQueue((linkTypeIds) => { 23 | this.dataProvider 24 | .linkTypesInfo({ linkTypeIds }) 25 | .then(this.onLinkTypesLoaded); 26 | }); 27 | private propertyTypeQueue = new BufferingQueue( 28 | (propertyIds) => { 29 | this.dataProvider 30 | .propertyInfo({ propertyIds }) 31 | .then(this.onPropertyTypesLoaded); 32 | } 33 | ); 34 | 35 | constructor(private graph: Graph, private dataProvider: DataProvider) {} 36 | 37 | fetchElementData(elementIris: readonly ElementIri[]): Promise { 38 | if (elementIris.length === 0) { 39 | return Promise.resolve(); 40 | } 41 | return this.dataProvider 42 | .elementInfo({ elementIds: [...elementIris] }) 43 | .then(this.onElementInfoLoaded); 44 | } 45 | 46 | private onElementInfoLoaded = (elements: Record) => { 47 | for (const element of this.graph.getElements()) { 48 | const loadedModel = elements[element.iri]; 49 | if (loadedModel) { 50 | element.setData(loadedModel); 51 | } 52 | } 53 | }; 54 | 55 | fetchClass(model: FatClassModel): void { 56 | this.classQueue.push(model.id); 57 | } 58 | 59 | private onClassesLoaded = (classInfos: ClassModel[]) => { 60 | for (const { id, label, count } of classInfos) { 61 | const model = this.graph.getClass(id); 62 | if (!model) { 63 | continue; 64 | } 65 | model.setLabel(label.values); 66 | if (typeof count === "number") { 67 | model.setCount(count); 68 | } 69 | } 70 | }; 71 | 72 | fetchLinkType(linkType: FatLinkType): void { 73 | this.linkTypeQueue.push(linkType.id); 74 | } 75 | 76 | private onLinkTypesLoaded = (linkTypesInfo: LinkType[]) => { 77 | for (const { id, label } of linkTypesInfo) { 78 | const model = this.graph.getLinkType(id); 79 | if (!model) { 80 | continue; 81 | } 82 | model.setLabel(label.values); 83 | } 84 | }; 85 | 86 | fetchPropertyType(propertyType: RichProperty): void { 87 | if (!this.dataProvider.propertyInfo) { 88 | return; 89 | } 90 | this.propertyTypeQueue.push(propertyType.id); 91 | } 92 | 93 | private onPropertyTypesLoaded = ( 94 | propertyModels: Record 95 | ) => { 96 | for (const propId in propertyModels) { 97 | if (!Object.prototype.hasOwnProperty.call(propertyModels, propId)) { 98 | continue; 99 | } 100 | const { id, label } = propertyModels[propId]; 101 | const targetProperty = this.graph.getProperty(id); 102 | if (targetProperty) { 103 | targetProperty.setLabel(label.values); 104 | } 105 | } 106 | }; 107 | } 108 | -------------------------------------------------------------------------------- /src/graph-explorer/editor/temporaryState.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElementModel, 3 | LinkModel, 4 | ElementIri, 5 | sameLink, 6 | hashLink, 7 | } from "../data/model"; 8 | 9 | import { HashMap, ReadonlyHashMap, cloneMap } from "../viewUtils/collections"; 10 | 11 | export interface TemporaryState { 12 | readonly elements: ReadonlyMap; 13 | readonly links: ReadonlyHashMap; 14 | } 15 | export const TemporaryState = { 16 | empty: { 17 | elements: new Map(), 18 | links: new HashMap(hashLink, sameLink), 19 | } as TemporaryState, 20 | 21 | addElement(state: TemporaryState, element: ElementModel) { 22 | const elements = cloneMap(state.elements); 23 | elements.set(element.id, element); 24 | return { ...state, elements }; 25 | }, 26 | 27 | deleteElement(state: TemporaryState, element: ElementModel) { 28 | const elements = cloneMap(state.elements); 29 | elements.delete(element.id); 30 | return { ...state, elements }; 31 | }, 32 | 33 | addLink(state: TemporaryState, link: LinkModel) { 34 | const links = state.links.clone(); 35 | links.set(link, link); 36 | return { ...state, links }; 37 | }, 38 | deleteLink(state: TemporaryState, link: LinkModel) { 39 | const links = state.links.clone(); 40 | links.delete(link); 41 | return { ...state, links }; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /src/graph-explorer/emptyModule.ts: -------------------------------------------------------------------------------- 1 | /** Empty module for conditional compilation. */ 2 | export default undefined; 3 | -------------------------------------------------------------------------------- /src/graph-explorer/forms/editLinkForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { MetadataApi } from "../data/metadataApi"; 4 | import { ElementModel, LinkModel, sameLink } from "../data/model"; 5 | 6 | import { EditorController } from "../editor/editorController"; 7 | import { DiagramView } from "../diagram/view"; 8 | import { LinkDirection } from "../diagram/elements"; 9 | 10 | import { Cancellation } from "../viewUtils/async"; 11 | 12 | import { ProgressBar, ProgressState } from "../widgets/progressBar"; 13 | 14 | import { 15 | LinkTypeSelector, 16 | LinkValue, 17 | validateLinkType, 18 | } from "./linkTypeSelector"; 19 | 20 | const CLASS_NAME = "graph-explorer-edit-form"; 21 | 22 | export interface Props { 23 | editor: EditorController; 24 | view: DiagramView; 25 | metadataApi: MetadataApi | undefined; 26 | link: LinkModel; 27 | source: ElementModel; 28 | target: ElementModel; 29 | onChange: (entity: LinkModel) => void; 30 | onApply: (entity: LinkModel) => void; 31 | onCancel: () => void; 32 | } 33 | 34 | export interface State { 35 | linkValue?: LinkValue; 36 | isValidating?: boolean; 37 | } 38 | 39 | export class EditLinkForm extends React.Component { 40 | private validationCancellation = new Cancellation(); 41 | 42 | constructor(props: Props) { 43 | super(props); 44 | this.state = { 45 | linkValue: { 46 | value: { link: props.link, direction: LinkDirection.out }, 47 | validated: true, 48 | allowChange: true, 49 | }, 50 | }; 51 | } 52 | 53 | componentDidMount() { 54 | this.validate(); 55 | } 56 | 57 | componentDidUpdate(prevProps: Props, prevState: State) { 58 | const { linkValue } = this.state; 59 | if (!sameLink(linkValue.value.link, prevState.linkValue.value.link)) { 60 | this.validate(); 61 | } 62 | if ( 63 | linkValue !== prevState.linkValue && 64 | linkValue.validated && 65 | linkValue.allowChange 66 | ) { 67 | this.props.onChange(linkValue.value.link); 68 | } 69 | } 70 | 71 | componentWillUnmount() { 72 | this.validationCancellation.abort(); 73 | } 74 | 75 | private validate() { 76 | const { editor, link: originalLink } = this.props; 77 | const { 78 | linkValue: { value }, 79 | } = this.state; 80 | this.setState({ isValidating: true }); 81 | 82 | this.validationCancellation.abort(); 83 | this.validationCancellation = new Cancellation(); 84 | const signal = this.validationCancellation.signal; 85 | 86 | validateLinkType(editor, value.link, originalLink).then((error) => { 87 | if (signal.aborted) { 88 | return; 89 | } 90 | this.setState(({ linkValue }) => ({ 91 | linkValue: { ...linkValue, ...error, validated: true }, 92 | isValidating: false, 93 | })); 94 | }); 95 | } 96 | 97 | render() { 98 | const { editor, view, metadataApi, source, target } = this.props; 99 | const { linkValue, isValidating } = this.state; 100 | const isValid = !linkValue.error; 101 | return ( 102 |
103 |
104 | 112 | this.setState({ 113 | linkValue: { 114 | value, 115 | error: undefined, 116 | validated: false, 117 | allowChange: false, 118 | }, 119 | }) 120 | } 121 | /> 122 | {isValidating ? ( 123 |
124 | 125 |
126 | ) : null} 127 |
128 |
129 | 136 | 142 |
143 |
144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/graph-explorer/forms/editLinkLabelForm.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Link } from "../diagram/elements"; 4 | import { DiagramView } from "../diagram/view"; 5 | 6 | const CLASS_NAME = "graph-explorer-edit-form"; 7 | 8 | export interface Props { 9 | view: DiagramView; 10 | link: Link; 11 | onApply: (label: string) => void; 12 | onCancel: () => void; 13 | } 14 | 15 | export interface State { 16 | label?: string; 17 | } 18 | 19 | export class EditLinkLabelForm extends React.Component { 20 | constructor(props: Props) { 21 | super(props); 22 | const label = this.computeLabel(); 23 | this.state = { label }; 24 | } 25 | 26 | componentDidUpdate(prevProps: Props) { 27 | if (this.props.link.typeId !== prevProps.link.typeId) { 28 | const label = this.computeLabel(); 29 | this.setState({ label }); 30 | } 31 | } 32 | 33 | private computeLabel(): string { 34 | const { view, link } = this.props; 35 | 36 | const linkType = view.model.getLinkType(link.typeId); 37 | const template = view.createLinkTemplate(linkType); 38 | const { label = {} } = template.renderLink(link); 39 | 40 | const labelTexts = 41 | label.attrs && label.attrs.text ? label.attrs.text.text : undefined; 42 | return labelTexts && labelTexts.length > 0 43 | ? view.selectLabel(labelTexts).value 44 | : view.formatLabel(linkType.label, linkType.id); 45 | } 46 | 47 | render() { 48 | const { onApply, onCancel } = this.props; 49 | const { label } = this.state; 50 | return ( 51 |
52 |
53 |
54 | 55 | 59 | this.setState({ label: (e.target as HTMLInputElement).value }) 60 | } 61 | /> 62 |
63 |
64 |
65 | 71 | 77 |
78 |
79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/graph-explorer/index.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-require-imports 2 | require("../../styles/main.scss"); 3 | 4 | export * from "./customization/props"; 5 | export * from "./customization/templates"; 6 | 7 | export * from "./data/model"; 8 | export * from "./data/metadataApi"; 9 | export * from "./data/validationApi"; 10 | export * from "./data/provider"; 11 | export { PLACEHOLDER_ELEMENT_TYPE, PLACEHOLDER_LINK_TYPE } from "./data/schema"; 12 | export * from "./data/demo/provider"; 13 | export { 14 | RdfNode, 15 | RdfIri, 16 | RdfLiteral, 17 | Triple, 18 | } from "./data/sparql/sparqlModels"; 19 | export * from "./data/sparql/sparqlDataProvider"; 20 | export * from "./data/composite/composite"; 21 | export * from "./data/sparql/sparqlDataProviderSettings"; 22 | export * from "./data/sparql/graphBuilder"; 23 | export * from "./data/sparql/sparqlGraphBuilder"; 24 | export { DIAGRAM_CONTEXT_URL_V1 } from "./data/schema"; 25 | 26 | export { 27 | RestoreGeometry, 28 | setElementExpanded, 29 | setElementData, 30 | setLinkData, 31 | } from "./diagram/commands"; 32 | export { 33 | Element, 34 | ElementEvents, 35 | ElementTemplateState, 36 | Link, 37 | LinkEvents, 38 | LinkTemplateState, 39 | LinkVertex, 40 | Cell, 41 | LinkDirection, 42 | } from "./diagram/elements"; 43 | export { EmbeddedLayer } from "./diagram/embeddedLayer"; 44 | export * from "./diagram/geometry"; 45 | export * from "./diagram/history"; 46 | export { DiagramModel, DiagramModelEvents } from "./diagram/model"; 47 | export * from "./diagram/view"; 48 | export { 49 | PointerEvent, 50 | PointerUpEvent, 51 | getContentFittingBox, 52 | ViewportOptions, 53 | ScaleOptions, 54 | } from "./diagram/paperArea"; 55 | 56 | export * from "./editor/asyncModel"; 57 | export { 58 | AuthoredEntity, 59 | AuthoredEntityProps, 60 | AuthoredEntityContext, 61 | } from "./editor/authoredEntity"; 62 | export * from "./editor/authoringState"; 63 | export { 64 | EditorOptions, 65 | EditorEvents, 66 | EditorController, 67 | PropertyEditor, 68 | PropertyEditorOptions, 69 | } from "./editor/editorController"; 70 | export { 71 | ValidationState, 72 | ElementValidation, 73 | LinkValidation, 74 | } from "./editor/validation"; 75 | 76 | export { 77 | LayoutData, 78 | LayoutElement, 79 | LayoutLink, 80 | SerializedDiagram, 81 | convertToSerializedDiagram, 82 | makeSerializedDiagram, 83 | LinkTypeOptions, 84 | makeLayoutData, 85 | } from "./editor/serializedDiagram"; 86 | export { 87 | calculateLayout, 88 | removeOverlaps, 89 | CalculatedLayout, 90 | UnzippedCalculatedLayout, 91 | LayoutNode, 92 | applyLayout, 93 | forceLayout, 94 | } from "./viewUtils/layout"; 95 | 96 | export { 97 | Cancellation, 98 | CancellationToken, 99 | CancelledError, 100 | } from "./viewUtils/async"; 101 | export * from "./viewUtils/events"; 102 | 103 | export { 104 | PropertySuggestionParams, 105 | PropertyScore, 106 | } from "./widgets/connectionsMenu"; 107 | 108 | export { DefaultToolbar, ToolbarProps } from "./workspace/toolbar"; 109 | export { 110 | Workspace, 111 | WorkspaceProps, 112 | WorkspaceState, 113 | WorkspaceLanguage, 114 | renderTo, 115 | } from "./workspace/workspace"; 116 | export { 117 | WorkspaceEventHandler, 118 | WorkspaceEventKey, 119 | } from "./workspace/workspaceContext"; 120 | export { DraggableHandle } from "./workspace/draggableHandle"; 121 | export * from "./workspace/layout/layout"; 122 | 123 | import * as InternalApi from "./internalApi"; 124 | export { InternalApi }; 125 | -------------------------------------------------------------------------------- /src/graph-explorer/internalApi.ts: -------------------------------------------------------------------------------- 1 | export { LINK_SHOW_IRI } from "./customization/defaultLinkStyles"; 2 | 3 | export { TemplateProperties } from "./data/schema"; 4 | 5 | export * from "./diagram/paper"; 6 | export * from "./diagram/paperArea"; 7 | 8 | export * from "./viewUtils/async"; 9 | export * from "./viewUtils/collections"; 10 | export * from "./viewUtils/keyedObserver"; 11 | export * from "./viewUtils/spinner"; 12 | 13 | export * from "./widgets/listElementView"; 14 | export * from "./widgets/searchResults"; 15 | 16 | export { 17 | WorkspaceContext, 18 | WorkspaceContextWrapper, 19 | WorkspaceContextTypes, 20 | } from "./workspace/workspaceContext"; 21 | 22 | export { 23 | groupForceLayout, 24 | groupRemoveOverlaps, 25 | padded, 26 | biasFreePadded, 27 | getContentFittingBoxForLayout, 28 | } from "./viewUtils/layout"; 29 | -------------------------------------------------------------------------------- /src/graph-explorer/viewUtils/events.ts: -------------------------------------------------------------------------------- 1 | export type Listener = ( 2 | data: Data[Key], 3 | key: Key 4 | ) => void; 5 | export type AnyListener = (data: Partial, key: string) => void; 6 | export type Unsubscribe = () => void; 7 | 8 | export interface PropertyChange { 9 | source: Source; 10 | previous: Value; 11 | } 12 | 13 | export interface AnyEvent { 14 | key: string; 15 | data: Partial; 16 | } 17 | 18 | export interface Events { 19 | on( 20 | eventKey: Key, 21 | listener: Listener 22 | ): void; 23 | off( 24 | eventKey: Key, 25 | listener: Listener 26 | ): void; 27 | onAny(listener: AnyListener): void; 28 | offAny(listener: AnyListener): void; 29 | } 30 | 31 | export class EventSource implements Events { 32 | private listeners = new Map[]>(); 33 | private anyListeners: AnyListener[] | undefined; 34 | 35 | on( 36 | eventKey: Key, 37 | listener: Listener 38 | ): void { 39 | let listeners = this.listeners.get(eventKey); 40 | if (!listeners) { 41 | listeners = []; 42 | this.listeners.set(eventKey, listeners); 43 | } 44 | listeners.push(listener); 45 | } 46 | 47 | onAny(listener: AnyListener): void { 48 | let listeners = this.anyListeners; 49 | if (!listeners) { 50 | listeners = []; 51 | this.anyListeners = listeners; 52 | } 53 | listeners.push(listener); 54 | } 55 | 56 | off( 57 | eventKey: Key, 58 | listener: Listener 59 | ): void { 60 | const listeners = this.listeners.get(eventKey); 61 | if (!listeners) { 62 | return; 63 | } 64 | const index = listeners.indexOf(listener); 65 | if (index >= 0) { 66 | listeners.splice(index, 1); 67 | } 68 | } 69 | 70 | offAny(listener: AnyListener): void { 71 | const listeners = this.anyListeners; 72 | if (!listeners) { 73 | return; 74 | } 75 | const index = listeners.indexOf(listener); 76 | if (index >= 0) { 77 | listeners.splice(index, 1); 78 | } 79 | } 80 | 81 | trigger(eventKey: Key, data: Data[Key]): void { 82 | const listeners = this.listeners.get(eventKey); 83 | if (listeners) { 84 | for (const listener of listeners) { 85 | listener(data, eventKey); 86 | } 87 | } 88 | 89 | if (this.anyListeners) { 90 | for (const anyListener of this.anyListeners) { 91 | anyListener({ [eventKey]: data } as any, eventKey as string); 92 | } 93 | } 94 | } 95 | } 96 | 97 | export class EventObserver { 98 | private unsubscribeByKey = new Map(); 99 | private onDispose: Unsubscribe[] = []; 100 | 101 | listen( 102 | events: Events, 103 | eventKey: Key, 104 | listener: Listener 105 | ) { 106 | events.on(eventKey, listener); 107 | this.onDispose.push(() => events.off(eventKey, listener)); 108 | } 109 | 110 | listenAny(events: Events, listener: AnyListener) { 111 | events.onAny(listener); 112 | this.onDispose.push(() => events.offAny(listener)); 113 | } 114 | 115 | listenOnce( 116 | events: Events, 117 | eventKey: Key, 118 | listener: Listener 119 | ) { 120 | let handled = false; 121 | const onceListener: Listener = (data, key) => { 122 | handled = true; 123 | events.off(eventKey, onceListener); 124 | listener(data, key); 125 | }; 126 | events.on(eventKey, onceListener); 127 | this.onDispose.push(() => { 128 | if (handled) { 129 | return; 130 | } 131 | events.off(eventKey, onceListener); 132 | }); 133 | } 134 | 135 | stopListening() { 136 | for (const unsubscribe of this.onDispose) { 137 | unsubscribe(); 138 | } 139 | this.onDispose.length = 0; 140 | 141 | this.unsubscribeByKey.forEach((unsubscribers) => { 142 | for (const unsubscribe of unsubscribers) { 143 | unsubscribe(); 144 | } 145 | }); 146 | this.unsubscribeByKey.clear(); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/graph-explorer/viewUtils/keyedObserver.ts: -------------------------------------------------------------------------------- 1 | import { ElementTypeIri, LinkTypeIri, PropertyTypeIri } from "../data/model"; 2 | 3 | import { 4 | FatClassModelEvents, 5 | FatLinkTypeEvents, 6 | RichPropertyEvents, 7 | } from "../diagram/elements"; 8 | import { DiagramModel } from "../diagram/model"; 9 | 10 | import { Unsubscribe, Listener } from "./events"; 11 | 12 | export class KeyedObserver { 13 | private observedKeys = new Map(); 14 | 15 | constructor(readonly subscribe: (key: Key) => Unsubscribe | undefined) {} 16 | 17 | observe(keys: readonly Key[]) { 18 | if (keys.length === 0 && this.observedKeys.size === 0) { 19 | return; 20 | } 21 | const newObservedKeys = new Map(); 22 | 23 | for (const key of keys) { 24 | if (newObservedKeys.has(key)) { 25 | continue; 26 | } 27 | let unsubscribe = this.observedKeys.get(key); 28 | if (!unsubscribe) { 29 | unsubscribe = this.subscribe(key); 30 | } 31 | newObservedKeys.set(key, unsubscribe); 32 | } 33 | 34 | this.observedKeys.forEach((unsubscribe, key) => { 35 | if (!newObservedKeys.has(key)) { 36 | unsubscribe(); 37 | } 38 | }); 39 | 40 | this.observedKeys = newObservedKeys; 41 | } 42 | 43 | stopListening() { 44 | this.observe([]); 45 | } 46 | } 47 | 48 | export function observeElementTypes( 49 | model: DiagramModel, 50 | event: Event, 51 | listener: Listener 52 | ) { 53 | return new KeyedObserver((key) => { 54 | const type = model.getClass(key); 55 | if (type) { 56 | type.events.on(event, listener); 57 | return () => type.events.off(event, listener); 58 | } 59 | return undefined; 60 | }); 61 | } 62 | 63 | export function observeProperties( 64 | model: DiagramModel, 65 | event: Event, 66 | listener: Listener 67 | ) { 68 | return new KeyedObserver((key) => { 69 | const property = model.getProperty(key); 70 | if (property) { 71 | property.events.on(event, listener); 72 | return () => property.events.off(event, listener); 73 | } 74 | return undefined; 75 | }); 76 | } 77 | 78 | export function observeLinkTypes( 79 | model: DiagramModel, 80 | event: Event, 81 | listener: Listener 82 | ) { 83 | return new KeyedObserver((key) => { 84 | const type = model.createLinkType(key); 85 | if (type) { 86 | type.events.on(event, listener); 87 | return () => type.events.off(event, listener); 88 | } 89 | return undefined; 90 | }); 91 | } 92 | -------------------------------------------------------------------------------- /src/graph-explorer/viewUtils/react.ts: -------------------------------------------------------------------------------- 1 | const anything: any = (): null => null; 2 | export const PropTypes = { 3 | anything, 4 | }; 5 | -------------------------------------------------------------------------------- /src/graph-explorer/viewUtils/spinner.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface SpinnerProps { 4 | size?: number; 5 | position?: { x: number; y: number }; 6 | maxWidth?: number; 7 | statusText?: string; 8 | errorOccured?: boolean; 9 | } 10 | 11 | const CLASS_NAME = "graph-explorer-spinner"; 12 | 13 | export class Spinner extends React.Component { 14 | render() { 15 | const { 16 | position = { x: 0, y: 0 }, 17 | size = 50, 18 | statusText, 19 | errorOccured, 20 | } = this.props; 21 | 22 | const textLeftMargin = 5; 23 | const pathGeometry = 24 | "m3.47,-19.7 a20,20 0 1,1 -6.95,0 m0,0 l-6,5 m6,-5 l-8,-0" + 25 | (errorOccured ? "M-8,-8L8,8M-8,8L8,-8" : ""); 26 | 27 | return ( 28 | 33 | 34 | 42 | 43 | 47 | {statusText} 48 | 49 | 50 | ); 51 | } 52 | } 53 | 54 | export class HtmlSpinner extends React.Component< 55 | { width: number; height: number }, 56 | {} 57 | > { 58 | render() { 59 | const { width, height } = this.props; 60 | const size = Math.min(width, height); 61 | return ( 62 | 63 | 64 | 65 | ); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/graph-explorer/widgets/classTree/index.ts: -------------------------------------------------------------------------------- 1 | export { ClassTree, ClassTreeProps } from "./classTree"; 2 | -------------------------------------------------------------------------------- /src/graph-explorer/widgets/classTree/treeModel.ts: -------------------------------------------------------------------------------- 1 | import { FatClassModel } from "../../diagram/elements"; 2 | 3 | export interface TreeNode { 4 | model: FatClassModel; 5 | label: string; 6 | derived: readonly TreeNode[]; 7 | } 8 | 9 | export const TreeNode = { 10 | setDerived: (node: TreeNode, derived: readonly TreeNode[]): TreeNode => ({ 11 | ...node, 12 | derived, 13 | }), 14 | }; 15 | -------------------------------------------------------------------------------- /src/graph-explorer/widgets/listElementView.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { hcl } from "d3-color"; 3 | 4 | import { ElementModel } from "../data/model"; 5 | import { DiagramView } from "../diagram/view"; 6 | 7 | export interface ListElementViewProps { 8 | className?: string; 9 | view: DiagramView; 10 | model: ElementModel; 11 | highlightText?: string; 12 | disabled?: boolean; 13 | selected?: boolean; 14 | onClick?: (event: React.MouseEvent, model: ElementModel) => void; 15 | onDragStart?: React.HTMLProps["onDragStart"]; 16 | } 17 | 18 | const CLASS_NAME = "graph-explorer-list-element-view"; 19 | 20 | export class ListElementView extends React.Component { 21 | render() { 22 | const { 23 | className, 24 | view, 25 | model, 26 | highlightText, 27 | disabled, 28 | selected, 29 | onDragStart, 30 | } = this.props; 31 | 32 | const { h, c, l } = view.getTypeStyle(model.types).color; 33 | const frontColor = 34 | selected && !disabled ? hcl(h, c, l * 1.2) : hcl("white"); 35 | 36 | let classNames = `${CLASS_NAME}`; 37 | classNames += disabled ? ` ${CLASS_NAME}--disabled` : ""; 38 | classNames += className ? ` ${className}` : ""; 39 | const localizedText = view.formatLabel(model.label.values, model.id); 40 | const classesString = 41 | model.types.length > 0 42 | ? `\nClasses: ${view.getElementTypeString(model)}` 43 | : ""; 44 | 45 | return ( 46 |
  • 54 |
    58 | {highlightSubstring(localizedText, highlightText)} 59 |
    60 |
  • 61 | ); 62 | } 63 | 64 | private onClick = (event: React.MouseEvent) => { 65 | const { disabled, model, onClick } = this.props; 66 | if (!disabled && onClick) { 67 | event.persist(); 68 | onClick(event, model); 69 | } 70 | }; 71 | } 72 | 73 | export function startDragElements( 74 | e: React.DragEvent<{}>, 75 | iris: readonly string[] 76 | ) { 77 | try { 78 | e.dataTransfer.setData( 79 | "application/x-graph-explorer-elements", 80 | JSON.stringify(iris) 81 | ); 82 | } catch (_ex) { 83 | // IE fix 84 | e.dataTransfer.setData("text", JSON.stringify(iris)); 85 | } 86 | return false; 87 | } 88 | 89 | const DEFAULT_HIGHLIGHT_PROPS: React.HTMLProps = { 90 | className: `graph-explorer-text-highlight`, 91 | }; 92 | 93 | export function highlightSubstring( 94 | text: string, 95 | substring: string | undefined, 96 | highlightProps = DEFAULT_HIGHLIGHT_PROPS 97 | ) { 98 | if (!substring) { 99 | return {text}; 100 | } 101 | 102 | const start = text.toLowerCase().indexOf(substring.toLowerCase()); 103 | if (start < 0) { 104 | return {text}; 105 | } 106 | 107 | const end = start + substring.length; 108 | const before = text.substring(0, start); 109 | const highlighted = text.substring(start, end); 110 | const after = text.substring(end); 111 | 112 | return ( 113 | 114 | {before} 115 | {highlighted} 116 | {after} 117 | 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /src/graph-explorer/widgets/progressBar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export enum ProgressState { 4 | none = "none", 5 | loading = "loading", 6 | error = "error", 7 | completed = "completed", 8 | } 9 | 10 | export interface ProgressBarProps { 11 | state: ProgressState; 12 | percent?: number; 13 | height?: number; 14 | } 15 | 16 | const CLASS_NAME = "graph-explorer-progress-bar"; 17 | 18 | export class ProgressBar extends React.Component { 19 | render() { 20 | const { state, percent = 100, height = 20 } = this.props; 21 | const className = `${CLASS_NAME} ${CLASS_NAME}--${state}`; 22 | const showBar = 23 | state === ProgressState.loading || state === ProgressState.error; 24 | return ( 25 |
    26 |
    34 |
    35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/graph-explorer/workspace/accordionItem.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { DraggableHandle } from "./draggableHandle"; 4 | 5 | export enum DockSide { 6 | Left = 1, 7 | Right, 8 | } 9 | 10 | export interface Props { 11 | heading?: React.ReactNode; 12 | bodyClassName?: string; 13 | bodyRef?: (body: HTMLDivElement) => void; 14 | children?: React.ReactNode; 15 | defaultSize?: number; 16 | defaultCollapsed?: boolean; 17 | collapsedSize?: number; 18 | minSize?: number; 19 | 20 | // props provided by Accordion 21 | collapsed?: boolean; 22 | size?: number | string; 23 | direction?: "vertical" | "horizontal"; 24 | dockSide?: DockSide; 25 | onChangeCollapsed?: (collapsed: boolean) => void; 26 | onBeginDragHandle?: () => void; 27 | onDragHandle?: (dx: number, dy: number) => void; 28 | onEndDragHandle?: () => void; 29 | } 30 | 31 | const CLASS_NAME = "graph-explorer-accordion-item"; 32 | 33 | export interface State { 34 | resizing?: boolean; 35 | } 36 | 37 | export class AccordionItem extends React.Component { 38 | static defaultProps: Partial = { 39 | direction: "vertical", 40 | }; 41 | 42 | private _element: HTMLDivElement; 43 | private _header: HTMLDivElement; 44 | 45 | constructor(props: Props) { 46 | super(props); 47 | this.state = { 48 | resizing: false, 49 | }; 50 | } 51 | 52 | get element() { 53 | return this._element; 54 | } 55 | get header() { 56 | return this._header; 57 | } 58 | 59 | private get isVertical() { 60 | return this.props.direction === "vertical"; 61 | } 62 | 63 | private renderToggleButton() { 64 | const { collapsed, dockSide, onChangeCollapsed } = this.props; 65 | if (!dockSide) { 66 | return null; 67 | } 68 | const side = dockSide === DockSide.Left ? "left" : "right"; 69 | return ( 70 |
    onChangeCollapsed(!collapsed)} 73 | /> 74 | ); 75 | } 76 | 77 | render() { 78 | const { 79 | heading, 80 | bodyClassName, 81 | children, 82 | bodyRef, 83 | collapsed, 84 | size, 85 | direction, 86 | onBeginDragHandle, 87 | onDragHandle, 88 | onEndDragHandle, 89 | dockSide, 90 | } = this.props; 91 | const { resizing } = this.state; 92 | const shouldRenderHandle = 93 | onBeginDragHandle && onDragHandle && onEndDragHandle; 94 | const style: React.CSSProperties = this.isVertical 95 | ? { height: size } 96 | : { width: size }; 97 | 98 | // unmount child component when the accordion item is collapsed and has dockSide 99 | const isMounted = !(collapsed && dockSide); 100 | 101 | return ( 102 |
    (this._element = element)} 108 | style={style} 109 | > 110 |
    111 | {heading ? ( 112 |
    (this._header = header)} 115 | onClick={() => this.props.onChangeCollapsed(!collapsed)} 116 | > 117 | {heading} 118 |
    119 | ) : null} 120 |
    121 | {children && isMounted ? ( 122 | children 123 | ) : ( 124 |
    125 | )} 126 |
    127 |
    128 | {shouldRenderHandle ? ( 129 | { 132 | this.setState({ resizing: true }); 133 | onBeginDragHandle(); 134 | }} 135 | onDragHandle={(_e, x, y) => onDragHandle(x, y)} 136 | onEndDragHandle={(_e) => { 137 | this.setState({ resizing: false }); 138 | onEndDragHandle(); 139 | }} 140 | /> 141 | ) : null} 142 | {this.renderToggleButton()} 143 |
    144 | ); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/graph-explorer/workspace/draggableHandle.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | export interface Props extends React.HTMLAttributes { 4 | onBeginDragHandle: (e: React.MouseEvent) => void; 5 | onDragHandle: (e: MouseEvent, dx: number, dy: number) => void; 6 | onEndDragHandle?: (e: MouseEvent) => void; 7 | } 8 | 9 | export class DraggableHandle extends React.Component { 10 | private isHoldingMouse = false; 11 | private originPageX: number; 12 | private originPageY: number; 13 | 14 | render() { 15 | // remove custom handlers from `div` props 16 | const { 17 | onBeginDragHandle: _onBeginDragHandle, 18 | onDragHandle: _onDragHandle, 19 | onEndDragHandle: _onEndDragHandle, 20 | ...props 21 | } = this.props; 22 | return ( 23 |
    24 | {this.props.children} 25 |
    26 | ); 27 | } 28 | 29 | componentWillUnmount() { 30 | this.removeListeners(); 31 | } 32 | 33 | private onHandleMouseDown = (e: React.MouseEvent) => { 34 | if (e.target !== e.currentTarget) { 35 | return; 36 | } 37 | if (this.isHoldingMouse) { 38 | return; 39 | } 40 | 41 | const LEFT_BUTTON = 0; 42 | if (e.button !== LEFT_BUTTON) { 43 | return; 44 | } 45 | 46 | this.isHoldingMouse = true; 47 | this.originPageX = e.pageX; 48 | this.originPageY = e.pageY; 49 | document.addEventListener("mousemove", this.onMouseMove); 50 | document.addEventListener("mouseup", this.onMouseUp); 51 | this.props.onBeginDragHandle(e); 52 | }; 53 | 54 | private onMouseMove = (e: MouseEvent) => { 55 | if (!this.isHoldingMouse) { 56 | return; 57 | } 58 | e.preventDefault(); 59 | this.props.onDragHandle( 60 | e, 61 | e.pageX - this.originPageX, 62 | e.pageY - this.originPageY 63 | ); 64 | }; 65 | 66 | private onMouseUp = (e: MouseEvent) => { 67 | this.removeListeners(); 68 | if (this.props.onEndDragHandle) { 69 | this.props.onEndDragHandle(e); 70 | } 71 | }; 72 | 73 | private removeListeners() { 74 | if (this.isHoldingMouse) { 75 | this.isHoldingMouse = false; 76 | document.removeEventListener("mousemove", this.onMouseMove); 77 | document.removeEventListener("mouseup", this.onMouseUp); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/graph-explorer/workspace/layout/layout.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { Accordion } from "../accordion"; 4 | import { AccordionItem, DockSide } from "../accordionItem"; 5 | 6 | const DEFAULT_HORIZONTAL_COLLAPSED_SIZE = 28; 7 | 8 | export enum WorkspaceLayoutType { 9 | Row = "row", 10 | Column = "column", 11 | Component = "component", 12 | } 13 | 14 | export type WorkspaceLayoutNode = Container | Component; 15 | 16 | interface Container { 17 | type: WorkspaceLayoutType.Row | WorkspaceLayoutType.Column; 18 | children: readonly WorkspaceLayoutNode[]; 19 | defaultSize?: number; 20 | defaultCollapsed?: boolean; 21 | collapsedSize?: number; 22 | minSize?: number; 23 | undocked?: boolean; 24 | animationDuration?: number; 25 | } 26 | 27 | interface Component { 28 | id: string; 29 | type: WorkspaceLayoutType.Component; 30 | content: React.ReactElement; 31 | heading?: React.ReactNode; 32 | defaultSize?: number; 33 | defaultCollapsed?: boolean; 34 | collapsedSize?: number; 35 | minSize?: number; 36 | undocked?: boolean; 37 | } 38 | 39 | export interface WorkspaceLayoutProps { 40 | layout: WorkspaceLayoutNode; 41 | _onStartResize?: (direction: "vertical" | "horizontal") => void; 42 | _onResize?: (direction: "vertical" | "horizontal") => void; 43 | } 44 | 45 | export class WorkspaceLayout extends React.Component { 46 | private renderAccordion({ 47 | children, 48 | direction, 49 | animationDuration, 50 | }: { 51 | children: readonly WorkspaceLayoutNode[]; 52 | direction: "horizontal" | "vertical"; 53 | animationDuration?: number; 54 | }) { 55 | const { _onStartResize, _onResize } = this.props; 56 | const items = children.map((child, index) => { 57 | let dockSide: DockSide; 58 | if (direction === "horizontal" && !child.undocked) { 59 | if (index === 0) { 60 | dockSide = DockSide.Left; 61 | } else if (index === children.length - 1) { 62 | dockSide = DockSide.Right; 63 | } 64 | } 65 | let collapsedSize = child.collapsedSize; 66 | if (collapsedSize === undefined && direction === "horizontal") { 67 | collapsedSize = DEFAULT_HORIZONTAL_COLLAPSED_SIZE; 68 | } 69 | return ( 70 | 83 | {this.renderLayout(child)} 84 | 85 | ); 86 | }); 87 | return ( 88 | 94 | {items} 95 | 96 | ); 97 | } 98 | 99 | private renderLayout(layout: WorkspaceLayoutNode) { 100 | if (layout.type === WorkspaceLayoutType.Row) { 101 | return this.renderAccordion({ 102 | children: layout.children, 103 | direction: "horizontal", 104 | animationDuration: layout.animationDuration, 105 | }); 106 | } 107 | if (layout.type === WorkspaceLayoutType.Column) { 108 | return this.renderAccordion({ 109 | children: layout.children, 110 | direction: "vertical", 111 | animationDuration: layout.animationDuration, 112 | }); 113 | } 114 | if (layout.type === WorkspaceLayoutType.Component) { 115 | return React.Children.only(layout.content); 116 | } 117 | return null; 118 | } 119 | 120 | render() { 121 | const { layout } = this.props; 122 | return this.renderLayout(layout); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/graph-explorer/workspace/resizableSidebar.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { DraggableHandle } from "./draggableHandle"; 4 | 5 | export interface Props { 6 | className?: string; 7 | dockSide?: DockSide; 8 | defaultLength?: number; 9 | minLength?: number; 10 | maxLength?: number; 11 | isOpen?: boolean; 12 | onOpenOrClose?: (open: boolean) => void; 13 | onStartResize: () => void; 14 | children?: React.ReactNode; 15 | } 16 | 17 | export enum DockSide { 18 | Left = 1, 19 | Right, 20 | Top, 21 | Bottom, 22 | } 23 | 24 | export interface State { 25 | readonly open?: boolean; 26 | readonly length?: number; 27 | } 28 | 29 | const CLASS_NAME = "graph-explorer-drag-resizable-column"; 30 | 31 | export class ResizableSidebar extends React.Component { 32 | static readonly defaultProps: Partial = { 33 | dockSide: DockSide.Left, 34 | minLength: 0, 35 | maxLength: 500, 36 | defaultLength: 275, 37 | isOpen: true, 38 | }; 39 | 40 | private originWidth: number; 41 | 42 | constructor(props: Props) { 43 | super(props); 44 | this.state = { 45 | open: this.props.isOpen, 46 | length: this.defaultWidth(), 47 | }; 48 | } 49 | 50 | componentDidUpdate(nextProps: Props) { 51 | if (this.state.open !== nextProps.isOpen) { 52 | this.toggle({ open: nextProps.isOpen }); 53 | } 54 | } 55 | 56 | private defaultWidth() { 57 | const { defaultLength, maxLength } = this.props; 58 | return Math.min(defaultLength, maxLength); 59 | } 60 | 61 | private getSideClass() { 62 | switch (this.props.dockSide) { 63 | case DockSide.Left: 64 | return `${CLASS_NAME}--docked-left`; 65 | case DockSide.Right: 66 | return `${CLASS_NAME}--docked-right`; 67 | case DockSide.Top: 68 | return `${CLASS_NAME}--docked-top`; 69 | case DockSide.Bottom: 70 | return `${CLASS_NAME}--docked-bottom`; 71 | default: 72 | return "docked-right"; 73 | } 74 | } 75 | 76 | private get isHorizontal(): boolean { 77 | return ( 78 | this.props.dockSide === DockSide.Top || 79 | this.props.dockSide === DockSide.Bottom 80 | ); 81 | } 82 | 83 | render() { 84 | const { open, length } = this.state; 85 | 86 | const className = 87 | `${CLASS_NAME} ` + 88 | `${this.getSideClass()} ` + 89 | `${CLASS_NAME}--${open ? "opened" : "closed"} ` + 90 | `${this.props.className || ""}`; 91 | 92 | const style: any = {}; 93 | style[this.isHorizontal ? "height" : "width"] = open ? length : 0; 94 | return ( 95 |
    96 | {open ? this.props.children : null} 97 | 102 |
    this.toggle({ open: !this.state.open })} 105 | >
    106 |
    107 |
    108 | ); 109 | } 110 | 111 | private onBeginDragHandle = () => { 112 | this.originWidth = this.state.open ? this.state.length : 0; 113 | this.props.onStartResize(); 114 | }; 115 | 116 | private onDragHandle = (e: MouseEvent, dx: number, dy: number) => { 117 | let difference = this.isHorizontal ? dy : dx; 118 | if (this.props.dockSide === DockSide.Right) { 119 | difference = -difference; 120 | } 121 | const newWidth = this.originWidth + difference; 122 | const clampedWidth = Math.max( 123 | Math.min(newWidth, this.props.maxLength), 124 | this.props.minLength 125 | ); 126 | const isOpen = 127 | this.props.minLength > 0 || clampedWidth > this.props.minLength; 128 | this.toggle({ open: isOpen, newWidth: clampedWidth }); 129 | }; 130 | 131 | private toggle(params: { open: boolean; newWidth?: number }) { 132 | const { open, newWidth } = params; 133 | const openChanged = open !== this.state.open; 134 | const onStateChanged = () => { 135 | if (openChanged && this.props.onOpenOrClose) { 136 | this.props.onOpenOrClose(open); 137 | } 138 | }; 139 | 140 | const useDefaultWidth = 141 | open && this.state.length === 0 && newWidth === undefined; 142 | if (useDefaultWidth) { 143 | this.setState({ open, length: this.defaultWidth() }, onStateChanged); 144 | } else { 145 | this.setState( 146 | newWidth === undefined ? { open } : { open, length: newWidth }, 147 | onStateChanged 148 | ); 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/graph-explorer/workspace/workspaceContext.ts: -------------------------------------------------------------------------------- 1 | import { PropTypes } from "../viewUtils/react"; 2 | import { EditorController } from "../editor/editorController"; 3 | 4 | export type WorkspaceEventHandler = (key: WorkspaceEventKey) => void; 5 | export enum WorkspaceEventKey { 6 | searchUpdateCriteria = "search:updateCriteria", 7 | searchQueryItem = "search:queryItems", 8 | connectionsLoadLinks = "connections:loadLinks", 9 | connectionsExpandLink = "connections:expandLink", 10 | connectionsLoadElements = "connections:loadElements", 11 | editorChangeSelection = "editor:changeSelection", 12 | editorToggleDialog = "editor:toggleDialog", 13 | editorAddElements = "editor:addElements", 14 | } 15 | 16 | export interface WorkspaceContextWrapper { 17 | workspace: WorkspaceContext; 18 | } 19 | 20 | export interface WorkspaceContext { 21 | editor: EditorController; 22 | triggerWorkspaceEvent: WorkspaceEventHandler; 23 | } 24 | 25 | export const WorkspaceContextTypes: { 26 | [K in keyof WorkspaceContextWrapper]: any; 27 | } = { 28 | workspace: PropTypes.anything, 29 | }; 30 | -------------------------------------------------------------------------------- /styles/diagram/_elementLayer.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-overlayed-element { 2 | cursor: move; 3 | outline: none; 4 | } 5 | 6 | .graph-explorer-overlayed-element, 7 | .graph-explorer-exported-element { 8 | // set defaults for all inherited properties 9 | box-sizing: border-box; 10 | color: black; 11 | font-family: "Helvetica Neue",Helvetica,Arial,sans-serif; 12 | font-size: 14px; 13 | line-height: 1.42857143; 14 | 15 | // http://stackoverflow.com/questions/6664460/line-height-affects-images 16 | img { vertical-align: middle; } 17 | } 18 | 19 | .graph-explorer-exported-element { 20 | *, *:before, *:after { 21 | box-sizing: inherit; 22 | } 23 | } 24 | 25 | .graph-explorer-overlayed-element--blurred { 26 | filter: grayscale(100%); 27 | opacity: 0.5; 28 | } 29 | -------------------------------------------------------------------------------- /styles/diagram/_linkLayer.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-link { 2 | &__wrap { 3 | stroke-width: 12px; 4 | stroke-linejoin: round; 5 | stroke-linecap: round; 6 | stroke: transparent; 7 | fill: none; 8 | } 9 | &:hover &__wrap { 10 | stroke: rgba(140, 140, 140, 0.44); 11 | } 12 | 13 | &__vertex { 14 | cursor: all-scroll; 15 | } 16 | &:not(:hover) &__vertex { 17 | fill: transparent; 18 | } 19 | 20 | &__vertex-tools { 21 | opacity: 0; 22 | cursor: pointer; 23 | > circle { fill: gray; } 24 | > path { stroke: white; } 25 | &:hover { 26 | > circle { fill: black; } 27 | } 28 | } 29 | &:hover &__vertex-tools { 30 | opacity: 0.8; 31 | } 32 | 33 | &--blurred { 34 | opacity: 0.5; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /styles/diagram/_paperArea.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-paper-area { 2 | flex: auto; 3 | width: 0; 4 | display: flex; 5 | position: relative; 6 | cursor: default; 7 | background: #fff; 8 | 9 | &__area { 10 | flex: auto; 11 | overflow: hidden; 12 | } 13 | 14 | &__widgets { 15 | position: absolute; 16 | left: 0; 17 | top: 0; 18 | } 19 | 20 | &__watermark { 21 | background-size: cover; 22 | width: 8%; 23 | max-width: 130px; 24 | min-width: 50px; 25 | position: absolute; 26 | top: 15px; 27 | right: 25px; 28 | cursor: pointer; 29 | opacity: 0.3; 30 | transition: opacity 0.3s; 31 | 32 | &:hover { 33 | opacity: 0.5; 34 | } 35 | } 36 | 37 | &--hide-scrollbars { 38 | overflow: hidden; 39 | } 40 | } 41 | 42 | .graph-explorer-paper { 43 | position: relative; 44 | } 45 | 46 | .graph-explorer-exported-watermark { 47 | opacity: 0.3; 48 | transition: opacity 0.3s; 49 | } 50 | -------------------------------------------------------------------------------- /styles/diagram/animation.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-paper-area--animated { 2 | .graph-explorer-overlayed-element { 3 | transition: transform 0.5s ease-in-out; 4 | } 5 | .graph-explorer-link-layer, .graph-explorer-paper-area__widgets { 6 | transition: none; 7 | opacity: 0; 8 | } 9 | } 10 | 11 | .graph-explorer-link-layer, .graph-explorer-paper-area__widgets { 12 | transition: opacity 0.5s ease-in-out; 13 | } 14 | -------------------------------------------------------------------------------- /styles/editor/_authoringState.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-authoring-state { 2 | &__item-error { 3 | align-self: flex-end; 4 | margin: 0px 5px; 5 | display: flex; 6 | align-items: center; 7 | cursor: help; 8 | } 9 | 10 | &__item-error-icon { 11 | background: url("../images/font-awesome/exclamation-triangle.svg"); 12 | height: 15px; 13 | width: 17px; 14 | } 15 | 16 | &__state-label { 17 | font-weight: bold; 18 | margin-right: 5px; 19 | } 20 | 21 | &__state-cancel { 22 | color: #3f87a6; 23 | cursor: pointer; 24 | &:hover { 25 | text-decoration: underline; 26 | } 27 | } 28 | 29 | &__state-indicator { 30 | position: absolute; 31 | } 32 | &__state-indicator-container { 33 | position: relative; 34 | } 35 | &__state-indicator-body { 36 | position: absolute; 37 | white-space: nowrap; 38 | display: flex; 39 | align-items: center; 40 | bottom: 0; 41 | background: rgba(255, 255, 255, 0.7); 42 | border-radius: 5px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /styles/editor/_loadingWidget.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-loading-widget { 2 | left: 0; 3 | right: 0; 4 | top: 0; 5 | bottom: 0; 6 | margin: auto; 7 | position: absolute; 8 | display: flex; 9 | align-items: center; 10 | justify-content: center; 11 | background: rgba(255, 255, 255, 0.9); 12 | z-index: 10; 13 | } 14 | -------------------------------------------------------------------------------- /styles/main.scss: -------------------------------------------------------------------------------- 1 | @use "viewUtils/spinner"; 2 | @use "viewUtils/clearfix"; 3 | @use "viewUtils/label"; 4 | @use "viewUtils/btn"; 5 | @use "viewUtils/btn-group"; 6 | @use "viewUtils/form-control"; 7 | @use "viewUtils/input-group"; 8 | @use "viewUtils/list-group"; 9 | @use "viewUtils/badge"; 10 | 11 | @use "diagram/elementLayer"; 12 | @use "diagram/linkLayer"; 13 | @use "diagram/paperArea"; 14 | @use "diagram/animation"; 15 | 16 | @use "editor/loadingWidget"; 17 | @use "editor/authoringState"; 18 | 19 | @use "widgets/authoringTools"; 20 | @use "widgets/classTree"; 21 | @use "widgets/connectionsMenu"; 22 | @use "widgets/dialog"; 23 | @use "widgets/editForm"; 24 | @use "widgets/halo"; 25 | @use "widgets/haloLink"; 26 | @use "widgets/instancesSearch"; 27 | @use "widgets/linksToolbox"; 28 | @use "widgets/listElementView"; 29 | @use "widgets/navigator"; 30 | @use "widgets/progressBar"; 31 | @use "widgets/searchResults"; 32 | 33 | @use "workspace/accordion"; 34 | @use "workspace/resizableSidebar"; 35 | @use "workspace/tutorial"; 36 | @use "workspace/toolbar"; 37 | @use "workspace/workspace"; 38 | 39 | @use "templates/icons"; 40 | @use "templates/defaultElement"; 41 | @use "templates/group"; 42 | @use "templates/standard"; 43 | -------------------------------------------------------------------------------- /styles/templates/_defaultElement.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-default-template { 2 | max-width: 450px; 3 | float: left; 4 | border-radius: 12px; 5 | border-style: solid; 6 | border-width: 1px; 7 | 8 | &__thumbnail { 9 | text-align: center; 10 | > img { 11 | max-width: 200px; 12 | } 13 | } 14 | 15 | &[data-expanded='true'] &__thumbnail > img { 16 | max-width: 350px; 17 | } 18 | } 19 | 20 | .graph-explorer-default-template_type-line { 21 | padding: 0px 7px; 22 | color: white; 23 | height: 18px; 24 | margin-bottom: 2px; 25 | overflow: hidden; 26 | display: flex; 27 | align-items: baseline; 28 | margin-top: -3px; 29 | } 30 | 31 | div.graph-explorer-default-template_type-line__icon { 32 | position: inherit !important; 33 | margin-right: 4px; 34 | } 35 | 36 | .graph-explorer-default-template_type-line_text-container { 37 | flex-grow: 1; 38 | overflow: hidden; 39 | text-overflow: ellipsis; 40 | width: 0; 41 | } 42 | .graph-explorer-default-template_type-line_text-container__text { 43 | display: inline; 44 | white-space: nowrap; 45 | font-size: 12px; 46 | } 47 | 48 | .graph-explorer-default-template_body { 49 | border-bottom-left-radius: 12px; 50 | border-bottom-right-radius: 12px; 51 | border-top-style: solid; 52 | border-top-width: 1px; 53 | background: white; 54 | padding: 7px 10px 8px 10px; 55 | overflow: hidden; 56 | display: flex; 57 | flex-direction: column; 58 | } 59 | 60 | .graph-explorer-default-template_body__label { 61 | font-size: 19px; 62 | font-weight: 100; 63 | overflow: hidden; 64 | text-overflow: ellipsis; 65 | margin-bottom: 0; 66 | white-space: nowrap; 67 | word-wrap: break-word; 68 | } 69 | 70 | .graph-explorer-default-template_body_expander { 71 | width: 100%; 72 | overflow: hidden; 73 | display: flex; 74 | } 75 | 76 | .graph-explorer-default-template_body_expander__iri_label { 77 | font-size: 12px; 78 | font-weight: 100; 79 | margin-right: 15px; 80 | color: #b6b6b6; 81 | } 82 | 83 | .graph-explorer-default-template_body_expander_iri { 84 | font-size: 12px; 85 | width: 100%; 86 | overflow: hidden; 87 | white-space: nowrap; 88 | text-overflow: ellipsis; 89 | } 90 | 91 | .graph-explorer-default-template_body_expander_iri__link { 92 | color: #b6b6b6; 93 | } 94 | 95 | .graph-explorer-default-template_body_expander__hr { 96 | margin: 5px 0px 5px 0px; 97 | } 98 | 99 | .graph-explorer-default-template_body_expander_property-table { 100 | font-size: 15px; 101 | font-weight: 100; 102 | margin-bottom: 5px; 103 | max-height: 200px; 104 | overflow-y: scroll; 105 | overflow-x: hidden; 106 | } 107 | 108 | .graph-explorer-default-template_body_expander_property-table_row { 109 | white-space: nowrap; 110 | } 111 | 112 | .graph-explorer-default-template_body_expander_property-table_row__key { 113 | display: inline-block; 114 | width: 50%; 115 | text-overflow: ellipsis; 116 | overflow: hidden; 117 | vertical-align: top; 118 | } 119 | 120 | .graph-explorer-default-template_body_expander_property-table_row_key { 121 | display: inline-block; 122 | width: 50%; 123 | text-overflow: ellipsis; 124 | overflow: hidden; 125 | vertical-align: top; 126 | } 127 | 128 | .graph-explorer-default-template_body_expander_property-table_row_key_values { 129 | display: inline-block; 130 | width: 50%; 131 | text-overflow: ellipsis; 132 | overflow: hidden; 133 | } 134 | 135 | .graph-explorer-default-template_body_expander_property-table_row_key_values__value { 136 | width: 100%; 137 | overflow: hidden; 138 | text-overflow: ellipsis; 139 | white-space: initial; 140 | padding-right: 10px; 141 | } 142 | -------------------------------------------------------------------------------- /styles/templates/_group.scss: -------------------------------------------------------------------------------- 1 | @use "./defaultElement"; 2 | 3 | .graph-explorer-group-template { 4 | overflow: hidden; 5 | 6 | &__wrap { 7 | @extend .graph-explorer-default-template; 8 | max-width: none; 9 | } 10 | 11 | &__type-line { 12 | @extend .graph-explorer-default-template_type-line; 13 | } 14 | 15 | &__type-line-icon { 16 | @extend .graph-explorer-default-template_type-line__icon; 17 | } 18 | 19 | &__type-line-text-container { 20 | @extend .graph-explorer-default-template_type-line_text-container; 21 | } 22 | 23 | &__type-line-text { 24 | @extend .graph-explorer-default-template_type-line_text-container__text; 25 | } 26 | 27 | &__body { 28 | @extend .graph-explorer-default-template_body; 29 | overflow: visible; 30 | } 31 | 32 | &__label { 33 | @extend .graph-explorer-default-template_body__label; 34 | } 35 | 36 | &__embedded-layer { 37 | margin-top: 7px; 38 | } 39 | 40 | .graph-explorer-paper__canvas { 41 | border-color: #fff; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /styles/templates/_icons.scss: -------------------------------------------------------------------------------- 1 | .jstree-icon.jstree-themeicon.jstree-themeicon-custom { 2 | background-size: contain !important; // HACK: override background-size to fit in custom icons 3 | } 4 | -------------------------------------------------------------------------------- /styles/templates/_standard.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-standard-template { 2 | min-width: 180px; 3 | max-width: 400px; 4 | float: left; 5 | 6 | &__main { 7 | border-radius: 2px; 8 | border: 1px solid; 9 | } 10 | 11 | &__body { 12 | margin-left: 8px; 13 | border-radius: 0 2px 2px 0; 14 | border-left: 1px solid; 15 | padding: 3px 0; 16 | background: #fafaf9; 17 | } 18 | 19 | &__body-horizontal { 20 | display: flex; 21 | align-items: center; 22 | justify-content: space-between; 23 | overflow: hidden; 24 | } 25 | 26 | &__body-content { 27 | flex-grow: 1; 28 | min-width: 0; 29 | margin-right: 12px; 30 | } 31 | 32 | &__label { 33 | font-size: 19px; 34 | white-space: nowrap; 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | } 38 | 39 | &__thumbnail { 40 | font-size: 26px; 41 | margin: 5px 10px; 42 | flex-shrink: 0; 43 | width: 50px; 44 | height: 50px; 45 | display: flex; 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | 50 | &__thumbnail-image { 51 | max-height: 100%; 52 | max-width: 100%; 53 | } 54 | 55 | &__thumbnail-icon { 56 | max-height: 26px; 57 | max-width: 26px; 58 | } 59 | 60 | &__photo { 61 | border-bottom: 1px solid; 62 | } 63 | 64 | &__photo-image { 65 | width: 100%; 66 | border-radius: 2px 2px 0 0; 67 | } 68 | 69 | &__type { 70 | white-space: nowrap; 71 | overflow: hidden; 72 | text-overflow: ellipsis; 73 | font-size: 11px; 74 | font-style: italic; 75 | color: #999; 76 | display: flex; 77 | } 78 | 79 | &__type-value { 80 | width: 0; 81 | flex-grow: 1; 82 | overflow: hidden; 83 | text-overflow: ellipsis; 84 | } 85 | 86 | &__iri { 87 | width: 100%; 88 | overflow: hidden; 89 | display: flex; 90 | } 91 | 92 | &__iri-key { 93 | font-size: 12px; 94 | margin-right: 4px; 95 | color: #b6b6b6; 96 | } 97 | 98 | &__iri-value { 99 | font-size: 12px; 100 | width: 100%; 101 | overflow: hidden; 102 | white-space: nowrap; 103 | text-overflow: ellipsis; 104 | } 105 | 106 | &__iri-value a { 107 | color: #b6b6b6; 108 | } 109 | 110 | &__hr { 111 | margin: 5px 0; 112 | } 113 | 114 | &__properties { 115 | font-size: 15px; 116 | max-height: 200px; 117 | overflow-y: scroll; 118 | overflow-x: hidden; 119 | } 120 | 121 | &__propertites-row { 122 | white-space: nowrap; 123 | } 124 | 125 | &__properties-key { 126 | display: inline-block; 127 | width: 50%; 128 | text-overflow: ellipsis; 129 | overflow: hidden; 130 | vertical-align: top; 131 | } 132 | 133 | &__properties-values { 134 | display: inline-block; 135 | width: 50%; 136 | text-overflow: ellipsis; 137 | overflow: hidden; 138 | } 139 | 140 | &__properties-value { 141 | width: 100%; 142 | overflow: hidden; 143 | text-overflow: ellipsis; 144 | white-space: initial; 145 | padding-right: 10px; 146 | } 147 | 148 | &__pinned-props { 149 | border-top: 1px solid; 150 | margin: 0 5px; 151 | } 152 | 153 | &__dropdown { 154 | border-radius: 2px; 155 | background-color: white; 156 | margin-top: 5px; 157 | border: 1px solid; 158 | } 159 | 160 | &__dropdown-content { 161 | width: 100%; 162 | padding: 9px; 163 | } 164 | 165 | &__actions { 166 | display: flex; 167 | justify-content: space-between; 168 | button { 169 | padding: 5px; 170 | min-width: 60px; 171 | } 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /styles/viewUtils/_badge.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-badge { 2 | display: inline-block; 3 | min-width: 10px; 4 | font-size: 12px; 5 | font-weight: 700; 6 | line-height: 1; 7 | color: rgb(255, 255, 255); 8 | text-align: center; 9 | white-space: nowrap; 10 | vertical-align: middle; 11 | background-color: rgb(119, 119, 119); 12 | padding: 3px 7px; 13 | border-radius: 10px; 14 | } 15 | -------------------------------------------------------------------------------- /styles/viewUtils/_btn-group.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-btn-group { 2 | position: relative; 3 | display: inline-block; 4 | vertical-align: middle; 5 | 6 | > .graph-explorer-btn, > .graph-explorer-btn-group { 7 | position: relative; 8 | float: left; 9 | } 10 | 11 | > .graph-explorer-btn:hover, 12 | > .graph-explorer-btn:focus, 13 | > .graph-explorer-btn.active, 14 | > .graph-explorer-btn.active:hover { 15 | z-index: 2; 16 | } 17 | 18 | .graph-explorer-btn + .graph-explorer-btn, 19 | .graph-explorer-btn + .graph-explorer-btn-group, 20 | .graph-explorer-btn-group + .graph-explorer-btn, 21 | .graph-explorer-btn-group + .graph-explorer-btn-group { 22 | margin-left: -1px; 23 | } 24 | 25 | > .graph-explorer-btn:first-child { 26 | margin-left: 0; 27 | } 28 | 29 | &-sm > .graph-explorer-btn { 30 | font-size: 12px; 31 | line-height: 1.5; 32 | padding: 5px 10px; 33 | } 34 | 35 | &-xs > .graph-explorer-btn { 36 | font-size: 12px; 37 | line-height: 1.5; 38 | padding: 1px 5px; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /styles/viewUtils/_btn.scss: -------------------------------------------------------------------------------- 1 | @use "sass:color"; 2 | .graph-explorer-btn { 3 | display: inline-block; 4 | padding: 6px 12px; 5 | margin-bottom: 0; 6 | font-size: 14px; 7 | font-weight: 400; 8 | line-height: 1.42857143; 9 | text-align: center; 10 | white-space: nowrap; 11 | vertical-align: middle; 12 | touch-action: manipulation; 13 | cursor: pointer; 14 | user-select: none; 15 | background-image: none; 16 | border: 1px solid transparent; 17 | text-transform: none; 18 | transition: background-color 0.3s; 19 | 20 | &[disabled] { 21 | cursor: not-allowed; 22 | opacity: .65; 23 | } 24 | } 25 | 26 | 27 | .graph-explorer-btn-default { 28 | background-color: #fff; 29 | border-color: #ccc; 30 | color: #333; 31 | 32 | &:hover { 33 | background-color: #e0e0e0; 34 | } 35 | 36 | &.active { 37 | background-color: #e0e0e0; 38 | border-color: #dbdbdb; 39 | } 40 | 41 | &.active:hover { 42 | background-color: #d4d4d4; 43 | border-color: #8c8c8c; 44 | } 45 | 46 | &[disabled], &[disabled]:hover, &[disabled].active, &[disabled].active:hover { 47 | background-color: #e0e0e0; 48 | } 49 | } 50 | 51 | .graph-explorer-btn-primary { 52 | color: rgb(255, 255, 255); 53 | background-color: rgb(51, 122, 183); 54 | border-color: #245580; 55 | 56 | &:hover, &:focus, &:active { 57 | background-color: #265a88; 58 | } 59 | 60 | &:hover, &:focus { 61 | border-color: #204d74; 62 | } 63 | 64 | &.active { 65 | border-color: #245580; 66 | } 67 | 68 | &[disabled], &[disabled]:hover, &[disabled]:focus, &[disabled]:active, &[disabled].active { 69 | background-color: #265a88; 70 | } 71 | } 72 | 73 | .graph-explorer-btn-success { 74 | background-color: #5cb85c; 75 | border-color: #5cb85c; 76 | color: #fff; 77 | transition: 0.3s; 78 | 79 | &:hover { 80 | background: color.adjust(#5cb85c, $lightness: -8%); 81 | } 82 | } 83 | 84 | .graph-explorer-btn-danger { 85 | background-color: #c9302c; 86 | border-color: #c9302c; 87 | color: #fff; 88 | transition: 0.3s; 89 | 90 | &:hover { 91 | background: color.adjust(#c9302c, $lightness: -8%); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /styles/viewUtils/_clearfix.scss: -------------------------------------------------------------------------------- 1 | .clearfix { 2 | 3 | &:before, &:after { 4 | display: table; 5 | content: ""; 6 | } 7 | 8 | &:after { 9 | clear: both; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /styles/viewUtils/_form-control.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-form-control { 2 | display: block; 3 | width: 100%; 4 | height: 34px; 5 | margin: 0; 6 | padding: 6px 12px; 7 | font-size: 14px; 8 | line-height: 1.42857143; 9 | color: #555; 10 | background: #fff; 11 | border: 1px solid #ccc; 12 | transition: border-color ease-in-out .15s; 13 | 14 | &:focus { 15 | border-color: #66afe9; 16 | outline: 0; 17 | } 18 | 19 | &:disabled { 20 | background-color: #eee; 21 | cursor: not-allowed; 22 | opacity: 0.6; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /styles/viewUtils/_input-group.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-input-group { 2 | position: relative; 3 | display: table; 4 | border-collapse: separate; 5 | 6 | .graph-explorer-form-control { 7 | display: table-cell; 8 | position: relative; 9 | z-index: 2; 10 | float: left; 11 | width: 100%; 12 | margin-bottom: 0; 13 | } 14 | 15 | .graph-explorer-form-control:first-child { 16 | border-top-right-radius: 0; 17 | border-bottom-right-radius: 0; 18 | } 19 | } 20 | 21 | .graph-explorer-input-group-btn { 22 | display: table-cell; 23 | width: 1%; 24 | vertical-align: middle; 25 | position: relative; 26 | font-size: 0; 27 | white-space: nowrap; 28 | 29 | &:last-child > .graph-explorer-btn { 30 | border-top-left-radius: 0; 31 | border-bottom-left-radius: 0; 32 | z-index: 2; 33 | margin-left: -1px; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /styles/viewUtils/_label.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-label { 2 | display: inline-block; 3 | max-width: 100%; 4 | margin-bottom: 5px; 5 | font-weight: 700; 6 | } 7 | -------------------------------------------------------------------------------- /styles/viewUtils/_list-group.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-list-group { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | .graph-explorer-list-group-item:last-child { 7 | margin-bottom: 0; 8 | } 9 | 10 | .graph-explorer-list-group-item { 11 | position: relative; 12 | display: block; 13 | padding: 10px 15px; 14 | margin-bottom: -1px; 15 | background-color: #fff; 16 | border: 1px solid #ddd; 17 | } 18 | -------------------------------------------------------------------------------- /styles/viewUtils/_spinner.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-spinner { 2 | &__arrow { 3 | animation-name: graph-explorer-spinner-rotation; 4 | animation-duration: 1.5s; 5 | animation-timing-function: cubic-bezier(0.645, 0.045, 0.355, 1); 6 | animation-iteration-count: infinite; 7 | } 8 | 9 | &[data-error='true'] &__arrow { 10 | animation-iteration-count: 1; 11 | } 12 | } 13 | 14 | @keyframes graph-explorer-spinner-rotation { 15 | 0% { transform: rotate(0deg); } 16 | 100% { transform: rotate(360deg); } 17 | } 18 | -------------------------------------------------------------------------------- /styles/widgets/_authoringTools.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-authoring-tools { 2 | margin: 10px; 3 | 4 | &__create-entity { 5 | display: flex; 6 | flex-wrap: wrap; 7 | align-items: center; 8 | justify-content: center; 9 | width: 100%; 10 | white-space: normal; 11 | } 12 | 13 | &__type-label { 14 | color: black; 15 | border: black 1px dashed; 16 | background: rgb(255, 210, 33); 17 | padding: 0 .5em 0 .5em; 18 | word-wrap: break-word; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /styles/widgets/_classTree.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-class-tree { 2 | flex: auto; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | &__filter { 7 | flex-shrink: 0; 8 | margin: 10px 0 10px 0; 9 | } 10 | 11 | &__filter-group { 12 | margin-left: 10px; 13 | margin-right: 10px; 14 | } 15 | 16 | &__only-creatable { 17 | display: block; 18 | margin-top: 5px; 19 | } 20 | 21 | &__tree { 22 | border-top: 1px solid rgb(221, 221, 221); 23 | } 24 | 25 | &__spinner { 26 | align-self: center; 27 | /* center vertically in flexbox */ 28 | margin: auto; 29 | } 30 | } 31 | 32 | .graph-explorer-class-leaf { 33 | margin: 1px 0; 34 | 35 | &__row { 36 | display: flex; 37 | align-items: center; 38 | white-space: nowrap; 39 | user-select: none; 40 | > * { flex-shrink: 0; } 41 | } 42 | 43 | &__body { 44 | display: flex; 45 | align-items: center; 46 | text-decoration: none; 47 | font-size: 15px; 48 | padding: 1px; 49 | border: 1px solid; 50 | border-color: transparent; 51 | &:hover { 52 | background: #dcebff91; 53 | border-color: #ccefff; 54 | cursor: pointer; 55 | } 56 | &--selected { 57 | background-color: #beebff; 58 | border-color: #8edcff; 59 | } 60 | } 61 | 62 | &__icon-container { 63 | height: 20px; 64 | } 65 | 66 | &__icon { 67 | display: block; 68 | height: 100%; 69 | } 70 | 71 | &__label { 72 | margin-left: 5px; 73 | color: black; 74 | } 75 | 76 | &__highlighted-term { 77 | font-weight: bold; 78 | } 79 | 80 | &__count { 81 | margin-left: 5px; 82 | } 83 | 84 | &__children { 85 | margin-left: 20px; 86 | } 87 | 88 | &__no-toggle { 89 | display: inline-block; 90 | width: 22px; 91 | height: 22px; 92 | } 93 | 94 | &__toggle { 95 | display: inline-block; 96 | width: 12px; 97 | height: 12px; 98 | margin: 5px; 99 | &:hover:not(:empty) { 100 | background: #dcebff91; 101 | cursor: pointer; 102 | } 103 | } 104 | 105 | &__toggle-icon { 106 | display: block; 107 | height: 100%; 108 | } 109 | 110 | &__create { 111 | margin-left: 5px; 112 | > button { 113 | cursor: move; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /styles/widgets/_dialog.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-dialog { 2 | background: #fff; 3 | border: 1px solid #333; 4 | box-shadow: 0 4px 15px 7px rgba(51, 51, 51, 0.05); 5 | position: absolute; 6 | 7 | &__caption { 8 | font-weight: bold; 9 | position: absolute; 10 | top: -11px; 11 | left: 10px; 12 | background: white; 13 | padding-left: 3px; 14 | padding-right: 3px; 15 | border-radius: 6px; 16 | } 17 | 18 | &__close-button { 19 | background: transparent url("../images/font-awesome/times-circle-regular.svg"); 20 | background-size: contain; 21 | border: 0 none; 22 | cursor: pointer; 23 | display: block; 24 | outline: none; 25 | padding: 0; 26 | position: absolute; 27 | top: -22px; 28 | right: -22px; 29 | height: 20px; 30 | width: 20px; 31 | 32 | opacity: 0.5; 33 | transition: 0.3s; 34 | 35 | &:hover { 36 | opacity: 1; 37 | } 38 | } 39 | } 40 | 41 | .graph-explorer-dialog__bottom-right-handle { 42 | position: absolute; 43 | bottom: 0; 44 | right: 0; 45 | width: 0; 46 | height: 0; 47 | border-style: solid; 48 | border-width: 0 0 10px 10px; 49 | border-color: transparent transparent rgba(0, 0, 0, 0.38) transparent; 50 | cursor: nwse-resize; 51 | 52 | &::before { 53 | content: ""; 54 | position: absolute; 55 | bottom: -10px; 56 | right: 0; 57 | width: 0; 58 | height: 0; 59 | border-style: solid; 60 | border-width: 0 0 5px 5px; 61 | border-color: transparent transparent rgba(0, 0, 0, 0.38) transparent; 62 | } 63 | 64 | &:hover { 65 | border-color: transparent transparent rgba(0, 0, 0, 0.5) transparent; 66 | } 67 | } 68 | 69 | .graph-explorer-dialog__bottom-handle, .graph-explorer-dialog__right-handle { 70 | position: absolute; 71 | opacity: 0; 72 | background-color: black; 73 | 74 | &:hover { 75 | opacity: 0.1; 76 | } 77 | } 78 | 79 | .graph-explorer-dialog__bottom-handle { 80 | bottom: 0; 81 | width: 100%; 82 | height: 5px; 83 | cursor: ns-resize; 84 | } 85 | 86 | .graph-explorer-dialog__right-handle { 87 | top: 0; 88 | right: 0; 89 | width: 5px; 90 | height: 100%; 91 | cursor: ew-resize; 92 | } 93 | -------------------------------------------------------------------------------- /styles/widgets/_editForm.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-edit-form { 2 | height: 100%; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | &__body { 7 | overflow: auto; 8 | padding: 10px; 9 | flex: 1 1 100%; 10 | display: flex; 11 | flex-direction: column; 12 | position: relative; 13 | } 14 | 15 | &__controls { 16 | border-top: 1px solid #ccc; 17 | padding: 10px; 18 | text-align: right; 19 | flex: 0 0 auto; 20 | } 21 | 22 | &__apply-button { 23 | margin-right: 5px; 24 | } 25 | 26 | &__form-row { 27 | display: block; 28 | margin-bottom: 10px; 29 | } 30 | 31 | &__element-selector { 32 | flex-grow: 1; 33 | display: flex; 34 | flex-direction: column; 35 | } 36 | 37 | &__search { 38 | flex-shrink: 0; 39 | position: relative; 40 | } 41 | 42 | &__search-icon { 43 | opacity: 0.6; 44 | position: absolute; 45 | top: 50%; 46 | left: 10px; 47 | margin-top: -7px; 48 | } 49 | 50 | &__search-input { 51 | padding-left: 33px; 52 | } 53 | 54 | &__existing-elements-list { 55 | flex: 1 1 0; 56 | margin-top: 7px; 57 | padding-right: 10px; 58 | overflow-y: scroll; 59 | } 60 | 61 | &__separator { 62 | margin: 7px 0; 63 | overflow: hidden; 64 | text-align: center; 65 | } 66 | 67 | &__separator-text { 68 | color: #555; 69 | display: inline-block; 70 | font-size: 13px; 71 | position: relative; 72 | 73 | &:before, &:after { 74 | content: ""; 75 | border-top: 1px solid; 76 | position: absolute; 77 | top: 50%; 78 | margin: 0 10px; 79 | width: 500px; 80 | } 81 | 82 | &:before { 83 | left: 100%; 84 | } 85 | 86 | &:after { 87 | right: 100%; 88 | } 89 | } 90 | 91 | &__progress { 92 | position: absolute; 93 | bottom: 0; 94 | left: 0; 95 | right: 0; 96 | } 97 | 98 | &__control-row { 99 | position: relative; 100 | padding-bottom: 18px; 101 | } 102 | 103 | &__control-error { 104 | color: red; 105 | position: absolute; 106 | bottom: 0; 107 | left: 0; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /styles/widgets/_halo.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-halo { 2 | position: absolute; 3 | pointer-events: none; 4 | 5 | border: 1.5px dashed #d8956d; 6 | border-radius: 2px; 7 | box-shadow: 0 0 5px 0 #d8956d inset; 8 | 9 | $buttonWidth: 20px; 10 | $buttonHeight: 20px; 11 | $buttonMargin: 2px; 12 | 13 | @mixin halo-button { 14 | position: absolute; 15 | background-color: transparent; 16 | background-size: contain; 17 | background-repeat: no-repeat; 18 | background-position: center; 19 | border: 0 none; 20 | cursor: pointer; 21 | outline: none; 22 | padding: 0; 23 | pointer-events: auto; 24 | width: $buttonWidth; 25 | height: $buttonHeight; 26 | 27 | opacity: 0.5; 28 | transition: opacity 0.3s; 29 | &:hover { 30 | opacity: 1; 31 | } 32 | 33 | &[disabled] { 34 | cursor: not-allowed; 35 | opacity: 0.2; 36 | } 37 | } 38 | 39 | @mixin spinner { 40 | position: absolute; 41 | width: $buttonWidth; 42 | height: $buttonHeight; 43 | } 44 | 45 | @mixin n-docked { 46 | top: -($buttonHeight + $buttonMargin); 47 | left: 50%; 48 | margin-left: -(calc($buttonWidth / 2)); 49 | } 50 | 51 | @mixin nw-docked { 52 | top: -($buttonHeight + $buttonMargin); 53 | left: -($buttonWidth + $buttonMargin); 54 | } 55 | 56 | @mixin ne-docked { 57 | top: -($buttonHeight + $buttonMargin); 58 | right: -($buttonWidth + $buttonMargin); 59 | } 60 | 61 | @mixin e-docked { 62 | top: 50%; 63 | margin-top: -(calc($buttonHeight / 2)); 64 | right: -($buttonWidth + $buttonMargin); 65 | } 66 | 67 | @mixin w-docked { 68 | top: 50%; 69 | margin-top: -(calc($buttonHeight / 2)); 70 | left: -($buttonWidth + $buttonMargin); 71 | } 72 | 73 | @mixin s-docked { 74 | bottom: -($buttonHeight + $buttonMargin); 75 | left: 50%; 76 | margin-left: -(calc($buttonWidth / 2)); 77 | } 78 | 79 | @mixin sw-docked { 80 | bottom: -($buttonHeight + $buttonMargin); 81 | left: -($buttonWidth + $buttonMargin); 82 | } 83 | 84 | @mixin se-docked { 85 | bottom: -($buttonHeight + $buttonMargin); 86 | right: -($buttonWidth + $buttonMargin); 87 | } 88 | 89 | &__navigate { 90 | @include halo-button; 91 | @include e-docked; 92 | 93 | &--open { 94 | background-image: url("../images/connections.svg"); 95 | } 96 | 97 | &--closed { 98 | background-image: url("../images/close-connections.svg"); 99 | } 100 | } 101 | 102 | &__folow { 103 | @include halo-button; 104 | @include w-docked; 105 | 106 | background-image: url("../images/link.svg"); 107 | } 108 | 109 | &__remove { 110 | @include halo-button; 111 | @include ne-docked; 112 | background-image: url("../images/delete.svg"); 113 | } 114 | 115 | &__expand { 116 | @include halo-button; 117 | @include s-docked; 118 | 119 | &--open { 120 | background-image: url("../images/expand-properties.png"); 121 | } 122 | 123 | &--closed { 124 | background-image: url("../images/collapse-properties.png"); 125 | } 126 | } 127 | 128 | &__add-to-filter { 129 | @include halo-button; 130 | @include se-docked; 131 | background-image: url("../images/add-to-filter.png"); 132 | } 133 | 134 | &__revert { 135 | @include halo-button; 136 | @include n-docked; 137 | background-image: url("../images/font-awesome/undo-solid.svg"); 138 | } 139 | 140 | &__establish-connection { 141 | @include halo-button; 142 | @include sw-docked; 143 | background-image: url("../images/font-awesome/plug.svg"); 144 | } 145 | 146 | &__establish-connection-spinner { 147 | @include spinner; 148 | @include sw-docked; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /styles/widgets/_haloLink.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-halo-link { 2 | 3 | &__button { 4 | background: transparent; 5 | border: 0 none; 6 | cursor: pointer; 7 | padding: 0; 8 | position: absolute; 9 | opacity: 0.8; 10 | outline: none; 11 | transition: opacity 0.3s; 12 | 13 | &:hover { 14 | opacity: 1; 15 | } 16 | 17 | &[disabled] { 18 | cursor: not-allowed; 19 | opacity: 0.5; 20 | } 21 | } 22 | 23 | @mixin button { 24 | background-color: #ccc; 25 | background-position: 50%; 26 | background-repeat: no-repeat; 27 | background-size: 60%; 28 | border-radius: 10px; 29 | height: 20px; 30 | width: 20px; 31 | } 32 | 33 | &__edit { 34 | @include button; 35 | background-image: url("../images/font-awesome/edit.svg"); 36 | } 37 | 38 | &__delete { 39 | @include button; 40 | background-image: url("../images/font-awesome/trash-alt.svg"); 41 | } 42 | 43 | &__spinner { 44 | position: absolute; 45 | } 46 | 47 | &__edit-label-button { 48 | background: transparent url("../images/font-awesome/pen-square-solid.svg") no-repeat; 49 | background-size: cover; 50 | border: 0 none; 51 | cursor: pointer; 52 | padding: 0; 53 | position: absolute; 54 | margin-left: 5px; 55 | outline: none; 56 | opacity: 0.5; 57 | transition: opacity 0.3s; 58 | 59 | &:hover { 60 | opacity: 1; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /styles/widgets/_instancesSearch.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-instances-search { 2 | flex: auto; 3 | display: flex; 4 | flex-direction: column; 5 | 6 | &__criteria { 7 | flex-shrink: 0; 8 | margin: 10px 0 10px 0; 9 | } 10 | 11 | &__criterions { 12 | padding-left: 15px; 13 | margin-bottom: 10px; 14 | &:empty { margin-bottom: 0; } 15 | } 16 | 17 | &__criterion { 18 | display: block; 19 | position: relative; 20 | width: 100%; 21 | } 22 | 23 | &__criterion-class { 24 | border: black 1px dashed; 25 | background: rgb(255, 210, 33); 26 | padding: 0 .5em 0 .5em; 27 | word-wrap: break-word; 28 | } 29 | 30 | &__criterion-element { 31 | border: black 1px dashed; 32 | background: rgb(255, 131, 92); 33 | padding: 0 .5em 0 .5em; 34 | word-wrap: break-word; 35 | } 36 | 37 | &__criterion-link-type { 38 | border: black 1px dashed; 39 | background: rgb(202, 255, 206); 40 | padding: 0 .5em 0 .5em; 41 | word-wrap: break-word; 42 | } 43 | 44 | &__link-direction { 45 | height: 1em; 46 | opacity: 0.5; 47 | } 48 | 49 | &__criterion-remove { 50 | float: right; 51 | margin: 0 10px 4px 4px; 52 | } 53 | 54 | &__text-criteria { margin: 0 10px; } 55 | 56 | &__rest { 57 | padding: 10px 10px 0 10px; 58 | border-top: 1px solid rgb(221, 221, 221); 59 | } 60 | 61 | &__results { 62 | padding-left: 0; 63 | padding-top: 10px; 64 | margin: 0 10px 0 10px; 65 | outline: none; 66 | } 67 | 68 | &[data-state='finished'] &__results:empty:before { 69 | content: 'No items correspond to the specified criteria.'; 70 | font-style: italic; 71 | } 72 | 73 | &__rest-end { margin: 5px 0 10px 0; } 74 | &__load-more { 75 | width: 100%; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /styles/widgets/_linksToolbox.scss: -------------------------------------------------------------------------------- 1 | .link-types-toolbox { 2 | flex: auto; 3 | display: flex; 4 | flex-direction: column; 5 | margin-bottom: 0; 6 | 7 | &__heading { 8 | padding: 10px; 9 | } 10 | 11 | &__searching-box { 12 | display: flex; 13 | align-items: center; 14 | } 15 | 16 | &__clearSearch { 17 | margin-left: -25px; 18 | -webkit-appearance: none; 19 | padding: 0; 20 | cursor: pointer; 21 | background: 0 0; 22 | border: 0; 23 | } 24 | 25 | &__switch-all { 26 | margin-top: 5px; 27 | } 28 | 29 | &__rest { 30 | border-top: 1px solid #dddddd; 31 | flex: auto; 32 | display: flex; 33 | flex-direction: column; 34 | } 35 | } 36 | 37 | .link-types-toolbox .panel-heading { 38 | flex-shrink: 0; 39 | -webkit-flex-shrink: 0; /* safari 8 */ 40 | } 41 | 42 | .link-types-toolbox ul { 43 | margin-bottom: 0; 44 | } 45 | .link-types-toolbox .links-heading { 46 | margin-left: .4em; 47 | } 48 | .link-types-toolbox .links-heading span { 49 | border: black 1px dashed; 50 | background: rgb(255, 131, 92); 51 | padding: 0 .5em 0 .5em; 52 | word-wrap: break-word; 53 | line-height: 1.3; 54 | } 55 | 56 | .link-types-toolbox__heading .link-types-toolbox-controls { 57 | padding: 5px 15px; 58 | font-size: 14px; 59 | font-style: italic; 60 | } 61 | .link-types-toolbox__heading .link-types-toolbox-controls > span { 62 | padding-left: .3em; 63 | } 64 | 65 | .graph-explorer-list-group .linkInToolBox { 66 | padding: 0 0 0 5px; 67 | word-break: break-word; 68 | } 69 | 70 | .linkInToolBox { 71 | & > div { display: inline; } 72 | 73 | &__new-tag { 74 | margin-left: .5em; 75 | white-space: normal; 76 | word-wrap: normal; 77 | border-radius: 2px; 78 | padding: 0 5px; 79 | background: orange; 80 | } 81 | 82 | .graph-explorer-badge { 83 | display: none; 84 | margin-left: .5em; 85 | white-space: normal; 86 | word-wrap: normal; 87 | } 88 | 89 | .link-title { 90 | font-size: 16px; 91 | color: black; 92 | display: inline; 93 | } 94 | 95 | .graph-explorer-btn-group { 96 | float: left; 97 | margin-top: 2px; 98 | margin-right: 6px; 99 | margin-bottom: 2px; 100 | } 101 | 102 | .graph-explorer-btn.graph-explorer-btn-default { 103 | border-color: rgb(195, 195, 195); 104 | } 105 | 106 | &__filter-button { 107 | display: none; 108 | float: right; 109 | background-image: url("../images/add-to-filter.png"); 110 | background-size: 20px 20px; 111 | width: 20px; 112 | height: 20px; 113 | margin: 3px; 114 | cursor: pointer; 115 | opacity: 0.4; 116 | transition: opacity 200ms 0ms; 117 | } 118 | &:hover &__filter-button { 119 | opacity: 1.0; 120 | transition: opacity 200ms 0ms; 121 | } 122 | } 123 | 124 | .connected-links .linkInToolBox .graph-explorer-badge { 125 | /* show connection count badge only in "Connected to Element" list */ 126 | display: inline; 127 | } 128 | .connected-links .linkInToolBox__filter-button { 129 | /* show filter button only in "Connected to Element" list */ 130 | display: inline; 131 | } 132 | -------------------------------------------------------------------------------- /styles/widgets/_listElementView.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-list-element-view { 2 | position: relative; 3 | display: block; 4 | background: #808080; 5 | padding: 1px 1px 1px 10px; 6 | border-radius: 2px; 7 | margin: 0 0 2px 0; 8 | opacity: 1.0; 9 | cursor: move; 10 | user-select: none; 11 | 12 | &--disabled { 13 | opacity: 0.4; 14 | cursor: default; 15 | } 16 | 17 | &__label { 18 | background: white; 19 | border-radius: 0 2px 2px 0; 20 | font-family: "Andale Mono", sans-serif; 21 | font-size: 15px; 22 | min-height: 1.3em; 23 | padding-left: 7px; 24 | padding-right: 5px; 25 | overflow-wrap: break-word; 26 | } 27 | } 28 | 29 | .graph-explorer-text-highlight { 30 | font-weight: bold; 31 | } 32 | -------------------------------------------------------------------------------- /styles/widgets/_navigator.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-navigator { 2 | display: block; 3 | position: absolute; 4 | bottom: 25px; 5 | right: 25px; 6 | overflow: hidden; 7 | 8 | // increase specificity to override default box-sizing, 9 | // so border size won't be included into total size 10 | .graph-explorer & { 11 | box-sizing: content-box; 12 | } 13 | 14 | background: #fff; 15 | border: 1px solid #ddd; 16 | box-shadow: 0 4px 15px 7px rgba(51, 51, 51, 0.05); 17 | 18 | $transition: 0.3s; 19 | transition: width $transition, height $transition; 20 | 21 | &--collapsed { 22 | width: 26px; 23 | height: 26px; 24 | } 25 | 26 | > canvas { 27 | transition: opacity $transition; 28 | } 29 | &--expanded > canvas { opacity: 1; } 30 | &--collapsed > canvas { opacity: 0; } 31 | 32 | &__toggle { 33 | position: absolute; 34 | background: transparent; 35 | border: none; 36 | outline: none; 37 | padding: 4px; 38 | 39 | opacity: 0.5; 40 | transition: opacity 0.3s; 41 | 42 | &:hover { 43 | // background: lightgray; 44 | opacity: 1; 45 | } 46 | } 47 | &--expanded &__toggle { 48 | top: 5px; 49 | left: 5px; 50 | } 51 | &--collapsed &__toggle { 52 | top: 0px; 53 | left: 0px; 54 | } 55 | 56 | &__toggle-icon { 57 | width: 18px; 58 | height: 18px; 59 | background-size: 18px 18px; 60 | background-repeat: no-repeat; 61 | } 62 | &--expanded &__toggle-icon { 63 | background-image: url('../images/font-awesome/minus-square-regular.svg'); 64 | } 65 | &--collapsed &__toggle-icon { 66 | background-image: url('../images/font-awesome/plus-square-regular.svg'); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /styles/widgets/_progressBar.scss: -------------------------------------------------------------------------------- 1 | @keyframes graph-explorer-progress-bar-stripes { 2 | from { 3 | background-position: 40px 0; 4 | } 5 | to { 6 | background-position: 0 0; 7 | } 8 | } 9 | 10 | .graph-explorer-progress-bar { 11 | flex-shrink: 0; 12 | -webkit-flex-shrink: 0; /* safari 8 */ 13 | width: 100%; 14 | 15 | background-color: #f5f5f5; 16 | background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%); 17 | background-repeat: repeat-x; 18 | overflow: hidden; 19 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); 20 | 21 | &__bar { 22 | float: left; 23 | height: 100%; 24 | font-size: 12px; 25 | line-height: 20px; 26 | color: #fff; 27 | text-align: center; 28 | transition: width .6s ease; 29 | 30 | background-color: #337ab7; 31 | background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); 32 | background-repeat: repeat-x; 33 | background-size: 40px 40px; 34 | 35 | animation: graph-explorer-progress-bar-stripes 2s linear infinite; 36 | } 37 | 38 | &--error &__bar { 39 | background-color: #E72F2F; 40 | } 41 | 42 | &--loading, 43 | &--error { 44 | /* property name | duration | delay */ 45 | transition: height 300ms 300ms; 46 | } 47 | 48 | &--completed { 49 | /* property name | duration | delay */ 50 | transition: height 200ms 0ms; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /styles/widgets/_searchResults.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-search-results { 2 | margin: 0; 3 | padding: 0; 4 | outline: none; 5 | } 6 | -------------------------------------------------------------------------------- /styles/workspace/_accordion.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-accordion { 2 | flex: auto; 3 | display: flex; 4 | height: 100%; 5 | width: 100%; 6 | 7 | &--scrollable { 8 | overflow: auto; 9 | } 10 | 11 | &--vertical { 12 | flex-direction: column; 13 | } 14 | 15 | &--vertical:not(&--resizing) > .graph-explorer-accordion-item { 16 | transition: height 0.3s ease-in-out; 17 | } 18 | } 19 | 20 | .graph-explorer-accordion-item { 21 | display: flex; 22 | position: relative; 23 | flex: auto; 24 | 25 | &--resizing > &__handle, &__handle:hover { 26 | background: rgba(0, 0, 0, 0.1); 27 | } 28 | 29 | &--vertical { 30 | border-top: 1px solid lightgray; 31 | 32 | &:first-child { 33 | border-top-width: 0; 34 | } 35 | } 36 | 37 | &--horizontal { 38 | border-right: 1px solid lightgray; 39 | 40 | &:last-child { 41 | border-right-width: 0; 42 | } 43 | } 44 | 45 | &__handle { 46 | position: absolute; 47 | z-index: 2; 48 | } 49 | 50 | &__handle-vertical { 51 | height: 5px; 52 | bottom: -2.5px; 53 | left: 0; 54 | width: 100%; 55 | cursor: ns-resize; 56 | } 57 | 58 | &__handle-horizontal { 59 | height: 100%; 60 | top: 0; 61 | right: -2.5px; 62 | width: 5px; 63 | cursor: ew-resize; 64 | } 65 | 66 | &__handle-btn { 67 | background: #fff; 68 | border: 2px solid #17b; 69 | box-shadow: 0 0 0 1px rgba(9, 30, 66, 0.08), 0 2px 4px 1px rgba(9, 30, 66, 0.08); 70 | border-radius: 10px; 71 | cursor: pointer; 72 | height: 20px; 73 | position: absolute; 74 | top: 50%; 75 | margin-top: -10px; 76 | width: 20px; 77 | z-index: 20; 78 | transition: 0.3s; 79 | 80 | &::before { 81 | background-position: 0 0; 82 | background-repeat: no-repeat; 83 | background-size: cover; 84 | content: ""; 85 | height: 8px; 86 | position: absolute; 87 | top: 50%; 88 | left: 50%; 89 | margin-top: -4px; 90 | margin-left: -4px; 91 | width: 8px; 92 | transition: background 0.3s; 93 | } 94 | 95 | &:hover { 96 | background: #17b; 97 | } 98 | } 99 | 100 | &__handle-btn-left { 101 | left: 100%; 102 | margin-left: -10px; 103 | &:before { background-image: url("../images/arrow-left.png"); } 104 | &:hover:before { background-image: url("../images/arrow-left1.png"); } 105 | } 106 | 107 | &__handle-btn-right { 108 | right: 100%; 109 | margin-right: -10px; 110 | &:before { background-image: url("../images/arrow-right.png"); } 111 | &:hover:before { background-image: url("../images/arrow-right1.png"); } 112 | } 113 | 114 | &--collapsed &__handle-btn:before { 115 | transform: rotate(180deg); 116 | } 117 | 118 | &__inner { 119 | flex: auto; 120 | display: flex; 121 | flex-direction: column; 122 | overflow: hidden; 123 | } 124 | 125 | &__body { 126 | flex: 1 1 0px; // IE 11 requires a unit to be added to the third argument 127 | display: flex; 128 | flex-direction: column; 129 | } 130 | 131 | &__header { 132 | padding-left: 20px; 133 | position: relative; 134 | 135 | flex-shrink: 0; 136 | font-size: 16px; 137 | background: #E4E4E4; 138 | cursor: default; 139 | /* unselectable */ 140 | -webkit-touch-callout: none; 141 | -webkit-user-select: none; 142 | -moz-user-select: none; 143 | -ms-user-select: none; 144 | user-select: none; 145 | 146 | &:before { 147 | border-top: 6px solid #555555; 148 | border-left: 3.5px solid transparent; 149 | border-right: 3.5px solid transparent; 150 | content: ""; 151 | position: absolute; 152 | top: 50%; 153 | left: 7px; 154 | margin-top: -3px; 155 | 156 | -webkit-transition: 0.1s; 157 | -moz-transition: 0.1s; 158 | transition: 0.1s; 159 | } 160 | } 161 | 162 | &--collapsed &__header:before { 163 | -webkit-transform: rotate(-90deg); 164 | -moz-transform: rotate(-90deg); 165 | -ms-transform: rotate(-90deg); 166 | transform: rotate(-90deg); 167 | } 168 | 169 | &--collapsed &__body { 170 | display: none; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /styles/workspace/_resizableSidebar.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer-drag-resizable-column.graph-explorer-drag-resizable-column--docked-bottom, 2 | .graph-explorer-drag-resizable-column.graph-explorer-drag-resizable-column--docked-top { 3 | .graph-explorer-drag-resizable-column__handle { 4 | height: 8px; 5 | width: 100%; 6 | top: initial; 7 | left: 0; 8 | cursor: ns-resize; 9 | } 10 | 11 | .graph-explorer-drag-resizable-column__handle-btn { 12 | height: 100%; 13 | width: 40px; 14 | top: 0; 15 | left: 50%; 16 | margin-left: -20px; 17 | margin-top: 0px; 18 | } 19 | } 20 | 21 | .graph-explorer-drag-resizable-column { 22 | display: flex; 23 | flex-direction: column; 24 | position: relative; 25 | 26 | &__handle { 27 | background: #fff url("../images/resizable-column-handle.png") repeat; 28 | width: 8px; 29 | height: 100%; 30 | position: absolute; 31 | top: 0; 32 | left: initial; 33 | z-index: 2; 34 | -webkit-transition: 0.3s; 35 | -moz-transition: 0.3s; 36 | transition: 0.3s; 37 | cursor: ew-resize; 38 | } 39 | 40 | &__handle-btn { 41 | background: #eee; 42 | cursor: pointer; 43 | height: 40px; 44 | position: absolute; 45 | top: 50%; 46 | left: 0; 47 | margin-top: -20px; 48 | width: 100%; 49 | z-index: 1; 50 | -webkit-transition: 0.3s; 51 | -moz-transition: 0.3s; 52 | transition: 0.3s; 53 | 54 | &::before { 55 | background-position: 0 0 ; 56 | background-repeat: no-repeat; 57 | background-size: cover; 58 | content: ""; 59 | height: 6px; 60 | position: absolute; 61 | top: 50%; 62 | left: 50%; 63 | margin-top: -3px; 64 | margin-left: -3px; 65 | width: 6px; 66 | -webkit-transition: 0.3s; 67 | -moz-transition: 0.3s; 68 | transition: 0.3s; 69 | } 70 | 71 | &:hover { 72 | background: #b3b3b3; 73 | transform: scale(1.2); 74 | } 75 | } 76 | 77 | &--closed &__handle-btn { 78 | &::before { transform: rotate(180deg); } 79 | } 80 | 81 | &--docked-left { margin-right: 8px; } 82 | &--docked-left &__handle { right: -8px; } 83 | &--docked-left &__handle-btn { 84 | &::before { background-image: url("../images/arrow-left.png"); } 85 | &:hover::before { background-image: url("../images/arrow-left1.png"); } 86 | } 87 | 88 | &--docked-right { margin-left: 8px; } 89 | &--docked-right &__handle { left: -8px; } 90 | &--docked-right &__handle-btn { 91 | &::before { background-image: url("../images/arrow-right.png"); } 92 | &:hover::before { background-image: url("../images/arrow-right1.png"); } 93 | } 94 | 95 | &--docked-top { margin-bottom: 8px; } 96 | &--docked-top &__handle { bottom: -8px; } 97 | &--docked-top &__handle-btn { 98 | &::before { background-image: url("../images/arrow-top.png"); } 99 | &:hover::before { background-image: url("../images/arrow-top1.png"); } 100 | } 101 | 102 | &--docked-bottom { margin-top: 8px; } 103 | &--docked-bottom &__handle { top: -8px; } 104 | &--docked-bottom &__handle-btn { 105 | &::before { background-image: url("../images/arrow-bottom.png"); } 106 | &:hover::before { background-image: url("../images/arrow-bottom1.png"); } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /styles/workspace/_toolbar.scss: -------------------------------------------------------------------------------- 1 | $toolbarHeight: 30px; 2 | 3 | .graph-explorer-toolbar { 4 | &__language-selector { 5 | margin-left: 5px; 6 | margin-right: 5px; 7 | 8 | > label { 9 | margin-right: 5px; 10 | > span { vertical-align: middle; } 11 | } 12 | 13 | > select { 14 | height: $toolbarHeight; 15 | } 16 | } 17 | 18 | &__toggle { 19 | line-height: 1; 20 | } 21 | 22 | &__toggle:after { 23 | background-position: 0 0; 24 | background-repeat: no-repeat; 25 | content: ''; 26 | display: block; 27 | height: 12px; 28 | margin: 3px 0; 29 | width: 12px; 30 | } 31 | 32 | &__toggle-left:after { 33 | background-image: url('../images/left-panel.svg'); 34 | } 35 | 36 | &__toggle-right:after { 37 | background-image: url('../images/right-panel.svg'); 38 | } 39 | 40 | &__toggle.active { 41 | border-color: #c3c3c3; 42 | } 43 | 44 | &__toggle-left.active:after { 45 | background-image: url('../images/left-panel-active.svg'); 46 | } 47 | 48 | &__toggle-right.active:after { 49 | background-image: url('../images/right-panel-active.svg'); 50 | } 51 | 52 | &__layout-group { 53 | float: left; 54 | margin-left: 5px; 55 | height: $toolbarHeight; 56 | 57 | > label { 58 | margin-right: 5px; 59 | } 60 | 61 | .btn-group,button { 62 | height: 100%; 63 | } 64 | } 65 | } 66 | 67 | .graph-explorer-toolbar .graph-explorer-toolbar__undo, 68 | .graph-explorer-toolbar .graph-explorer-toolbar__redo { 69 | display: none; 70 | } 71 | 72 | -------------------------------------------------------------------------------- /styles/workspace/_tutorial.scss: -------------------------------------------------------------------------------- 1 | .introjs-tooltiptext { 2 | color: #333; 3 | font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 4 | font-size: 14px; 5 | line-height: 1.42857; 6 | } 7 | -------------------------------------------------------------------------------- /styles/workspace/_workspace.scss: -------------------------------------------------------------------------------- 1 | .graph-explorer { 2 | display: flex; 3 | flex-direction: column; 4 | height: 100%; 5 | // defaults for inheritable properties 6 | box-sizing: border-box; 7 | white-space: initial; 8 | color: #333; 9 | font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; 10 | font-size: 14px; 11 | line-height: 1.42857; 12 | 13 | *, *:before, *:after { 14 | box-sizing: inherit; 15 | } 16 | 17 | &__toolbar-widget { 18 | position: absolute; 19 | left: 10px; 20 | top: 10px; 21 | } 22 | 23 | &__workspace { 24 | display: flex; 25 | flex: auto; 26 | overflow: hidden; 27 | flex-direction: column; 28 | } 29 | 30 | &__main-panel { 31 | display: flex; 32 | flex: auto; 33 | overflow: hidden; 34 | width: 0; 35 | } 36 | 37 | &--unselectable { 38 | -webkit-touch-callout: none; 39 | -webkit-user-select: none; 40 | -moz-user-select: none; 41 | -ms-user-select: none; 42 | user-select: none; 43 | } 44 | 45 | &--horizontal-resizing * { cursor: ew-resize !important; } 46 | &--vertical-resizing * { cursor: ns-resize !important; } 47 | 48 | h4 { 49 | font-size: 18px; 50 | font-weight: 500; 51 | line-height: 1.1; 52 | margin-top: 10px; 53 | margin-bottom: 10px; 54 | } 55 | 56 | select { 57 | color: inherit; 58 | margin: 0; 59 | font: inherit; 60 | line-height: inherit; 61 | text-transform: none; 62 | } 63 | 64 | hr { 65 | border: 0; 66 | border-top: 1px solid #eee; 67 | height: 0; 68 | } 69 | } 70 | 71 | .graph-explorer-scrollable { 72 | flex: 1 1 0px; // IE 11 requires a unit to be added to the third argument 73 | overflow-y: scroll; 74 | } 75 | 76 | // fixes the bug in Chrome with redrawing children in scrollable elements 77 | .graph-explorer-scrollable > * { 78 | position: relative; 79 | } 80 | -------------------------------------------------------------------------------- /thirdparty.txt: -------------------------------------------------------------------------------- 1 | Third Party Libraries 2 | 3 | MIT License: 4 | - Twitter Bootstrap (modified CSS styles): https://github.com/twbs/bootstrap 5 | 6 | CC BY 4.0 7 | - Font Awesome Free (unmodified SVG icons): https://fontawesome.com/license 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "jsx": "react", 6 | "lib": ["dom", "es2015"], 7 | "importHelpers": true, 8 | "strict": true, 9 | "strictNullChecks": false 10 | }, 11 | "include": [ 12 | "./examples/**/*.ts", 13 | "./examples/**/*.tsx", 14 | "./src/**/*.ts", 15 | "./src/**/*.tsx", 16 | "./typings/typings.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "emitDeclarationOnly": true, 6 | "skipLibCheck": true, 7 | "outDir": "./dist/temp/dts" 8 | }, 9 | "include": ["./src/**/*.ts", "./src/**/*.tsx", "./typings/typings.d.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /typings/local/file-saverjs/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'file-saverjs' { 2 | const saveAs: { 3 | (file: Blob, fileName: string): void; 4 | }; 5 | export = saveAs; 6 | } 7 | -------------------------------------------------------------------------------- /typings/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/triple-slash-reference */ 2 | /// 3 | 4 | // placeholders for webcola 5 | declare module "d3-dispatch"; 6 | declare module "d3-timer"; 7 | declare module "d3-drag"; 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | 3 | const path = require('path'); 4 | const NodePolyfillPlugin = require("node-polyfill-webpack-plugin"); 5 | 6 | // if BUNDLE_PEERS is set, we'll produce bundle with all dependencies 7 | const BUNDLE_PEERS = Boolean(process.env.BUNDLE_PEERS); 8 | // always include IE support in full bundle 9 | const SUPPORT_IE = BUNDLE_PEERS || Boolean(process.env.SUPPORT_IE); 10 | 11 | const aliases = {}; 12 | if (!SUPPORT_IE) { 13 | const emptyModule = path.resolve( 14 | __dirname, 15 | 'src', 16 | 'graph-explorer', 17 | 'emptyModule.ts' 18 | ); 19 | aliases['es6-promise/auto'] = emptyModule; 20 | } 21 | 22 | module.exports = { 23 | mode: BUNDLE_PEERS ? 'production' : 'none', 24 | entry: './src/graph-explorer/index.ts', 25 | resolve: { 26 | alias: aliases, 27 | extensions: ['.ts', '.tsx', '.js'], 28 | }, 29 | plugins: [ 30 | new NodePolyfillPlugin(), 31 | ], 32 | module: { 33 | rules: [ 34 | { test: /\.ts$|\.tsx$/, use: ['ts-loader'] }, 35 | { test: /\.css$/, use: ['style-loader', { loader: 'css-loader', options: { esModule: false } }] }, 36 | { 37 | test: /\.scss$/, 38 | use: ['style-loader', { loader: 'css-loader', options: { esModule: false } }, 'sass-loader'], 39 | }, 40 | { 41 | test: /\.(jpe?g|gif|png|svg)$/, 42 | use: [{ loader: 'url-loader' }], 43 | }, 44 | { test: /\.ttl$/, use: ['raw-loader'] }, 45 | ], 46 | }, 47 | output: { 48 | path: path.join(__dirname, 'dist'), 49 | filename: BUNDLE_PEERS 50 | ? 'graph-explorer-full.min.js' 51 | : SUPPORT_IE 52 | ? 'graph-explorer-ie.js' 53 | : 'graph-explorer.js', 54 | library: 'GraphExplorer', 55 | libraryTarget: 'umd', 56 | }, 57 | devtool: 'source-map', 58 | externals: BUNDLE_PEERS 59 | ? [] 60 | : [ 61 | 'd3-color', 62 | 'file-saverjs', 63 | 'lodash', 64 | 'n3', 65 | 'react', 66 | 'react-dom', 67 | 'webcola', 68 | ], 69 | performance: { 70 | maxEntrypointSize: 2048000, 71 | maxAssetSize: 2048000, 72 | }, 73 | }; 74 | --------------------------------------------------------------------------------