Skip to content
Open
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
20 changes: 20 additions & 0 deletions eu_fact_force/exploration/cytoscape/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Exploration - Dash Cytoscape

This folder contains a first version of a local Dash app to explore Cytoscape capabilities.

# Repo structure
- `app.py`: the main app file.
- `assets/`: the app asset folder, with custom css, icon and plotly template.
- `utils/`: app utility files, including d4g colors and random graph generator.


## Setup
- Install `graph` group depedencies using `uv sync --group graph`.
- Start app from here with `pyhon app.py`.
- Visit `http://127.0.0.1:8050/` to see the app on your local.

## App overview
- This app contains a search bar with an "Search" button to simulate search.
- On search button click, a random network graph will be generated.
- Clicking on a node in the chart will open an offcanevas displaying node metadata.
- A list of all nodes in the graph will also be generated, with node metadatWHen a in each element.
254 changes: 254 additions & 0 deletions eu_fact_force/exploration/cytoscape/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
from dash import Dash, dcc, html
from dash.dependencies import Input, Output, State
from dash.exceptions import PreventUpdate
import dash_bootstrap_components as dbc
import plotly.io as pio
import plotly.graph_objects as go
import dash_cytoscape as cyto

import json

from utils.colors import AppColors
from utils.graph import RandomGraphGenerator

# Plotly template
with open("assets/template.json", "r") as f:
debate_template = json.load(f)
pio.templates["app_template"] = go.layout.Template(debate_template)
pio.templates.default = "app_template"

# Dash app
app = Dash(
__name__,
suppress_callback_exceptions=True,
external_stylesheets=["custom.css", dbc.themes.BOOTSTRAP],
)

# Dash params
DASHBOARD_NAME = "EU Fact Force"

# Custom dash app tab and logo
app.title = DASHBOARD_NAME
app._favicon = "icon.png"

# Graph generator
generator = RandomGraphGenerator()

# Header
header = html.Div(
dbc.Row(
dbc.Col(
html.Div(
[
html.Img(src="assets/icon.png", alt="image", height=50),
html.H1(
DASHBOARD_NAME,
style={
"color": AppColors.blue,
"font-weight": "bold",
"margin": "0",
"padding": "0",
},
),
],
style={
"display": "flex",
"alignItems": "center",
"gap": "0px",
},
),
width=12,
),
className="g-0",
),
style={
"padding": "1rem",
"background-color": AppColors.green,
"position": "fixed",
"width": "100%",
"zIndex": 1000,
},
)
# Content
search_bar = html.Div(
children=[
dbc.Row(
[
dbc.Col(
dbc.Input(
id="search-input",
placeholder="Naratif de désinformation...",
style={"overflow": "hidden"},
)
),
dbc.Col(
dbc.Button(
"Rechercher",
id="search-button",
color="primary",
className="me-1",
n_clicks=0,
disabled=True,
),
width="auto",
),
],
align="center",
)
],
id="search",
style={
"border-radius": "15px",
"padding": "20px",
"background-color": AppColors.white,
},
)

graph = html.Div(
children=cyto.Cytoscape(
id="graph-cytoscape",
stylesheet=generator.stylesheet,
layout={"name": "cose"},
style={"width": "100%", "height": "400px"},
),
id="graph",
style={
"border-radius": "15px",
"padding": "20px",
"background-color": AppColors.white,
"display": "none",
},
)

list_elements = html.Div(
id="list",
style={
"border-radius": "15px",
"padding": "20px",
"background-color": AppColors.white,
"display": "none",
},
)

offcanevas = dbc.Offcanvas(
id="offcanvas",
title="Selectionné",
is_open=False,
placement="end",
style={"width": "50%"},
)


content = html.Div(
[search_bar, html.Br(), graph, html.Br(), list_elements, offcanevas],
style={
"margin-left": "1rem",
"margin-right": "1rem",
"padding": "1rem",
"padding-top": "120px",
},
id="page-content",
)


# Layout
app.layout = html.Div([dcc.Location(id="url", refresh=False), header, content])


# --------------------
# Callbacks
# --------------------


# Callback search button activate
@app.callback(
Output("search-button", "disabled"),
inputs=[Input("search-input", "value"), Input("graph", "children")],
)
def activate_search_buton(search_text, graph):
if search_text is None or search_text == "":
return True
else:
return False


# Callback update graph
@app.callback(
[
Output("graph-cytoscape", "elements"),
Output("list", "children"),
Output("graph", "style"),
Output("list", "style"),
Output("search-input", "value"),
],
inputs=[Input("search-button", "n_clicks")],
state=[State("search-input", "value")],
prevent_updates=True,
)
def update_graph(n_clicks, search_text):
if n_clicks > 0:
graph_elements = generator.get_graph_data()
list_elements = [x["data"] for x in graph_elements if "id" in x["data"]]
list_elements = sorted(list_elements, key=lambda x: x["id"])
return [
graph_elements,
dbc.Accordion(
[
dbc.AccordionItem(
dcc.Markdown(
"\n".join(
[f"- {key.capitalize()} : __{x[key]}__" for key in x]
)
),
title=x["label"],
)
for x in list_elements
],
start_collapsed=True,
),
{
"border-radius": "15px",
"padding": "20px",
"background-color": AppColors.white,
"display": "block",
},
{
"border-radius": "15px",
"padding": "20px",
"background-color": AppColors.white,
"display": "block",
},
"",
]
else:
raise PreventUpdate


# Callback show selected element
@app.callback(
[
Output("offcanvas", "is_open"),
Output("offcanvas", "children"),
],
inputs=[Input("graph-cytoscape", "tapNodeData")],
state=[State("offcanvas", "is_open")],
prevent_initial_call=True,
)
def toggle_offcanvas(node_data, is_open):
if node_data:
return [
not is_open,
dcc.Markdown(
"\n".join(
[
f"- {key.capitalize()} : __{node_data[key]}__"
for key in node_data
if key != "timeStamp"
]
)
),
]


if __name__ == "__main__":
app.run(debug=True)
29 changes: 29 additions & 0 deletions eu_fact_force/exploration/cytoscape/assets/custom.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/* Colors */
:root {
/* Principal Colors */
--color-0: #CBDF40;
--color-1: #36C3D7;
--color-2: #F5A414;
--color-gray: #F1F1F1;
}


/* Body and text style */
body {
font-family: "Helvetica", sans-serif;
background-color: var(--color-gray);
font-size: 14px;
}

h1,
h2,
h3,
h4,
h5,
h6 {
color: black;
}

hr {
color: black;
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
46 changes: 46 additions & 0 deletions eu_fact_force/exploration/cytoscape/assets/template.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"layout": {
"title": {
"x": 0.02
},
"colorway": [
"#CBDF40",
"#36C3D7",
"#F5A414"
],
"font": {
"size": 16,
"color": "black"
},
"plot_bgcolor": "white",
"paper_bgcolor": "rgb(0,0,0,0)",
"xaxis": {
"tickfont": {
"size": 12
},
"title": {
"font": {
"size": 14
}
},
"showgrid": false
},
"yaxis": {
"tickfont": {
"size": 12
},
"title": {
"font": {
"size": 14
}
},
"showgrid": false
},
"legend": {
"font": {
"size": 12
},
"bgcolor": "white"
}
}
}
6 changes: 6 additions & 0 deletions eu_fact_force/exploration/cytoscape/utils/colors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class AppColors:
green = "#CBDF40"
blue = "#36C3D7"
orange = "#F5A414"
grey = "#F1F1F1"
white = "#FFFFFF"
Loading
Loading