├── .babelrc
├── .gitignore
├── .travis.yml
├── CHANGELOG.md
├── LICENSE
├── README.md
├── TAGS
├── app
├── CaptionDisplaySettings.js
├── EdgeDisplaySettings.js
├── NodeDisplaySettings.js
├── actions.js
├── components
│ ├── AddCaptionForm.jsx
│ ├── AddConnectedNodesForm.jsx
│ ├── AddEdgeForm.jsx
│ ├── AddNodeForm.jsx
│ ├── AddNodeInput.jsx
│ ├── AddNodeResult.jsx
│ ├── AnnotationsTracker.jsx
│ ├── BaseComponent.jsx
│ ├── Caption.jsx
│ ├── ChangeColorInput.jsx
│ ├── DeleteSelectedButton.jsx
│ ├── Edge.jsx
│ ├── EdgeArrowSelector.jsx
│ ├── EdgeDashSelector.jsx
│ ├── EdgeDropdown.jsx
│ ├── EditButton.jsx
│ ├── EditButtons.jsx
│ ├── EditTools.jsx
│ ├── Editor.jsx
│ ├── EmbeddedGraphAnnotation.jsx
│ ├── EmbeddedGraphAnnotations.jsx
│ ├── EmbeddedNavBar.jsx
│ ├── EmbeddedNavButtons.jsx
│ ├── Graph.jsx
│ ├── GraphAnnotation.jsx
│ ├── GraphAnnotationForm.jsx
│ ├── GraphAnnotationList.jsx
│ ├── GraphAnnotationListItem.jsx
│ ├── GraphAnnotations.jsx
│ ├── GraphByLine.jsx
│ ├── GraphHeader.jsx
│ ├── GraphLinks.jsx
│ ├── GraphNavButtons.jsx
│ ├── GraphSettingsForm.jsx
│ ├── GraphTitle.jsx
│ ├── GraphTitleForm.jsx
│ ├── HelpButton.jsx
│ ├── HelpScreen.jsx
│ ├── LayoutButtons.jsx
│ ├── Node.jsx
│ ├── NodeCircle.jsx
│ ├── NodeLabel.jsx
│ ├── Root.jsx
│ ├── SaveButton.jsx
│ ├── SettingsButton.jsx
│ ├── UndoButtons.jsx
│ ├── UpdateCaptionForm.jsx
│ ├── UpdateEdgeForm.jsx
│ ├── UpdateNodeForm.jsx
│ ├── ZoomButtons.jsx
│ └── __tests__
│ │ ├── AnnotationsTracker-test.jsx
│ │ ├── Caption-test.jsx
│ │ ├── ChangeColorInput-test.jsx
│ │ ├── DeleteSelectedButton-test.js
│ │ ├── Edge-test.jsx
│ │ ├── EdgeArrowSelector-test.jsx
│ │ ├── EdgeDashSelector-test.jsx
│ │ ├── EdgeDropdown-test.jsx
│ │ ├── EditTools-test.jsx
│ │ ├── EmbeddedGraphAnnotation-test.jsx
│ │ ├── EmbeddedGraphAnnotations-test.jsx
│ │ ├── EmbeddedNavBar-test.jsx
│ │ ├── EmbeddedNavButtons-test.jsx
│ │ ├── Graph-test.jsx
│ │ ├── GraphAnnotation-test.jsx
│ │ ├── GraphAnnotationForm-test.jsx
│ │ ├── GraphAnnotationList-test.jsx
│ │ ├── GraphHeader-test.jsx
│ │ ├── GraphNavButtons-test.jsx
│ │ ├── GraphTitle-test.jsx
│ │ ├── Node-test.jsx
│ │ ├── NodeLabel-test.jsx
│ │ ├── Root-test.jsx
│ │ ├── UpdateCaptionForm-test.jsx
│ │ ├── UpdateEdgeForm-test.jsx
│ │ ├── UpdateNodeForm-test.jsx
│ │ └── support.js
├── fonts
│ └── glyphicons-halflings-regular.woff
├── helpers.js
├── main.jsx
├── models
│ ├── Annotation.js
│ ├── Caption.js
│ ├── Edge.js
│ ├── Graph.js
│ ├── Helpers.js
│ ├── Node.js
│ └── __tests__
│ │ ├── Annotation-test.js
│ │ ├── Caption-test.js
│ │ ├── Edge-test.js
│ │ ├── Graph-test.js
│ │ ├── Helpers-test.js
│ │ └── Node-test.js
├── reducers.js
├── reducers
│ ├── __tests__
│ │ ├── annotations-test.js
│ │ └── graph-test.js
│ ├── annotations.js
│ ├── editTools.js
│ ├── graph.js
│ ├── position.js
│ ├── selection.js
│ ├── settings.js
│ ├── showHelpScreen.js
│ ├── showSettings.js
│ ├── title.js
│ ├── undoable-graph.js
│ └── zoom.js
└── styles
│ ├── bootstrap-3.3.6.css
│ ├── oligrapher.annotations.css
│ ├── oligrapher.css
│ ├── oligrapher.editor.css
│ ├── oligrapher.embedded.css
│ └── test
│ └── styleMock.js
├── build
├── GoogleSheetDataSource.js
├── LsDataConverter.js
├── LsDataSource.js
├── PopoloDataConverter.js
├── dev.html
├── embedded.html
├── fake_news.html
├── index.html
├── oligrapher-demo-data.js
├── oligrapher.js
├── oligrapher.min.js
├── puerto-rico-sample-data.js
└── puerto_rico.html
├── package.json
├── webpack.dev.config.js
└── webpack.prod.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015", "react"]
3 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 |
5 | # Runtime data
6 | pids
7 | *.pid
8 | *.seed
9 |
10 | # Emacs-specific stuff
11 | .tern-port
12 |
13 | # Directory for instrumented libs generated by jscoverage/JSCover
14 | lib-cov
15 |
16 | # Coverage directory used by tools like istanbul
17 | coverage
18 |
19 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
20 | .grunt
21 |
22 | # node-waf configuration
23 | .lock-wscript
24 |
25 | # Compiled binary addons (http://nodejs.org/api/addons.html)
26 | build/Release
27 |
28 | # Dependency directory
29 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git
30 | node_modules
31 |
32 | build/oligrapher-dev.js
33 |
34 | # mac stuff
35 | .DS_Store
36 |
37 | flycheck
38 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 |
3 | node_js:
4 | - "6.10.3"
5 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Oligrapher CHANGELOG
2 |
3 | ## [0.2.1] - 2017-05-25
4 | ### added
5 | - url option to configuration to enable linkable title
6 |
7 | ### removed
8 | - 'Click to view this...' text in embedded mode
9 |
10 |
11 | ## [0.2.0] - 2017-05-18
12 | ### added
13 | - this changelog :)
14 |
15 | ### changed
16 | - Updated React to 15.5
17 | - Updated other libraries
18 | - Use prop-types library as it is now depreciated from react's core
19 |
--------------------------------------------------------------------------------
/app/CaptionDisplaySettings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | lineHeight: 20,
3 | cornerRadius: 5,
4 | highlightFillColor: "#ff0",
5 | selectFillColor: "#0f0",
6 | textOpacity: {
7 | "normal": 1,
8 | "highlighted": 1,
9 | "faded": 0.2
10 | },
11 | highlightOpacity: 0.5,
12 | selectOpacity: 0.5
13 | };
--------------------------------------------------------------------------------
/app/EdgeDisplaySettings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | curveStrength: 0.5,
3 | lineColor: {
4 | normal: "#999",
5 | highlighted: "#999",
6 | faded: "#ddd"
7 | },
8 | textColor: {
9 | normal: "#999",
10 | highlighted: "#444",
11 | faded: "#ddd"
12 | },
13 | bgColor: {
14 | normal: "#fff",
15 | highlighted: "#ff0",
16 | faded: "#fff"
17 | },
18 | selectColor: "#0f0",
19 | bgOpacity: {
20 | normal: 0,
21 | highlighted: 0.5,
22 | faded: 0
23 | },
24 | selectOpacity: 0.5,
25 | bgWidthDiff: 5,
26 | selectWidthDiff: 12
27 | };
--------------------------------------------------------------------------------
/app/NodeDisplaySettings.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | circleRadius: 25,
3 | circleSpacing: 4,
4 | textMarginTop: 20,
5 | lineHeight: 20,
6 | imageScale: 3,
7 | cornerRadius: 5,
8 | circleColor: {
9 | normal: "#ccc",
10 | highlighted: "#ccc",
11 | faded: "#ccc"
12 | },
13 | textColor: {
14 | normal: "#000",
15 | highlighted: "#000",
16 | faded: "#000"
17 | },
18 | textOpacity: {
19 | normal: 1,
20 | highlighted: 1,
21 | faded: 0.2
22 | },
23 | bgColor: {
24 | normal: "#fff",
25 | highlighted: "#ff0",
26 | faded: "#fff"
27 | },
28 | selectColor: "#0f0",
29 | bgOpacity: {
30 | normal: 0,
31 | highlighted: 0.5,
32 | faded: 0
33 | },
34 | imageOpacity: {
35 | normal: 1,
36 | highlighted: 1,
37 | faded: 0.2
38 | },
39 | circleOpacity: {
40 | normal: 1,
41 | highlighted: 1,
42 | faded: 0.2
43 | },
44 | bgRadiusDiff: 4,
45 | selectionRadiusDiff: 10
46 | };
--------------------------------------------------------------------------------
/app/components/AddCaptionForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import { HotKeys } from 'react-hotkeys';
5 |
6 | export default class AddCaptionForm extends BaseComponent {
7 | constructor(props) {
8 | super(props);
9 | this.bindAll('_handleSubmit', '_handleScaleChange');
10 |
11 | this.state = {
12 | scaleValue: 1
13 | };
14 | }
15 |
16 | render() {
17 |
18 | const scales = [
19 | [null, "Scale"],
20 | [1, "1x"],
21 | [1.25, "1.25x"],
22 | [1.5, "1.5x"],
23 | [2, "2x"],
24 | [2.5, "2.5x"],
25 | [3, "3x"],
26 | [4, "4x"],
27 | [5, "5x"]
28 | ];
29 |
30 | return (
31 |
32 |
45 |
46 | );
47 | }
48 |
49 | _handleScaleChange() {
50 | this.setState({scaleValue: this.refs.scale.value})
51 | }
52 |
53 | _handleSubmit(e) {
54 | let text = this.refs.text.value.trim();
55 | let scale = this.state.scaleValue;
56 |
57 | this.props.addCaption({ display: { text, scale } });
58 | this._clear();
59 | e.preventDefault();
60 | }
61 |
62 | _clear() {
63 | this.refs.text.value = '';
64 | this.refs.scale.value = 1;
65 | }
66 | }
--------------------------------------------------------------------------------
/app/components/AddConnectedNodesForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import { HotKeys } from 'react-hotkeys';
5 | import mapKeys from 'lodash/mapKeys';
6 |
7 | export default class AddConnectedNodesForm extends BaseComponent {
8 | constructor(props) {
9 | super(props);
10 | this.bindAll('_handleSubmit');
11 | }
12 |
13 | render() {
14 | const keyMap = {
15 | 'esc': 'esc'
16 | };
17 |
18 | const keyHandlers = {
19 | 'esc': () => this.props.closeAddForm()
20 | };
21 |
22 | return (
23 |
24 |
25 |
32 | { this._renderOptions() }
33 | Add Connections
34 |
35 |
36 | );
37 | }
38 |
39 | _renderOptions() {
40 | let options = this.props.source.getConnectedNodesOptions;
41 | return options ? Object.keys(options).map(key => {
42 | return (
43 | { Object.keys(options[key]).map(val => {
44 | return {options[key][val]}
45 | }) }
46 | );
47 | }) : null;
48 | }
49 |
50 | _handleSubmit(e) {
51 | let num = parseInt(this.refs.num.value);
52 | let nodeId = this.props.data.id;
53 | let nodeIds = Object.keys(this.props.graph.nodes);
54 | let options = this._options();
55 |
56 | this.props.source.getConnectedNodes(nodeId, nodeIds, options, (data) => {
57 | this.props.addSurroundingNodes(nodeId, data.nodes);
58 |
59 | data.edges.forEach(edge => {
60 | if (!this.props.graph.edges[edge.id]) {
61 | this.props.addEdge(edge);
62 | }
63 | });
64 | });
65 |
66 | e.preventDefault();
67 | }
68 |
69 | _options() {
70 | return Object.keys(this.refs).reduce((result, ref) => {
71 | result[ref] = this.refs[ref].value;
72 | return result;
73 | }, {});
74 | }
75 |
76 | _clear() {
77 | this.refs.num.value = "5";
78 | Object.keys(this.props.source.getConnectedNodesOptions).forEach(key => {
79 | this.refs[key].value = null
80 | });
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/components/AddEdgeForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import values from 'lodash/values';
5 | import sortBy from 'lodash/sortBy';
6 | import { HotKeys } from 'react-hotkeys';
7 |
8 | export default class AddEdgeForm extends BaseComponent {
9 | constructor(props) {
10 | super(props);
11 | this.bindAll('_handleSubmit');
12 | }
13 |
14 | render() {
15 | let node1Id, node2Id;
16 |
17 | if (Array.isArray(this.props.data) && this.props.data.length == 2) {
18 | node1Id = this.props.data[0].id;
19 | node2Id = this.props.data[1].id;
20 | } else {
21 | node1Id = this.props.data ? this.props.data.id : null;
22 | node2Id = null;
23 | }
24 |
25 | const keyMap = {
26 | 'altN': ['alt+n', 'ctrl+n'],
27 | 'esc': 'esc'
28 | };
29 |
30 | const keyHandlers = {
31 | 'altN': () => this.props.closeAddForm(),
32 | 'esc': () => this._clear()
33 | };
34 |
35 | let nodes = sortBy(values(this.props.nodes), (node) => node.display.name);
36 |
37 | return (
38 |
39 |
40 |
55 |
56 |
57 | );
58 | }
59 |
60 | _handleSubmit(e) {
61 | let node1Id = this.refs.node1Id.value;
62 | let node2Id = this.refs.node2Id.value;
63 | let label = this.refs.label.value.trim();
64 |
65 | if (node1Id && node2Id && label) {
66 | this.props.addEdge({ node1_id: node1Id, node2_id: node2Id, display: { label } });
67 | this._clear();
68 | this.props.closeAddForm();
69 | }
70 |
71 | e.preventDefault();
72 | }
73 |
74 | _clear() {
75 | this.refs.node1Id.value = '';
76 | this.refs.node2Id.value = '';
77 | this.refs.label.value = '';
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/app/components/AddNodeForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import AddNodeResult from './AddNodeResult';
5 | import { HotKeys } from 'react-hotkeys';
6 |
7 | export default class AddNodeForm extends BaseComponent {
8 | constructor(props) {
9 | super(props);
10 | this.bindAll('_handleSubmit', '_handleSearch');
11 | }
12 |
13 | render() {
14 | const keyMap = {
15 | 'altN': ['alt+n', 'ctrl+n'],
16 | 'esc': 'esc'
17 | };
18 |
19 | const keyHandlers = {
20 | 'altN': () => this.props.closeAddForm(),
21 | 'esc': () => this.props.closeAddForm()
22 | };
23 |
24 | // filter existing nodes out of results
25 | const results = this.props.results.filter(node => !this.props.nodes[node.id]);
26 |
27 | return (
28 |
29 |
30 | Add Node
31 |
48 |
49 |
50 | );
51 | }
52 |
53 | componentWillUnmount() {
54 | window.clearTimeout(this.timeout);
55 | }
56 |
57 | _handleSubmit(e) {
58 | let name = this.refs.name.value.trim();
59 | this.props.addNode({ display: { name } });
60 | this._clear();
61 | this.props.closeAddForm();
62 | e.preventDefault();
63 | }
64 |
65 | _handleSearch() {
66 | // text and source required for search
67 | if (this.props.source) {
68 | let that = this;
69 |
70 | // cancel previously queued search
71 | window.clearTimeout(this.timeout);
72 |
73 | // queue new search
74 | this.timeout = setTimeout(() => {
75 | let query = that.refs.name.value.trim();
76 |
77 | if (query) {
78 | that.props.source.findNodes(query, nodes => that._addResults(nodes));
79 | }
80 | }, 200);
81 | }
82 | }
83 |
84 | _addResults(nodes) {
85 | this.props.setNodeResults(nodes);
86 | }
87 |
88 | _clear() {
89 | this.refs.name.value = '';
90 | }
91 | }
--------------------------------------------------------------------------------
/app/components/AddNodeInput.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import BaseComponent from './BaseComponent';
5 | import AddNodeResult from './AddNodeResult';
6 | import { HotKeys } from 'react-hotkeys';
7 |
8 | export default class AddNodeInput extends BaseComponent {
9 | constructor(props) {
10 | super(props);
11 | this.bindAll('_handleSubmit', '_handleSearch');
12 | }
13 |
14 | render() {
15 | // filter existing nodes out of results
16 | const results = this.props.results.filter(node => !this.props.nodes[node.id]);
17 |
18 | const keyMap = {
19 | 'esc': 'esc'
20 | };
21 |
22 | const keyHandlers = {
23 | 'esc': () => this.clear()
24 | };
25 |
26 | return (
27 |
47 | );
48 | }
49 |
50 | componentWillUnmount() {
51 | window.clearTimeout(this.timeout);
52 | }
53 |
54 | clear() {
55 | this.refs.name.value = '';
56 | this.refs.name.blur();
57 | this.props.setNodeResults([]);
58 | }
59 |
60 | _handleSubmit(e) {
61 | let name = this.refs.name.value.trim();
62 | this.props.addNode({ display: { name } });
63 | this.clear();
64 | this.props.closeAddForm();
65 | e.preventDefault();
66 | }
67 |
68 | _handleSearch() {
69 | // text and source required for search
70 | if (this.props.source) {
71 | let that = this;
72 |
73 | // cancel previously queued search
74 | window.clearTimeout(this.timeout);
75 |
76 | // queue new search
77 | this.timeout = setTimeout(() => {
78 | let query = that.refs.name.value.trim();
79 |
80 | if (query) {
81 | that.props.source.findNodes(query, nodes => that._addResults(nodes));
82 | } else {
83 | this.setState({ results: [] })
84 | }
85 | }, 200);
86 | }
87 | }
88 |
89 | _addResults(nodes) {
90 | this.props.setNodeResults(nodes);
91 | }
92 | }
--------------------------------------------------------------------------------
/app/components/AddNodeResult.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 |
5 | export default class AddNodeResult extends BaseComponent {
6 | constructor(props) {
7 | super(props);
8 | this.bindAll('_handleClick');
9 | }
10 |
11 | render() {
12 | return (
13 | {this.props.node.display.name}
14 | );
15 | }
16 |
17 | _handleClick(e) {
18 | let { source, node, nodes } = this.props;
19 |
20 | if (source) {
21 | let nodeIds = Object.keys(nodes);
22 |
23 | let callback = (data) => {
24 | this.props.addNode(data.node);
25 | data.edges.forEach(edge => this.props.addEdge(edge));
26 | this.props.clearResults();
27 | };
28 |
29 | source.getNodeWithEdges(node.id, nodeIds, callback);
30 | }
31 | }
32 | }
33 |
34 |
--------------------------------------------------------------------------------
/app/components/AnnotationsTracker.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import times from 'lodash/times';
5 |
6 | export default class AnnotationsTracker extends BaseComponent {
7 | constructor(props) {
8 | super(props);
9 | this.bindAll('_circle');
10 | }
11 |
12 | render () {
13 | return (
14 |
15 | { times(this.props.annotationCount, this._circle) }
16 |
17 | )
18 | }
19 |
20 | _circle (i) {
21 | if (i === this.props.currentIndex) {
22 | return (
);
23 | } else {
24 | return (
);
25 | }
26 | }
27 |
28 | }
29 |
30 | AnnotationsTracker.propTypes = {
31 | annotationCount: PropTypes.number.isRequired,
32 | currentIndex: PropTypes.number.isRequired
33 | }
34 |
--------------------------------------------------------------------------------
/app/components/BaseComponent.jsx:
--------------------------------------------------------------------------------
1 | import { Component } from 'react';
2 |
3 | class BaseComponent extends Component {
4 | bindAll(...methods) {
5 | methods.forEach(method => { this[method] = this[method].bind(this); });
6 | }
7 | }
8 |
9 | module.exports = BaseComponent;
10 |
--------------------------------------------------------------------------------
/app/components/Caption.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import BaseComponent from './BaseComponent';
5 | import { DraggableCore } from 'react-draggable';
6 | import ds from '../CaptionDisplaySettings';
7 | import merge from 'lodash/merge';
8 | import { calculateDeltas } from '../helpers';
9 |
10 | export default class Caption extends BaseComponent {
11 | constructor(props) {
12 | super(props);
13 | this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick');
14 | this.state = props.caption.display;
15 | }
16 |
17 | render() {
18 | let { x, y, text, scale, status } = this.state;
19 | let transform = `translate(${x}, ${y})`;
20 | let highlighted = status == "highlighted";
21 |
22 | return (
23 |
28 |
29 | { highlighted ? this._highlightRect() : null }
30 | { this.props.selected ? this._selectionRect() : null }
31 | {text}
32 |
33 |
34 | );
35 | }
36 |
37 | componentDidMount() {
38 | this._setRectWidths();
39 | }
40 |
41 | componentDidUpdate(prevProps) {
42 | let prevDisplay = prevProps.caption.display;
43 | let display = this.props.caption.display;
44 | this._setRectWidths();
45 | }
46 |
47 | componentWillReceiveProps(props) {
48 | let newState = merge({ text: null }, props.caption.display);
49 | this.setState(newState);
50 | }
51 |
52 | shouldComponentUpdate(nextProps, nextState) {
53 | return nextProps.selected !== this.props.selected ||
54 | JSON.stringify(nextState) !== JSON.stringify(this.state);
55 | }
56 |
57 | _setRectWidths() {
58 | let element = ReactDOM.findDOMNode(this);
59 | let text = element.querySelector(".handle");
60 | let highlightRect = this.refs.highlightRect;
61 | let selectRect = this.refs.selectRect;
62 | let heightAdj = -6 + this.props.caption.display.scale * 3;
63 | let textWidth = text.getComputedTextLength();
64 | let textRect = text.getBoundingClientRect()
65 | let textHeight = textRect.bottom - textRect.top;
66 |
67 | if (highlightRect) {
68 | highlightRect.setAttribute("width", textWidth + 10);
69 | highlightRect.setAttribute("x", -5);
70 | highlightRect.setAttribute("height", textHeight + 10);
71 | highlightRect.setAttribute("y", -textHeight + heightAdj);
72 | }
73 |
74 | if (selectRect) {
75 | selectRect.setAttribute("width", textWidth + 10);
76 | selectRect.setAttribute("x", -5);
77 | selectRect.setAttribute("height", textHeight + 10);
78 | selectRect.setAttribute("y", - textHeight + heightAdj);
79 | }
80 | }
81 |
82 | _handleDragStart(e, data) {
83 | e.preventDefault();
84 | this._startDrag = data;
85 | this._startPosition = {
86 | x: this.state.x,
87 | y: this.state.y
88 | };
89 | }
90 |
91 | _handleDrag(e, data) {
92 | if (this.props.isLocked) return;
93 | this._dragging = true;
94 | let { x, y } = calculateDeltas(data, this._startPosition, this._startDrag, this.graph.state.actualZoom);
95 | this.setState({ x, y });
96 | }
97 |
98 | _handleDragStop(e, data) {
99 | // event fires every mouseup so we check for actual drag before updating store
100 | if (this._dragging) {
101 | this.props.moveCaption(this.props.caption.id, this.state.x, this.state.y);
102 | }
103 | }
104 |
105 | _handleClick() {
106 | if (this._dragging) {
107 | this._dragging = false;
108 | } else if (this.props.clickCaption) {
109 | this.props.clickCaption(this.props.caption.id);
110 | }
111 | }
112 |
113 | _selectionRect() {
114 | let width = this.state.text.length * 8;
115 | let height = ds.lineHeight;
116 |
117 | return (
118 |
127 | );
128 | }
129 |
130 | _highlightRect() {
131 | let width = this.state.text.length * 8;
132 | let height = ds.lineHeight;
133 |
134 | return (
135 |
144 | );
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/components/ChangeColorInput.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import { CompactPicker } from 'react-color';
5 | import ds from '../NodeDisplaySettings';
6 |
7 | export default class ChangeColorInput extends BaseComponent {
8 |
9 | constructor(props) {
10 | super(props);
11 | this.bindAll("handleClick", "handleClose", "handleValueChange", "handleClearClick", "onChange");
12 | this.state = {
13 | displayColorPicker: false,
14 | color: props.value
15 | };
16 | }
17 |
18 | handleClick() {
19 | this.setState({ displayColorPicker: !this.state.displayColorPicker });
20 | }
21 |
22 | handleClose() {
23 | this.setState({ displayColorPicker: false });
24 | }
25 |
26 | handleClearClick() {
27 | this.handleClose();
28 | this.onChange(ds.circleColor[this.props.status]);
29 | }
30 |
31 | handleValueChange(color) {
32 | this.onChange(color.hex);
33 | }
34 |
35 | onChange(newColor) {
36 | this.setState({ color: newColor });
37 | this.props.onChange(newColor);
38 | }
39 |
40 | render() {
41 | return (
42 |
43 |
49 |
50 | { this.state.displayColorPicker &&
51 |
55 | }
56 |
57 |
58 | );
59 | }
60 |
61 | componentWillReceiveProps(props) {
62 | this.setState({ color: props.value || ds.circleColor[props.status] });
63 | }
64 | }
--------------------------------------------------------------------------------
/app/components/DeleteSelectedButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import { HotKeys } from 'react-hotkeys';
5 | import mapKeys from 'lodash/mapKeys';
6 |
7 | export default class DeleteSelectedButton extends BaseComponent {
8 | constructor(props) {
9 | super(props);
10 | }
11 |
12 | render() {
13 | const whichClass = this.props.currentForm === "UpdateNodeForm" ? "nodeDelete" : "edgeDelete";
14 |
15 | return (
16 |
19 |
23 | Delete Selected
24 |
25 |
26 | );
27 | }
28 |
29 | }
30 |
--------------------------------------------------------------------------------
/app/components/EdgeArrowSelector.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import { newArrowState } from '../helpers';
5 | import capitalize from 'lodash/capitalize';
6 |
7 | export default class EdgeArrowSelector extends BaseComponent {
8 |
9 | constructor() {
10 | super();
11 | this.state = { isOpen: false };
12 | this.bindAll('_updateArrow', '_arrowClass');
13 | }
14 |
15 | render() {
16 | let { arrowSide } = this.props;
17 | return(
18 |
19 |
this.setState({isOpen: true})} >
21 |
22 |
23 |
24 |
25 | { this.state.isOpen &&
26 |
27 | this._updateArrow(false) }>
29 |
30 |
31 | this._updateArrow(true) }>
33 |
34 |
35 |
36 | }
)
37 | }
38 |
39 | _arrowClass() {
40 | let { arrow, arrowSide } = this.props;
41 | if (this._displayArrow(arrowSide, arrow)) {
42 | return `svgDropdown${capitalize(arrowSide)}Arrow`
43 | } else {
44 | return '';
45 | }
46 | }
47 |
48 | _updateArrow(showArrow) {
49 | const oldArrowState = this.props.arrow;
50 | const arrowSide = this.props.arrowSide;
51 | const _newArrowState = newArrowState(oldArrowState, arrowSide, showArrow)
52 | if (oldArrowState !== _newArrowState) {
53 | this.props.updateEdge(this.props.edgeId, {display: {arrow: _newArrowState }});
54 | }
55 | this.setState({isOpen: false});
56 | }
57 |
58 | _sideToNode(side) {
59 | if (side === 'left') {
60 | return 'node1'
61 | } else if (side === 'right') {
62 | return 'node2'
63 | } else {
64 | console.error('sideToNode only accepts left or right')
65 | return '';
66 | }
67 | }
68 |
69 | _displayArrow(arrowSide, arrowState) {
70 | let node = this._sideToNode(arrowSide);
71 | let node1 = (node === 'node1');
72 | let node2 = (node === 'node2');
73 | if (arrowState === 'both') {
74 | return true;
75 | }
76 | if (node1 && arrowState === '2->1') {
77 | return true;
78 | } else if (node2 && arrowState === '1->2') {
79 | return true;
80 | } else {
81 | return false;
82 | }
83 | }
84 | }
85 |
86 | /*
87 | arrowSide must be exactly 'left' or 'right'
88 | Possible arrow states: 'left', 'right', 'both', false/true
89 | */
90 | EdgeArrowSelector.propTypes = {
91 | updateEdge: PropTypes.func.isRequired,
92 | edgeId: PropTypes.any.isRequired,
93 | arrowSide: PropTypes.string.isRequired,
94 | arrow: PropTypes.any
95 | };
96 |
--------------------------------------------------------------------------------
/app/components/EdgeDashSelector.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 |
5 | export default class EdgeDashSelector extends BaseComponent {
6 |
7 | constructor(){
8 | super();
9 | this.state = { isOpen: false };
10 | this.bindAll('_menuOptions', '_updateEdge');
11 | }
12 |
13 | render(){
14 | return(
15 |
16 |
17 |
this.setState({isOpen: true})} >
20 |
21 |
22 | {this.state.isOpen && this._menuOptions() }
23 |
24 |
)
25 | }
26 |
27 | _menuOptions(){
28 | return (
29 |
30 | this._updateEdge(false) } >
32 |
33 |
34 | this._updateEdge(true) } >
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | _updateEdge(newDashState) {
43 | let { updateEdge, edgeId} = this.props;
44 | updateEdge(edgeId, {display: {dash: newDashState }});
45 | this.setState({isOpen: false});
46 | }
47 |
48 | }
49 |
50 | EdgeDashSelector.propTypes = {
51 | isDashed: PropTypes.bool.isRequired,
52 | updateEdge: PropTypes.func.isRequired,
53 | edgeId: PropTypes.any.isRequired
54 | };
55 |
--------------------------------------------------------------------------------
/app/components/EdgeDropdown.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import BaseComponent from './BaseComponent';
3 | import { legacyArrowConverter } from '../helpers';
4 | import EdgeArrowSelector from './EdgeArrowSelector';
5 | import EdgeDashSelector from './EdgeDashSelector';
6 | import NodeLabel from './NodeLabel';
7 |
8 | export default class EdgeDropdown extends Component {
9 |
10 | render() {
11 | const graph = this.props.getGraph();
12 | const edge = graph.edges[this.props.edgeId];
13 | const node1_name = graph.nodes[edge.node1_id].display.name;
14 | const node2_name = graph.nodes[edge.node2_id].display.name;
15 | const arrow = this.props.arrow;
16 |
17 | return (
18 |
19 |
20 | {this._nodeCircle(this._truncateName(node1_name))}
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | {this._nodeCircle(this._truncateName(node2_name))}
29 |
30 |
31 | );
32 | }
33 |
34 | _nodeCircle(nodeName) {
35 | return (
36 |
37 |
38 |
39 | {nodeName}
40 |
41 | );
42 | };
43 |
44 | _truncateName(name) {
45 | if (name.length > 33) {
46 | return `${name.slice(0,30)}...`;
47 | } else {
48 | return name;
49 | }
50 | }
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/app/components/EditButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class EditButton extends Component {
5 |
6 | render() {
7 | return (
8 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/EditButtons.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import AddNodeInput from './AddNodeInput';
4 |
5 | export default class EditButtons extends Component {
6 |
7 | render() {
8 | return (
9 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/components/EditTools.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import UndoButtons from './UndoButtons';
5 | import LayoutButtons from './LayoutButtons';
6 | import EditButtons from './EditButtons';
7 | import AddEdgeForm from './AddEdgeForm';
8 | import AddCaptionForm from './AddCaptionForm';
9 | import AddConnectedNodesForm from './AddConnectedNodesForm';
10 | import DeleteSelectedButton from './DeleteSelectedButton';
11 | import UpdateNodeForm from './UpdateNodeForm';
12 | import UpdateEdgeForm from './UpdateEdgeForm';
13 | import UpdateCaptionForm from './UpdateCaptionForm';
14 | import HelpScreen from './HelpScreen';
15 |
16 | export default class EditTools extends BaseComponent {
17 |
18 | constructor(props) {
19 | super(props);
20 | this.bindAll('_handleDelete');
21 | }
22 |
23 | render() {
24 | let { graphApi, source, data, graph, addForm, currentForm, helpScreen,
25 | clearGraph, closeAddForm, toggleHelpScreen, toggleAddEdgeForm } = this.props;
26 |
27 | let { zoomIn, zoomOut, resetZoom, prune, circleLayout,
28 | addNode, addEdge, addCaption, addSurroundingNodes,
29 | updateNode, updateEdge, updateCaption, deselectAll,
30 | deleteAll, getGraph } = graphApi;
31 |
32 | return (
33 |
100 | );
101 | }
102 |
103 | _handleDelete() {
104 | this.props.delete();
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/components/Editor.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import { HotKeys } from 'react-hotkeys';
5 | import BaseComponent from './BaseComponent';
6 | import ZoomButtons from './ZoomButtons';
7 | import EditTools from './EditTools';
8 | import merge from 'lodash/merge';
9 | import values from 'lodash/values';
10 | import cloneDeep from 'lodash/cloneDeep';
11 | import pick from 'lodash/pick';
12 | require('../styles/bootstrap-3.3.6.css');
13 | require('../styles/oligrapher.editor.css');
14 |
15 | export default class Editor extends BaseComponent {
16 | constructor(props) {
17 | super(props);
18 | this.state = { helpScreen: false };
19 | }
20 |
21 | render() {
22 | let zoomIn, zoomOut, resetZoom;
23 |
24 | if (this.props.graphApi) {
25 | zoomIn = () => this.props.graphApi.zoomIn();
26 | zoomOut = () => this.props.graphApi.zoomOut();
27 | resetZoom = () => this.props.graphApi.resetZoom();
28 | }
29 |
30 | const keyMap = {
31 | 'altP': ['alt+p', 'ctrl+p'],
32 | 'altO': ['alt+o', 'ctrl+o'],
33 | 'altN': ['alt+n', 'ctrl+n'],
34 | 'altE': ['alt+e', 'ctrl+e'],
35 | 'altH': ['alt+h', 'ctrl+h'],
36 | 'altR': ['alt+r', 'ctrl+r'],
37 | 'esc': 'esc',
38 | 'enter': 'enter'
39 | };
40 |
41 | const keyHandlers = {
42 | 'altP': () => this.props.graphApi.prune(),
43 | 'altO': () => this.props.graphApi.circleLayout(),
44 | 'altN': () => this._focusAddNodeInput(),
45 | 'altE': () => this._toggleAddEdgeForm(),
46 | 'altH': () => this._toggleHelpScreen(),
47 | 'altR': () => this._toggleAddConnectedNodesForm(),
48 | 'esc': () => this._clearForms()
49 | };
50 |
51 | let _closeAddForm = () => this.props.toggleAddForm(null);
52 |
53 | let { currentForm, formData, addForm } = this._computeEditForms(this.props.selection);
54 |
55 | let fetchInterlocks = () => {};
56 | let showInterlocksButton = (
57 | this.props.isEditor && this.props.dataSource && this.props.dataSource.getInterlocks && formData && formData.length == 2
58 | );
59 |
60 | if (showInterlocksButton) {
61 | let node1Id = formData[0].id;
62 | let node2Id = formData[1].id;
63 | let nodeIds = Object.keys(this.props.graph.nodes);
64 | fetchInterlocks = () => {
65 | this.props.fetchInterlocks(node1Id, node2Id, nodeIds, this.props.dataSource.getInterlocks);
66 | }
67 | }
68 |
69 | return (
70 |
71 |
72 |
73 | { this.props.showEditButton && this.props.isEditor &&
74 | this.props.toggleEditTools()}>
78 |
79 |
80 | }
81 | { this.props.showEditTools &&
82 | this._toggleAddEdgeForm()}
88 | toggleHelpScreen={() => this._toggleHelpScreen()}
89 | clearGraph={() => this._clearGraph()}
90 | data={formData}
91 | addForm={addForm}
92 | currentForm={currentForm}
93 | showInterlocksButton={showInterlocksButton}
94 | fetchInterlocks={fetchInterlocks}
95 | delete={this.props.delete} />
96 | }
97 |
98 |
99 | );
100 | }
101 |
102 | _computeEditForms() {
103 | let currentForm = null;
104 | let formData = null;
105 | let addForm = this.props.addForm;
106 |
107 | let { nodeIds, edgeIds, captionIds } = this.props.selection;
108 | let graph = this.props.graph;
109 | let nodes = pick(graph.nodes, nodeIds);
110 | let edges = pick(graph.edges, edgeIds);
111 | let captions = pick(graph.captions, captionIds);
112 | let nodeCount = Object.keys(nodes).length;
113 | let edgeCount = Object.keys(edges).length;
114 | let captionCount = Object.keys(captions).length;
115 |
116 | if (nodeCount == 1 && edgeCount == 0 && captionCount == 0) {
117 | if (addForm != 'AddEdgeForm') { currentForm = 'UpdateNodeForm'; }
118 | formData = values(nodes)[0];
119 | } else if (nodeCount == 0 && edgeCount == 1 && captionCount == 0) {
120 | currentForm = 'UpdateEdgeForm';
121 | addForm = null;
122 | formData = values(edges)[0];
123 | } else if (nodeCount == 0 && edgeCount == 0 && captionCount == 1) {
124 | currentForm = 'UpdateCaptionForm';
125 | addForm = null;
126 | formData = values(captions)[0];
127 | } else if (nodeCount == 2 && edgeCount == 0 && captionCount == 0) {
128 | currentForm = null;
129 | addForm = 'AddEdgeForm';
130 | formData = values(nodes);
131 | } else {
132 | currentForm = null;
133 | formData = null;
134 | }
135 |
136 | return { currentForm, formData, addForm };
137 | }
138 |
139 | _toggleAddEdgeForm() {
140 | this.props.toggleAddForm('AddEdgeForm');
141 | }
142 |
143 | _toggleAddCaptionForm() {
144 | this.props.toggleAddForm('AddCaptionForm');
145 | }
146 |
147 | _toggleAddConnectedNodesForm() {
148 | this.props.toggleAddForm('AddConnectedNodesForm');
149 | }
150 |
151 | _toggleHelpScreen() {
152 | this.props.toggleHelpScreen();
153 | }
154 |
155 | _clearGraph() {
156 | if (confirm("Are you sure you want to clear the graph? This can't be undone!")) {
157 | this.props.graphApi.deleteAll();
158 | this.props.toggleAddForm(null);
159 | }
160 | }
161 |
162 | _clearForms() {
163 | this.props.toggleAddForm(null);
164 | this.props.graphApi.deselectAll();
165 | this.refs.editTools.refs.editButtons.refs.addNodeInput.clear();
166 | }
167 |
168 | _focusAddNodeInput() {
169 | this.refs.editTools.refs.editButtons.refs.addNodeInput.refs.name.focus();
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/app/components/EmbeddedGraphAnnotation.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import { Scrollbars } from 'react-custom-scrollbars';
4 |
5 | // some magic numbers:
6 | // the hight of the embeddedNavBar (set via css) + container margin offset
7 | const TRACKER_OFFSET = 38;
8 | // bootstrap adds negative margins of -15
9 | const MARGIN_OFFSET = 15;
10 |
11 | export default class EmbeddedGraphAnnotation extends Component {
12 |
13 | render() {
14 | let { header, text } = this.props.annotation;
15 | let hasTracker = this.props.hasTracker;
16 | let { annotationHeight } = this.props.embedded;
17 | let height = annotationHeight - MARGIN_OFFSET - (hasTracker ? TRACKER_OFFSET : 0);
18 |
19 | let divStyle = {
20 | marginTop: '10px'
21 | }
22 |
23 | let scrollbarStyle = {
24 | height: height,
25 | width: '100%'
26 | }
27 |
28 | return (
29 |
30 |
33 | {header}
34 |
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | }
43 |
44 | EmbeddedGraphAnnotation.propTypes = {
45 | annotation: PropTypes.object,
46 | embedded: PropTypes.object,
47 | hasTracker: PropTypes.bool
48 | }
49 |
--------------------------------------------------------------------------------
/app/components/EmbeddedGraphAnnotations.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import EmbeddedNavBar from './EmbeddedNavBar';
4 | import EmbeddedGraphAnnotation from './EmbeddedGraphAnnotation';
5 |
6 | export default class EmbeddedGraphAnnotations extends Component {
7 | render () {
8 | let hasTracker = this.props.annotationCount > 1;
9 | let { logoUrl, annotationHeight, logoWidth } = this.props.embedded;
10 |
11 | return (
12 |
13 |
14 | { hasTracker &&
15 | }
21 |
22 |
23 |
24 |
25 |
26 |
27 | { logoUrl &&
28 |
29 |
30 |
31 | }
32 |
33 | );
34 | }
35 | }
36 |
37 | EmbeddedGraphAnnotations.propTypes = {
38 | embedded: PropTypes.object,
39 | annotationCount: PropTypes.number.isRequired,
40 | currentIndex: PropTypes.number.isRequired,
41 | annotation: PropTypes.object,
42 | nextClick: PropTypes.func,
43 | prevClick: PropTypes.func
44 | }
45 |
--------------------------------------------------------------------------------
/app/components/EmbeddedNavBar.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import EmbeddedNavButtons from './EmbeddedNavButtons';
5 | import AnnotationsTracker from './AnnotationsTracker';
6 |
7 | export default class EmbeddedNavBar extends BaseComponent {
8 | constructor(props) {
9 | super(props);
10 | }
11 | render () {
12 | return (
13 |
24 | )
25 | }
26 |
27 | }
28 |
29 | EmbeddedNavBar.propTypes = {
30 | currentIndex: PropTypes.number,
31 | annotationCount: PropTypes.number.isRequired,
32 | nextClick: PropTypes.func.isRequired,
33 | prevClick: PropTypes.func.isRequired
34 | }
35 |
--------------------------------------------------------------------------------
/app/components/EmbeddedNavButtons.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from "./BaseComponent";
4 |
5 | export default class EmbeddedNavButtons extends BaseComponent {
6 | constructor(props) {
7 | super(props);
8 | this.bindAll('_isBackButtonDisabled', '_isNextButtonDisabled');
9 | }
10 | render () {
11 | return (
12 |
13 | Back
18 |
19 | Next
24 |
25 | )
26 | }
27 |
28 | _isBackButtonDisabled() {
29 | return this.props.currentIndex === 0;
30 | }
31 |
32 | _isNextButtonDisabled() {
33 | return (this.props.currentIndex + 1) === this.props.annotationCount;
34 | }
35 |
36 | }
37 |
38 | EmbeddedNavButtons.propTypes = {
39 | currentIndex: PropTypes.number.isRequired,
40 | annotationCount: PropTypes.number.isRequired,
41 | nextClick: PropTypes.func.isRequired,
42 | prevClick: PropTypes.func.isRequired
43 | }
44 |
--------------------------------------------------------------------------------
/app/components/GraphAnnotation.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class GraphAnnotation extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
{this.props.annotation.header}
10 |
13 | { this.props.isEditor ?
Edit : null }
17 |
18 | );
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/components/GraphAnnotationForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import merge from 'lodash/merge';
5 | import pick from 'lodash/pick';
6 | import Editor from 'react-medium-editor';
7 |
8 | export default class GraphAnnotationForm extends BaseComponent {
9 | constructor(props) {
10 | super(props);
11 | this.bindAll('_handleHeaderChange', '_handleTextChange', '_handleRemove');
12 | }
13 |
14 | render() {
15 | let editorOptions = {
16 | toolbar: { buttons: [
17 | 'bold', 'italic', 'underline', 'anchor', 'h2', 'h3', 'quote', 'unorderedlist', 'orderedlist'
18 | ] },
19 | targetBlank: true,
20 | placeholder: { text: "annotation text" }
21 | }
22 |
23 |
24 | return (
25 |
26 |
35 |
43 | Remove
47 |
48 | );
49 | }
50 |
51 | _handleRemove() {
52 | if (confirm("Are you sure you want to delete this annotation?")) {
53 | this.props.remove();
54 | }
55 | }
56 |
57 | _handleHeaderChange() {
58 | this.props.update({ header: this.refs.header.value });
59 | }
60 |
61 | _handleTextChange(text) {
62 | this.props.update({ text });
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/components/GraphAnnotationList.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 |
5 | export default class GraphAnnotationList extends BaseComponent {
6 | constructor(props) {
7 | super(props);
8 | this.bindAll('_handleClick', '_handleDragOver', '_handleDragStart', '_handleDragEnd');
9 | this._placeholder = document.createElement("li");
10 | this._placeholder.className = "placeholder";
11 | }
12 |
13 | render() {
14 | return (
15 |
16 |
17 | { this.props.annotations.map((annotation, index) =>
18 |
26 | {annotation.header.trim().length > 0 ? annotation.header : "Untitled Annotation"}
27 |
28 | ) }
29 |
30 | { this.props.isEditor ?
31 |
36 | New Annotation
37 | : null }
38 |
39 | );
40 | }
41 |
42 | _handleClick(e) {
43 | this.props.show(parseInt(e.target.dataset.id));
44 |
45 | if (this.props.isEditor) {
46 | this.props.hideEditTools();
47 | };
48 | }
49 |
50 | _handleDragStart(e) {
51 | this._startY = e.clientY;
52 | this._dragged = e.currentTarget;
53 | this._placeholder.innerHTML = e.currentTarget.innerHTML;
54 |
55 | e.dataTransfer.effectAllowed = "move";
56 | e.dataTransfer.setData("text/html", e.currentTarget);
57 | }
58 |
59 | _handleDragEnd(e) {
60 | this._dragged.style.display = "block";
61 | this._dragged.parentNode.removeChild(this._placeholder);
62 |
63 | // update store
64 | let from = Number(this._dragged.dataset.id);
65 | let to = Number(this._over.dataset.id);
66 |
67 | this.props.move(from, to);
68 |
69 | this._startY = undefined;
70 | }
71 |
72 | _handleDragOver(e) {
73 | e.preventDefault();
74 |
75 | let thisHeight = this._dragged.offsetHeight;
76 | this._dragged.style.display = "none";
77 |
78 | if (e.target.className == "placeholder") return;
79 |
80 | this._over = e.target;
81 |
82 | let relY = e.clientY - this._startY;
83 | let height = (this._over.offsetHeight || thisHeight) / 2;
84 | let parent = e.target.parentNode;
85 |
86 | if (relY > height) {
87 | parent.insertBefore(this._placeholder, e.target.nextElementSibling);
88 | }
89 | else if (relY < height) {
90 | parent.insertBefore(this._placeholder, e.target);
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/app/components/GraphAnnotationListItem.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 |
5 | export default class GraphAnnotationListItem extends BaseComponent {
6 | constructor(props) {
7 | super(props);
8 | this.bindAll('_handleDragStart', '_handleDragEnd');
9 | }
10 |
11 | render() {
12 | let active = this.props.currentIndex == this.props.index;
13 |
14 | return (
15 |
21 | {this.props.annotation.header}
22 |
23 | );
24 | }
25 |
26 | _handleDragStart() {
27 |
28 | }
29 |
30 | _handleDragEnd() {
31 |
32 | }
33 |
34 | _handleClick() {
35 | this.props.show(this.props.index);
36 | }
37 | }
--------------------------------------------------------------------------------
/app/components/GraphAnnotations.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from "./BaseComponent";
4 | import GraphNavButtons from './GraphNavButtons';
5 | import GraphAnnotationList from './GraphAnnotationList';
6 | import GraphAnnotation from './GraphAnnotation';
7 | import GraphAnnotationForm from './GraphAnnotationForm';
8 |
9 | export default class GraphAnnotations extends BaseComponent {
10 | constructor(props) {
11 | super(props);
12 | this.bindAll('_remove', '_update');
13 | }
14 |
15 | render() {
16 | let { prevClick, nextClick, isEditor, editForm, navList,
17 | swapAnnotations, annotation, currentIndex,
18 | update, remove, swapEditForm, annotations, show,
19 | create, move, canClickPrev, canClickNext } = this.props;
20 |
21 | let navComponent = (
22 |
23 | );
24 |
25 | let formComponent = (
26 |
30 | );
31 |
32 | let annotationComponent = (
33 |
34 | );
35 |
36 | let navListComponent = (
37 |
38 | );
39 |
40 | return (
41 |
42 | { (annotation || isEditor) && navComponent }
43 | { isEditor && navList && navListComponent }
44 | { annotation && (isEditor ? formComponent : annotationComponent) }
45 |
46 | );
47 | }
48 |
49 | _remove() {
50 | this.props.remove(this.props.currentIndex);
51 | }
52 |
53 | _update(data) {
54 | this.props.update(this.props.currentIndex, data);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/app/components/GraphByLine.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class GraphByLine extends Component {
5 |
6 | render() {
7 | let { user, date } = this.props;
8 |
9 | return (
10 |
11 | { user ? this._renderUser(user) : null }
12 | { date ? {date} : null }
13 |
14 | );
15 | }
16 |
17 | _renderUser(user) {
18 | return (
19 | by { user.url ? {user.name} : {user.name} }
20 | );
21 | }
22 | }
--------------------------------------------------------------------------------
/app/components/GraphHeader.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import GraphTitle from './GraphTitle';
4 | import GraphTitleForm from './GraphTitleForm';
5 | import GraphByLine from './GraphByLine';
6 | import GraphLinks from './GraphLinks';
7 |
8 |
9 | export default class GraphHeader extends Component {
10 |
11 | render() {
12 | let { user, date, links, title, isEditor, updateTitle, url, isEmbedded, embedded } = this.props;
13 |
14 | return (
15 |
24 | );
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/components/GraphLinks.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class GraphLinks extends Component {
5 |
6 | render() {
7 | let { links } = this.props;
8 |
9 | return (
10 |
11 | { links.map((link, i) => {
12 | return (link.method == "POST" ? this._postLink(link, i) : this._getLink(link, i));
13 | }) }
14 |
15 | );
16 | }
17 |
18 | _getLink(link, i) {
19 | return {link.text} ;
20 | }
21 |
22 | _postLink(link, i) {
23 | return (
24 |
28 | );
29 | }
30 |
31 | _renderUser(user) {
32 | return (
33 | by { user.url ? {user.name} : {user.name} }
34 | );
35 | }
36 | }
--------------------------------------------------------------------------------
/app/components/GraphNavButtons.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class GraphNavButtons extends Component {
5 |
6 | render() {
7 | var shouldShowNav = this.props.isEditor || this.props.annotations.length > 1;
8 |
9 | return (
10 |
35 | );
36 | }
37 | }
--------------------------------------------------------------------------------
/app/components/GraphSettingsForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import titleize from 'titleize';
4 |
5 | export default class GraphSettingsForm extends Component {
6 |
7 | render() {
8 | return (
9 |
22 | );
23 | }
24 |
25 | handleChange(event) {
26 | let key = event.target.name;
27 | let value = event.target.checked;
28 | this.props.updateSettings({ [key]: value });
29 | }
30 | }
--------------------------------------------------------------------------------
/app/components/GraphTitle.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class GraphTitle extends Component {
5 |
6 | render() {
7 | let h1Style = Boolean(this.props.isEmbedded) ? this.props.embedded.headerFontStyle : {};
8 | return (
9 | { this.props.url ? {this.props.title} : this.props.title }
10 | );
11 | }
12 | }
13 |
14 | GraphTitle.PropTypes = {
15 | url: PropTypes.string,
16 | title: PropTypes.string,
17 | isEmbedded: PropTypes.boolean,
18 | embedded: PropTypes.object
19 | }
20 |
--------------------------------------------------------------------------------
/app/components/GraphTitleForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class GraphTitleForm extends Component {
5 |
6 | render() {
7 | return (
8 |
9 | this._handleChange(event)} />
15 |
16 | );
17 | }
18 |
19 | _handleChange(event) {
20 | this.props.updateTitle(this.refs.title.value);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/components/HelpButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class HelpButton extends Component {
5 |
6 | render() {
7 | return (
8 |
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/HelpScreen.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class HelpScreen extends Component {
5 |
6 | render() {
7 | return (
8 |
9 |
10 |
User Guide
11 | Use this editor to create a network graph along with an optional series of annotations overlaying the graph. Annotations consists of a title, a text body, and a highlighted section of the graph.
12 |
13 | The pencil button swaps between graph editing mode and annotation editing mode. It will appear green when editing the graph and yellow when editing annotations. You can edit the graph title at the top of the screen.
14 |
15 | When you are finished editing, click the SAVE button to save your changes.
16 |
17 |
Graph Editing Mode
18 |
19 | Type a name in the "add node" box and press enter to add the node to the graph.
20 | { this.props.source ? ` Nodes from ${this.props.source.name} matching the name you type will appear below; click on them to add them to the graph.` : "" }
21 |
22 | ALT+C opens a form for adding a new caption in the top right of the graph.
23 |
24 | CLICK a node, edge, or caption to select or deselect it.
25 | SHIFT+CLICK to select mutiple nodes, edges, or captions.
26 |
27 | Select a single node, edge, or caption to view an editing form in the top-right corner of the map. Changes you make in the form will upate the item immediately. Selecting two nodes will display a form to add an edge between them.
28 | { this.props.source && this.props.source.getInterlocks && `Selecting two nodes will also display a button to add interlocks, meaning nodes that they are both connected to.` }
29 |
30 |
31 | The CIRCLE button arranges nodes in a circle.
32 | The PRUNE button removes unconnected nodes.
33 | The CLEAR button deletes all content from the graph.
34 | The HELP button displays this user guide.
35 |
36 |
Annotation Editing Mode
37 |
38 | Annotations are edited using the sidebar on the right. Click the big "A" button to hide or show the sidebar.
39 |
40 | Click the NEW ANNOTATION button create a new annotation and display a form for editing it. Click on any annotation title in the list to edit it. A REMOVE button at the bottom of the edit form will delete the annotation. When editing an annotation, click on nodes, edges, or captions from the graph to highlight them in that annotation. Drag annotaions up and down the list to reorder them.
41 |
42 |
Shortcut Keys
43 |
44 | LEFT & RIGHT ARROWS navigate to the previous and next annotations.
45 | ALT+H toggles this user guide.
46 | ALT+H swaps the editing mode between graph editing and annotations.
47 | ALT+D deletes selected nodes and edges.
48 | ALT+E adds an edge. Selected nodes will be auto-populated in the form.
49 | ALT+C adds a caption.
50 |
51 | If ALT keys interfere with your browser or operating system shortcuts, all of the above shortcuts will work with CTRL instead of ALT.
52 |
53 | CTRL+"=" zooms in.
54 | CTRL+"-" zooms out.
55 | CTRL+0 resets zoom.
56 |
57 | ESC closes all forms.
58 |
59 | );
60 | }
61 | }
--------------------------------------------------------------------------------
/app/components/LayoutButtons.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class LayoutButtons extends Component {
5 |
6 | render() {
7 | return (
8 |
9 | prune
10 | circle
11 | clear
12 |
13 | );
14 | }
15 | }
--------------------------------------------------------------------------------
/app/components/Node.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import NodeLabel from './NodeLabel';
5 | import NodeCircle from './NodeCircle';
6 | import ds from '../NodeDisplaySettings';
7 | import { DraggableCore } from 'react-draggable';
8 | import Graph from '../models/Graph';
9 | import merge from 'lodash/merge';
10 | import classNames from 'classnames';
11 | import Helpers from '../models/Helpers';
12 | import { calculateDeltas } from '../helpers';
13 |
14 | export default class Node extends BaseComponent {
15 | constructor(props) {
16 | super(props);
17 | this.bindAll('_handleDragStart', '_handleDrag', '_handleDragStop', '_handleClick');
18 | this.state = props.node.display;
19 | }
20 |
21 | render() {
22 | const n = this.props.node;
23 | const { x, y, name } = this.state;
24 | const groupId = `node-${n.id}`;
25 | const transform = `translate(${x}, ${y})`;
26 |
27 | return (
28 |
33 |
38 |
39 | { this.state.name ? : null }
40 |
41 |
42 | );
43 | }
44 |
45 | componentWillReceiveProps(props) {
46 | let newState = merge({ name: null, image: null, url: null }, props.node.display);
47 | this.setState(newState);
48 | }
49 |
50 | shouldComponentUpdate(nextProps, nextState) {
51 | return nextProps.selected !== this.props.selected ||
52 | JSON.stringify(nextState) !== JSON.stringify(this.state);
53 | }
54 |
55 | // keep initial position for comparison with drag position
56 | _handleDragStart(e, data) {
57 | e.preventDefault();
58 | this._startDrag = data;
59 | this._startPosition = {
60 | x: this.state.x,
61 | y: this.state.y
62 | }
63 | }
64 |
65 | // while dragging node and its edges are updated only in state, not store
66 | _handleDrag(e, data) {
67 | if (this.props.isLocked) return;
68 |
69 | this._dragging = true; // so that _handleClick knows it's not just a click
70 |
71 | let n = this.props.node;
72 |
73 | let { x, y } = calculateDeltas(data, this._startPosition, this._startDrag, this.graph.state.actualZoom);
74 | this.setState({ x, y });
75 |
76 | // update state of connecting edges
77 | let edges = Graph.edgesConnectedToNode(this.props.graph, n.id);
78 |
79 | edges.forEach(edge => {
80 | let thisNodeNum = edge.node1_id == n.id ? 1 : 2;
81 | let newEdge = Graph.moveEdgeNode(edge, thisNodeNum, x, y);
82 | this.graph.edges[edge.id].setState(newEdge.display);
83 | });
84 | }
85 |
86 | // store updated once dragging is done
87 | _handleDragStop(e, data) {
88 | // event fires every mouseup so we check for actual drag before updating store
89 | if (this._dragging) {
90 | this.props.moveNode(this.props.node.id, this.state.x, this.state.y);
91 | }
92 | }
93 |
94 | _handleClick() {
95 | if (this._dragging) {
96 | this._dragging = false;
97 | } else if (this.props.clickNode) {
98 | this.props.clickNode(this.props.node.id);
99 | }
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/app/components/NodeCircle.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import BaseComponent from './BaseComponent';
5 | import ds from '../NodeDisplaySettings';
6 |
7 | export default class NodeCircle extends BaseComponent {
8 | render() {
9 | return (
10 |
11 | { this.props.selected ? this._selectionCirlce() : null }
12 | {this._bgCircle()}
13 | {this._circle()}
14 |
15 | );
16 | }
17 |
18 | componentDidMount() {
19 | let element = ReactDOM.findDOMNode(this);
20 | let images = element.querySelectorAll("image");
21 |
22 | for (var i = 0; i < images.length; ++i) {
23 | images[i].ondragstart = (e) => { e.preventDefault(); return false; };
24 | }
25 | }
26 |
27 | _selectionCirlce() {
28 | const { scale } = this.props.node.display;
29 | const r = ds.circleRadius * scale;
30 | const bgColor = ds.selectColor;
31 | const bgOpacity = 0.5;
32 | const bgRadius = r + (ds.selectionRadiusDiff * scale);
33 | return ;
34 | }
35 |
36 | _bgCircle() {
37 | const { scale, status } = this.props.node.display;
38 | const r = ds.circleRadius * scale;
39 | const bgColor = ds.bgColor[status];
40 | const bgOpacity = ds.bgOpacity[status];
41 | const bgRadius = r + (ds.bgRadiusDiff * scale);
42 | return ;
43 | }
44 |
45 | _circle() {
46 | const n = this.props.node;
47 | const { scale, status, color, image } = n.display;
48 | const r = ds.circleRadius * scale;
49 | const clipId = `image-clip-${n.id}`;
50 | const clipPath = `url(#${clipId})`;
51 | const imageWidth = r * ds.imageScale;
52 | const imageOpacity = ds.imageOpacity[status];
53 | const innerHTML = { __html:
54 | `
55 |
56 |
57 |
67 | `
68 | };
69 |
70 | return image ?
71 | :
72 |
77 | ;
78 | }
79 | }
--------------------------------------------------------------------------------
/app/components/NodeLabel.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import ReactDOM from 'react-dom';
4 | import ds from '../NodeDisplaySettings';
5 |
6 | export default class NodeLabel extends Component {
7 | render() {
8 | let n = this.props.node;
9 | let { name, url, scale, status } = n.display;
10 | let r = ds.circleRadius * scale;
11 | let textOffsetY = ds.textMarginTop + r;
12 | let textLines = this._textLines(name);
13 |
14 | let tspans = textLines.map((line, i) =>
15 |
22 | {line}
23 |
24 | );
25 |
26 | let rects = textLines.map((line, i) => {
27 | let width = line.length * 8;
28 | let height = ds.lineHeight;
29 | let y = r + 4 + (i * ds.lineHeight);
30 | return ;
41 | });
42 |
43 | return (
44 |
45 | {rects}
46 |
47 |
48 | { tspans }
49 |
50 |
51 |
52 | );
53 | }
54 |
55 | componentDidMount() {
56 | this._setRectWidths();
57 | }
58 |
59 | componentDidUpdate(prevProps) {
60 | if (prevProps.node.display.name !== this.props.node.display.name) {
61 | this._setRectWidths();
62 | }
63 | }
64 |
65 | _setRectWidths() {
66 | let element = ReactDOM.findDOMNode(this);
67 | let texts = element.querySelectorAll(".nodeLabelText");
68 | let rects = element.querySelectorAll(".nodeLabelRect");
69 |
70 | for (var i = 0; i < rects.length; i++) {
71 | let textWidth = texts[i].getComputedTextLength();
72 | let width = textWidth + 10;
73 | rects[i].setAttribute("width", width);
74 | rects[i].setAttribute("x", -width/2);
75 | }
76 | }
77 |
78 | _textLines(text){
79 | const maxWidth = text.length > 40 ? 25 : 18;
80 | let words = text.trim().split(/\s+/g),
81 | word,
82 | lines = [],
83 | lineNumber = 1,
84 | line = "",
85 | lineWords = [];
86 |
87 | while (word = words.shift()) {
88 | lineWords.push(word);
89 | line = lineWords.join(" ");
90 |
91 | if (line.length > maxWidth) {
92 | lineWords.pop();
93 | line = lineWords.join(" ");
94 | lines.push(line);
95 | lineNumber += 1;
96 | lineWords = [word];
97 | }
98 | }
99 |
100 | if (line = lineWords.join(" ")) {
101 | if (line.length < 4 && lines.length > 0) {
102 | lines.push(lines.pop() + " " + line);
103 | } else {
104 | lines.push(line);
105 | }
106 | }
107 |
108 | return lines;
109 | }
110 | }
--------------------------------------------------------------------------------
/app/components/SaveButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class SaveButton extends Component {
5 |
6 | render() {
7 | return (
8 |
13 | Save
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/SettingsButton.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class SettingsButton extends Component {
5 |
6 | render() {
7 | return (
8 | this.props.toggleSettings()}>
13 |
14 |
15 | );
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/app/components/UndoButtons.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class UndoButtons extends Component {
5 |
6 | render() {
7 | return (
8 |
9 | undo
10 | redo
11 |
12 | );
13 | }
14 | }
--------------------------------------------------------------------------------
/app/components/UpdateCaptionForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import { HotKeys } from 'react-hotkeys';
5 | import merge from 'lodash/merge';
6 |
7 | export default class UpdateCaptionForm extends BaseComponent {
8 |
9 | render() {
10 | let { display } = this.props.data;
11 |
12 | const scales = [
13 | [null, "Scale"],
14 | [1, "1x"],
15 | [1.25, "1.25x"],
16 | [1.5, "1.5x"],
17 | [2, "2x"],
18 | [2.5, "2.5x"],
19 | [3, "3x"],
20 | [4, "4x"],
21 | [5, "5x"]
22 | ];
23 |
24 | const keyMap = {
25 | 'esc': 'esc',
26 | 'enter': 'enter'
27 | };
28 |
29 | const keyHandlers = {
30 | 'esc': () => this.props.deselect(),
31 | 'enter': () => this.props.deselect()
32 | };
33 |
34 | return (
35 |
36 |
37 |
38 | this.apply()} />
45 | this.apply()}>
50 | { scales.map((scale, i) =>
51 | {scale[1]}
52 | ) }
53 |
54 |
55 |
56 | );
57 | }
58 |
59 |
60 | apply() {
61 | if (this.props.data) {
62 | let text = this.refs.text.value.trim();
63 | let scale = parseFloat(this.refs.scale.value);
64 | this.props.updateCaption(this.props.data.id, { display: { text, scale } });
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/components/UpdateEdgeForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import EdgeDropdown from './EdgeDropdown';
5 | import { HotKeys } from 'react-hotkeys';
6 | import values from 'lodash/values';
7 | import sortBy from 'lodash/sortBy';
8 | import merge from 'lodash/merge';
9 |
10 | export default class UpdateEdgeForm extends BaseComponent {
11 |
12 | render() {
13 | let { display } = this.props.data;
14 | const keyMap = {
15 | 'esc': 'esc'
16 | };
17 |
18 | const keyHandlers = {
19 | 'esc': () => this.props.deselect()
20 | };
21 |
22 |
23 | const scales = [
24 | [null, "Scale"],
25 | [1, "1x"],
26 | [1.5, "1.5x"],
27 | [2, "2x"],
28 | [3, "3x"]
29 | ];
30 |
31 | return (
32 |
72 | );
73 | }
74 |
75 | apply(whichArrow, isDashed) {
76 | let label = this.refs.label.value;
77 | let arrow = whichArrow || this.refs.edgeDropdown.props.whichArrow;
78 | let dash = isDashed;
79 | let scale = parseFloat(this.refs.scale.value);
80 | let url = this.refs.url.value.trim();
81 | this.props.updateEdge(this.props.data.id, { display: { label, arrow, dash, scale, url } });
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/components/UpdateNodeForm.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 | import BaseComponent from './BaseComponent';
4 | import ChangeColorInput from './ChangeColorInput';
5 | import { HotKeys } from 'react-hotkeys';
6 | import merge from 'lodash/merge';
7 | import ds from '../NodeDisplaySettings';
8 |
9 | export default class UpdateNodeForm extends BaseComponent {
10 |
11 | render() {
12 | let { display } = this.props.data;
13 |
14 | if (!display.color) {
15 | display.color = ds.circleColor[display.status];
16 | }
17 |
18 | const keyMap = {
19 | 'esc': 'esc'
20 | };
21 |
22 | const keyHandlers = {
23 | 'esc': () => this.props.deselect()
24 | };
25 |
26 | const scales = [
27 | [1, "1x"],
28 | [1.5, "1.5x"],
29 | [2, "2x"],
30 | [3, "3x"]
31 | ];
32 |
33 | return (
34 |
85 | );
86 | }
87 |
88 | apply(newColor) {
89 | if (this.props.data) {
90 | let name = this.refs.name.value;
91 | let image = this.refs.image.value.trim();
92 | let color = newColor || this.refs.color.state.color;
93 | let scale = parseFloat(this.refs.scale.value);
94 | let url = this.refs.url.value.trim();
95 | this.props.updateNode(this.props.data.id, { display: { name, image, color, scale, url } });
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/components/ZoomButtons.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import React, { Component } from 'react';
3 |
4 | export default class ZoomButtons extends Component {
5 |
6 | render() {
7 | return (
8 |
9 | +
10 | –
11 |
12 | );
13 | }
14 | }
--------------------------------------------------------------------------------
/app/components/__tests__/AnnotationsTracker-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import AnnotationsTracker from '../AnnotationsTracker';
4 |
5 | describe('AnnotationsTracker', () => {
6 |
7 | it('has correct number of circles', () => {
8 | expect(shallow( ).find('.tracker-circle').length).toEqual(10);
9 | });
10 |
11 | it('has only one selected circle', () => {
12 | expect(shallow( ).find('.tracker-circle-selected').length).toEqual(1);
13 | });
14 |
15 | it('highlights correct circle', () => {
16 | let annotationsTracker = shallow( )
17 | expect(annotationsTracker.find('#annotationsTracker').childAt(2).hasClass('tracker-circle-selected')).toEqual(true);
18 | });
19 |
20 | });
21 |
--------------------------------------------------------------------------------
/app/components/__tests__/Caption-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from "enzyme";
3 | import Caption from '../Caption';
4 |
5 | describe("Caption Component", () => {
6 |
7 | const data = { id: 1, display: { text: "Here's an interesting fact!" } };
8 |
9 | it("should have an svg transform", () => {
10 | let wrapper = shallow(
11 |
12 | );
13 | let element = wrapper.find("g.caption");
14 | let { x, y } = data.display;
15 |
16 | expect(element.props().transform).toBe(`translate(${x}, ${y})`);
17 | });
18 |
19 | it("should display text", () => {
20 | let wrapper = shallow(
21 |
22 | );
23 | let text = wrapper.find("text");
24 |
25 | expect(text.text()).toBe(data.display.text);
26 | });
27 |
28 | it("should call click callback if clicked", () => {
29 | let clickCaption = jest.genMockFunction();
30 | let wrapper = shallow(
31 |
32 | );
33 | let element = wrapper.find("g.caption");
34 | element.simulate("click");
35 |
36 | expect(clickCaption.mock.calls[0][0]).toBe(data.id);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/app/components/__tests__/ChangeColorInput-test.jsx:
--------------------------------------------------------------------------------
1 | jest.disableAutomock();
2 |
3 | import React from "react";
4 | import { shallow } from "enzyme";
5 |
6 | import ChangeColorInput from "../ChangeColorInput";
7 | import ds from "../../NodeDisplaySettings";
8 | import { CompactPicker } from "react-color";
9 |
10 | describe("ChangeColorInput", () => {
11 | let wrapper;
12 | let onChange;
13 |
14 | beforeEach(() => {
15 | onChange = jest.genMockFunction();
16 | wrapper = shallow(
17 |
21 | );
22 | });
23 |
24 | it("shows a swatch with the currently selected color", () => {
25 | let swatch = wrapper.find(".nodeColorInputSwatch");
26 | expect(swatch.props().style).toEqual({ background: "#abc" });
27 | });
28 |
29 | it("shows a clear button", () => {
30 | let clearer = wrapper.find(".nodeColorInputClearer");
31 | let glyph = wrapper.find(".glyphicon-remove-sign");
32 | expect(clearer.length).toBe(1);
33 | expect(glyph.length).toBe(1);
34 | });
35 |
36 | it("clears color and hides picker when clear button is clicked", () => {
37 | wrapper.setState({ displayColorPicker: true });
38 | let clearer = wrapper.find(".nodeColorInputClearer");
39 | clearer.simulate("click");
40 | expect(wrapper.state().color).toBe(ds.circleColor["highlighted"]);
41 | });
42 |
43 | it ("shows and closes color picker when swatch is clicked", () => {
44 | let swatch = wrapper.find(".nodeColorInputSwatch");
45 | swatch.simulate("click");
46 |
47 | let picker = wrapper.find(CompactPicker);
48 | expect(picker.length).toBe(1);
49 | expect(picker.props().color).toBe("#abc");
50 |
51 | swatch.simulate("click");
52 | picker = wrapper.find(CompactPicker);
53 | expect(picker.length).toBe(0);
54 | });
55 |
56 | it("updates color when receiving new props", () => {
57 | wrapper.setProps({ value: "#def" });
58 | expect(wrapper.state().color).toBe("#def");
59 | });
60 |
61 | it("calls onChange when CompactPicker changes color", () => {
62 | wrapper.setState({ displayColorPicker: true });
63 | let picker = wrapper.find(CompactPicker);
64 | let pickerOnChange = picker.props().onChange;
65 | pickerOnChange({ hex: "#def" });
66 |
67 | expect(onChange.mock.calls.length).toBe(1);
68 | expect(onChange.mock.calls[0][0]).toBe("#def");
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/app/components/__tests__/DeleteSelectedButton-test.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import DeleteSelectedButton from '../DeleteSelectedButton';
4 |
5 | describe('DeleteSelectedButton', function(){
6 | let doDeleteMock = jest.fn();
7 | let wrapper = shallow(
8 | );
11 |
12 | it('sets correct class names on div', () => {
13 | expect(wrapper.hasClass('editForm')).toEqual(true);
14 | expect(wrapper.hasClass('form-inline')).toEqual(true);
15 | expect(wrapper.hasClass('nodeDelete')).toEqual(true);
16 | expect(wrapper.hasClass('edgeDelete')).toEqual(false);
17 | });
18 |
19 | it('clicking on button triggers doDelete', () => {
20 | wrapper.find('button').simulate('click');
21 | expect(doDeleteMock.mock.calls.length).toEqual(1);
22 | });
23 |
24 |
25 | });
26 |
--------------------------------------------------------------------------------
/app/components/__tests__/Edge-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import TestUtils from 'react-addons-test-utils';
4 | import Edge from '../Edge';
5 | import merge from 'lodash/merge';
6 |
7 | describe("Edge Component", () => {
8 |
9 | const data = {
10 | id: 93362,
11 | node1_id: 34963,
12 | node2_id: 15957,
13 | display: {
14 | label: "former chair",
15 | cx: null,
16 | cy: null,
17 | scale: 1,
18 | arrow: true,
19 | dash: null,
20 | status: "normal",
21 | url: "//littlesis.org/relationship/view/id/93362",
22 | x1: 569.3762350992062,
23 | y1: -255.7819366545154,
24 | x2: -56.66444213745007,
25 | y2: -402.53869654584355,
26 | s1: 1,
27 | s2: 1.5
28 | }
29 | };
30 |
31 | it("should render a curve with a control point", () => {
32 | let getArrow = jest.genMockFunction();
33 | let edge = TestUtils.renderIntoDocument(
34 |
35 | );
36 | let element = ReactDOM.findDOMNode(edge);
37 | let curve = element.querySelector(".edge-line");
38 |
39 | expect(curve.getAttribute("d")).toMatch(/M [-\d.]+, [-\d.]+ Q [-\d.]+, [-\d.]+, [-\d.]+, [-\d.]+/);
40 | });
41 |
42 | it("should render a label", () => {
43 | let getArrow = jest.genMockFunction();
44 | let edge = TestUtils.renderIntoDocument(
45 |
46 | );
47 | let element = ReactDOM.findDOMNode(edge);
48 | let label = element.querySelector("text");
49 |
50 | expect(label.textContent).toBe(data.display.label);
51 | });
52 |
53 | /*not sure of most effective way of testing arrow rendering*/
54 | it("should render an arrow", () => {
55 | let getArrow = jest.genMockFunction().mockImplementation(function () {
56 | data["display"]["arrow"] = "1->2";
57 | return data;
58 | });
59 | let updateArrow = jest.genMockFunction();
60 |
61 | let edge = TestUtils.renderIntoDocument(
62 |
63 | );
64 | let element = ReactDOM.findDOMNode(edge);
65 | let curve = element.querySelector(".edge-line");
66 | let hasMarker = curve.getAttribute("marker-start") || curve.getAttribute("marker-end");
67 |
68 | expect(hasMarker).toBeTruthy();
69 | });
70 |
71 | it("should call click callback if clicked", () => {
72 | let getArrow = jest.genMockFunction();
73 | let updateArrow = jest.genMockFunction();
74 | let clickEdge = jest.genMockFunction();
75 | let edge = TestUtils.renderIntoDocument(
76 |
77 | );
78 | let element = ReactDOM.findDOMNode(edge);
79 | let select = element.querySelector(".edgeSelect");
80 |
81 | TestUtils.Simulate.click(select);
82 | expect(clickEdge.mock.calls[0][0]).toBe(data.id);
83 | });
84 |
85 | it('should calculate correct arrow attribute for markerStart', () => {
86 | let edge = TestUtils.renderIntoDocument( );
87 | expect(edge._markerStartArrow('1->2', false)).toEqual('');
88 | expect(edge._markerStartArrow('1->2', true)).toEqual("url(#marker2)");
89 | expect(edge._markerStartArrow('2->1', false)).toEqual("url(#marker2)");
90 | expect(edge._markerStartArrow('2->1', true)).toEqual("");
91 | expect(edge._markerStartArrow('both', true)).toEqual("url(#marker2)");
92 | expect(edge._markerStartArrow('both', false)).toEqual("url(#marker2)");
93 | });
94 |
95 | it('should calculate correct arrow attribute for markerEnd', () => {
96 | let edge = TestUtils.renderIntoDocument( );
97 | expect(edge._markerEndArrow('1->2', false)).toEqual('url(#marker1)');
98 | expect(edge._markerEndArrow('1->2', true)).toEqual('');
99 | expect(edge._markerEndArrow('2->1', false)).toEqual('');
100 | expect(edge._markerEndArrow('2->1', true)).toEqual('url(#marker1)');
101 | expect(edge._markerEndArrow('both', true)).toEqual("url(#marker1)");
102 | expect(edge._markerEndArrow('both', false)).toEqual("url(#marker1)");
103 | });
104 |
105 | });
106 |
--------------------------------------------------------------------------------
/app/components/__tests__/EdgeArrowSelector-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from "enzyme";
3 | import EdgeArrowSelector from '../EdgeArrowSelector';
4 | import { newArrowState } from '../../helpers';
5 |
6 |
7 | const element = () => shallow( );
8 |
9 | describe('EdgeArrowSelector', () => {
10 |
11 | it('has dropDownHolder', () => expect(element().find('.dropdownHolder').length).toEqual(1));
12 | it('has selectedEdgeDisplay', () => expect(element().find('.selectedEdgeDisplay').length).toEqual(1));
13 | it('sets correct class name for left-left', () =>{
14 | let e = shallow( );
15 | expect(e.find('.svgDropdownLeftArrow').length).toEqual(1);
16 | });
17 | it('sets correct class name for left-right', () =>{
18 | let e = shallow( );
19 | expect(e.find('.svgDropdownLeftArrow').length).toEqual(0);
20 | });
21 | it('sets correct class name for right-right', () =>{
22 | let e = shallow( );
23 | expect(e.find('.svgDropdownRightArrow').length).toEqual(1);
24 | });
25 | it('sets correct class name for right-left', () =>{
26 | let e = shallow( );
27 | expect(e.find('.svgDropdownRightArrow').length).toEqual(0);
28 | });
29 | it('sets correct class name for right-both', () =>{
30 | let e = shallow( );
31 | expect(e.find('.svgDropdownRightArrow').length).toEqual(1);
32 | });
33 | it('sets correct class name for left-both', () =>{
34 | let e = shallow( );
35 | expect(e.find('.svgDropdownLeftArrow').length).toEqual(1);
36 | });
37 |
38 | it('updates state and re-renders when clicked on', () => {
39 | let e = element();
40 | expect(e.state().isOpen).toBe(false);
41 | expect(e.find('.edgeDropdownOptions').length).toEqual(0);
42 | e.find('.selectedEdgeDisplay').simulate('click');
43 | expect(e.state().isOpen).toBe(true);
44 | expect(e.find('.edgeDropdownOptions').length).toEqual(1);
45 | });
46 |
47 | it('updates Edge with new arrow setting', () => {
48 | let updateEdgeMock = jest.fn();
49 | let e = shallow( );
50 | e.find('.selectedEdgeDisplay').simulate('click');
51 | e.find('.svgDropdownLeftNoArrow').simulate('click');
52 | expect(e.state().isOpen).toBe(false);
53 | expect(updateEdgeMock.mock.calls.length).toEqual(1);
54 | expect(updateEdgeMock.mock.calls[0]).toEqual(['x', {display: {arrow: false} } ]);
55 | });
56 |
57 | });
58 |
59 |
60 | describe('newArrowState()', ()=>{
61 |
62 | it('when current state is "1->2"', () =>{
63 | expect(newArrowState('1->2', 'left', false)).toEqual('1->2');
64 | expect(newArrowState('1->2', 'left', true)).toEqual('both');
65 | expect(newArrowState('1->2', 'right', false)).toEqual(false);
66 | expect(newArrowState('1->2', 'right', true)).toEqual('1->2');
67 | });
68 |
69 | it('when current state is "2->1"', ()=> {
70 | expect(newArrowState('2->1', 'left', false)).toEqual(false);
71 | expect(newArrowState('2->1', 'left', true)).toEqual('2->1');
72 | expect(newArrowState('2->1', 'right', false)).toEqual('2->1');
73 | expect(newArrowState('2->1', 'right', true)).toEqual('both');
74 | });
75 |
76 | it('when current state is both', ()=> {
77 | expect(newArrowState('both', 'left', false)).toEqual('1->2');
78 | expect(newArrowState('both', 'left', true)).toEqual('both');
79 | expect(newArrowState('both', 'right', false)).toEqual('2->1');
80 | expect(newArrowState('both', 'right', true)).toEqual('both');
81 | });
82 |
83 | });
84 |
85 |
--------------------------------------------------------------------------------
/app/components/__tests__/EdgeDashSelector-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from "enzyme";
3 | import EdgeDashSelector from '../EdgeDashSelector';
4 |
5 | describe('EdgeDashSelector', () => {
6 |
7 | it('sets svgDropdownDashed if dashed', () => {
8 | let wrapper = shallow( );
9 | expect(wrapper.find('.svgDropdownDashed').length).toEqual(1);
10 | });
11 |
12 | it('does not set svgDropdownDashed if not dashed', () => {
13 | let wrapper = shallow( );
14 | expect(wrapper.find('.svgDropdownDashed').length).toEqual(0);
15 | });
16 |
17 | describe('click on menu', () => {
18 | let wrapper = shallow( );
19 | it('updates state and re-renders', ()=>{
20 | expect(wrapper.state().isOpen).toBe(false);
21 | expect(wrapper.find('.edgeDropdownOptions').length).toEqual(0);
22 | wrapper.find('.selectedEdgeDisplay').childAt(0).simulate('click');
23 | expect(wrapper.state().isOpen).toBe(true);
24 | expect(wrapper.find('.edgeDropdownOptions').length).toEqual(1);
25 | });
26 | });
27 |
28 | describe('select an option', ()=>{
29 | it('calls updateEdge with correct arg - undashed', () =>{
30 | let updateEdgeMock = jest.fn();
31 | let wrapper = shallow( );
32 | wrapper.find('.selectedEdgeDisplay').childAt(0).simulate('click');
33 | wrapper.find('.svgDropdownUndashed').simulate('click');
34 | expect(updateEdgeMock.mock.calls.length).toEqual(1);
35 | expect(updateEdgeMock.mock.calls[0]).toEqual(['x', {display: {dash: false} } ]);
36 | expect(wrapper.state().isOpen).toBe(false);
37 |
38 | });
39 | it('calls updateEdge with correct arg - dashed', () =>{
40 | let updateEdgeMock = jest.fn();
41 | let wrapper = shallow( );
42 | wrapper.find('.selectedEdgeDisplay').childAt(0).simulate('click');
43 | wrapper.find('.svgDropdownDashed').simulate('click');
44 | expect(updateEdgeMock.mock.calls.length).toEqual(1);
45 | expect(updateEdgeMock.mock.calls[0]).toEqual(['x', {display: {dash: true}} ]);
46 | expect(wrapper.state().isOpen).toBe(false);
47 | });
48 | });
49 |
50 | });
51 |
--------------------------------------------------------------------------------
/app/components/__tests__/EdgeDropdown-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from 'react-dom';
3 | import { shallow } from "enzyme";
4 | import EdgeDropdown from "../EdgeDropdown";
5 | import EdgeArrowSelector from '../EdgeArrowSelector';
6 | import EdgeDashSelector from '../EdgeDashSelector';
7 |
8 | describe("EdgeDropdown", () => {
9 |
10 | const graph = {
11 | "nodes": {
12 | "1": {
13 | "id": "1",
14 | "display": {
15 | "x": 0,
16 | "y": 0,
17 | "scale": 1,
18 | "name": "n1"
19 | }
20 | },
21 | "2": {
22 | "id": "2",
23 | "display": {
24 | "x": 0,
25 | "y": 0,
26 | "scale": 1,
27 | "name": "n2"
28 | }
29 | }
30 | },
31 | "edges": {
32 | "123": {
33 | "id": "123",
34 | "display": {
35 | "scale": 1,
36 | "arrow": true,
37 | "label": "edge!",
38 | "x1": 0,
39 | "y1": 0,
40 | "x2": 122.87581699346406,
41 | "y2": -5.228758169934641,
42 | "s1": 1,
43 | "s2": 1
44 | },
45 | "node1_id": "1",
46 | "node2_id": "2"
47 | }
48 | }
49 | };
50 |
51 | const getGraph = () => graph;
52 |
53 | describe('layout', () => {
54 | let wrapper = shallow( );
60 |
61 | it('has EdgeDashSelector', () => expect(wrapper.find(EdgeDashSelector).length).toEqual(1));
62 | it('has two EdgeArrowSelector', () => expect(wrapper.find(EdgeArrowSelector).length).toEqual(2));
63 | it('has 1 strokeDropdown div', () => expect(wrapper.find('.strokeDropdowns').length).toEqual(1));
64 | it('has 2 arrow-node-name divs', () => expect(wrapper.find('.arrow-node-name').length).toEqual(2));
65 | it('contains two svgs', ()=> expect(wrapper.find('svg').length).toEqual(2));
66 | it('contains with node names', ()=>{
67 | expect(wrapper.find('text').map( node => node.text())).toEqual(['n1', 'n2']);
68 | });
69 | });
70 |
71 | });
72 |
--------------------------------------------------------------------------------
/app/components/__tests__/EmbeddedGraphAnnotation-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 |
4 | import EmbeddedGraphAnnotation from "../EmbeddedGraphAnnotation";
5 | import { Scrollbars } from 'react-custom-scrollbars';
6 |
7 |
8 | describe('EmbeddedGraphAnnotation', () => {
9 | let wrapper;
10 | let annotation = {
11 | header: "Header",
12 | text: "A modest amount of annotation text ."
13 | };
14 | let embedded = {annotationHeight: 100, linkUrl: null, linkText: null};
15 |
16 | describe('layout', () => {
17 | beforeEach( () => wrapper = shallow( ) )
18 | it('shows title', () => expect(wrapper.find('strong').text()).toEqual('Header') );
19 | it('shows text', () => {
20 | expect(wrapper.find('#oligrapherEmbeddedGraphAnnotationText').render().text()).toEqual("A modest amount of annotation text.");
21 | })
22 | it('has Scrollbars', () => expect(wrapper.find(Scrollbars).length).toEqual(1));
23 | })
24 |
25 | describe('tracker option', () => {
26 | it('sets height correctly if there is a tracker', () => {
27 | let element = shallow( );
28 | expect(element.find(Scrollbars).prop('style').height).toEqual(47);
29 | });
30 | it('sets height correctly if there is not a tracker', () => {
31 | let element = shallow( );
32 | expect(element.find(Scrollbars).prop('style').height).toEqual(85);
33 | });
34 | });
35 |
36 | });
37 |
--------------------------------------------------------------------------------
/app/components/__tests__/EmbeddedGraphAnnotations-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import noop from 'lodash/noop';
4 | import merge from 'lodash/merge';
5 | import EmbeddedGraphAnnotations from '../EmbeddedGraphAnnotations';
6 | import EmbeddedNavBar from '../EmbeddedNavBar';
7 | import EmbeddedGraphAnnotation from '../EmbeddedGraphAnnotation';
8 |
9 |
10 | describe('EmbeddedGraphAnnotations', () => {
11 | const embedded = {annotationSize: '100px'};
12 |
13 | it('should have EmbeddedNavbar', () => {
14 | let wrapper = shallow( );
15 | expect(wrapper.find(EmbeddedNavBar).length).toEqual(1);
16 | });
17 |
18 | it('should have two columns if there is a tracker', () => {
19 | let wrapper = shallow( );
20 | expect(wrapper.find('div.col-sm-12').length).toEqual(2);
21 | });
22 |
23 | it('should not have EmbeddedNavbar if annotation count is 1', () => {
24 | let wrapper = shallow( );
25 | expect(wrapper.find(EmbeddedNavBar).length).toEqual(0);
26 | });
27 |
28 | it('should have EmbeddedGraphAnnotation', () => {
29 | let wrapper = shallow( );
30 | expect(wrapper.find(EmbeddedGraphAnnotation).length).toEqual(1);
31 | });
32 |
33 | it('should have img if provided logoUrl', () => {
34 | let e = merge(embedded, {logoUrl: '/logo.png'})
35 | let wrapper = shallow( );
36 | expect(wrapper.find('img').length).toEqual(1);
37 | });
38 |
39 | it('should not have img if there is no provided logoUrl', () => {
40 | let wrapper = shallow( );
41 | expect(wrapper.find('img').length).toEqual(1);
42 | });
43 |
44 | });
45 |
--------------------------------------------------------------------------------
/app/components/__tests__/EmbeddedNavBar-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import noop from 'lodash/noop';
4 | import EmbeddedNavBar from '../EmbeddedNavBar';
5 | import EmbeddedNavButtons from '../EmbeddedNavButtons';
6 | import AnnotationsTracker from '../AnnotationsTracker';
7 |
8 |
9 | describe('EmbeddedNavBar', () => {
10 | it('should have EmbeddedNavButtons', () => {
11 | let wrapper = shallow( )
12 | expect(wrapper.find(EmbeddedNavButtons).length).toEqual(1);
13 | });
14 |
15 | it('should have AnnotationsTracker', () => {
16 | let wrapper = shallow( )
17 | expect(wrapper.find(EmbeddedNavButtons).length).toEqual(1);
18 | });
19 | });
20 |
--------------------------------------------------------------------------------
/app/components/__tests__/EmbeddedNavButtons-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import sinon from 'sinon';
4 | import noop from 'lodash/noop';
5 | import EmbeddedNavButtons from '../EmbeddedNavButtons';
6 |
7 |
8 | describe('EmbeddedNavButtons', () => {
9 | it('should have 2 buttons', () => {
10 | let wrapper = shallow( );
11 | expect(wrapper.find('button').length).toEqual(2);
12 | expect(wrapper.find('button.btn-annotation-next').prop('disabled')).toEqual(false);
13 | expect(wrapper.find('button.btn-annotation-back').prop('disabled')).toEqual(false);
14 | });
15 |
16 | it('has next button and back button is disabled when current index is 0', () => {
17 | let wrapper = shallow( );
18 | expect(wrapper.find('button.btn-annotation-back').prop('disabled')).toEqual(true);
19 | expect(wrapper.find('button.btn-annotation-next').prop('disabled')).toEqual(false);
20 | expect(wrapper.find('button.btn-annotation-next').length).toEqual(1);
21 | });
22 |
23 | it('has back button and next button is disabled when current index is the last', () => {
24 | let wrapper = shallow( );
25 | expect(wrapper.find('button').length).toEqual(2);
26 | expect(wrapper.find('button.btn-annotation-back').prop('disabled')).toEqual(false);
27 | expect(wrapper.find('button.btn-annotation-next').prop('disabled')).toEqual(true);
28 | });
29 |
30 | describe('clicking', () => {
31 | it('triggers prevClick', () => {
32 | let spy = sinon.spy();
33 | let wrapper = shallow( );
34 | wrapper.find('button.btn-annotation-back').simulate('click');
35 | expect(spy.calledOnce).toEqual(true);
36 | });
37 |
38 | it('triggers nextClick', () => {
39 | let spy = sinon.spy();
40 | let wrapper = shallow( );
41 | wrapper.find('button.btn-annotation-next').simulate('click');
42 | expect(spy.calledOnce).toEqual(true);
43 | });
44 | });
45 | });
46 |
--------------------------------------------------------------------------------
/app/components/__tests__/Graph-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from "enzyme";
3 | import Graph from '../Graph';
4 | import Node from "../Node";
5 | import Edge from "../Edge";
6 | import Caption from "../Caption";
7 |
8 | describe("Graph Component", () => {
9 |
10 | const data = { id: "someid", nodes: { 1: { id: 1, display: { name: "Node 1", scale: 1, status: "normal", x: 260.81190983749696, y: 78.09522392060452 } }, 2: { id: 2, display: { name: "Node 2", scale: 1, status: "normal", x: 1.3981085859366804, y: -5.974907363126558 } }, 3: { id: 3, display: { name: "Node 3", scale: 1, status: "normal", x: -258.01571204120563, y: -90.04497885992393 } } }, edges: { 1: { id: 1, node1_id: 1, node2_id: 2, display: { label: "Edge 1", cx: null, cy: null, scale: 1, arrow: false, status: "normal", x1: 260.81190983749696, y1: 78.09522392060452, x2: 1.3981085859366804, y2: -5.974907363126558, s1: 1, s2: 1 } }, 2: { id: 2, node1_id: 2, node2_id: 3, display: { label: "Edge 1", cx: null, cy: null, scale: 1, arrow: false, status: "normal", x1: 1.3981085859366804, y1: -5.974907363126558, x2: -258.01571204120563, y2: -90.04497885992393, s1: 1, s2: 1 } } }, captions: { 1: { id: 1, display: { text: "Caption 1", x: -458.01571204120563, y: -90.04497885992393 } } } };
11 |
12 | it("sould render nodes", () => {
13 | let wrapper = shallow(
14 |
15 | );
16 | let nodes = wrapper.find(Node);
17 |
18 | expect(nodes.length).toBe(Object.keys(data.nodes).length);
19 | });
20 |
21 | it("sould render edges", () => {
22 | let wrapper = shallow(
23 |
24 | );
25 | let edges = wrapper.find(Edge);
26 |
27 | expect(edges.length).toBe(Object.keys(data.edges).length);
28 | });
29 |
30 | it("sould render captions", () => {
31 | let wrapper = shallow(
32 |
33 | );
34 | let captions = wrapper.find(Caption);
35 |
36 | expect(captions.length).toBe(Object.keys(data.captions).length);
37 | });
38 | });
39 |
--------------------------------------------------------------------------------
/app/components/__tests__/GraphAnnotation-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import GraphAnnotation from "../GraphAnnotation";
4 |
5 | describe("GraphAnnotation", () => {
6 | let wrapper;
7 | let annotation = {
8 | header: "Header",
9 | text: "
A modest amount of annotation text ."
10 | };
11 |
12 | beforeEach(() => {
13 | wrapper = shallow(
14 |
15 | );
16 | });
17 |
18 | it("shows header", () => {
19 | let header = wrapper.find("h2");
20 | expect(header.text()).toBe(annotation.header);
21 | });
22 |
23 | it("shows text", () => {
24 | let text = wrapper.find("#oligrapherGraphAnnotationText");
25 | expect(text.props().dangerouslySetInnerHTML.__html).toBe(annotation.text);
26 | });
27 | });
28 |
--------------------------------------------------------------------------------
/app/components/__tests__/GraphAnnotationForm-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow, mount } from "enzyme";
3 | import GraphAnnotationForm from "../GraphAnnotationForm";
4 | import Editor from "react-medium-editor";
5 |
6 | describe("GraphAnnotationForm", () => {
7 | let wrapper;
8 | let annotation = {
9 | header: "Header",
10 | text: "This is some text to test."
11 | };
12 | let remove;
13 | let update;
14 |
15 | beforeEach(() => {
16 | remove = jest.genMockFunction();
17 | update = jest.genMockFunction();
18 | wrapper = mount(
19 |
23 | );
24 | });
25 |
26 | describe("rendering", () => {
27 | it("shows header input", () => {
28 | let header = wrapper.ref("header");
29 | expect(header.props().value).toBe(annotation.header);
30 | });
31 |
32 | it("shows text editor", () => {
33 | let text = wrapper.find(Editor);
34 | expect(text.props().text).toBe(annotation.text);
35 | });
36 |
37 | it("shows delete button", () => {
38 | let button = wrapper.find("button");
39 | expect(button.text()).toBe("Remove");
40 | });
41 | });
42 |
43 | describe("behavior", () => {
44 | it("updates annotation with new header", () => {
45 | let header = wrapper.ref("header");
46 | header.get(0).value = "New Header";
47 | header.simulate("change");
48 |
49 | expect(update.mock.calls.length).toBe(1);
50 | expect(update.mock.calls[0][0].header).toBe("New Header");
51 | });
52 |
53 | it("updates annotation with new text", () => {
54 | let editor = wrapper.ref("text");
55 | let newText = "This is some EDITED text to test.";
56 | editor.get(0).props.onChange(newText);
57 |
58 | expect(update.mock.calls.length).toBe(1);
59 | expect(update.mock.calls[0][0].text).toBe(newText);
60 | });
61 |
62 | it("removes annotation when delete button is clicked", () => {
63 | let button = wrapper.find("button");
64 | spyOn(window, 'confirm').and.returnValue(true);
65 | button.simulate("click");
66 |
67 | expect(remove.mock.calls.length).toBe(1);
68 | });
69 | });
70 | });
71 |
--------------------------------------------------------------------------------
/app/components/__tests__/GraphAnnotationList-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow, mount } from "enzyme";
3 |
4 | import GraphAnnotationList from "../GraphAnnotationList";
5 |
6 | describe("GraphAnnotationList", () => {
7 | let wrapper;
8 | let annotations = [
9 | { id: 1, header: "Annotation 1", text: "
annotation text 1
" },
10 | { id: 2, header: "Annotation 2", text: "annotation text 2
" },
11 | { id: 3, header: "", text: "annotation text 3
" },
12 | { id: 4, header: "Annotation 4", text: "annotation text 4
" }
13 | ];
14 |
15 | describe("rendering", () => {
16 | let currentIndex;
17 |
18 | beforeEach(() => {
19 | currentIndex = 3;
20 | wrapper = shallow(
21 |
29 | );
30 | });
31 |
32 | it("shows annotations", () => {
33 | let items = wrapper.find("li");
34 | items.forEach((item, i) => {
35 | expect(item.props()["data-id"]).toBe(i);
36 | expect(item.props().draggable).toBe(true);
37 | expect(item.text()).toBe(annotations[i].header || "Untitled Annotation");
38 |
39 | if (i == currentIndex) {
40 | expect(item.props().className).toBe("active");
41 | }
42 | });
43 | });
44 |
45 | it("shows add button", () => {
46 | let button = wrapper.find("button");
47 | expect(button.text()).toBe("New Annotation");
48 | });
49 | });
50 |
51 | describe("behavior", () => {
52 | let currentIndex = 3;
53 | let create, show, move, hideEditTools;
54 |
55 | beforeEach(() => {
56 | create = jest.genMockFunction();
57 | show = jest.genMockFunction();
58 | move = jest.genMockFunction();
59 | hideEditTools = jest.genMockFunction();
60 | wrapper = mount(
61 |
69 | );
70 | });
71 |
72 | it("shows annotation when clicked", () => {
73 | let items = wrapper.find("li");
74 | items.forEach(item => {
75 | item.simulate("click", { target: { dataset: { id: item.props()["data-id"] } } });
76 | });
77 |
78 | expect(show.mock.calls.length).toBe(items.length);
79 | expect(show.mock.calls.map(call => call[0])).toEqual(annotations.map((a, i) => i));
80 | expect(hideEditTools.mock.calls.length).toBe(items.length);
81 | });
82 |
83 | it("creates annotation", () => {
84 | let button = wrapper.find("button");
85 | button.simulate("click");
86 | expect(create.mock.calls.length).toBe(1);
87 | });
88 |
89 | // TODO: test dragging
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/app/components/__tests__/GraphHeader-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow, mount } from "enzyme";
3 | import merge from 'lodash/merge';
4 |
5 | import GraphHeader from '../GraphHeader';
6 | import GraphTitle from '../GraphTitle';
7 | import GraphTitleForm from '../GraphTitleForm';
8 | import GraphByLine from '../GraphByLine';
9 | import GraphLinks from '../GraphLinks';
10 |
11 | describe('GraphHeader', () => {
12 |
13 | const defaultProps = {
14 | user: { name: 'name', url: 'http://url.com' },
15 | date: 'right now',
16 | title: 'eyes on the ties',
17 | updateTitle: function(){},
18 | url: 'oligrapher.info',
19 | isEmbedded: false
20 | }
21 |
22 | let embedded = {headerFontSize: '20px'};
23 |
24 | const graphHeader = props => shallow( );
25 | const hasComponent = (root, c) => expect(root.find(c).length).toEqual(1);
26 | const hasNoComponent = (root, c) => expect(root.find(c).length).toEqual(0);
27 |
28 | describe('When editor', () => {
29 | it('has GraphTitleForm', () => hasComponent(graphHeader({isEditor: true}), GraphTitleForm));
30 | it('does not have GraphTitle', () => hasNoComponent(graphHeader({isEditor: true}), GraphTitle));
31 | });
32 |
33 | describe('When not editor', () => {
34 | it('does not GraphTitleForm', () => hasNoComponent(graphHeader({isEditor: false}), GraphTitleForm));
35 | it('has GraphTitle', () => hasComponent(graphHeader({isEditor: false}), GraphTitle));
36 | });
37 |
38 | describe('when user and date is provided', () => {
39 | it('has GraphByLine', () => hasComponent(graphHeader({}), GraphByLine));
40 | });
41 |
42 | describe('when user and date is provided and isEmbedded is true', () => {
43 | it('does not have GraphByLine', () => {
44 | hasNoComponent(graphHeader({isEmbedded: true, embedded: embedded}), GraphByLine);
45 | });
46 | });
47 |
48 | describe('when user or date is not provided', () => {
49 | it('does not have graphByLine', () => {
50 | hasNoComponent(graphHeader({user: null, date: null}), GraphByLine)
51 | })
52 | });
53 |
54 | describe('graph links', () => {
55 | it('has Graphlinks', () => {
56 | let gh = shallow( );
57 | hasComponent(gh, GraphLinks);
58 | });
59 |
60 | it('has no Graph links', () => hasNoComponent(graphHeader({links: null}), GraphLinks));
61 |
62 | it('does not have graphLinks when embedded is true', () => {
63 | hasNoComponent(graphHeader({links: [], isEmbedded: true}), GraphLinks);
64 | });
65 | });
66 |
67 | });
68 |
--------------------------------------------------------------------------------
/app/components/__tests__/GraphNavButtons-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow, mount } from "enzyme";
3 |
4 | import GraphNavButtons from "../GraphNavButtons";
5 |
6 | describe("GraphNavButtons", () => {
7 |
8 | describe("rendering", () => {
9 | let wrapper;
10 |
11 | beforeEach(() => {
12 | wrapper = mount(
13 |
20 | );
21 | });
22 |
23 | it("shows prev button", () => {
24 | let prev = wrapper.find("button").filterWhere(button => button.text() == "Prev");
25 | expect(prev.length).toBe(1);
26 | expect(prev.get(0).disabled).toBe(false);
27 |
28 | wrapper.setProps({ canClickPrev: false });
29 | expect(prev.get(0).disabled).toBe(true);
30 | });
31 |
32 | it("shows next button", () => {
33 | let next = wrapper.find("button").filterWhere(button => button.text() == "Next");
34 | expect(next.length).toBe(1);
35 | expect(next.get(0).disabled).toBe(false);
36 |
37 | wrapper.setProps({ canClickNext: false });
38 | expect(next.get(0).disabled).toBe(true);
39 | });
40 |
41 | it("shows hide button", () => {
42 | let hide = wrapper.find("#oligrapherHideAnnotationsButton");
43 | expect(hide.length).toBe(1);
44 | });
45 |
46 |
47 | });
48 |
49 | describe("alternateRendering", () => {
50 | let wrapper;
51 | let annotations = [
52 | { id: 1, header: "Annotation 1", text: "annotation text 1
" }
53 | ];
54 |
55 | beforeEach(() => {
56 | wrapper = mount(
57 |
65 | );
66 | });
67 |
68 | it("doesn't show prev button", () => {
69 | let prev = wrapper.find("button").filterWhere(button => button.text() == "Prev");
70 | expect(prev.length).toBe(0);
71 | });
72 |
73 | it("shows next button", () => {
74 | let next = wrapper.find("button").filterWhere(button => button.text() == "Next");
75 | expect(next.length).toBe(0);
76 | });
77 |
78 | it("shows hide button", () => {
79 | let hide = wrapper.find("#oligrapherHideAnnotationsButton");
80 | expect(hide.length).toBe(1);
81 | });
82 |
83 |
84 | });
85 |
86 | describe("behavior", () => {
87 | let prevClick = jest.genMockFunction();
88 | let nextClick = jest.genMockFunction();
89 | let swapAnnotations = jest.genMockFunction();
90 | let wrapper;
91 |
92 | beforeEach(() => {
93 | wrapper = shallow(
94 |
101 | );
102 | });
103 |
104 | it("uses prevClick", () => {
105 | let prev = wrapper.find("button").filterWhere(button => button.text() == "Prev");
106 | prev.simulate("click");
107 | expect(prevClick.mock.calls.length).toBe(1);
108 | });
109 |
110 | it("uses nextClick", () => {
111 | let next = wrapper.find("button").filterWhere(button => button.text() == "Next");
112 | next.simulate("click");
113 | expect(nextClick.mock.calls.length).toBe(1);
114 | });
115 |
116 | it("uses swapAnnotations", () => {
117 | let hide = wrapper.find("#oligrapherHideAnnotationsButton");
118 | hide.simulate("click");
119 | expect(swapAnnotations.mock.calls.length).toBe(1);
120 | });
121 | });
122 | });
123 |
--------------------------------------------------------------------------------
/app/components/__tests__/GraphTitle-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import GraphTitle from '../GraphTitle';
4 |
5 | describe('GraphTitle', () => {
6 |
7 | it('has h1', () => {
8 | expect(shallow( ).find('h1').length).toEqual(1);
9 | });
10 |
11 | it('does not have fontSize set when isEmedded is false', () => {
12 | let graphTitle = shallow( )
13 | expect(graphTitle.find('h1').prop('style').fontSize).toBeUndefined();
14 | });
15 |
16 | it('does not have fontSize set when isEmedded is undefined', () => {
17 | let graphTitle = shallow( )
18 | expect(graphTitle.find('h1').prop('style').fontSize).toBeUndefined();
19 | });
20 |
21 | it('has fontSize set when isEmedded', () => {
22 | let graphTitle = shallow( )
23 | expect(graphTitle.find('h1').prop('style').fontSize).toEqual('20px');
24 | })
25 |
26 | it('does not have a link by default', () => {
27 | let graphTitle = shallow( )
28 | expect(graphTitle.find('a').exists()).toEqual(false);
29 | })
30 |
31 | it('create link when provided url as prop', () => {
32 | let graphTitle = shallow( )
33 | expect(graphTitle.find('a').exists()).toEqual(true);
34 | })
35 |
36 |
37 | });
38 |
--------------------------------------------------------------------------------
/app/components/__tests__/Node-test.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { shallow } from "enzyme";
3 | import Node from '../Node';
4 | import NodeCircle from "../NodeCircle";
5 | import NodeLabel from "../NodeLabel";
6 | import TestUtils from 'react-addons-test-utils';
7 |
8 |
9 | describe("Node Component", () => {
10 |
11 | const data = {
12 | id: 34627,
13 | display: {
14 | name: "Ben Bernanke",
15 | x: 332.1385584381518,
16 | y: -305.26797977275623,
17 | scale: 1,
18 | status: "normal",
19 | image: "//s3.amazonaws.com/pai-littlesis/images/profile/6a7809b213a39c95b7d15334a33fe1f41e417a7b_1232040065.png",
20 | url: "//littlesis.org/person/34627/Ben_Bernanke"
21 | }
22 | };
23 |
24 | it("should have an svg transform", () => {
25 | let wrapper = shallow(
26 |
27 | );
28 | let element = wrapper.find("g.node");
29 | let { x, y } = data.display;
30 |
31 | expect(element.props().transform).toBe(`translate(${x}, ${y})`);
32 | });
33 |
34 | it("should display a NodeCircle", () => {
35 | let wrapper = shallow(
36 |
37 | );
38 | let circle = wrapper.find(NodeCircle);
39 |
40 | expect(circle.props().node).toBe(data);
41 | expect(circle.props().selected).toBe(true);
42 | });
43 |
44 | it("should display a NodeLabel", () => {
45 | let wrapper = shallow(
46 |
47 | );
48 | let label = wrapper.find(NodeLabel);
49 |
50 | expect(label.props().node).toBe(data);
51 | });
52 |
53 | it("should call click callback if clicked", () => {
54 | let clickNode = jest.genMockFunction();
55 | let wrapper = shallow(
56 |
57 | );
58 | let element = wrapper.find("g.node");
59 | element.simulate("click");
60 |
61 | expect(clickNode.mock.calls[0][0]).toBe(data.id);
62 | });
63 |
64 | // NOT WORKING: TO FIND .handle WE NEED FULL RENDER, WHICH ISN'T WORKING FOR SVG
65 | xit("can be dragged to a new position", () => {
66 | let moveNode = jest.genMockFunction();
67 | let wrapper = shallow(
68 |
69 | );
70 | let handle = wrapper.find(".handle");
71 |
72 | handle.simulate("dragStart");
73 | handle.simulate("drag");
74 | handle.simulate("dragEnd");
75 |
76 | expect(moveNode.mock.calls.length).toBe(1);
77 | });
78 | });
79 |
--------------------------------------------------------------------------------
/app/components/__tests__/NodeLabel-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import NodeLabel from "../NodeLabel";
4 | import ds from "../../NodeDisplaySettings";
5 | import merge from "lodash/merge";
6 |
7 | describe("NodeLabel", () => {
8 | let wrapper;
9 | const data = {
10 | id: 34627,
11 | display: {
12 | name: "Benjamin Shalom Bernanke",
13 | x: 332.1385584381518,
14 | y: -305.26797977275623,
15 | scale: 1,
16 | status: "highlighted",
17 | image: "//s3.amazonaws.com/pai-littlesis/images/profile/6a7809b213a39c95b7d15334a33fe1f41e417a7b_1232040065.png",
18 | url: "//littlesis.org/person/34627/Ben_Bernanke"
19 | }
20 | };
21 |
22 | beforeEach(() => {
23 | wrapper = shallow(
24 |
25 | );
26 | });
27 |
28 | it("should show text with tspans", () => {
29 | let text = wrapper.find("text");
30 | expect(text.length).toBe(1);
31 | expect(text.props().textAnchor).toBe("middle");
32 |
33 | let tspans = wrapper.find("tspan");
34 | expect(tspans.length).toBe(2);
35 | expect(tspans.map(tspan => tspan.text()).join(" ")).toBe(data.display.name.replace(/\s+/g, " "));
36 |
37 | tspans.forEach(tspan => {
38 | expect(tspan.props().fill).toBe(ds.textColor[data.display.status]);
39 | expect(tspan.props().opacity).toBe(ds.textOpacity[data.display.status]);
40 | });
41 | });
42 |
43 | it("should show link", () => {
44 | let link = wrapper.find("a");
45 | expect(link.length).toBe(1);
46 | expect(link.props().xlinkHref).toBe(data.display.url);
47 | expect(link.props().target).toBe("_blank");
48 | });
49 |
50 | it("should show rects", () => {
51 | let rects = wrapper.find("rect");
52 | expect(rects.length).toBe(2);
53 | });
54 | });
55 |
--------------------------------------------------------------------------------
/app/components/__tests__/UpdateCaptionForm-test.jsx:
--------------------------------------------------------------------------------
1 | jest.disableAutomock();
2 |
3 | import React from "react";
4 | import { mount } from "enzyme";
5 |
6 | import UpdateCaptionForm from "../UpdateCaptionForm";
7 |
8 | describe("UpdateCaptionForm", () => {
9 | let wrapper;
10 | let updateCaption;
11 | let data = {
12 | id: 1,
13 | node1_id: 1,
14 | node2_id: 2,
15 | display: {
16 | text: "Caption",
17 | scale: 3,
18 | status: "highlighted"
19 | }
20 | };
21 |
22 | beforeEach(() => {
23 | updateCaption = jest.genMockFunction();
24 | wrapper = mount(
25 |
29 | );
30 | });
31 |
32 | describe("rendering", () => {
33 | it("shows text input", () => {
34 | let input = wrapper.ref("text");
35 | expect(input.length).toBe(1);
36 | expect(input.props().value).toBe(data.display.text);
37 | });
38 |
39 | it("shows scale dropdown with selected option", () => {
40 | let select = wrapper.ref("scale");
41 | expect(select.length).toBe(1);
42 | expect(select.props().value).toBe(data.display.scale);
43 |
44 | let option = select.children().findWhere(child => child.props().value === data.display.scale);
45 | expect(option.length).toBe(1);
46 | expect(option.text()).toBe(data.display.scale + "x");
47 | });
48 | });
49 |
50 | describe("behavior", () => {
51 | it("passes updated text to updateCaption", () => {
52 | let input = wrapper.ref("text");
53 | input.get(0).value = "Caption 2 ";
54 | input.props().onChange();
55 |
56 | expect(updateCaption.mock.calls.length).toBe(1);
57 | expect(updateCaption.mock.calls[0][1].display.text).toBe("Caption 2");
58 | });
59 |
60 | it("passes updated scale to updateCaption", () => {
61 | let select = wrapper.ref("scale");
62 | select.get(0).value = 1.5;
63 | select.props().onChange();
64 |
65 | expect(updateCaption.mock.calls.length).toBe(1);
66 | expect(updateCaption.mock.calls[0][1].display.scale).toBe(1.5);
67 | });
68 | });
69 | });
--------------------------------------------------------------------------------
/app/components/__tests__/UpdateEdgeForm-test.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { shallow } from "enzyme";
3 | import UpdateEdgeForm from "../UpdateEdgeForm";
4 | import EdgeDropdown from "../EdgeDropdown";
5 |
6 | describe("UpdateEdgeForm", () => {
7 | let wrapper;
8 | let updateEdge;
9 | let data = {
10 | id: 1,
11 | node1_id: 1,
12 | node2_id: 2,
13 | display: {
14 | label: "Edge",
15 | url: "url",
16 | scale: 3,
17 | arrow: "right",
18 | dash: false,
19 | status: "highlighted"
20 | }
21 | };
22 |
23 | beforeEach(() => {
24 | updateEdge = jest.genMockFunction();
25 | wrapper = shallow(
26 |
31 | );
32 | });
33 |
34 | describe("rendering", () => {
35 |
36 | it("shows 2 inputs", () => {
37 | expect(wrapper.find('input').length).toBe(2);
38 | });
39 |
40 | it('shows input with value label', () => {
41 | expect(wrapper.find('input[value="Edge"]').length).toBe(1);
42 |
43 | });
44 |
45 | it('shows input with value url', () => {
46 | expect(wrapper.find('input[value="url"]').length).toBe(1);
47 | });
48 |
49 | it('renders EdgeDropDown', () => {
50 | expect(wrapper.find(EdgeDropdown).length).toBe(1);
51 | });
52 |
53 | it('passes correct props to EdgeDrop down', () => {
54 | let edgeProps = wrapper.find(EdgeDropdown).props();
55 | expect(edgeProps.arrow).toBe(data.display.arrow);
56 | expect(edgeProps.dash).toBe(data.display.dash);
57 | });
58 |
59 | it('shows ', ()=>{
60 | expect(wrapper.find('select').length).toBe(1);
61 | });
62 |
63 | it("shows scale dropdown with selected option", () => {
64 | let select = wrapper.find("select");
65 | expect(select.props().value).toBe(data.display.scale);
66 | let option = select.children().findWhere(child => child.props().value === data.display.scale);
67 | expect(option.length).toBe(1);
68 | expect(option.text()).toBe(data.display.scale + "x");
69 | });
70 | });
71 |
72 | });
73 |
--------------------------------------------------------------------------------
/app/components/__tests__/UpdateNodeForm-test.jsx:
--------------------------------------------------------------------------------
1 | jest.disableAutomock();
2 |
3 | import React from "react";
4 | import { mount } from "enzyme";
5 |
6 | import UpdateNodeForm from "../UpdateNodeForm";
7 | import ChangeColorInput from "../ChangeColorInput";
8 |
9 | describe("UpdateNodeForm", () => {
10 | let wrapper;
11 | let updateNode;
12 | let data = {
13 | id: 1,
14 | display: {
15 | name: "Node",
16 | url: "http://example.com/node",
17 | image: "http://example.com/image.png",
18 | color: "#abc",
19 | scale: 3,
20 | status: "highlighted"
21 | }
22 | };
23 |
24 | beforeEach(() => {
25 | updateNode = jest.genMockFunction();
26 | wrapper = mount(
27 |
31 | );
32 | });
33 |
34 | describe("rendering", () => {
35 | it("shows name input", () => {
36 | let input = wrapper.ref("name");
37 | expect(input.length).toBe(1);
38 | expect(input.props().value).toBe(data.display.name);
39 | });
40 |
41 | it("shows image input", () => {
42 | let input = wrapper.ref("image");
43 | expect(input.length).toBe(1);
44 | expect(input.props().value).toBe(data.display.image);
45 | });
46 |
47 | it("shows color picker with current node color", () => {
48 | let picker = wrapper.find(ChangeColorInput);
49 | expect(picker.length).toBe(1);
50 | expect(picker.props().value).toBe(data.display.color);
51 | expect(picker.props().status).toBe(data.display.status);
52 | });
53 |
54 | it("shows scale dropdown with selected option", () => {
55 | let select = wrapper.ref("scale");
56 | expect(select.length).toBe(1);
57 | expect(select.props().value).toBe(data.display.scale);
58 |
59 | let option = select.children().findWhere(child => child.props().value === data.display.scale);
60 | expect(option.length).toBe(1);
61 | expect(option.text()).toBe(data.display.scale + "x");
62 | });
63 |
64 | it("shows url input", () => {
65 | let input = wrapper.ref("url");
66 | expect(input.length).toBe(1);
67 | expect(input.props().value).toBe(data.display.url);
68 | });
69 | });
70 |
71 | describe("behavior", () => {
72 | it("passes updated name to updateNode", () => {
73 | let input = wrapper.ref("name");
74 | input.get(0).value = "Noder";
75 | input.props().onChange();
76 |
77 | expect(updateNode.mock.calls.length).toBe(1);
78 | expect(updateNode.mock.calls[0][1].display.name).toBe("Noder");
79 | });
80 |
81 | it("passes updated image to updateNode", () => {
82 | let input = wrapper.ref("image");
83 | input.get(0).value = " ";
84 | input.props().onChange();
85 |
86 | expect(updateNode.mock.calls.length).toBe(1);
87 | expect(updateNode.mock.calls[0][1].display.image).toBe("");
88 | });
89 |
90 | it("passes new color to updateNode", () => {
91 | let picker = wrapper.find(ChangeColorInput);
92 | let pickerOnChange = picker.props().onChange;
93 | pickerOnChange("#def");
94 |
95 | expect(updateNode.mock.calls.length).toBe(1);
96 | expect(updateNode.mock.calls[0][1].display.color).toBe("#def");
97 | });
98 |
99 | it("passes updated scale to updateNode", () => {
100 | let select = wrapper.ref("scale");
101 | select.get(0).value = 1.5;
102 | select.props().onChange();
103 |
104 | expect(updateNode.mock.calls.length).toBe(1);
105 | expect(updateNode.mock.calls[0][1].display.scale).toBe(1.5);
106 | });
107 |
108 | it("passes updated url to updateNode", () => {
109 | let input = wrapper.ref("url");
110 | let newUrl = "htt://example.com/noder ";
111 | input.get(0).value = newUrl;
112 | input.props().onChange();
113 |
114 | expect(updateNode.mock.calls.length).toBe(1);
115 | expect(updateNode.mock.calls[0][1].display.url).toBe(newUrl.trim());
116 | });
117 | });
118 | });
--------------------------------------------------------------------------------
/app/components/__tests__/support.js:
--------------------------------------------------------------------------------
1 | /*
2 | This contains the props for the root element of a very simple graph that contains:
3 | 4 nodes, 3 edges, 1 caption, and 1 annotation with nothing selected.
4 | */
5 |
6 | export const props = {};
7 | props.canUndo = false;
8 | props.canRedo = false;
9 | props.selection = {"nodeIds":[],"edgeIds":[],"captionIds":[]};
10 | props.zoom = 1;
11 | props.showEditTools = true;
12 | props.addForm = null;
13 | props.nodeResults = [];
14 | props.title = "just a test map";
15 | props.currentIndex = 0;
16 | props.numAnnotations = 1;
17 | props.annotation = {
18 | "id": "BJQrZSrD",
19 | "header": "Node 4 is up to something.",
20 | "text": "",
21 | "nodeIds": ["rylk-HBv"],
22 | "edgeIds": [],
23 | "captionIds": []
24 | };
25 | props.annotations = [{"id":"BJQrZSrD","header":"Node 4 is up to something.","text":"","nodeIds":["rylk-HBv"],"edgeIds":[],"captionIds":[]}];
26 | props.visibleAnnotations = true;
27 | props.graphSettings = {
28 | is_private:false,
29 | is_featured:false
30 | };
31 | props.hasSettings = true;
32 | props.showHelpScreen = false;
33 | props.showSettings = false;
34 |
35 | props.height = 500;
36 | props.embedded = {
37 | headerSize: '100px',
38 | headerFontSize: '20px',
39 | annotationSize: '100px'
40 | };
41 |
42 | props.graph = {
43 | "nodes": {
44 | "r11ooVSP": {
45 | "id": "r11ooVSP",
46 | "display": {
47 | "x": 94.16432563629611,
48 | "y": 0,
49 | "scale": 1,
50 | "status": "normal",
51 | "name": "node1"
52 | }
53 | },
54 | "Hkfoo4BP": {
55 | "id": "Hkfoo4BP",
56 | "display": {
57 | "x": -183.90870191102516,
58 | "y": 36.69999920637521,
59 | "scale": 1,
60 | "status": "normal",
61 | "name": "node2",
62 | "color": "#ccc"
63 | }
64 | },
65 | "H1SisNBv": {
66 | "id": "H1SisNBv",
67 | "display": {
68 | "x": -47.0821628181481,
69 | "y": -81.54869813126268,
70 | "scale": 1,
71 | "status": "normal",
72 | "name": "node3"
73 | }
74 | },
75 | "Hk2TjVrw": {
76 | "id": "Hk2TjVrw",
77 | "display": {
78 | "x": -110.20110811532244,
79 | "y": -226.64049288336088,
80 | "scale": 1,
81 | "status": "normal",
82 | "name": "node4"
83 | }
84 | }
85 | },
86 | "edges": {
87 | "S1Qhs4rw": {
88 | "id": "S1Qhs4rw",
89 | "display": {
90 | "scale": 1,
91 | "arrow": false,
92 | "status": "normal",
93 | "label": "connection",
94 | "x1": -183.90870191102516,
95 | "y1": 36.69999920637521,
96 | "x2": -47.0821628181481,
97 | "y2": -81.54869813126268,
98 | "s1": 1,
99 | "s2": 1,
100 | "cx": -49.01543706943632,
101 | "cy": -97.94717582738568
102 | },
103 | "node1_id": "Hkfoo4BP",
104 | "node2_id": "H1SisNBv"
105 | },
106 | "SyBajVBv": {
107 | "id": "SyBajVBv",
108 | "display": {
109 | "scale": 1,
110 | "arrow": false,
111 | "status": "normal",
112 | "label": "shhhh",
113 | "x1": 94.16432563629611,
114 | "y1": 0,
115 | "x2": -47.0821628181481,
116 | "y2": -81.54869813126268,
117 | "s1": 1,
118 | "s2": 1
119 | },
120 | "node1_id": "r11ooVSP",
121 | "node2_id": "H1SisNBv"
122 | },
123 | "r1EV24BP": {
124 | "id": "r1EV24BP",
125 | "display": {
126 | "scale": 1,
127 | "arrow": false,
128 | "status": "normal",
129 | "label": "love/hate",
130 | "x1": -110.20110811532244,
131 | "y1": -226.64049288336088,
132 | "x2": -47.0821628181481,
133 | "y2": -81.54869813126268,
134 | "s1": 1,
135 | "s2": 1,
136 | "cx": null,
137 | "cy": null
138 | },
139 | "node1_id": "Hk2TjVrw",
140 | "node2_id": "H1SisNBv"
141 | }
142 | },
143 | "captions": {
144 | "rJkd24rv": {
145 | "id": "rJkd24rv",
146 | "display": {
147 | "text": "corporate lies!",
148 | "x": -302.6755074222323,
149 | "y": -128.26558970864625,
150 | "scale": "2",
151 | "status": "normal"
152 | }
153 | }
154 | }
155 | };
156 |
--------------------------------------------------------------------------------
/app/fonts/glyphicons-halflings-regular.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/skomputer/oligrapher/c465af3ffc9cd5a2b7378cbaeb4f855c60bde386/app/fonts/glyphicons-halflings-regular.woff
--------------------------------------------------------------------------------
/app/helpers.js:
--------------------------------------------------------------------------------
1 | import includes from 'lodash/includes';
2 | import merge from 'lodash/merge';
3 | import mapValues from 'lodash/mapValues';
4 | import round from 'lodash/round';
5 | import isNil from 'lodash/isNil';
6 |
7 | /**
8 | * Calculates New Position for Draggable Components
9 | * @param {object} draggableData - data from react-draggable callback
10 | * @param {object} startPosition - initial x & y position
11 | * @param {object} startDrag - initial draggableData from start of drag
12 | * @param {number} actualZoom - zoom value
13 | * @returns {object} = x, y
14 | */
15 | export const calculateDeltas = (draggableData, startPosition, startDrag, actualZoom) => {
16 | let deltaX = (draggableData.x - startDrag.x) / actualZoom;
17 | let deltaY = (draggableData.y - startDrag.y) / actualZoom;
18 | let x = deltaX + startPosition.x;
19 | let y = deltaY + startPosition.y;
20 | return { x, y };
21 | };
22 |
23 | /**
24 | * Previously arrow could only go in one direction
25 | * @param {string|boolean|null} arrow
26 | */
27 | export const legacyArrowConverter = arrow => {
28 | if (includes(['1->2', '2->1', 'both'], arrow)){
29 | return arrow;
30 | } else if (arrow === true) {
31 | return '1->2';
32 | } else if (arrow === 'left') {
33 | return '2->1';
34 | } else if (arrow === 'right') {
35 | return '1->2';
36 | } else {
37 | return false;
38 | }
39 | };
40 |
41 |
42 | export const legacyEdgesConverter = edges => {
43 | return mapValues(edges, edge => merge(edge, {display: {arrow: legacyArrowConverter(edge.display.arrow)}} ) );
44 | };
45 |
46 | export const newArrowState = (oldArrowState, arrowSide, showArrow) => {
47 |
48 | // By convention, Node 1 is the arrow on the left side
49 | // and Node 2 is the arrow on the right.
50 | const node1 = (arrowSide === 'left');
51 | const node2 = (arrowSide === 'right');
52 |
53 | if(oldArrowState === '1->2') {
54 | if (node1 && showArrow) {
55 | return 'both';
56 | }
57 | if (node2 && !showArrow) {
58 | return false;
59 | }
60 | } else if (oldArrowState === '2->1') {
61 | if (node1 && !showArrow) {
62 | return false;
63 | }
64 | if (node2 && showArrow) {
65 | return 'both';
66 | }
67 | } else if (oldArrowState === 'both') {
68 | if (node1 && !showArrow) {
69 | return '1->2';
70 | }
71 | if (node2 && !showArrow) {
72 | return '2->1';
73 | }
74 | } else if (oldArrowState === false) {
75 | if (node1 && showArrow) {
76 | return '2->1';
77 | }
78 | if (node2 && showArrow) {
79 | return '1->2';
80 | }
81 | }
82 | // default case
83 | return oldArrowState;
84 | };
85 |
86 | export const pxStr = num => num.toString() + 'px';
87 |
88 |
89 | // {options} -> {embedded}
90 | export const configureEmbedded = configOptions => {
91 | const defaults = {
92 | headerPct: 8,
93 | annotationPct: 25,
94 | logoUrl: null,
95 | linkUrl: null
96 | };
97 |
98 | let embedded = isNil(configOptions.embedded) ? {} : configOptions.embedded;
99 | let height = configOptions.height;
100 | embedded = merge(defaults, embedded);
101 |
102 | let headerHeight = height * (embedded.headerPct / 100);
103 | let annotationHeight = height * (embedded.annotationPct / 100);
104 | let graphHeight = height - (headerHeight + annotationHeight);
105 | let graphContainerHeight = graphHeight + headerHeight;
106 |
107 | // size of overall container -- all elements
108 | embedded.containerSize = pxStr(height);
109 | // size of header
110 | embedded.headerSize = pxStr(headerHeight);
111 | embedded.headerFontStyle = { fontSize: pxStr(round(headerHeight * 0.6)), lineHeight: pxStr(headerHeight * 0.9) };
112 | // size of graph column (includes header)
113 | embedded.graphColumnHeight = graphContainerHeight;
114 | embedded.graphColumnSize = pxStr(graphContainerHeight);
115 | // size of graph
116 | embedded.graphHeight = graphHeight;
117 | embedded.graphSize = pxStr(graphHeight);
118 | // size of annotation section, including tracker
119 | embedded.annotationHeight = annotationHeight;
120 | embedded.annotationSize = pxStr(annotationHeight);
121 | // max width of logo
122 | embedded.logoWidth = window.innerWidth / 9;
123 |
124 | return embedded;
125 | };
126 |
127 |
--------------------------------------------------------------------------------
/app/models/Annotation.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 | import shortid from 'shortid';
3 |
4 | export default class Annotation {
5 | static defaults() {
6 | return {
7 | id: shortid.generate(),
8 | header: "Untitled Annotation",
9 | text: "",
10 | nodeIds: [],
11 | edgeIds: [],
12 | captionIds: []
13 | };
14 | }
15 |
16 | static setDefaults(annotation) {
17 | return merge({}, this.defaults(), annotation);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/models/Caption.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 | import Helpers from './Helpers';
3 |
4 | class Caption {
5 | static defaults() {
6 | return {
7 | id: Helpers.generateId(),
8 | display: {
9 | text: "Caption",
10 | x: 0,
11 | y: 0,
12 | scale: 1,
13 | status: "normal"
14 | }
15 | };
16 | }
17 |
18 | static setDefaults(caption) {
19 | return merge({}, this.defaults(), caption);
20 | }
21 | }
22 |
23 | module.exports = Caption;
24 |
--------------------------------------------------------------------------------
/app/models/Edge.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 | import Helpers from './Helpers';
3 |
4 | export default class Edge {
5 | static defaults() {
6 | return {
7 | id: Helpers.generateId(),
8 | display: {
9 | scale: 1,
10 | arrow: false,
11 | status: "normal",
12 | }
13 | };
14 | }
15 |
16 | static setDefaults(edge) {
17 | return merge(this.defaults(), edge);
18 | }
19 |
20 | static combine(edge1, edge2) {
21 | if (!edge2) {
22 | return edge1;
23 | }
24 |
25 | return merge({}, edge1, {
26 | display: {
27 | label: edge1.display.label + ", " + edge2.display.label,
28 | scale: Math.max(edge1.display.scale, edge2.display.scale),
29 | arrow: edge1.display.arrow && edge2.display.arrow && (edge1.node1_id == edge2.node1_id && edge1.node2_id == edge2.node2_id),
30 | status: this.combineStatuses(edge1.display.status, edge2.display.status)
31 | }
32 | });
33 | }
34 |
35 | static combineStatuses(status1, status2) {
36 | if (status1 == status2) {
37 | return status1;
38 | }
39 | else if (status1 == "highlighted" || status2 == "highlighted") {
40 | return "highlighted";
41 | }
42 | else if (status1 == "normal" || status2 == "normal") {
43 | return "normal";
44 | }
45 | else {
46 | return "faded";
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/app/models/Helpers.js:
--------------------------------------------------------------------------------
1 | import shortid from 'shortid';
2 | import clone from 'lodash/clone';
3 | import each from 'lodash/each';
4 | import isEmpty from 'lodash/isEmpty';
5 | import isNumber from 'lodash/isNumber';
6 | import isBoolean from 'lodash/isBoolean';
7 | import isArray from 'lodash/isArray';
8 | import isPlainObject from 'lodash/isPlainObject';
9 |
10 | const compact = (o) => {
11 | let cloned = clone(o);
12 |
13 | if (isArray(cloned) || isPlainObject(cloned)) {
14 | each(cloned, (v, k) => {
15 | let newV = compact(v);
16 |
17 | if (isEmpty(newV) && !isNumber(newV) && !isBoolean(newV)) {
18 | delete cloned[k];
19 | } else {
20 | cloned[k] = newV;
21 | }
22 | });
23 | }
24 |
25 | return cloned;
26 | };
27 |
28 | export default {
29 | generateId: () => shortid.generate(),
30 | compactObject: compact
31 | };
32 |
--------------------------------------------------------------------------------
/app/models/Node.js:
--------------------------------------------------------------------------------
1 | import merge from 'lodash/merge';
2 | import isNumber from 'lodash/isNumber';
3 | import Helpers from './Helpers';
4 |
5 | class Node {
6 | static defaults() {
7 | return {
8 | id: Helpers.generateId(),
9 | display: {
10 | x: 0,
11 | y: 0,
12 | scale: 1,
13 | status: "normal"
14 | }
15 | };
16 | }
17 |
18 | static setDefaults(node) {
19 | return merge({}, this.defaults(), node);
20 | }
21 |
22 | static hasPosition(node) {
23 | return isNumber(node.display.x) && isNumber(node.display.y);
24 | }
25 | }
26 |
27 | module.exports = Node;
28 |
--------------------------------------------------------------------------------
/app/models/__tests__/Annotation-test.js:
--------------------------------------------------------------------------------
1 | import Annotation from '../Annotation';
2 |
3 | describe('Annotation', ()=>{
4 |
5 | describe('defaults', ()=>{
6 | it('returns an object', ()=> expect(typeof Annotation.defaults()).toEqual('object') );
7 | });
8 |
9 | describe('setDefaults', ()=> {
10 |
11 | it('provides defaults when given an empty object', () =>{
12 | const a = Annotation.setDefaults({});
13 | expect(a.header).toEqual("Untitled Annotation");
14 | expect(a.text).toEqual('');
15 | expect(a.nodeIds).toEqual([]);
16 | expect(a.captionIds).toEqual([]);
17 | expect(a.edgeIds).toEqual([]);
18 | expect(a.id).toBeDefined();
19 | });
20 |
21 | it('merges defaults when provided an annotation object', ()=>{
22 | expect(Annotation.setDefaults({header: "new header"}).header).toEqual('new header');
23 | expect(Annotation.setDefaults({header: "new header"}).nodeIds).toEqual([]);
24 | });
25 | });
26 |
27 | });
28 |
--------------------------------------------------------------------------------
/app/models/__tests__/Caption-test.js:
--------------------------------------------------------------------------------
1 | const Caption = require('../Caption');
2 |
3 | describe("Caption", () => {
4 |
5 | describe("setDefaults", () => {
6 |
7 | it("gives a caption a display text, position, scale, status if it doesn't have them", () => {
8 | let basicCaption = { id: 5 };
9 | let captionWithDefaults = Caption.setDefaults(basicCaption);
10 | let defaults = Caption.defaults();
11 |
12 | expect(captionWithDefaults.display.text).toBe(defaults.display.text);
13 | expect(captionWithDefaults.display.x).toBe(defaults.display.x);
14 | expect(captionWithDefaults.display.y).toBe(defaults.display.y);
15 | expect(captionWithDefaults.display.scale).toBe(defaults.display.scale);
16 | expect(captionWithDefaults.display.status).toBe(defaults.display.status);
17 | });
18 |
19 | it("doesn't change a caption if it has all default fields", () => {
20 | let fullCaption= { id: 5, display: { text: "Bob is good", x: 100, y: 200, scale: 2, status: "highlighted" } };
21 | let captionWithDefaults = Caption.setDefaults(fullCaption);
22 |
23 | expect(captionWithDefaults.id).toBe(fullCaption.id);
24 | expect(captionWithDefaults.display.text).toBe(fullCaption.display.text);
25 | expect(captionWithDefaults.display.x).toBe(fullCaption.display.x);
26 | expect(captionWithDefaults.display.y).toBe(fullCaption.display.y);
27 | expect(captionWithDefaults.display.scale).toBe(fullCaption.display.scale);
28 | expect(captionWithDefaults.display.status).toBe(fullCaption.display.status);
29 | });
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/app/models/__tests__/Edge-test.js:
--------------------------------------------------------------------------------
1 | import Edge from '../Edge';
2 |
3 | describe("Edge", () => {
4 |
5 | describe("setDefaults", () => {
6 |
7 | it("gives an edge a display scale, arrow, and status if it doesn't have them", () => {
8 | let basicEdge = { id: 5, node1_id: 32, node2_id: 128, display: { label: "Best Friend" } };
9 | let edgeWithDefaults = Edge.setDefaults(basicEdge);
10 |
11 | expect(edgeWithDefaults.display.scale).toBe(1);
12 | expect(edgeWithDefaults.display.arrow).toBe(false);
13 | expect(edgeWithDefaults.display.status).toBe("normal");
14 | });
15 |
16 | it("doesn't change an edge if it has a display scale, arrow, and status", () => {
17 | let fullEdge = {
18 | id: 5,
19 | node1_id: 32,
20 | node2_id: 128,
21 | display: {
22 | label: "Best Friend",
23 | scale: 2,
24 | arrow: true,
25 | status: "highlighted"
26 | }
27 | };
28 | let edgeWithDefaults = Edge.setDefaults(fullEdge);
29 |
30 | expect(edgeWithDefaults.display.scale).toBe(2);
31 | expect(edgeWithDefaults.display.arrow).toBe(true);
32 | expect(edgeWithDefaults.display.status).toBe("highlighted");
33 | });
34 | });
35 | });
36 |
--------------------------------------------------------------------------------
/app/models/__tests__/Helpers-test.js:
--------------------------------------------------------------------------------
1 | import Helpers from '../Helpers';
2 |
3 | describe("Helpers", () => {
4 |
5 | describe("generateId()", () => {
6 |
7 | it("should generate a short string id", () => {
8 | let id = Helpers.generateId();
9 |
10 | expect(id.length).toBeGreaterThan(5);
11 | expect(id.length).toBeLessThan(10);
12 | });
13 | });
14 |
15 | describe("compactObject()", () => {
16 |
17 | it("should recursviely remove empty values from an object", () => {
18 | let obj = { id: 100, display: { name: "Bob", scale: 1, image: { bar: null } }, foo: [], bar: false };
19 | let newObj = Helpers.compactObject(obj);
20 |
21 | expect(newObj.id).toBe(obj.id);
22 | expect(newObj.display.name).toBe(obj.display.name);
23 | expect(newObj.display.scale).toBe(obj.display.scale);
24 | expect(newObj.display.image).toBeUndefined();
25 | expect(newObj.foo).toBeUndefined();
26 | expect(newObj.bar).toBe(false);
27 | });
28 | });
29 | });
30 |
--------------------------------------------------------------------------------
/app/models/__tests__/Node-test.js:
--------------------------------------------------------------------------------
1 | const Node = require('../Node');
2 |
3 | describe("Node", () => {
4 |
5 | describe("setDefaults", () => {
6 |
7 | it("gives a node a display scale and status if it doesn't have them", () => {
8 | let basicNode = { id: 5, display: { name: "Bob" } };
9 | let nodeWithDefaults = Node.setDefaults(basicNode);
10 |
11 | expect(nodeWithDefaults.display.scale).toBe(1);
12 | expect(nodeWithDefaults.display.status).toBe("normal");
13 | });
14 |
15 | it("doesn't change a node if it has a display scale and status", () => {
16 | let fullNode = { id: 5, display: { name: "Bob", scale: 2, status: "highlighted" } };
17 | let nodeWithDefaults = Node.setDefaults(fullNode);
18 |
19 | expect(nodeWithDefaults.display.scale).toBe(2);
20 | expect(nodeWithDefaults.display.status).toBe("highlighted");
21 | });
22 | });
23 | });
24 |
--------------------------------------------------------------------------------
/app/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import graph from './reducers/undoable-graph';
3 | import selection from './reducers/selection';
4 | import zoom from './reducers/zoom';
5 | import editTools from './reducers/editTools';
6 | import title from './reducers/title';
7 | import annotations from './reducers/annotations';
8 | import settings from './reducers/settings';
9 | import showHelpScreen from './reducers/showHelpScreen';
10 | import showSettings from './reducers/showSettings';
11 |
12 | export default combineReducers({
13 | graph,
14 | selection,
15 | zoom,
16 | editTools,
17 | title,
18 | annotations,
19 | settings,
20 | showHelpScreen,
21 | showSettings
22 | });
--------------------------------------------------------------------------------
/app/reducers/__tests__/annotations-test.js:
--------------------------------------------------------------------------------
1 | import reducer from "../annotations";
2 | import {loadAnnotations, toggleAnnotations} from '../../actions';
3 |
4 | describe("annotations reducer", ()=>{
5 |
6 | it("should return initial state", () => {
7 | expect(reducer(undefined, {})).toEqual({list:[], visible: true, currentIndex: 0});
8 | });
9 |
10 | it('stores the annotations in state.list when LOAD_ANNOTATIONS is triggered', () =>{
11 | let annotations = [{
12 | id: '123',
13 | header: "header",
14 | text: "some text here",
15 | nodeIds: ["x1","33180","15957"],
16 | edgeIds: [],
17 | captionIds: []
18 | }];
19 |
20 |
21 | expect(reducer(undefined, loadAnnotations(annotations))).toEqual({
22 | list: [{
23 | id: '123',
24 | header: "header",
25 | text: "some text here",
26 | nodeIds: ["x1","33180","15957"],
27 | edgeIds: [],
28 | captionIds: []
29 | }],
30 | visible: true,
31 | currentIndex: 0
32 | });
33 |
34 | });
35 |
36 | describe('TOGGLE_ANNOTATIONS', ()=>{
37 |
38 | it('flips the visible state from false to true', ()=>{
39 | expect( reducer({visible: false}, toggleAnnotations({})) ).toEqual({visible: true});
40 | });
41 |
42 | it('flips the visible state from true to false', ()=>{
43 | expect( reducer({visible: true}, toggleAnnotations({})) ).toEqual({visible: false});
44 | });
45 |
46 | });
47 |
48 | });
49 |
--------------------------------------------------------------------------------
/app/reducers/annotations.js:
--------------------------------------------------------------------------------
1 | import Annotation from '../models/Annotation';
2 | import { LOAD_ANNOTATIONS, UPDATE_ANNOTATION, DELETE_ANNOTATION,
3 | CREATE_ANNOTATION, MOVE_ANNOTATION, TOGGLE_ANNOTATIONS,
4 | SHOW_ANNOTATION, SWAP_NODE_HIGHLIGHT, SWAP_EDGE_HIGHLIGHT,
5 | SWAP_CAPTION_HIGHLIGHT, DELETE_ALL } from '../actions';
6 | import merge from 'lodash/merge';
7 | import range from 'lodash/range';
8 | import assign from 'lodash/assign';
9 | import keys from 'lodash/keys';
10 | import cloneDeep from 'lodash/cloneDeep';
11 | import isNumber from 'lodash/isNumber';
12 |
13 | const initState = { list: [], visible: true, currentIndex: 0 };
14 |
15 | export default function annotations(state = initState, action) {
16 | switch (action.type) {
17 |
18 | case LOAD_ANNOTATIONS:
19 | return merge({}, state, { list: action.annotations.map(a => Annotation.setDefaults(a)) });
20 |
21 | case SHOW_ANNOTATION:
22 | return isNumber(action.id) && action.id !== state.currntIndex ?
23 | assign({}, state, { currentIndex: action.id }) : state;
24 |
25 | case UPDATE_ANNOTATION:
26 | return merge({}, state, { list: [
27 | ...state.list.slice(0, action.id),
28 | assign({}, state.list[action.id], action.data),
29 | ...state.list.slice(action.id + 1)
30 | ] });
31 |
32 | case DELETE_ANNOTATION:
33 | return assign({}, state, {
34 | currentIndex: Math.max(0, state.currentIndex - 1),
35 | list: [
36 | ...state.list.slice(0, action.id),
37 | ...state.list.slice(action.id + 1)
38 | ]
39 | });
40 |
41 | case CREATE_ANNOTATION:
42 | return merge({}, state, {
43 | list: [...state.list, Annotation.defaults()],
44 | currentIndex: typeof action.newIndex !== "undefined" ? action.newIndex : state.currentIndex
45 | });
46 |
47 | case MOVE_ANNOTATION:
48 | let { fromIndex, toIndex } = action;
49 |
50 | let annotations = cloneDeep(state.list);
51 | annotations.splice(toIndex, 0, annotations.splice(fromIndex, 1)[0]);
52 |
53 | let indexes = range(Math.max(fromIndex, toIndex, state.currentIndex) + 1);
54 | indexes.splice(toIndex, 0, indexes.splice(fromIndex, 1)[0]);
55 |
56 | return assign({}, state, {
57 | list: annotations,
58 | currentIndex: indexes.indexOf(state.currentIndex)
59 | });
60 |
61 | case TOGGLE_ANNOTATIONS:
62 | let visible = !state.visible;
63 | return merge({}, state, { visible });
64 |
65 | case SWAP_NODE_HIGHLIGHT:
66 | let nodeId = String(action.nodeId);
67 | let annotation = state.list[state.currentIndex];
68 | let nodeIds = annotation.nodeIds;
69 | let index = nodeIds.indexOf(nodeId);
70 |
71 | if (index == -1) {
72 | nodeIds = nodeIds.concat([nodeId]);
73 | } else {
74 | nodeIds.splice(index, 1);
75 | }
76 |
77 | return assign({}, state, { list: [
78 | ...state.list.slice(0, state.currentIndex),
79 | assign({}, annotation, { nodeIds }),
80 | ...state.list.slice(state.currentIndex + 1)
81 | ] });
82 |
83 | case SWAP_EDGE_HIGHLIGHT:
84 | let edgeId = String(action.edgeId);
85 | annotation = state.list[state.currentIndex];
86 | let edgeIds = annotation.edgeIds;
87 | index = edgeIds.indexOf(edgeId);
88 |
89 | if (index == -1) {
90 | edgeIds = edgeIds.concat([edgeId]);
91 | } else {
92 | edgeIds.splice(index, 1);
93 | }
94 |
95 | return assign({}, state, { list: [
96 | ...state.list.slice(0, state.currentIndex),
97 | assign({}, annotation, { edgeIds }),
98 | ...state.list.slice(state.currentIndex + 1)
99 | ] });
100 |
101 | case SWAP_CAPTION_HIGHLIGHT:
102 | let captionId = String(action.captionId);
103 | annotation = state.list[state.currentIndex];
104 | let captionIds = annotation.captionIds;
105 | index = captionIds.indexOf(captionId);
106 |
107 | if (index == -1) {
108 | captionIds = captionIds.concat([captionId]);
109 | } else {
110 | captionIds.splice(index, 1);
111 | }
112 |
113 | return assign({}, state, { list: [
114 | ...state.list.slice(0, state.currentIndex),
115 | assign({}, annotation, { captionIds }),
116 | ...state.list.slice(state.currentIndex + 1)
117 | ] });
118 |
119 | case DELETE_ALL:
120 | return assign({}, state, { list: [] });
121 |
122 | default:
123 | return state;
124 | }
125 | };
126 |
--------------------------------------------------------------------------------
/app/reducers/editTools.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_EDIT_TOOLS, TOGGLE_ADD_FORM, SET_NODE_RESULTS,
2 | CREATE_ANNOTATION } from '../actions';
3 |
4 | const initState = {
5 | visible: false,
6 | addForm: null,
7 | nodeResults: []
8 | };
9 |
10 | export default function editTools(state = initState, action) {
11 | switch (action.type) {
12 |
13 | case TOGGLE_EDIT_TOOLS:
14 | let visible = typeof action.value === "undefined" ? !state.visible : action.value;
15 | return Object.assign({}, state, { visible });
16 |
17 | case TOGGLE_ADD_FORM:
18 | let addForm = action.form == state.addForm ? null : action.form;
19 | return Object.assign({}, state, { addForm });
20 |
21 | case SET_NODE_RESULTS:
22 | return Object.assign({}, state, { nodeResults: action.nodes });
23 |
24 | case CREATE_ANNOTATION:
25 | return Object.assign({}, state, { visible: false });
26 |
27 | default:
28 | return state;
29 | }
30 | };
--------------------------------------------------------------------------------
/app/reducers/graph.js:
--------------------------------------------------------------------------------
1 | import { LOAD_GRAPH, SHOW_GRAPH, NEW_GRAPH,
2 | MOVE_NODE, MOVE_EDGE, MOVE_CAPTION,
3 | SWAP_NODE_HIGHLIGHT, SWAP_EDGE_HIGHLIGHT, SWAP_CAPTION_HIGHLIGHT,
4 | ADD_NODE, ADD_EDGE, ADD_CAPTION, ADD_SURROUNDING_NODES,
5 | ADD_INTERLOCKS,
6 | DELETE_NODE, DELETE_EDGE, DELETE_CAPTION, DELETE_SELECTION, DELETE_ALL,
7 | UPDATE_NODE, UPDATE_EDGE, UPDATE_CAPTION,
8 | PRUNE_GRAPH, LAYOUT_CIRCLE,
9 | SET_HIGHLIGHTS, TOGGLE_EDIT_TOOLS } from '../actions';
10 | import Graph from '../models/Graph';
11 | import Edge from '../models/Edge';
12 |
13 | export default function graph(state = null, action) {
14 | let newState, graph;
15 |
16 | switch (action.type) {
17 |
18 | case NEW_GRAPH:
19 | case LOAD_GRAPH:
20 | return Graph.prepare(action.graph);
21 |
22 | case MOVE_NODE:
23 | return Graph.moveNode(state, action.nodeId, action.x, action.y);
24 |
25 | case MOVE_EDGE:
26 | return Graph.moveEdge(state, action.edgeId, action.cx, action.cy);
27 |
28 | case MOVE_CAPTION:
29 | return Graph.moveCaption(state, action.captionId, action.x, action.y)
30 |
31 | case SWAP_NODE_HIGHLIGHT:
32 | return Graph.swapNodeHighlight(state, action.nodeId, action.singleSelect);
33 |
34 | case SWAP_EDGE_HIGHLIGHT:
35 | return Graph.swapEdgeHighlight(state, action.edgeId, action.singleSelect);
36 |
37 | case SWAP_CAPTION_HIGHLIGHT:
38 | return Graph.swapCaptionHighlight(state, action.captionId, action.singleSelect);
39 |
40 | case ADD_NODE:
41 | return Graph.addNode(state, action.node);
42 |
43 | case ADD_EDGE:
44 | return Graph.addEdge(state, action.edge);
45 |
46 | case ADD_CAPTION:
47 | return Graph.addCaption(state, action.caption);
48 |
49 | case ADD_SURROUNDING_NODES:
50 | return Graph.addSurroundingNodes(state, action.centerId, action.nodes);
51 |
52 | case ADD_INTERLOCKS:
53 | return Graph.addInterlocks(state, action.node1Id, action.node2Id, action.data);
54 |
55 | case DELETE_NODE:
56 | return Graph.deleteNode(state, action.nodeId);
57 |
58 | case DELETE_EDGE:
59 | return Graph.deleteEdge(state, action.edgeId);
60 |
61 | case DELETE_CAPTION:
62 | return Graph.deleteCaption(state, action.captionId);
63 |
64 | case DELETE_SELECTION:
65 | return Graph.deleteCaptions(
66 | Graph.deleteNodes(
67 | Graph.deleteEdges(
68 | state,
69 | action.selection.edgeIds
70 | ),
71 | action.selection.nodeIds
72 | ),
73 | action.selection.captionIds
74 | );
75 |
76 | case DELETE_ALL:
77 | return Graph.defaults();
78 |
79 | case UPDATE_NODE:
80 | // update connected edges to ensure that endpoints are correct in case node scale changed
81 | return Graph.prepareEdges(
82 | Graph.updateNode(state, action.nodeId, action.data),
83 | Graph.edgesConnectedToNode(state, action.nodeId)
84 | );
85 |
86 | case UPDATE_EDGE:
87 | return Graph.updateEdge(state, action.edgeId, action.data);
88 |
89 | case UPDATE_CAPTION:
90 | return Graph.updateCaption(state, action.captionId, action.data);
91 |
92 | case PRUNE_GRAPH:
93 | return Graph.prune(state);
94 |
95 | case LAYOUT_CIRCLE:
96 | return Graph.prepareEdges(Graph.circleLayout(state, true));
97 |
98 | case SET_HIGHLIGHTS:
99 | return Graph.setHighlights(state, action.highlights, action.otherwiseFaded);
100 |
101 | default:
102 | return state;
103 | }
104 | }
--------------------------------------------------------------------------------
/app/reducers/position.js:
--------------------------------------------------------------------------------
1 | import { SHOW_GRAPH, LOAD_GRAPH, NEW_GRAPH } from '../actions';
2 | import merge from 'lodash/merge';
3 |
4 | const initState = {
5 | currentId: null
6 | };
7 |
8 | export default function position(state = initState, action) {
9 | switch (action.type) {
10 |
11 | case NEW_GRAPH:
12 | return merge({}, state, { currentId: action.graph.id });
13 |
14 | case LOAD_GRAPH:
15 | return merge({}, state, { loadedId: action.id });
16 |
17 | case SHOW_GRAPH:
18 | return merge({}, state, { currentId: action.id });
19 |
20 | default:
21 | return state;
22 | }
23 | };
24 |
--------------------------------------------------------------------------------
/app/reducers/selection.js:
--------------------------------------------------------------------------------
1 | import { SWAP_NODE_SELECTION, SWAP_EDGE_SELECTION, SWAP_CAPTION_SELECTION,
2 | DESELECT_ALL, DELETE_SELECTION, DELETE_ALL,
3 | SHOW_GRAPH, NEW_GRAPH } from '../actions';
4 | import merge from 'lodash/merge';
5 | import assign from 'lodash/assign';
6 |
7 | const initState = { nodeIds: [], edgeIds: [], captionIds: [] };
8 |
9 | function swapElement(ary, elem, singleSelect = true) {
10 | let newAry = merge([], ary);
11 | let index = newAry.indexOf(elem);
12 |
13 | if (index === -1) {
14 | newAry = singleSelect ? [elem] : newAry.concat(elem);
15 | } else {
16 | newAry.splice(index, 1);
17 | }
18 |
19 | return newAry;
20 | }
21 |
22 | export default function selection(state = initState, action) {
23 | let newState, nodeIds, edgeIds, captionIds;
24 |
25 | switch (action.type) {
26 |
27 | // clear selection if graph is shown or selection deleted
28 | case DELETE_ALL:
29 | case DELETE_SELECTION:
30 | case SHOW_GRAPH:
31 | case NEW_GRAPH:
32 | case DESELECT_ALL:
33 | return initState;
34 |
35 | case SWAP_NODE_SELECTION:
36 | nodeIds = swapElement(state.nodeIds, action.nodeId, action.singleSelect);
37 | return action.singleSelect ? merge({}, initState, { nodeIds }) : assign({}, state, { nodeIds });
38 |
39 | case SWAP_EDGE_SELECTION:
40 | edgeIds = swapElement(state.edgeIds, action.edgeId, action.singleSelect);
41 | return action.singleSelect ? merge({}, initState, { edgeIds }) : assign({}, state, { edgeIds });
42 |
43 | case SWAP_CAPTION_SELECTION:
44 | captionIds = swapElement(state.captionIds, action.captionId, action.singleSelect);
45 | return action.singleSelect ? merge({}, initState, { captionIds }) : assign({}, state, { captionIds });
46 |
47 | default:
48 | return state;
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/app/reducers/settings.js:
--------------------------------------------------------------------------------
1 | import { SET_SETTINGS } from '../actions';
2 | import merge from 'lodash/merge';
3 |
4 | export default function title(state = {}, action) {
5 | switch (action.type) {
6 |
7 | case SET_SETTINGS:
8 | return merge({}, state, action.settings);
9 |
10 | default:
11 | return state;
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/app/reducers/showHelpScreen.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_HELP_SCREEN } from '../actions';
2 |
3 | export default function showHelpScreen(state = false, action) {
4 | switch (action.type) {
5 |
6 | case TOGGLE_HELP_SCREEN:
7 | return typeof action.value == "undefined" ? !state : action.value;
8 |
9 | default:
10 | return state;
11 | }
12 | };
--------------------------------------------------------------------------------
/app/reducers/showSettings.js:
--------------------------------------------------------------------------------
1 | import { TOGGLE_SETTINGS } from '../actions';
2 |
3 | export default function showSettings(state = false, action) {
4 | switch (action.type) {
5 |
6 | case TOGGLE_SETTINGS:
7 | return typeof action.value == "undefined" ? !state : action.value;
8 |
9 | default:
10 | return state;
11 | }
12 | };
--------------------------------------------------------------------------------
/app/reducers/title.js:
--------------------------------------------------------------------------------
1 | import { SET_TITLE } from '../actions';
2 |
3 | export default function title(state = null, action) {
4 | switch (action.type) {
5 |
6 | case SET_TITLE:
7 | return action.title;
8 |
9 | default:
10 | return state;
11 | }
12 | };
--------------------------------------------------------------------------------
/app/reducers/undoable-graph.js:
--------------------------------------------------------------------------------
1 | import graph from "./graph";
2 | import undoable, { excludeAction, distinctState } from 'redux-undo';
3 | import { LOAD_GRAPH, SHOW_GRAPH } from "../actions";
4 |
5 | export default undoable(graph, { filter: function filterState(action, currentState, previousState) {
6 | // only add to history if not initializing graph and state changed
7 | return ([LOAD_GRAPH, SHOW_GRAPH].indexOf(action.type) === -1) && (currentState !== previousState);
8 | } });
--------------------------------------------------------------------------------
/app/reducers/zoom.js:
--------------------------------------------------------------------------------
1 | import { ZOOM_IN, ZOOM_OUT, RESET_ZOOM, SHOW_GRAPH } from '../actions';
2 |
3 | export default function zoom(state = 1, action) {
4 | switch (action.type) {
5 |
6 | case ZOOM_IN:
7 | case ZOOM_OUT:
8 | return state * action.factor;
9 |
10 | case RESET_ZOOM:
11 | case SHOW_GRAPH:
12 | return 1;
13 |
14 | default:
15 | return state;
16 | }
17 | };
--------------------------------------------------------------------------------
/app/styles/oligrapher.annotations.css:
--------------------------------------------------------------------------------
1 | #oligrapherAnnotationsContainer {
2 | position: relative;
3 | }
4 |
5 | #oligrapherGraphContainer {
6 | position: relative;
7 | }
8 |
9 | #oligrapherHeader {
10 | margin-bottom: 10px;
11 | }
12 |
13 | #oligrapherTitle {
14 | font-size: 50px;
15 | font-weight: 300;
16 | margin-top: 3px;
17 | margin-bottom: 5px;
18 | }
19 |
20 | #oligrapherTitle.oligrapherTitleInput {
21 | margin-top: 0;
22 | margin-bottom: 1px;
23 | }
24 |
25 | #oligrapherTitle a {
26 | text-decoration: none;
27 | }
28 |
29 | #oligrapherTitleInput {
30 | border: 1px solid #ccc;
31 | padding: 0;
32 | width: 100%;
33 | }
34 |
35 | #oligrapherByLine {
36 | margin-right: 18px;
37 | }
38 |
39 | #oligrapherDate {
40 | color: #888;
41 | }
42 |
43 | #oligrapherGraphLinks {
44 | margin-left: 2px;
45 | }
46 |
47 | #oligrapherGraphLinks a {
48 | display: inline-block;
49 | margin-right: 12px;
50 | font-size: 12px;
51 | cursor: pointer;
52 | }
53 |
54 | #oligrapherGraphLinks form {
55 | display: inline-block;
56 | }
57 |
58 | #oligrapherMetaButtons {
59 | position: absolute;
60 | top: 25px;
61 | left: 15px;
62 | }
63 |
64 | #oligrapherMetaButtons button {
65 | display: block;
66 | border: 1px solid #ccc;
67 | background-color: rgba(255, 255, 255, 0.8);
68 | width: 32px;
69 | padding-top: 7px;
70 | margin-bottom: 10px;
71 | }
72 |
73 | #oligrapherEditButton.editContentMode {
74 | background-color: rgba(0, 255, 0, 0.5);
75 | }
76 |
77 | #oligrapherEditButton.editAnnotationsMode {
78 | background-color: rgba(255, 255, 0, 0.5);
79 | }
80 |
81 | #oligrapherSaveButton {
82 | position: fixed;
83 | bottom: 20px;
84 | left: 20px;
85 | background-color: #f8f8f8;
86 | }
87 |
88 | #oligrapherShowAnnotations {
89 | position: absolute;
90 | top: 5px;
91 | right: 10px;
92 | }
93 |
94 | #oligrapherShowAnnotations button {
95 | font-size: 24px;
96 | font-weight: 200;
97 | }
98 |
99 | #oligrapherGraph {
100 | border: 1px solid #f8f8f8;
101 | }
102 |
103 | #oligrapherAnnotationsNavHolder {
104 | display: inline-block;
105 | }
106 |
107 | #oligrapherNavButtons {
108 | margin-top: 5px;
109 | margin-bottom: 25px;
110 | }
111 |
112 | #oligrapherNavButtons button {
113 | margin-right: 10px;
114 | font-size: 24px;
115 | font-weight: 200;
116 | border: 0 solid #eee;
117 | background-color: #eee;
118 |
119 | &:disabled {
120 | color: #ccc;
121 | background-color: #f8f8f8;
122 | opacity: 1;
123 | }
124 | }
125 |
126 | #oligrapherNavButtons button#oligrapherNavListButton,
127 | #oligrapherNavButtons button#oligrapherHideAnnotationsButton,
128 | #oligrapherShowAnnotations button {
129 | border: 1px solid #eee;
130 | background-color: #fff;
131 | }
132 |
133 | .clickplz:enabled:not(:focus) {
134 | animation: clickplz 3s;
135 | -webkit-animation: clickplz 3s;
136 | animation-iteration-count: infinite;
137 | -webkit-animation-iteration-count: infinite;
138 | }
139 |
140 | @keyframes clickplz {
141 | 0% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); }
142 | 50% { color: #666; background-color:#ffd; text-shadow: 0 0 9px rgba(255,255,128,0.75); }
143 | 100% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); }
144 | }
145 |
146 | @-webkit-keyframes clickplz {
147 | 0% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); }
148 | 50% { color: #666; background-color:#ffd; text-shadow: 0 0 9px rgba(255,255,128,0.75); }
149 | 100% { color: #666; background-color:#eee; text-shadow: 0 0 9px rgba(255,255,255,0); }
150 | }
151 |
152 | #oligrapherAnnotationListItems {
153 | padding: 0;
154 | list-style-type: none;
155 | }
156 |
157 | #oligrapherAnnotationListItems li.placeholder {
158 | background: rgb(255, 240, 120);
159 | }
160 |
161 | #oligrapherAnnotationListItems li.placeholder:before {
162 | color: rgb(225, 210, 90);
163 | }
164 |
165 | #oligrapherAnnotationListItems li {
166 | font-size: 18px;
167 | line-height: 34px;
168 | padding-left: 10px;
169 | padding-right: 10px;
170 | margin-left: -10px;
171 | border-bottom: 1px solid #eee;
172 | cursor: pointer;
173 | width: 100%;
174 | white-space: nowrap;
175 | overflow: hidden;
176 | text-overflow: ellipsis;
177 | border-radius: 3px;
178 | }
179 |
180 | #oligrapherAnnotationListItems li.active {
181 | background-color: #eee;
182 | }
183 |
184 | #oligrapherAnnotationList button {
185 | margin-top: 0px;
186 | margin-bottom: 5px;
187 | }
188 |
189 | #oligrapherGraphAnnotationEditButton {
190 | margin-top: 10px;
191 | }
192 |
193 | #oligrapherGraphAnnotationForm {
194 | margin-top: 10px;
195 | }
196 |
197 | #oligrapherGraphAnnotationForm textarea {
198 | margin-bottom: 10px;
199 | }
200 |
201 | #oligrapherGraphAnnotationFormText, #oligrapherGraphAnnotationFormHeader {
202 | /* height: 400px; */
203 | padding: 10px;
204 | padding-left: 16px;
205 | padding-right: 16px;
206 | border: 1px solid #eee;
207 | border-radius: 6px;
208 | }
209 |
210 | #oligrapherGraphAnnotationFormText {
211 | margin-bottom: 20px;
212 | }
213 |
214 | #oligrapherSettingsForm {
215 | position: absolute;
216 | display: inline-block;
217 | z-index: 20;
218 | bottom: 15px;
219 | right: 15px;
220 | margin: 0 auto;
221 | text-align: right;
222 | }
223 |
224 | #oligrapherHelpScreen {
225 | background: white;
226 | width: 800px;
227 | padding: 20px;
228 | border-radius: 5px;
229 | position: absolute;
230 | z-index: 20;
231 | top: 20px;
232 | right: 20px;
233 | margin: 0 auto;
234 | box-shadow: 0px 0px 20px #888;
235 | }
236 |
237 | #oligrapherHelpScreen h3 {
238 | margin-top: 0;
239 | }
240 |
241 | #oligrapherHelpScreenCloseButton {
242 | position: absolute;
243 | top: 20px;
244 | right: 20px;
245 | color: #888;
246 | }
247 |
--------------------------------------------------------------------------------
/app/styles/oligrapher.css:
--------------------------------------------------------------------------------
1 | #oligrapherContainer:focus, #oligrapherContainer :focus {
2 | outline: none;
3 | }
4 |
5 | a.nodeLabel {
6 | cursor: pointer;
7 | }
8 |
9 | a.nodeLabel:hover {
10 | text-decoration: none;
11 | }
12 |
13 | a.nodeLabel text tspan {
14 | font-family: Helvetica, Arial, sans-serif;
15 | }
16 |
17 | .edge text {
18 | font-family: Helvetica, Arial, sans-serif;
19 | }
20 |
21 | #oligrapherContainer:hover .edge:not(.dragging) text {
22 | display: none;
23 | }
24 |
25 | #oligrapherContainer:hover .edge:hover text, .edge.selected text, .edge.highlighted text {
26 | display: inline;
27 | }
28 |
29 | .edge:hover text.link {
30 | cursor: pointer;
31 | }
32 |
33 | .caption {
34 | font-family: Helvetica, Arial, sans-serif;
35 | cursor: default;
36 | }
37 |
38 | /* from annotations app */
39 |
40 | #oligrapherContainer {
41 | position: relative;
42 | }
43 |
44 | #oligrapherGraphContainer {
45 | position: relative;
46 | border: 1px solid #f8f8f8;
47 | }
48 |
49 | #oligrapherHeader {
50 | margin-bottom: 10px;
51 | }
52 |
53 | #oligrapherTitle {
54 | font-size: 50px;
55 | font-weight: 300;
56 | margin-top: 3px;
57 | margin-bottom: 5px;
58 | }
59 |
60 | #oligrapherTitle.oligrapherTitleInput {
61 | margin-top: 0;
62 | margin-bottom: 1px;
63 | }
64 |
65 | #oligrapherTitle a {
66 | text-decoration: none;
67 | color: black;
68 | }
69 |
70 | #oligrapherTitleInput {
71 | border: 1px solid #ccc;
72 | padding: 0;
73 | width: 100%;
74 | }
75 |
76 | #oligrapherByLine {
77 | margin-right: 18px;
78 | }
79 |
80 | #oligrapherDate {
81 | color: #888;
82 | }
83 |
84 | #oligrapherGraphLinks {
85 | margin-left: 2px;
86 | }
87 |
88 | #oligrapherGraphLinks a {
89 | display: inline-block;
90 | margin-right: 12px;
91 | font-size: 12px;
92 | cursor: pointer;
93 | }
94 |
95 | #oligrapherGraphLinks form {
96 | display: inline-block;
97 | }
--------------------------------------------------------------------------------
/app/styles/oligrapher.editor.css:
--------------------------------------------------------------------------------
1 | #oligrapherEditorContainer:focus,
2 | #oligrapherEditorContainer div:focus,
3 | #oligrapherEditorContainer button:not(#zoomIn, #zoomOut):focus,
4 | #oligrapherEditorContainer input[type="checkbox"]:focus {
5 | outline: none;
6 | background-color: #f8f8f8;
7 | }
8 |
9 | #oligrapherEditorContainer button:not(.nodeColorInputClearer) {
10 | background-color: #f8f8f8;
11 | }
12 |
13 | #zoomButtons {
14 | position: absolute;
15 | top: 15px;
16 | right: 20px;
17 | border: 1px solid #ccc;
18 | border-radius: 5px;
19 | padding: 5px;
20 | background-color: rgba(255, 255, 255, 0.8);
21 | }
22 |
23 | #zoomOut {
24 | margin-top: 5px;
25 | }
26 |
27 | #zoomButtons button {
28 | display: block;
29 | width: 20px;
30 | height: 20px;
31 | text-align: center;
32 | background-color: rgba(0, 0, 0, 0);
33 | border: 0px solid #ddd;
34 | cursor: pointer;
35 | padding: 0px;
36 | color: #666;
37 | font-size: 16px;
38 | }
39 |
40 | #zoomButtons button:focus {
41 | outline: none;
42 | }
43 |
44 | button#toggleEditTools {
45 | position: absolute;
46 | top: 85px;
47 | left: 15px;
48 | border: 1px solid #ccc;
49 | background-color: #fff;
50 | padding-top: 7px;
51 | width: 32px;
52 | outline: none;
53 | }
54 |
55 | #buttons {
56 | position: absolute;
57 | top: 15px;
58 | left: 60px;
59 | width:250px;
60 | pointer-events: none;
61 | }
62 |
63 | #buttons .buttonGroup {
64 | display: inline-block;
65 | margin-right: 10px;
66 | margin-bottom: 10px;
67 | pointer-events: all;
68 | }
69 |
70 | #addNodeInput {
71 | position: relative;
72 | display: inline-block;
73 | margin-right: 10px;
74 | }
75 |
76 | #addInterlocksButton {
77 | margin-left: 10px;
78 | }
79 |
80 | .addNodeResults {
81 | display: block;
82 | }
83 |
84 | #deleteSelected {
85 | position: absolute;
86 | z-index: 1;
87 | }
88 |
89 | .editForm.nodeDelete {
90 | margin-top: 120px;
91 | }
92 |
93 | .editForm.edgeDelete {
94 | margin-top: 140px;
95 | }
96 |
97 | .editForm {
98 | position: absolute;
99 | display: inline-block;
100 | z-index: 20;
101 | top: 15px;
102 | right: 15px;
103 | margin: 0 auto;
104 | text-align: right;
105 | }
106 |
107 | .editForm h3 {
108 | margin-top: 0;
109 | margin-bottom: 10px;
110 | }
111 |
112 | .editForm input:not([type='checkbox']), .editForm select {
113 | width: 150px;
114 | }
115 |
116 | #addEdgeForm input, #addEdgeForm select {
117 | margin-bottom: 10px;
118 | }
119 |
120 | #addConnectedNodes {
121 | margin-top: 80px;
122 | z-index: 2;
123 | }
124 |
125 | #addConnectedNodesNum {
126 | width: 40px;
127 | }
128 |
129 | #addConnectedNodes select {
130 | margin-left: 5px;
131 | }
132 |
133 | .addNodeInput {
134 | margin-bottom: 5px;
135 | }
136 |
137 | .addNodeResult {
138 | margin-top: 3px;
139 | }
140 |
141 | .addNodeResult {
142 | color: #008;
143 | cursor: pointer;
144 | }
145 |
146 | #addEdgeForm {
147 | }
148 |
149 | #addEdgeForm input, #addEdgeForm select {
150 | margin-bottom: 5px;
151 | }
152 |
153 | #helpScreen {
154 | background: white;
155 | width: 400px;
156 | padding: 20px;
157 | border-radius: 5px;
158 | position: absolute;
159 | z-index: 20;
160 | top: 20px;
161 | right: 20px;
162 | margin: 0 auto;
163 | box-shadow: 0px 0px 20px #888;
164 | }
165 |
166 | #helpScreen h3 {
167 | margin-top: 0;
168 | }
169 |
170 | .updateForm .form-control {
171 | margin-bottom: 10px;
172 | }
173 |
174 | #nodeUrlInput {
175 | width: 363px;
176 | float: right;
177 | }
178 |
179 | .nodeColorInputWrapper {
180 | position: relative;
181 | display: inline-block;
182 | }
183 |
184 | .nodeColorInputWrapper .nodeColorInput {
185 | width: 50px;
186 | padding: 4px 8px;
187 | cursor: pointer;
188 | position: relative;
189 | display: inline-block;
190 | }
191 |
192 | .nodeColorInputSwatch {
193 | width: 22px;
194 | height: 22px;
195 | border-radius: 22px;
196 | margin-top: -1px;
197 | margin-left: -5px;
198 | cursor: pointer;
199 | }
200 |
201 | .nodeColorInputClearer {
202 | background: none;
203 | border: none;
204 | position: absolute;
205 | right: -2px;
206 | line-height: 29px;
207 | top: 0px;
208 | }
209 |
210 | .nodeColorPickerWrapper {
211 | position: absolute;
212 | right: 0px;
213 | }
214 |
215 | #edgeUrlInput {
216 | width: 337px;
217 | }
218 |
219 | .svgDropdown{
220 | height: auto;
221 | background-color: #fff;
222 | border: 1px solid #ccc;
223 | border-radius: 3px;
224 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
225 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075);
226 | -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s;
227 | -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;
228 | list-style-type: none;
229 | padding:0px;
230 | margin:0px;
231 | top:0px;
232 | }
233 |
234 | .strokeMain{
235 | width:25%;
236 | margin-right: 2%;
237 | margin-left: 2%;
238 | }
239 |
240 | .arrowHead{
241 | width:20%;
242 | }
243 |
244 | .dropdownHolder{
245 | /* display: inline-block; */
246 | margin-top: 10px;
247 | width: 45px;
248 | float: left;
249 | }
250 |
251 | .svgDropdown li, .svgDropdown div{
252 | width: 100%;
253 | height: 29px;
254 | list-style: none;
255 | }
256 |
257 | .svgDropdown li:not(:last-child){
258 | border-bottom: 1px solid #ccc;
259 | }
260 |
261 | /*displays oddly in chrome if there is no border on last element for some reason*/
262 | .svgDropdown li:last-child, .svgDropdown div{
263 | border-bottom: 1px solid rgba(0,0,0,0);
264 | }
265 |
266 | .strokeDropdowns{
267 | width: 300px;
268 | height: 54px;
269 | }
270 |
271 |
272 | .svgDropdown li svg, .svgDropdown div svg{
273 | width: 100%;
274 | height: 100%;
275 | shape-rendering: crispEdges;
276 | cursor: pointer;
277 | }
278 |
279 | .svgDropdown li:hover{
280 | background-color: yellow;
281 | }
282 |
283 | .svgDropdown li svg line, .svgDropdown div svg line{
284 | stroke:#aaa;
285 | stroke-width:1px;
286 | stroke-linecap: butt;
287 | }
288 |
289 | .svgDropdownDashed svg line{
290 | stroke-dasharray: 5, 3;
291 | }
292 |
293 | .svgDropdownLeftArrow svg line{
294 | marker-end: url(#marker1);
295 | }
296 |
297 | .svgDropdownRightArrow svg line{
298 | marker-start: url(#marker2);
299 | }
300 |
301 | .edgeDropdownOptions{
302 | width: 100%;
303 | }
304 |
305 | .oligrapherEdgeWidthDropdown{
306 | width: auto;
307 | text-align: right;
308 | }
309 |
310 | .arrow-node-name {
311 | float: left;
312 | width: 70px;
313 | }
314 |
315 | .dropdownHolder-left {
316 | margin-left: 25px;
317 | }
318 |
319 | .dropdownHolder-right {
320 | margin-right: -80px;
321 | }
--------------------------------------------------------------------------------
/app/styles/oligrapher.embedded.css:
--------------------------------------------------------------------------------
1 | #embeddedNavBar {
2 | height: 28px;
3 | }
4 |
5 | #oligrapher.embedded h1 {
6 | font-color: rgb(51, 51, 51);
7 | }
8 |
9 | #annotationsTracker {
10 | display: inline-block;
11 | }
12 |
13 | .tracker-circle {
14 | margin-right: 2px;
15 | display: inline-block;
16 |
17 | background-color: #ccc;
18 | border-radius: 50%;
19 | width: 10px;
20 | height: 10px;
21 | }
22 |
23 | #oligrapherNavButtonsEmbedded {
24 | display: inline;
25 | margin-right: 10px;
26 | }
27 |
28 | #oligrapherNavButtonsEmbedded button {
29 | font-size: 10.5px;
30 | }
31 |
32 | .btn-annotation-next {
33 | margin-left: 5px;
34 | }
35 |
36 | .tracker-circle-selected {
37 | background-color: #337ab7;
38 | }
39 |
40 | .embedded-footer-wrapper {
41 | position: relative;
42 | height: 100%;
43 | pointer-events: none;
44 | }
45 |
46 | .embedded-link-wrapper {
47 | position: absolute;
48 | bottom: 0;
49 | padding-left: 15px;
50 | pointer-events: auto;
51 | }
52 |
53 | img.embedded-logo {
54 | position: absolute;
55 | bottom: 10px;
56 | right: 0;
57 | opacity: 0.6;
58 | margin-right: 5px;
59 | }
--------------------------------------------------------------------------------
/app/styles/test/styleMock.js:
--------------------------------------------------------------------------------
1 | // Return an object to emulate css modules (if you are using them)
2 | module.exports = {};
3 |
4 |
--------------------------------------------------------------------------------
/build/LsDataConverter.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (function() {
4 | var root = this;
5 | var previous_LsDataConverter = root.LsDataConverter;
6 |
7 | var LsDataConverter = {
8 | convertUrl: function(url) {
9 | return url ? (url.match(/littlesis\.org/) ? url : "//littlesis.org" + url) : null;
10 | },
11 |
12 | convertEntity: function(e) {
13 | return {
14 | id: e.id,
15 | display: {
16 | name: e.name,
17 | x: e.x,
18 | y: e.y,
19 | scale: e.scale ? e.scale : 1,
20 | status: e.status ? e.status : "normal",
21 | image: e.image,
22 | url: this.convertUrl(e.url)
23 | }
24 | };
25 | },
26 |
27 | convertRel: function(r) {
28 | return {
29 | id: r.id,
30 | node1_id: r.entity1_id,
31 | node2_id: r.entity2_id,
32 | display: {
33 | label: r.label,
34 | cx: r.x1,
35 | cy: r.y1,
36 | scale: r.scale ? r.scale : 1,
37 | arrow: r.is_directional,
38 | dash: r.is_current,
39 | status: r.status ? r.status : "normal",
40 | url: this.convertUrl(r.url)
41 | }
42 | };
43 | },
44 |
45 | convertText: function(t, id) {
46 | return {
47 | id: id,
48 | display: {
49 | text: t.text,
50 | x: t.x,
51 | y: t.y
52 | }
53 | };
54 | },
55 |
56 | convertMapData: function(data) {
57 | var that = this;
58 |
59 | var nodes = data.entities.reduce(function(result, e) {
60 | result[e.id] = that.convertEntity(e);
61 | return result;
62 | }, {});
63 |
64 | var edges = data.rels.reduce(function(result, r) {
65 | result[r.id] = that.convertRel(r);
66 | return result;
67 | }, {});
68 |
69 | var captions = data.texts.reduce(function(result, t, i) {
70 | result[i+1] = that.convertText(t, i+1);
71 | return result;
72 | }, {});
73 |
74 | return {
75 | id: data.id,
76 | title: data.title,
77 | description: data.description,
78 | nodes: nodes,
79 | edges: edges,
80 | captions: captions
81 | };
82 | },
83 |
84 | convertMapCollectionData: function(data) {
85 | var that = this;
86 |
87 | return {
88 | id: data.id,
89 | title: data.title,
90 | graphs: data.maps.map(function(map) {
91 | return that.convertMapData(map);
92 | })
93 | };
94 | }
95 | }
96 |
97 | LsDataConverter.noConflict = function() {
98 | root.LsDataConverter = previous_LsDataConverter;
99 | return LsDataConverter;
100 | }
101 |
102 | if (typeof exports !== 'undefined') {
103 | if (typeof module !== 'undefined' && module.exports) {
104 | exports = module.exports = LsDataConverter;
105 | }
106 |
107 | exports.LsDataConverter = LsDataConverter;
108 | }
109 | else {
110 | root.LsDataConverter = LsDataConverter;
111 | }
112 |
113 | }).call(this);
--------------------------------------------------------------------------------
/build/LsDataSource.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (function() {
4 | var root = this;
5 | var previous_LsDataSource = root.LsDataSource;
6 |
7 | var toQueryString = function(obj) {
8 | var parts = [];
9 |
10 | for (var i in obj) {
11 | if (obj.hasOwnProperty(i)) {
12 | if (Array.isArray(obj[i])) {
13 | obj[i].forEach(function(val) {
14 | parts.push(encodeURIComponent(i+"[]") + "=" + encodeURIComponent(val));
15 | });
16 | } else {
17 | parts.push(encodeURIComponent(i) + "=" + encodeURIComponent(obj[i]));
18 | }
19 | }
20 | }
21 |
22 | return parts.join("&");
23 | };
24 |
25 | var get = function(url, data, onSucess, onFail) {
26 | var httpRequest = new XMLHttpRequest();
27 |
28 | if (!httpRequest) {
29 | console.error('Giving up :( Cannot create an XMLHTTP instance');
30 | return false;
31 | }
32 |
33 | if (data) {
34 | var fullUrl = url + "?" + toQueryString(data);
35 | } else {
36 | console.error('Cannot make a request without data!');
37 | return false;
38 | }
39 |
40 | httpRequest.onreadystatechange = function() {
41 | if (httpRequest.readyState === 4) {
42 | if (httpRequest.status === 200) {
43 | onSucess(JSON.parse(httpRequest.responseText));
44 | } else {
45 | if (onFail) {
46 | onFail({ status: httpRequest.status, error: httpRequest.responseText });
47 | }
48 | }
49 | }
50 | };
51 |
52 | httpRequest.open('GET', fullUrl);
53 | httpRequest.send();
54 | };
55 |
56 | var LsDataSource = {
57 | name: 'LittleSis',
58 | baseUrl: '//littlesis.org',
59 |
60 | findNodes: function(text, callback) {
61 | get(
62 | this.baseUrl + '/maps/find_nodes',
63 | { num: 12, desc: true, with_ids: true, q: text },
64 | callback
65 | );
66 | },
67 |
68 | getNodeWithEdges: function(nodeId, nodeIds, callback) {
69 | get(
70 | this.baseUrl + '/maps/node_with_edges',
71 | { node_id: nodeId, node_ids: nodeIds },
72 | callback
73 | );
74 | },
75 |
76 | getConnectedNodesOptions: {
77 | category_id: {
78 | 1: "Position",
79 | 2: "Education",
80 | 3: "Membership",
81 | 4: "Family",
82 | 5: "Donation",
83 | 6: "Transaction",
84 | 7: "Lobbying",
85 | 8: "Social",
86 | 9: "Professional",
87 | 10: "Ownership",
88 | 11: "Hierarchy",
89 | 12: "Generic"
90 | }
91 | },
92 |
93 | getConnectedNodes: function(nodeId, nodeIds, options, callback) {
94 | options = options || {};
95 | options.node_id = nodeId;
96 | options.node_ids = nodeIds;
97 |
98 | get(
99 | this.baseUrl + '/maps/edges_with_nodes',
100 | options,
101 | callback
102 | );
103 | },
104 |
105 | getInterlocks: function(node1Id, node2Id, nodeIds, options, callback) {
106 | options = options || {};
107 | options.node1_id = node1Id;
108 | options.node2_id = node2Id;
109 | options.node_ids = nodeIds;
110 |
111 | get(
112 | this.baseUrl + '/maps/interlocks',
113 | options,
114 | callback
115 | );
116 | }
117 | };
118 |
119 | LsDataSource.noConflict = function() {
120 | root.LsDataSource = previous_LsDataSource;
121 | return LsDataSource;
122 | };
123 |
124 | if (typeof exports !== 'undefined') {
125 | if (typeof module !== 'undefined' && module.exports) {
126 | exports = module.exports = LsDataSource;
127 | }
128 |
129 | exports.LsDataSource = LsDataSource;
130 | }
131 | else {
132 | root.LsDataSource = LsDataSource;
133 | }
134 |
135 | }).call(this);
136 |
--------------------------------------------------------------------------------
/build/PopoloDataConverter.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | (function() {
4 | var root = this;
5 | var previous_PopoloDataConverter = root.PopoloDataConverter;
6 |
7 | var PopoloDataConverter = {
8 | convertEntity: function(data) {
9 | return {
10 | id: data.id,
11 | display: {
12 | name: data.name
13 | }
14 | };
15 | },
16 |
17 | convertMembership: function(data) {
18 | return {
19 | id: data.id,
20 | node1_id: data.person_id,
21 | node2_id: data.organization_id,
22 | display: {
23 | label: data.role,
24 | arrow: true
25 | }
26 | }
27 | },
28 |
29 | convertGraphData: function(data) {
30 | var graph = { nodes: {}, edges: {} };
31 | var that = this;
32 |
33 | data.organizations.forEach(function(org) {
34 | graph.nodes[org.id] = that.convertEntity(org);
35 | });
36 |
37 | data.persons.forEach(function(person) {
38 | graph.nodes[person.id] = that.convertEntity(person);
39 |
40 | person.memberships.forEach(function(membership) {
41 | graph.edges[membership.id] = that.convertMembership(membership);
42 | });
43 | });
44 |
45 | return graph;
46 | }
47 | }
48 |
49 | PopoloDataConverter.noConflict = function() {
50 | root.PopoloDataConverter = previous_PopoloDataConverter;
51 | return PopoloDataConverter;
52 | }
53 |
54 | if (typeof exports !== 'undefined') {
55 | if (typeof module !== 'undefined' && module.exports) {
56 | exports = module.exports = PopoloDataConverter;
57 | }
58 |
59 | exports.PopoloDataConverter = PopoloDataConverter;
60 | }
61 | else {
62 | root.PopoloDataConverter = PopoloDataConverter;
63 | }
64 |
65 | }).call(this);
--------------------------------------------------------------------------------
/build/dev.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Oligrapher Demo
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/build/embedded.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Oligrapher Demo
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/build/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Oligrapher Demo
6 |
7 |
8 |
9 |
14 |
15 |
16 |
17 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/build/puerto_rico.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Oligrapher Demo
6 |
7 |
8 |
9 |
10 |
11 |
16 |
17 |
18 |
19 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "oligrapher2",
3 | "version": "0.2.0",
4 | "description": "network graph visualizer",
5 | "main": "app/main.jsx",
6 | "scripts": {
7 | "dev-build": "webpack-dev-server --devtool eval --progress --history-api-fallback --colors --hot --content-base build --port 8090 --config webpack.dev.config",
8 | "fake-news-server": "cd build && python3 -m http.server 8091",
9 | "prod-build": "NODE_ENV=production webpack --display-modules --config webpack.prod.config.js --output-filename=oligrapher.js",
10 | "watch": "webpack --watch --config webpack.prod.config.js --output-filename=oligrapher.js",
11 | "min-build": "NODE_ENV=production webpack -p --optimize-dedupe --display-modules --config webpack.prod.config.js --output-filename=oligrapher.min.js",
12 | "build-all": "npm run prod-build && npm run min-build",
13 | "dev-package": "cat build/oligrapher.min.js build/LsDataSource.js build/LsDataConverter.js > build/oligrapher-dev.js",
14 | "test": "jest",
15 | "test:watch": "jest --watch"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "https://github.com/skomputer/oligrapher2.git"
20 | },
21 | "author": "skomputer",
22 | "license": "GPL-3.0",
23 | "bugs": {
24 | "url": "https://github.com/skomputer/oligrapher2/issues"
25 | },
26 | "homepage": "https://github.com/skomputer/oligrapher2/",
27 | "dependencies": {
28 | "classnames": "^2.2.0",
29 | "lodash": "^4.17.4",
30 | "prop-types": "^15.5.10",
31 | "react": "^15.5.4",
32 | "react-anything-sortable": "^1.7.2",
33 | "react-color": "^2.11.7",
34 | "react-custom-scrollbars": "^4.1.2",
35 | "react-dom": "^15.5.4",
36 | "react-draggable": "^2.2.6",
37 | "react-hotkeys": "^0.10.0",
38 | "react-medium-editor": "^1.8.1",
39 | "react-redux": "^5.0.5",
40 | "redux": "^3.6.0",
41 | "redux-thunk": "^2.2.0",
42 | "redux-undo": "^0.6.0",
43 | "shortid": "^2.2.8",
44 | "springy": "^2.7.1",
45 | "titleize": "^1.0.0"
46 | },
47 | "devDependencies": {
48 | "babel-core": "^6.24.1",
49 | "babel-jest": "^20.0.3",
50 | "babel-loader": "^6.2.5",
51 | "babel-polyfill": "^6.23.0",
52 | "babel-preset-es2015": "^6.24.1",
53 | "babel-preset-react": "^6.24.1",
54 | "css-loader": "^0.26.1",
55 | "enzyme": "^2.8.2",
56 | "file-loader": "^0.9.0",
57 | "jest-cli": "^20.0.3",
58 | "react-addons-test-utils": "^15.5.1",
59 | "react-dom": "^15.5.4",
60 | "react-hot-loader": "^1.2.7",
61 | "react-test-renderer": "^15.5.4",
62 | "redux-logger": "^2.10.2",
63 | "sinon": "^1.17.7",
64 | "style-loader": "^0.13.0",
65 | "url-loader": "^0.5.7",
66 | "webpack": "^1.13.2",
67 | "webpack-dev-server": "^1.16.1"
68 | },
69 | "jest": {
70 | "transform": {
71 | ".*": "/node_modules/babel-jest"
72 | },
73 | "moduleFileExtensions": [
74 | "js",
75 | "jsx",
76 | "json"
77 | ],
78 | "moduleNameMapper": {
79 | "^.+\\.(css)$": "/app/styles/test/styleMock.js"
80 | },
81 | "testRegex": "(/__tests__/(?!support).*|\\.(test|spec))\\.jsx?$"
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var webpack = require('webpack');
3 | var node_modules = path.resolve(__dirname, 'node_modules/');
4 |
5 | var config = {
6 | entry: {
7 | app: [
8 | 'webpack/hot/dev-server',
9 | path.resolve(__dirname, 'app/main.jsx'),
10 | ],
11 | },
12 | output: {
13 | path: path.resolve(__dirname, 'build'),
14 | filename: 'oligrapher.min.js',
15 | publicPath: 'http://localhost:8090/build',
16 | library: 'Oligrapher',
17 | libraryTarget: 'umd'
18 | },
19 | plugins: [
20 | new webpack.optimize.CommonsChunkPlugin('vendors', 'vendors.js'),
21 | new webpack.DefinePlugin({ "process.env": JSON.stringify(process.env)})
22 | ],
23 | module: {
24 | loaders: [
25 | { test: /\.jsx?$/,
26 | exclude: [node_modules],
27 | loaders: ['react-hot', 'babel'] },
28 | { test: /\.css$/,
29 | loader: "style-loader!css-loader" },
30 | { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url?limit=30000' }
31 | ],
32 | noParse:[]
33 | },
34 | resolve: {
35 | alias: {
36 | 'react/lib': path.resolve(node_modules, 'react/lib')
37 | },
38 | extensions: ['', '.js', '.jsx']
39 | }
40 | };
41 |
42 | module.exports = config;
43 |
--------------------------------------------------------------------------------
/webpack.prod.config.js:
--------------------------------------------------------------------------------
1 | var path = require('path');
2 | var node_modules = path.resolve(__dirname, 'node_modules');
3 | var webpack = require('webpack');
4 |
5 | var config = {
6 | entry: [
7 | path.resolve(__dirname, 'app/main.jsx'),
8 | ],
9 | output: {
10 | path: 'build',
11 | library: 'Oligrapher',
12 | libraryTarget: 'umd'
13 | },
14 | plugins: [
15 | new webpack.DefinePlugin({ "process.env": { 'NODE_ENV': JSON.stringify('production') } })
16 | ],
17 | module: {
18 | loaders: [
19 | { test: /\.jsx?$/,
20 | exclude: [node_modules],
21 | loader: 'babel' },
22 | { test: /\.css$/,
23 | loader: "style-loader!css-loader" },
24 | { test: /\.(woff2?|ttf|eot|svg)$/, loader: 'url?limit=30000' }
25 | ]
26 | },
27 | resolve: {
28 | extensions: ['', '.js', '.jsx']
29 | }
30 | };
31 |
32 |
33 | module.exports = config;
34 |
--------------------------------------------------------------------------------