diff --git a/src/components/Graph/Graph.js b/src/components/Graph/Graph.js new file mode 100644 index 00000000..6e795f9a --- /dev/null +++ b/src/components/Graph/Graph.js @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +// we need to require from react-sigma/lib/ to make build work +import { + Sigma, + EdgeShapes, + NodeShapes, + ForceAtlas2, + RandomizeNodePositions, +} from 'react-sigma/lib/'; + +const Graph = ({graph, settings, edgeShape}) => { + const layoutOptions = { + iterationsPerRender: 40, + edgeWeightInfluence: 0, + timeout: 2000, + adjustSizes: false, + gravity: 3, + slowDown: 5, + linLogMode: true, + outboundAttractionDistribution: false, + strongGravityMode: false, + }; + + const layout = ; + + let sigma = null; + if (graph && graph.nodes.length > 0) { + sigma = ( + + + + {layout} + + ); + } + + return sigma; +}; + +Graph.propTypes = { + graph: PropTypes.shape({ + nodes: PropTypes.array.isRequired, + edges: PropTypes.array.isRequired, + }).isRequired, + settings: PropTypes.object.isRequired, + edgeShape: PropTypes.string.isRequired, +}; + +export default Graph; diff --git a/src/components/Graph/NetworkGraph.js b/src/components/Graph/NetworkGraph.js new file mode 100644 index 00000000..7a70d2b8 --- /dev/null +++ b/src/components/Graph/NetworkGraph.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {makeGraph} from '../../network'; + +import Graph from './Graph'; + +const NetworkGraph = ({characters, play}) => { + const graph = makeGraph(characters, play, 'cooccurence'); + + const settings = { + maxEdgeSize: 5, + defaultLabelSize: 15, + defaultEdgeColor: '#61affe65', // FIXME: this does not seem to work + defaultNodeColor: '#61affe', + labelThreshold: 5, + labelSize: 'fixed', + drawLabels: true, + mouseWheelEnabled: false, + drawEdges: true, + }; + + return ; +}; + +NetworkGraph.propTypes = { + characters: PropTypes.array.isRequired, + play: PropTypes.shape({ + relations: PropTypes.array.isRequired, + segments: PropTypes.array.isRequired, + }).isRequired, +}; + +export default NetworkGraph; diff --git a/src/components/Graph/RelationsGraph.js b/src/components/Graph/RelationsGraph.js new file mode 100644 index 00000000..296feebe --- /dev/null +++ b/src/components/Graph/RelationsGraph.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import {makeGraph} from '../../network'; +import Graph from './Graph'; + +const RelationsGraph = ({characters, play}) => { + const graph = makeGraph(characters, play, 'relation'); + + const settings = { + maxEdgeSize: 5, + defaultLabelSize: 14, + defaultEdgeColor: '#61affe65', // FIXME: this does not seem to work + defaultNodeColor: '#61affe', + edgeLabelColor: 'edge', + labelThreshold: 3, + labelSize: 'fixed', + drawLabels: true, + drawEdges: true, + drawEdgeLabels: true, + edgeLabelSize: 'proportional', + minArrowSize: 10, + }; + + return ; +}; + +RelationsGraph.propTypes = { + characters: PropTypes.array.isRequired, + play: PropTypes.shape({ + relations: PropTypes.array.isRequired, + segments: PropTypes.array.isRequired, + }).isRequired, +}; + +export default RelationsGraph; diff --git a/src/components/NetworkGraph.js b/src/components/NetworkGraph.js deleted file mode 100644 index e60c40d7..00000000 --- a/src/components/NetworkGraph.js +++ /dev/null @@ -1,75 +0,0 @@ -import React, {Component} from 'react'; -import PropTypes from 'prop-types'; -// we need to require from react-sigma/lib/ to make build work -import { - Sigma, - EdgeShapes, - NodeShapes, - ForceAtlas2, - RelativeSize, - RandomizeNodePositions, -} from 'react-sigma/lib/'; - -class NetworkGraph extends Component { - render() { - const {graph, nodeColor, edgeColor} = this.props; - - const settings = { - maxEdgeSize: 5, - defaultLabelSize: 15, - defaultEdgeColor: edgeColor, // FIXME: this does not seem to work - defaultNodeColor: nodeColor, - labelThreshold: 5, - labelSize: 'fixed', - drawLabels: true, - mouseWheelEnabled: false, - drawEdges: true, - }; - - const layoutOptions = { - iterationsPerRender: 40, - edgeWeightInfluence: 0, - timeout: 2000, - adjustSizes: false, - gravity: 3, - slowDown: 5, - linLogMode: true, - outboundAttractionDistribution: false, - strongGravityMode: false, - }; - - const layout = ; - - let sigma = null; - if (graph && graph.nodes.length > 0) { - sigma = ( - - - - - {layout} - - - - ); - } - - return sigma; - } -} - -NetworkGraph.propTypes = { - graph: PropTypes.shape({ - nodes: PropTypes.array.isRequired, - edges: PropTypes.array.isRequired, - }).isRequired, - nodeColor: PropTypes.string.isRequired, - edgeColor: PropTypes.string.isRequired, -}; - -export default NetworkGraph; diff --git a/src/components/Play.js b/src/components/Play.js index 2f891abf..11969d9c 100644 --- a/src/components/Play.js +++ b/src/components/Play.js @@ -3,15 +3,14 @@ import PropTypes from 'prop-types'; import {Container} from 'reactstrap'; import {Helmet} from 'react-helmet'; import api from '../api'; -import {makeGraph} from '../network'; import PlayDetailsHeader from './PlayDetailsHeader'; import PlayDetailsNav from './PlayDetailsNav'; import PlayDetailsTab from './PlayDetailsTab'; import CastList from './CastList'; import SourceInfo from './SourceInfo'; import DownloadLinks from './DownloadLinks'; -import NetworkGraph from './NetworkGraph'; -import RelationsGraph from './RelationsGraph'; +import NetworkGraph from './Graph/NetworkGraph'; +import RelationsGraph from './Graph/RelationsGraph'; import SpeechDistribution, {SpeechDistributionNav} from './SpeechDistribution'; import TEIPanel from './TEIPanel'; import PlayMetrics from './PlayMetrics'; @@ -21,16 +20,6 @@ import './Play.scss'; const apiUrl = api.getBaseURL(); -const edgeColor = '#61affe65'; -const nodeColor = '#61affe'; - -const nodeProps = (node) => { - const {sex} = node; - const color = sex === 'MALE' || sex === 'FEMALE' ? '#1f2448' : '#61affe'; - const type = sex === 'MALE' ? 'square' : 'circle'; - return {color, type}; -}; - const navItems = [ {name: 'network', label: 'Network'}, {name: 'relations', label: 'Relations'}, @@ -43,7 +32,7 @@ const tabNames = new Set(navItems.map((item) => item.name)); const PlayInfo = ({corpusId, playId}) => { const [play, setPlay] = useState(null); - const [graph, setGraph] = useState(null); + const [characters, setCharacters] = useState(null); const [error, setError] = useState(null); const [chartType, setChartType] = useState('sapogov'); @@ -55,10 +44,7 @@ const PlayInfo = ({corpusId, playId}) => { try { const response = await api.get(url); if (response.ok) { - const {characters, segments} = response.data; - const graph = makeGraph(characters, segments, nodeProps, edgeColor); setPlay(response.data); - setGraph(graph); } else if (response.status === 404) { setError(new Error('not found')); } else { @@ -72,6 +58,27 @@ const PlayInfo = ({corpusId, playId}) => { fetchPlay(); }, [corpusId, playId]); + useEffect(() => { + async function fetchCharacters() { + setError(null); + const url = `/corpora/${corpusId}/plays/${playId}/characters`; + try { + const response = await api.get(url); + if (response.ok) { + setCharacters(response.data); + } else if (response.status === 404) { + setError(new Error('not found')); + } else { + setError(response.originalError); + } + } catch (error) { + console.error(error); + } + } + + fetchCharacters(); + }, [corpusId, playId]); + if (error && error.message === 'not found') { return

No such play!

; } @@ -81,16 +88,11 @@ const PlayInfo = ({corpusId, playId}) => { return

Error!

; } - if (!play) { + if (!play || !characters) { return

Loading...

; } - if (!graph) { - return

No Graph!

; - } - console.log('PLAY', play); - console.log('GRAPH', graph); const groups = play.characters .filter((m) => Boolean(m.isGroup)) @@ -109,7 +111,7 @@ const PlayInfo = ({corpusId, playId}) => { let tabContent = null; let description = null; - let characters = null; + let cast = null; let metrics = null; let segments = null; @@ -143,8 +145,8 @@ const PlayInfo = ({corpusId, playId}) => { ); segments = ; } else if (tab === 'relations') { - tabContent = ; - characters = castList; + tabContent = ; + cast = castList; description = (

This tab visualises kinship and other relationship data, following the @@ -156,8 +158,8 @@ const PlayInfo = ({corpusId, playId}) => {

); } else { - tabContent = ; - characters = castList; + tabContent = ; + cast = castList; metrics = playMetrics; description = (

@@ -184,7 +186,7 @@ const PlayInfo = ({corpusId, playId}) => { { - const nodes = play.characters.map((c) => ({ - id: c.id, - label: c.name || `#${c.id}`, - })); - const edges = (play.relations || []).map((r, i) => ({ - id: i, - source: r.source, - target: r.target, - label: r.type, - color: edgeColors[r.type] || edgeColor, - type: r.directed ? 'curvedArrow' : 'curve', - })); - const graph = {nodes, edges}; - - const settings = { - maxEdgeSize: 5, - defaultLabelSize: 14, - defaultEdgeColor: edgeColor, // FIXME: this does not seem to work - defaultNodeColor: nodeColor, - edgeLabelColor: 'edge', - labelThreshold: 3, - labelSize: 'fixed', - drawLabels: true, - drawEdges: true, - drawEdgeLabels: true, - edgeLabelSize: 'proportional', - minNodeSize: 2, - minArrowSize: 10, - }; - - const layoutOptions = { - iterationsPerRender: 40, - edgeWeightInfluence: 0, - timeout: 2000, - adjustSizes: false, - gravity: 3, - slowDown: 5, - linLogMode: true, - outboundAttractionDistribution: false, - strongGravityMode: true, - }; - - const layout = ; - - let sigma = null; - if (graph && graph.nodes.length > 0) { - sigma = ( - - - - - {layout} - - - - ); - } - - return sigma; -}; - -RelationsGraph.propTypes = { - play: PropTypes.shape({ - characters: PropTypes.array.isRequired, - relations: PropTypes.array.isRequired, - }).isRequired, - nodeColor: PropTypes.string.isRequired, - edgeColor: PropTypes.string.isRequired, -}; - -export default RelationsGraph; diff --git a/src/network.js b/src/network.js index 319d07b5..759c7d61 100644 --- a/src/network.js +++ b/src/network.js @@ -1,5 +1,34 @@ // network graph utility functions +/* eslint-disable camelcase */ +const edgeColors = { + parent_of: '#6f42c1', // purple + lover_of: '#f93e3e', // red + related_with: '#fca130', // orange + associated_with: '#61affe', // blue + siblings: '#49cc90', // green + spouses: '#e83e8c', // pink + friends: '#1F2448', // navy +}; +/* eslint-enable camelcase */ + +const nodeProps = (node) => { + const {gender} = node; + const color = + gender === 'MALE' || gender === 'FEMALE' ? '#1f2448' : '#61affe'; + const type = gender === 'MALE' ? 'square' : 'circle'; + return {color, type}; +}; + +function interpolateNodeSize(minWords, maxWords, numOfWords) { + const MAX_SIZE = 30; + const MIN_SIZE = 15; + return ( + MIN_SIZE + + ((numOfWords - minWords) / (maxWords - minWords)) * (MAX_SIZE - MIN_SIZE) + ); +} + function getCooccurrences(segments) { const map = {}; segments.forEach((s) => { @@ -35,30 +64,47 @@ function getCooccurrences(segments) { return cooccurrences; } -export function makeGraph( - characters, - segments, - nodeProps = {}, - edgeColor = 'black' -) { +export function makeGraph(characters, play, type = 'cooccurence') { + const edgeColor = '#61affe65'; + const maxWords = Math.max(...characters.map((c) => c.numOfWords)); + const minWords = Math.min(...characters.map((c) => c.numOfWords)); const nodes = []; - characters.forEach((p) => { - const props = typeof nodeProps === 'function' ? nodeProps(p) : nodeProps; - const node = {id: p.id, label: p.name || `#${p.id}`, ...props}; + characters.forEach((c) => { + const props = nodeProps(c); + const nodeSize = interpolateNodeSize(minWords, maxWords, c.numOfWords); + const node = { + id: c.id, + label: c.name, + size: nodeSize || `#${c.id}`, + ...props, + }; nodes.push(node); }); - const cooccurrences = getCooccurrences(segments); - const edges = []; - cooccurrences.forEach((cooc) => { - edges.push({ - id: cooc[0] + '|' + cooc[1], - source: cooc[0], - target: cooc[1], - size: cooc[2], - // NB: we set the edge color here since the defaultEdgeColor in Sigma - // settings does not to have any effect - color: edgeColor, + + let edges = []; + if (type === 'cooccurence') { + const cooccurrences = getCooccurrences(play.segments); + cooccurrences.forEach((cooc) => { + edges.push({ + id: cooc[0] + '|' + cooc[1], + source: cooc[0], + target: cooc[1], + size: cooc[2], + // NB: we set the edge color here since the defaultEdgeColor in Sigma + // settings does not to have any effect + color: edgeColor, + }); }); - }); + } else if (type === 'relation') { + edges = (play.relations || []).map((r, i) => ({ + id: i, + source: r.source, + target: r.target, + label: r.type, + color: edgeColors[r.type] || edgeColor, + type: r.directed ? 'curvedArrow' : 'curve', + })); + } + return {nodes, edges}; }