don't display quiet nodes on graph

This commit is contained in:
Tao Bror Bojlén 2019-04-19 15:29:45 +01:00
parent 8d00ba9372
commit e9b8901aa5
No known key found for this signature in database
GPG key ID: C6EC7AAB905F9E6F
5 changed files with 132 additions and 89 deletions

View file

@ -32,8 +32,6 @@ class NodeView(viewsets.ReadOnlyModelViewSet):
""" """
Endpoint to get a list of the graph's nodes in a SigmaJS-friendly format. Endpoint to get a list of the graph's nodes in a SigmaJS-friendly format.
""" """
queryset = Instance.objects.filter(status='success')\ queryset = Instance.objects.filter(status='success', x_coord__isnull=False, y_coord__isnull=False, user_count__isnull=False)\
.filter(x_coord__isnull=False)\ .exclude(sources__isnull=True, targets__isnull=True)
.filter(y_coord__isnull=False)\
.filter(user_count__isnull=False)
serializer_class = NodeSerializer serializer_class = NodeSerializer

View file

@ -0,0 +1,24 @@
# Generated by Django 2.1.7 on 2019-04-19 13:46
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('scraper', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='edge',
name='source',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='targets', to='scraper.Instance'),
),
migrations.AlterField(
model_name='edge',
name='target',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sources', to='scraper.Instance'),
),
]

View file

@ -51,8 +51,8 @@ class Edge(models.Model):
It aggregates stats from the asymmetrical PeerRelationship to a symmetrical one that's suitable for serving It aggregates stats from the asymmetrical PeerRelationship to a symmetrical one that's suitable for serving
to the front-end. to the front-end.
""" """
source = models.ForeignKey(Instance, related_name='+', on_delete=models.CASCADE) source = models.ForeignKey(Instance, related_name='targets', on_delete=models.CASCADE)
target = models.ForeignKey(Instance, related_name='+', on_delete=models.CASCADE) target = models.ForeignKey(Instance, related_name='sources', on_delete=models.CASCADE)
weight = models.FloatField(blank=True, null=True) weight = models.FloatField(blank=True, null=True)
# Metadata # Metadata

View file

@ -1,102 +1,107 @@
import * as React from 'react'; import * as React from "react";
import { connect } from 'react-redux'; import { connect } from "react-redux";
import { Sigma, Filter, ForceAtlas2 } from 'react-sigma'; import { Sigma, Filter, ForceAtlas2 } from "react-sigma";
import { selectAndLoadInstance } from '../redux/actions'; import { selectAndLoadInstance } from "../redux/actions";
import { ErrorState } from './ErrorState'; import { ErrorState } from "./ErrorState";
const STYLE = { const STYLE = {
bottom: "0", bottom: "0",
left: "0", left: "0",
position: "absolute", position: "absolute",
right: "0", right: "0",
top: "50px", top: "50px"
} };
const DEFAULT_NODE_COLOR = "#CED9E0"; const DEFAULT_NODE_COLOR = "#CED9E0";
const SELECTED_NODE_COLOR = "#48AFF0"; const SELECTED_NODE_COLOR = "#48AFF0";
const SETTINGS = { const SETTINGS = {
defaultEdgeColor: "#5C7080", defaultEdgeColor: "#5C7080",
defaultLabelColor: "#F5F8FA", defaultLabelColor: "#F5F8FA",
defaultNodeColor: DEFAULT_NODE_COLOR, defaultNodeColor: DEFAULT_NODE_COLOR,
drawEdges: true, drawEdges: true,
drawLabels: true, drawLabels: true,
edgeColor: "default", edgeColor: "default",
labelColor: "default", labelColor: "default",
labelThreshold: 10, labelThreshold: 10,
maxEdgeSize: 1, maxEdgeSize: 1,
minEdgeSize: 0.3, minEdgeSize: 0.3
} };
class GraphImpl extends React.Component { class GraphImpl extends React.Component {
constructor(props) {
super(props);
this.sigmaComponent = React.createRef();
}
constructor(props) { render() {
super(props); let graph = this.props.graph;
this.sigmaComponent = React.createRef(); if (!graph) {
return <ErrorState />;
} }
// Check that all nodes have size & coordinates; otherwise the graph will look messed up
const lengthBeforeFilter = graph.nodes.length;
graph = { ...graph, nodes: graph.nodes.filter(n => n.size && n.x && n.y) };
if (graph.nodes.length !== lengthBeforeFilter) {
// tslint:disable-next-line:no-console
console.error(
"Some nodes were missing details: " +
this.props.graph.nodes.filter(n => !n.size || !n.x || !n.y).map(n => n.label)
);
return <ErrorState />;
}
return (
<Sigma
graph={graph}
renderer="webgl"
settings={SETTINGS}
style={STYLE}
onClickNode={this.onClickNode}
onClickStage={this.onClickStage}
ref={this.sigmaComponent}
>
<Filter neighborsOf={this.props.currentInstanceName} />
<ForceAtlas2 iterationsPerRender={1} timeout={10000} />
</Sigma>
);
}
render() { componentDidUpdate() {
let graph = this.props.graph; const sigma = this.sigmaComponent && this.sigmaComponent.current.sigma;
if (!graph) { // Check if sigma exists s.t. nothing breaks if the graph didn't load (for whatever reason)
return <ErrorState />; if (sigma) {
} sigma.graph.nodes().map(this.colorNodes);
// Check that all nodes have size & coordinates; otherwise the graph will look messed up sigma.refresh();
const lengthBeforeFilter = graph.nodes.length;
graph = {...graph, nodes: graph.nodes.filter(n => n.size && n.x && n.y)};
if (graph.nodes.length !== lengthBeforeFilter) {
// tslint:disable-next-line:no-console
console.error("Some nodes were missing details: " + this.props.graph.nodes.filter(n => !n.size || !n.x || !n.y).map(n => n.label));
return <ErrorState />;
}
return (
<Sigma
graph={graph}
renderer="webgl"
settings={SETTINGS}
style={STYLE}
onClickNode={this.onClickNode}
onClickStage={this.onClickStage}
ref={this.sigmaComponent}
>
<Filter neighborsOf={this.props.currentInstanceName} />
<ForceAtlas2 iterationsPerRender={1} timeout={10000}/>
</Sigma>
)
} }
}
componentDidUpdate() { onClickNode = e => {
const sigma = this.sigmaComponent && this.sigmaComponent.current.sigma; this.props.selectAndLoadInstance(e.data.node.label);
// Check if sigma exists s.t. nothing breaks if the graph didn't load (for whatever reason) };
if (sigma) {
sigma.graph.nodes().map(this.colorNodes);
sigma.refresh();
}
}
onClickNode = (e) => { onClickStage = e => {
this.props.selectAndLoadInstance(e.data.node.label); // Deselect the instance (unless this was a drag event)
if (!e.data.captor.isDragging) {
this.props.selectAndLoadInstance(null);
} }
};
onClickStage = (e) => { colorNodes = n => {
// Deselect the instance (unless this was a drag event) if (this.props.currentInstanceName && n.id === this.props.currentInstanceName) {
if (!e.data.captor.isDragging) { n.color = SELECTED_NODE_COLOR;
this.props.selectAndLoadInstance(null); } else {
} n.color = DEFAULT_NODE_COLOR;
}
colorNodes = (n) => {
if (this.props.currentInstanceName && n.id === this.props.currentInstanceName) {
n.color = SELECTED_NODE_COLOR;
} else {
n.color = DEFAULT_NODE_COLOR;
}
} }
};
} }
const mapStateToProps = (state) => ({ const mapStateToProps = state => ({
currentInstanceName: state.currentInstance.currentInstanceName, currentInstanceName: state.currentInstance.currentInstanceName,
graph: state.data.graph, graph: state.data.graph
}) });
const mapDispatchToProps = (dispatch) => ({ const mapDispatchToProps = dispatch => ({
selectAndLoadInstance: (instanceName) => dispatch(selectAndLoadInstance(instanceName)), selectAndLoadInstance: instanceName => dispatch(selectAndLoadInstance(instanceName))
}) });
export const Graph = connect(mapStateToProps, mapDispatchToProps)(GraphImpl) export const Graph = connect(
mapStateToProps,
mapDispatchToProps
)(GraphImpl);

View file

@ -80,6 +80,12 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
return this.renderMissingDataState(); return this.renderMissingDataState();
} else if (this.props.instanceLoadError) { } else if (this.props.instanceLoadError) {
return <ErrorState />; return <ErrorState />;
} else if (
this.props.graph &&
this.props.instanceName &&
this.props.graph.nodes.map(n => n.id).indexOf(this.props.instanceName) < 0
) {
return this.renderQuietInstanceState();
} }
return ( return (
<div> <div>
@ -299,6 +305,16 @@ class SidebarImpl extends React.Component<ISidebarProps, ISidebarState> {
); );
}; };
private renderQuietInstanceState = () => {
return (
<NonIdealState
icon={IconNames.CLEAN}
title="No interactions"
description="Users on this instance have not publicly interacted with any other instances recently. "
/>
);
};
private openInstanceLink = () => { private openInstanceLink = () => {
window.open("https://" + this.props.instanceName, "_blank"); window.open("https://" + this.props.instanceName, "_blank");
}; };