Graphen interaktiv und Browser? I'm lost...

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
CodingForScotch
User
Beiträge: 8
Registriert: Sonntag 10. Februar 2019, 21:50

Hallo Leute,

heute wende ich mich an Euch, weil ich absolut den roten Faden verloren habe. Könnte jetzt echt ne Menge Text werden. ;-)

Darum geht's konkret:
In einem Dokument, sagen wir mal Doc1, wird auf andere Dokumente verwiesen. Auf Doc1 wird allerdings auch von
anderen Dokumenten aus verwiesen. Das bedeutet, wenn ich Änderungen in Doc1 vornehme, muss ich wissen welche
anderen Dokumente auf Doc1 verweisen.
Denn es könnte ja sein, dass ich im Zuge einer Überarbeitung von Doc1 beispielsweise das Kapitel 3 komplett
streiche. Wäre dann natürlich doof, wenn irgendein anderes Dokument auf Kapitel 3 von Doc1 verweisen würde.
Somit müsste ich alle anderen Dokumente kontrollieren (nur die, die auf Doc1 verweisen), damit keine Verweise
entstehen, die ins Leere verlaufen.

Zu Testzwecken habe ich eine Excel-Tabelle erstellt, in der ich sehen kann auf welche Dokumente mein Doc1 verweist.
Anhand der Tabelle kann ich natürlich auch sehen, welche Dokumente auf Doc1 verweisen.

Hier mal der Aufbau der Excel-Datei (das PDF heisst hier noch sop1):
Bild

Die erste Spalte listet alle Dokumente untereinander auf (Spalte: Hauptdokument) und die Spalten l1 bis l3 enthalten
die Verweise auf andere Dokumente.
Doc1 verweist beispielsweise auf Doc2, Doc3 und Doc4.
Doc5 verweist auf kein anderes Dokument, allerdings verweisen Doc3 und Doc4 auf Doc5.
Zu guter Letzt habe ich Doc6 als "Satelliten" definiert.
Doc6 zeigt auf kein Dokument und kein anderes Dokument verweist auf Doc6.

...und hier mein Code (einige Zeilen auskommentiert...Testzwecke):

Code: Alles auswählen

#!/usr/bin/env python
# -*- utf-8 -*-

import networkx as nx
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
import os

os.system('clear')
columns=0;
df = pd.read_excel("docs1.xlsx", sheet_name=0)
for zaehler in df: columns=columns+1
rows = df.size/columns
G = nx.DiGraph()

myLists = [[] for i in range(columns)]

for z1 in range(columns):
    for z2 in range(rows):
        ausgabe = df.loc[z2].values[z1]
        myLists[(z1)].append(ausgabe)


'''
# ----------------------------------
# --- Nur einen  Knoten einlesen ---
# ----------------------------------

suchstring='Doc5'
G.add_node(suchstring)
for z in range(columns):
    for j in [j for j,y in enumerate(myLists[(z)]) if y == suchstring]:
        if z != 0:
            #print("j="+str(j)+", zugehoeriges Dokument: "+str(myLists[(0)][j]))
            G.add_edge(myLists[(0)][j], suchstring, weight=0)
'''


# ----------------------------------
# ---    Alle Knoten einlesen    ---
# ----------------------------------

for suchstring in myLists[(0)]:
    #print("Suchstrings: "+str(suchstring))
    G.add_node(suchstring)
    for z in range(columns):
        for j in [j for j,y in enumerate(myLists[(z)]) if y == suchstring]:
            if z != 0:
                #print("j="+str(j)+", zugehoeriges Dokument: "+str(myLists[(0)][j]))
                G.add_edge(myLists[(0)][j], suchstring, weight=0)





# -----------------------------------------------------------------------------------------------

#Graph in entsprechender Form zeichen 
plt.figure(1, figsize=(9,9))
plt.title("====================\nBeziehungen zwischen den Docs\n====================\n")
#nx.draw_graphviz(gr, prog='fdp', node_size = sizes)
#nx.draw_networkx(G, with_labels=True)
#nx.draw_shell(G, with_labels=True, node_size=groesse)
nx.draw_shell(G, with_labels=True, node_size=2000, node_color='#9999FF', edge_color='#FF0000', arrowsize=30)
#nx.draw_spring(G, with_labels=True)
#nx.draw_random(G, with_labels=True)
#nx.draw_circular(G, with_labels=True)
#nx.draw_spectral(G, with_labels=True)
#plt.tick_params(axis='x', which='both', bottom=False, top=False, labelbottom=False)
#plt.tick_params(axis='y', which='both', right=False, left=False, labelleft=False)
for posi in ['right','top','bottom','left']:
    plt.gca().spines[posi].set_visible(False)

#plt.figure(1,figsize=(2000,2000))
#plt.savefig("graph.png", dpi = 1000)

plt.show()
Da so eine Tabelle nicht wirklich "schön" ist, habe ich ein kleines Tool geschrieben,
dass die Excel-Tabelle einliest und mir einen gerichteten Graphen baut.

Hier zwei Bilder der "Auswertung":
Bild


Bild
Bild

Ihr seht schon am Code, dass ich immer einen Block auskommentiere. Je nach dem, ob ich den gesamten Graph sehen will,
oder nur einen bestimmten Knoten. Ferner ist zu sehen, dass ich einen gerichteten Graphen mittels networkx und
matplotlib erzeuge. Die Daten für den Graphen kommen aus einer Excel-Datei, was hier aber erstmal völlig unwichtig ist.
Ich würde zum Testen auch im Code hinterlegte Daten nehmen. ;-)

Nun ist es ja so, dass der Graph total unübersichtlich wird, je mehr Dokumente und Verweise ich habe.

Bei 50 Dokumenten hätte ich ja schon 50 Knoten im Graphen. Wenn dann noch bis zu 10 Verweise pro Dokument möglich wären,
dann gute Nacht bzgl. Übersichtlichkeit.

Ich benötige eine Möglichkeit, dass der User entweder
1.) per Pull Down-Menü seinen Knoten auswählt (Doc5) und dann die Kanten hervorgehben werden, die auf diesen Knoten zeigen.
2.) oder die ganze Sache interaktiv ist. Entweder MouseOver oder Klick und die Kanten werden hervorgehoben.

Noch geiler wäre eine zusätzliche dynamische Tabelle, die alle Dokumente auflistet, die auf z.B. Doc5 verweisen, sobald
der Anwender auf Doc5 klickt.

Ich habe den Überblick bzgl. tkinter, bokeh, D3, NodeJS usw... völlig verloren.
Was genau kann ich nun eigentlich sinnvoll einsetzen?

Eine webbasierte Lösung fände ich gut, muss es aber nicht zwingend sein.

Bin echt n bisse lost und würde gerne wissen, ob Ihr ähnliche/vergleichbare Projekte habt und wie Ihr das gelöst habt.

Aktuell handelt es sich hier bei mir um ein rein privates Projekt, um mich dem Thema Visualisierung zu nähern.
Ich könnte ja schließlich auch einfach die Excel-Tabelle entsprechend auswerten und gut...aber Graphen sind ja so cool. ;-)

Vielen Dank schonmal für Eure Geduld
CfS
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Für ein Problem dessen Daten letztlich fest sind (die Dokumente verweisen ja aufeinander oder eben nicht) und es dann “nur” um die Erforschung im Browser geht ist die Antwort für mich klar D3.
Sirius3
User
Beiträge: 18270
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Anzahl der Columns ermittelt man mit der len Funktion. Die Anzahl der Rows ebenso. Beides ist aber auch gar nicht nötig, weil man zum Einen direkt über die Zeilen eines Dataframes integrieren kann, und auch über die Spalten. Listen initialisiert man nicht vor, sondern füllt sie in der Schleife. Besser man läßt Pandas direkt die Liste erzeugen, noch besser, man arbeitet immer mit dem Dataframe.
myList ist wie df ein schlechter Variablenname, weil er nicht aussagt, was der Inhalt ist.
CodingForScotch
User
Beiträge: 8
Registriert: Sonntag 10. Februar 2019, 21:50

Herzlichen Dank bzgl. des Hinweises zu D3.

Nun gut ... den Umgang mit DataFrames schaue ich mir auch nochmal genauer an.

Gibt somit für heute und morgen genug zu tun. ;-)

CfS
Benutzeravatar
__blackjack__
User
Beiträge: 14040
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@CodingForScotch: Ich weiss ja nicht ob `pandas` hier überhaupt nötig ist. Das benutzt zum Einlesen von Excel-Dateien die `xlrd`-Bibliothek und die kann man auch direkt selbst benutzen, denn ansonsten wird von der Funktionalität von `pandas` hier ja nicht wirklich etwas verwendet.

Und ich würde immer den kompletten Graphen aufbauen, denn das filtern eines Subgraphen kann man mit `networkx` erledigen. Die `ego_graph()`-Funktion nimmt einen Graphen, einen Knoten, und einen Radius (Default ist 1) und liefert einen Subgraphen mit dem gegebenen Knoten und allen Nachbarknoten die `radius` Schritte entfernt sind.

Mal als komplette Mini-Webapp die den Graphen serverseitig rendert (zusätzliche Abhängigkeiten `bottle` und `jinja2`):

Code: Alles auswählen

#!/usr/bin/env python3
from contextlib import closing
import io
from pathlib import Path

import matplotlib
from matplotlib import pyplot
import networkx
import xlrd
from bottle import (
    abort,
    default_app,
    Jinja2Template,
    jinja2_view as view,
    response,
    route,
    run,
)

matplotlib.use("agg")

SELF_PATH = Path(__file__).absolute().parent
EXCEL_FILE_PATH = SELF_PATH / "docs1.xlsx"

Jinja2Template.defaults = {
    "app": {"title": "Document Graph"},
    "get_url": default_app().get_url,
}


def read_excel(filename):
    with xlrd.open_workbook(filename, ragged_rows=True) as workbook:
        rows = workbook.sheet_by_index(0).get_rows()

        headers = next(rows, None)
        if headers is None:
            raise ValueError("empty document")
        if headers[0].value != "Hauptdokument":
            raise ValueError(
                f"unexpected value in top left cell: {headers[0].value}"
            )

        for row in rows:
            if row and row[0].value != "":
                yield (
                    row[0].value,
                    [cell.value for cell in row[1:] if cell.value],
                )


def check_document_name(name, names):
    if name is not None and name not in names:
        abort(404, f"Document {name!r} not found.")


@route("/image", name="image_all")
@route("/image/<document_name>", name="image")
def get_image(document_name=None):
    with closing(read_excel(EXCEL_FILE_PATH)) as document_references:
        graph = networkx.DiGraph(dict(document_references))

    check_document_name(document_name, graph)

    if document_name is not None:
        graph = networkx.ego_graph(graph, document_name, undirected=True)

    figure = pyplot.figure(1, figsize=(9, 9))
    try:
        networkx.draw_shell(
            graph,
            ax=figure.gca(),
            with_labels=True,
            arrowsize=30,
            node_size=2000,
            node_color="#9999FF",
            edge_color="#FF0000",
        )
        image_file = io.BytesIO()
        figure.savefig(image_file, format="png", dpi=72)
        image_file.seek(0)
        response.set_header("Content-Type", "image/png")
        return image_file
    finally:
        pyplot.close(figure)


@route("/")
@route("/document", name="document_all")
@route("/document/<document_name>", name="document")
@view("index.html")
def index(document_name=None):
    with closing(read_excel(EXCEL_FILE_PATH)) as document_references:
        document_names = [dr[0] for dr in document_references]

    check_document_name(document_name, document_names)

    return {
        "current_document_name": document_name,
        "document_names": document_names,
    }


def main():
    run(reloader=True, debug=True)


if __name__ == "__main__":
    main()
Die erwartet die Exceldatei im gleichen Verzeichnis, und folgende `index.html` in einem `views/`-Unterverzeichnis:

Code: Alles auswählen

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{app.title}}</title>
</head>
<body>
  <h1>
    {{app.title}}
    {%- if current_document_name %} for {{current_document_name}}{% endif %}
  </h1>
  <p>Document names: <strong><a href="{{get_url('document_all')}}">All</a></strong>,
  {% for document_name in document_names -%}
    <a href="{{get_url('document', document_name=document_name)}}">{{document_name}}</a>{{"." if loop.last else ", "}}
  {%- endfor %}</p>
  <img src="{{get_url('image', document_name=current_document_name) if current_document_name else get_url('image_all')}}">
</body>
</html>
„A life is like a garden. Perfect moments can be had, but not preserved, except in memory. LLAP” — Leonard Nimoy's last tweet.
Antworten