Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 171 additions & 5 deletions api/app.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import json
import os
from io import StringIO

import networkx as nx
from datacitekit.extractors import extract_doi
from datacitekit.related_works import get_full_corpus_doi_attributes
from datacitekit.resource_type_graph import RelatedWorkReports
from flask import Flask, jsonify
from pyvis.network import Network

DOI_API = os.getenv("DATACITE_API_URL", "https://api.stage.datacite.org/dois/")
app = Flask(__name__)


@app.route("/api/doi/related-graph/<path:doi>", methods=["GET"])
def related_works(doi):
def get_graph_data(doi):
doi = extract_doi(doi)
if not doi:
return jsonify({"error": "Does not match DOI format"}), 400

full_doi_attributes = get_full_corpus_doi_attributes(
doi_query=doi, parser=RelatedWorkReports.parser, api_url=DOI_API
)
Expand All @@ -25,8 +25,174 @@ def related_works(doi):
non_zero_nodes = [agg for agg in report.aggregate_counts if agg["count"] > 0]
graph = {"nodes": non_zero_nodes, "links": report.type_connection_report}

return graph


@app.route("/api/doi/related-graph/<path:doi>", methods=["GET"])
def related_works(doi):
doi = extract_doi(doi)
if not doi:
return jsonify({"error": "Does not match DOI format"}), 400

graph = get_graph_data(doi)

return jsonify(graph)


@app.route("/api/doi/network-view/<path:doi>", methods=["GET"])
def network_view(doi):
doi = extract_doi(doi)
if not doi:
return jsonify({"error": "Does not match DOI format"}), 400
network = get_network(doi)
return network.generate_html()


@app.route("/api/doi/network-graph/<path:doi>", methods=["GET"])
def network_graph(doi):
network = get_network(doi)

nodes, edges, heading, height, width, options = network.get_network_data()
return jsonify(
{
"nodes": nodes,
"edges": edges,
"options": json.loads(options),
}
)


def get_network(doi):
graph_data = get_graph_data(doi)

# Define the color mapping
domain = [
"Audiovisual",
"Book",
"Book Chapter",
"Collection",
"Computational Notebook",
"Conference Paper",
"Conference Proceeding",
"Data Paper",
"Dataset",
"Dissertation",
"Event",
"Image",
"Instrument",
"Interactive Resource",
"Journal",
"Journal Article",
"Model",
"Output Management Plan",
"Peer Review",
"Physical Object",
"Preprint",
"Project",
"Report",
"Service",
"Software",
"Sound",
"Standard",
"Study Registration",
"Text",
"Workflow",
"Other",
"People",
"Organizations",
]
color_range = [
"#AEC7E8",
"#FF7F0E",
"#FFBB78",
"#D62728",
"#FF9896",
"#9467BD",
"#C5B0D5",
"#8C564B",
"#1F77B4",
"#C49C94",
"#E377C2",
"#F7B6D2",
"#35424A",
"#7F7F7F",
"#C7C7C7",
"#BCBD22",
"#DBDB8D",
"#17BECF",
"#9EDAE5",
"#3182BD",
"#6BAED6",
"#AB8DF8",
"#9ECAE1",
"#C6DBEF",
"#E6550D",
"#FD8D3C",
"#FDAE6B",
"#6DBB5E",
"#FDD0A2",
"#9F4639",
"#C59088",
"#A83",
"#FAD",
]

# Define the color mapping

color_map = dict(zip(domain, color_range))

G = nx.DiGraph()
net = Network(
height="750px",
width="100%",
notebook=False,
)

# Add nodes with their attributes
for node in graph_data["nodes"]:
G.add_node(node["title"], count=node["count"])

# Add edges with their attributes
for link in graph_data["links"]:
G.add_edge(link["source"], link["target"], count=link["count"])

# Create a PyVis network
net = Network(notebook=True, directed=True, cdn_resources="remote")
# Load the NetworkX graph into the PyVis network
net.from_nx(G)
# Customize the appearance of nodes and edges
for node in net.nodes:
node["label"] = f"{node['id']}\n {G.nodes[node['id']]['count']}"
node["value"] = G.nodes[node["id"]]["count"]
# Set color based on type mapping. Default to grey if no match
node["color"] = color_map.get(node["id"], "#999999")

for edge in net.edges:
edge["title"] = f"{edge['from']} -> {edge['to']}: {edge['count']}"
edge["width"] = edge["count"] * 0.5

net.set_options(
"""
var options = {
"nodes": {
"labelHighlightBold": true,
"font": {
"align": "center"
}
},
"interaction": {
"zoomView": false,
"dragView": true,
"multiselect": false,
"navigationButtons": true,
"hoverConnectedEdges": true
}
}
"""
)

return net


if __name__ == "__main__":
app.run(debug=False)
1 change: 1 addition & 0 deletions api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
Flask
https://github.com/datacite/datacitekit/archive/main.zip
pyvis
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"vega": "^5.30.0",
"vega-embed": "^6.25.0",
"vega-lite": "^5.19.0",
"vis-network": "^9.1.9",
"wait-on": "^5.0.1"
},
"devDependencies": {
Expand Down
44 changes: 44 additions & 0 deletions src/app/doi.org/[...doi]/NetworkGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import React from 'react'
import { Col, Row } from "src/components/Layout";
import { getRelatedNetworkGraph, GraphData } from 'src/data/queries/relatedWorks'
import VisGraph from 'src/components/VisGraph/VisGraph'
// import Graph from 'react-graph-vis'
// import ForceDirectedGraph from 'src/components/ForceDirectedGraph/ForceDirectedGraph'
import EmptyChart from 'src/components/EmptyChart/EmptyChart'
import styles from "./RelatedAggregateGraph.module.scss"

interface Props {
doi: string
}

export default async function NetworkGraph( {doi}: Props) {
const data :GraphData = await getRelatedNetworkGraph(doi)
const gData = {
'nodes':data.nodes,
'edges':data.edges,
}
const titleText = "Connections"
const emptyTitleText = "No connections"
const helpText = 'The “relatedIdentifier” and “resourceTypeGeneral” fields in the metadata of the primary DOI and related work DOIs were used to generate this graph.'
const explanitoryText="The network graph visualizes the connections between different work types. It shows the number of instances of each work type, and hovering over a connection reveals the number of links between any two types."
const graphExists = data.nodes.length >0;
const innerGraph = (graphExists) ?
<VisGraph
graph={ gData }
titleText={titleText}
options={data.options}
tooltipText={helpText}
/>:<EmptyChart title={emptyTitleText}/>

return (<Row>
<Col mdOffset={3} className="panel panel-transparent">
<div className="panel-body">
{innerGraph}
{ graphExists &&
<p className={styles.explanitoryText}>{explanitoryText}</p>
}
</div>
</Col>
</Row>
)
}
8 changes: 4 additions & 4 deletions src/app/doi.org/[...doi]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { redirect, notFound } from 'next/navigation'
import Script from 'next/script'
import truncate from 'lodash/truncate'

import { rorFromUrl, isProject, isDMP, isAwardGrant } from 'src/utils/helpers'
import { rorFromUrl, isProject, isDMP } from 'src/utils/helpers'

import apolloClient from 'src/utils/apolloClient/apolloClient'
import { CROSSREF_FUNDER_GQL } from 'src/data/queries/crossrefFunderQuery'
import Content from './Content'
import { DOI_METADATA_QUERY, MetadataQueryData, MetadataQueryVar } from 'src/data/queries/doiQuery'
import RelatedContent from './RelatedContent'
import RelatedAggregateGraph from './RelatedAggregateGraph'
import NetworkGraph from './NetworkGraph'
import Loading from 'src/components/Loading/Loading'


Expand Down Expand Up @@ -145,11 +145,11 @@ export default async function Page({ params, searchParams }: Props) {
if (!data) notFound()

const showSankey = isDMP(data.work) || isProject(data.work)
const showGraph = isDMP(data.work) || isProject(data.work) || isAwardGrant(data.work)
const showGraph = true

const projectGraph = showGraph
? <Suspense>
<RelatedAggregateGraph doi={doi} />
<NetworkGraph doi={doi} />
</Suspense>
: ''

Expand Down
11 changes: 11 additions & 0 deletions src/components/VisGraph/VisGraph.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.chartTitle {
font-size: 21px;
font-weight: 700;

color: #34495E;

display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
87 changes: 87 additions & 0 deletions src/components/VisGraph/VisGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use client'
import React, { useRef, useEffect } from 'react'
import HelpIcon from '../HelpIcon/HelpIcon'
import {Node, Edge, Options} from 'src/data/queries/relatedWorks'
import {DataSet, Network} from 'vis-network/standalone'
import styles from './VisGraph.module.scss'

interface VisNetworkProps {
nodes: Node[];
edges: Edge[];
options?: Options;
}

const VisNetwork: React.FC<VisNetworkProps> = ({ nodes, edges, options }) => {
const networkRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const container = networkRef.current;

if (container) {
const data = {
nodes: new DataSet<Node>(nodes),
edges: new DataSet<Edge>(edges),
};

// Network options will be overridden if provided
const networkOptions: Options = options || {
autoResize: true,
height: '100%',
width: '100%',
interaction: {
zoomView: false,
dragView: true,
dragNodes: true,
hover: true,
hoverConnectedEdges: true,
selectConnectedEdges: true,
navigationButtons: true,
},
physics: {
enabled: true,
},
};

const network = new Network(container, data, networkOptions);

return () => {
network.destroy();
};
}
}, [nodes, edges, options]);

return <div ref={networkRef} style={{ width: '100%', height: '500px' }} />;
};



type VisGraphData = {
nodes: Node[]
edges: Edge[]
}
type Props = {
titleText: string | string[]
graph: VisGraphData
options?: Options
tooltipText?: string
}

const VisGraph: React.FunctionComponent<Props> = ({ titleText, graph, options, tooltipText }) => {
return (
<div className="panel panel-transparent">
<div className="panel-body production-chart">
<div className={styles.chartTitle}>
{titleText}
{tooltipText && <HelpIcon text={tooltipText} padding={25} position='inline' color='#34495E' />}
</div>
<VisNetwork
nodes={graph.nodes}
edges={graph.edges}
options={options}
/>
</div>
</div>
)
}

export default VisGraph
Loading