From c8c2010bf29dbef998f700e7654407de6207670d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A1s=20Schmelczer?= Date: Tue, 12 Apr 2022 20:58:00 +0200 Subject: [PATCH] UX improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: AndrĂ¡s Schmelczer --- .../src/good_ai/good_ai/helper/__init__.py | 1 + .../good_ai/helper/text_to_hex_color.py | 13 ++ .../good_ai/good_ai/metrics/assets/github.png | Bin 0 -> 4268 bytes .../good_ai/good_ai/metrics/assets/index.css | 113 +++++++++++++++++- .../good_ai/metrics/create_dash_app.py | 61 +++++++--- .../good_ai/metrics/get_description.py | 19 ++- .../src/good_ai/good_ai/metrics/get_footer.py | 22 ++++ good_ai/src/good_ai/good_ai/views/query.py | 14 +++ 8 files changed, 217 insertions(+), 26 deletions(-) create mode 100644 good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py create mode 100644 good_ai/src/good_ai/good_ai/metrics/assets/github.png create mode 100644 good_ai/src/good_ai/good_ai/metrics/get_footer.py diff --git a/good_ai/src/good_ai/good_ai/helper/__init__.py b/good_ai/src/good_ai/good_ai/helper/__init__.py index e84cb5d..32e14ef 100644 --- a/good_ai/src/good_ai/good_ai/helper/__init__.py +++ b/good_ai/src/good_ai/good_ai/helper/__init__.py @@ -1,2 +1,3 @@ from .get_args import get_args from .snake_case_to_text import snake_case_to_text +from .text_to_hex_color import text_to_hex_color diff --git a/good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py b/good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py new file mode 100644 index 0000000..e906a0c --- /dev/null +++ b/good_ai/src/good_ai/good_ai/helper/text_to_hex_color.py @@ -0,0 +1,13 @@ +import colorsys +from hashlib import md5 + + +def text_to_hex_color(text: str) -> str: + ascii_bytes = text.encode("ascii") + digest = md5( + ascii_bytes + ).hexdigest() # the built-in hash function is salted differently in each process + integer = int(digest, 16) + hue = integer % 6311 / 6311.0 + rgb = colorsys.hsv_to_rgb(hue, 0.75, 0.6) + return "#" + "".join("%02X" % round(i * 255) for i in rgb) diff --git a/good_ai/src/good_ai/good_ai/metrics/assets/github.png b/good_ai/src/good_ai/good_ai/metrics/assets/github.png new file mode 100644 index 0000000000000000000000000000000000000000..ea6ff545a246caa64074ba809bbc86fcb8589071 GIT binary patch literal 4268 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 XLyRvu{aX3a{{tgEGu={c*U0|?Dtn-9 literal 0 HcmV?d00001 diff --git a/good_ai/src/good_ai/good_ai/metrics/assets/index.css b/good_ai/src/good_ai/good_ai/metrics/assets/index.css index f5469de..5e48b88 100644 --- a/good_ai/src/good_ai/good_ai/metrics/assets/index.css +++ b/good_ai/src/good_ai/good_ai/metrics/assets/index.css @@ -1,3 +1,112 @@ +:root { + --accent-color: #47c2d0; + --error-color: #a30808; + --background-color: #edf5f6; + --small-padding: 10px; + --medium-padding: 20px; + --large-padding: 40px; + --border-radius: 10px; + --shadow: 0 4px 6px -1px rgb(0 0 0 / 10%), 0 2px 4px -1px rgb(0 0 0 / 6%); +} + +* { + margin: 0; + box-sizing: border-box; +} + body { - background-color: hotpink; -} \ No newline at end of file + background-color: var(--background-color); + font-family: Arial, Helvetica, sans-serif; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + margin: var(--medium-padding) 0 var(--small-padding) 0; +} + +.glance, +.table-container, +.parallel-coords { + padding: var(--large-padding); + margin: var(--large-padding); + border-radius: var(--border-radius); + box-shadow: var(--shadow); + background-color: white; + overflow: hidden; +} + +.glance { + display: flex; +} + +.glance .description { + width: 350px; + display: flex; + flex-direction: column; + justify-content: center; +} + +.glance .dash-graph { + flex: 1; +} + +.table-container { + padding: 0; +} + +.table-container h2 { + padding: var(--small-padding); + margin: 0; +} + +.table-container h2, +footer.watermark { + opacity: 0.35; +} + +.dash-spreadsheet { + overflow: auto; +} + +th, +td { + max-width: 350px; +} + +.parallel-coords { + padding: 0; +} + +footer.watermark { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--large-padding); + background-color: #ddd; + position: relative; +} + +footer.watermark div { + display: flex; + flex-direction: column; +} + +h6 { + font-size: 3rem; + margin-top: 0; +} + +a img { + width: 64px; + height: 64px; + cursor: pointer; + transition: transform 300ms; +} + +a img:hover { + transform: scale(1.1); +} diff --git a/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py b/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py index f214e3d..24c12d9 100644 --- a/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py +++ b/good_ai/src/good_ai/good_ai/metrics/create_dash_app.py @@ -10,13 +10,16 @@ from flask import Flask from good_ai.utilities.unique import unique from ..context import get_context -from ..helper import snake_case_to_text +from ..helper import snake_case_to_text, text_to_hex_color from ..views import SortBy from .get_description import get_description from .get_filter_from_datatable import get_filter_from_datatable +from .get_footer import get_footer def create_dash_app(function_name: str) -> Flask: + accent_color = text_to_hex_color(function_name) + flask_app = Flask(__name__) app = Dash( function_name, @@ -32,24 +35,37 @@ def create_dash_app(function_name: str) -> Flask: documents = get_context().persistence.get_documents() df = pd.DataFrame(documents) + execution_time_histogram = dcc.Graph(config={"displaylogo": False}) + table = dash_table.DataTable( + columns=[{"name": i, "id": i} for i in df.columns], + page_current=0, + page_size=20, + page_action="custom", + filter_action="custom", + filter_query="", + sort_action="custom", + sort_mode="multi", + sort_by=[ + {"column_id": "created", "direction": "desc"}, + ], + ) + app.layout = html.Div( [ - get_description(function_name), html.Div( - table := dash_table.DataTable( - columns=[{"name": i, "id": i} for i in df.columns], - page_current=0, - page_size=20, - page_action="custom", - filter_action="custom", - filter_query="", - sort_action="custom", - sort_mode="multi", - sort_by=[], - ), + [ + get_description( + function_name=function_name, accent_color=accent_color + ), + execution_time_histogram, + ], + className="glance", ), - execution_time_histogram := dcc.Graph(), - parallel_coords := dcc.Graph(), + html.Div([html.H2("Latest traces"), table], className="table-container"), + parallel_coords := dcc.Graph( + className="parallel-coords", config={"displaylogo": False} + ), + get_footer(), interval := dcc.Interval( interval=4 * 1000, # in milliseconds n_intervals=0, @@ -100,15 +116,23 @@ def create_dash_app(function_name: str) -> Flask: ) df = pd.DataFrame(rows) - return px.histogram( + fig = px.histogram( df, x="execution_time_ms", labels={"execution_time_ms": "Execution time (ms)"}, nbins=20, - title="Execution times", + height=400, log_y=True, + color_discrete_sequence=[accent_color], ) + fig.update_layout( + autosize=True, + margin=dict(l=0, r=0, b=0, t=0, pad=0), + ) + + return fig + @app.callback( Output(parallel_coords, "figure"), Input(table, "filter_query"), @@ -131,7 +155,8 @@ def create_dash_app(function_name: str) -> Flask: get_dimension_descriptor(df, c) for c in df.columns if not c.startswith("arg:") and c not in {"id", "created"} - ] + ], + line_color=accent_color, ) ) diff --git a/good_ai/src/good_ai/good_ai/metrics/get_description.py b/good_ai/src/good_ai/good_ai/metrics/get_description.py index 8976963..4059688 100644 --- a/good_ai/src/good_ai/good_ai/metrics/get_description.py +++ b/good_ai/src/good_ai/good_ai/metrics/get_description.py @@ -1,12 +1,11 @@ -from dash.dcc import Markdown +from dash import dcc, html from ..helper import snake_case_to_text -def get_description(function_name: str) -> Markdown: +def get_description(function_name: str, accent_color: str) -> html.Div: markdown_text = f""" - # {snake_case_to_text(function_name)} - metrics - > A human-friendly framework for robust end-to-end AI deployments + View the live data of your deployments here. ## Using the API @@ -14,7 +13,15 @@ def get_description(function_name: str) -> Markdown: ## Metrics - The recent traces and aggregated metrics are presented below. + Recent traces and aggregated metrics are presented below. Try filtering the table. """ - return Markdown(markdown_text) + return html.Div( + [ + html.H1( + f"{snake_case_to_text(function_name)} - metrics", + style={"color": accent_color}, + ), + dcc.Markdown(markdown_text, className="description"), + ] + ) diff --git a/good_ai/src/good_ai/good_ai/metrics/get_footer.py b/good_ai/src/good_ai/good_ai/metrics/get_footer.py new file mode 100644 index 0000000..d7239a3 --- /dev/null +++ b/good_ai/src/good_ai/good_ai/metrics/get_footer.py @@ -0,0 +1,22 @@ +from dash import html + + +def get_footer() -> html.Footer: + return html.Footer( + [ + html.Div( + [ + html.H6("GoodAI"), + html.P( + "A human-friendly framework for robust end-to-end AI deployments." + ), + ] + ), + html.A( + html.Img(src="/assets/github.png"), + href="https://github.com/ScoutinScience/good-ai", + target="_blank", + ), + ], + className="watermark", + ) diff --git a/good_ai/src/good_ai/good_ai/views/query.py b/good_ai/src/good_ai/good_ai/views/query.py index e91cbec..e72b187 100644 --- a/good_ai/src/good_ai/good_ai/views/query.py +++ b/good_ai/src/good_ai/good_ai/views/query.py @@ -11,3 +11,17 @@ class Query(BaseModel): sort: List[SortBy] = [] skip: int = 0 take: int = 100 + + class Config: + schema_extra = { + "example": { + "filter": [ + {"property": "execution_time_ms", "operator": ">", "value": 100} + ], + "sort": [ + {"column_id": "execution_time_ms", "direction": "asc"}, + {"column_id": "id", "direction": "desc"}, + ], + "take": 10, + } + }