Source code for gofigr.widget
"""\
Copyright (c) 2024, Flagstaff Solutions, LLC
All rights reserved.
"""
from abc import ABC
# pylint: disable=trailing-whitespace, ungrouped-imports
from base64 import b64encode
from uuid import uuid4
import humanize
from IPython.core.display import HTML
from gofigr.annotators import NotebookMetadataAnnotator, NOTEBOOK_NAME
try:
from IPython.core.display_functions import display
except ModuleNotFoundError:
from IPython.core.display import display
from gofigr.utils import read_resource_b64, read_resource_text
BUTTON_STYLE = "padding-top: 0.25em; padding-bottom: 0.25em; padding-left: 0.5em; padding-right: 0.5em; " + \
"color: #fff; display: inline-block; font-weight: 400; line-height: 1; " + \
"text-align: center; vertical-align: middle; cursor: pointer; border: 1px solid transparent; " + \
"font-size: 0.875rem; border-radius: 0.35rem; margin-top: auto; margin-bottom: auto; "
VIEW_BUTTON_STYLE = BUTTON_STYLE + "background-color: #ed6353; border-color: #ed635e; margin-left: 0.5rem; "
DOWNLOAD_BUTTON_STYLE = BUTTON_STYLE + "background-color: #0d53b1; border-color: #0d53b1; margin-left: 0.5rem; "
COPY_BUTTON_STYLE = BUTTON_STYLE + "background-color: #2c7df5; border-color: #2c7df5; margin-left: 0.5rem; "
WIDGET_STYLE = "margin-top: 1rem; margin-bottom: 1rem; margin-left: auto; margin-right: auto;" + \
"display: flex !important; border: 1px solid transparent; border-color: #cedcef; padding: 0.75em; " + \
"border-radius: 0.35rem; flex-wrap: wrap; "
COMPACT_WIDGET_STYLE = "margin-top: 1rem; margin-bottom: 1rem; margin-left: auto; margin-right: auto;" + \
"display: flex !important; border: 1px solid transparent; border-color: #cedcef; padding: 0.25em; " + \
"padding-top: 0.5em; " + \
"border-radius: 0.35rem; flex-wrap: wrap; "
MINIMAL_WIDGET_STYLE = "margin-top: 1rem; margin-bottom: 1rem; margin-left: auto; margin-right: auto;" + \
"display: flex !important; padding: 0.25em; " + \
"padding-top: 0.5em; flex-wrap: wrap; "
ME2 = "margin-right: 1rem"
MESSAGE_STYLE = "padding-top: 0.25rem; "
ROW_STYLE = "width: 100%; display: flex; flex-wrap: wrap;"
BREAK_STYLE = "flex-basis: 100%; height: 0;"
FA_VIEW = """<svg xmlns="http://www.w3.org/2000/svg" height="0.85rem" width="0.85rem" viewBox="0 0 512 512"><!--!Font
Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright
2024 Fonticons, Inc.--><path fill="#ffffff" d="M352 0c-12.9 0-24.6 7.8-29.6 19.8s-2.2 25.7 6.9 34.9L370.7 96 201.4
265.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L416 141.3l41.4 41.4c9.2 9.2 22.9 11.9 34.9 6.9s19.8-16.6
19.8-29.6V32c0-17.7-14.3-32-32-32H352zM80 32C35.8 32 0 67.8 0 112V432c0 44.2 35.8 80 80 80H400c44.2 0 80-35.8
80-80V320c0-17.7-14.3-32-32-32s-32 14.3-32 32V432c0 8.8-7.2 16-16 16H80c-8.8 0-16-7.2-16-16V112c0-8.8 7.2-16
16-16H192c17.7 0 32-14.3 32-32s-14.3-32-32-32H80z"/></svg>"""
FA_FEATHER = """<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
<!--!Font Awesome Free v7.0.1 by @fontawesome - https://fontawesome.com License -
https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.-->
<path d="M416 64C457 64 496.3 80.3 525.2 109.2L530.7 114.7C559.7 143.7 576 183 576 223.9C576 248 570.3
271.5 559.8 292.7C557.9 296.4 554.5 299.2 550.5 300.4L438.5 334C434.6 335.2 432 338.7 432 342.8C432 347.9 436.1 352
441.2 352L473.4 352C487.7 352 494.8 369.2 484.7 379.3L462.3 401.7C460.4 403.6 458.1 404.9 455.6 405.7L374.6 430C370.7
431.2 368.1 434.7 368.1 438.8C368.1 443.9 372.2 448 377.3 448C390.5 448 396.2 463.7 385.1 470.9C344 497.5 295.8 512
246.1 512L160.1 512L112.1 560C103.3 568.8 88.9 568.8 80.1 560C71.3 551.2 71.3 536.8 80.1 528L320 288C328.8 279.2
328.8 264.8 320 256C311.2 247.2 296.8 247.2 288 256L143.5 400.5C137.8 406.2 128 402.2 128 394.1C128 326.2 155
261.1 203 213.1L306.8 109.2C335.7 80.3 375 64 416 64z"/></svg>
"""
FA_COPY = """<svg xmlns="http://www.w3.org/2000/svg" height="1rem" width="0.85rem" viewBox="0 0 448 512"><!--!Font
Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright
2024 Fonticons, Inc.--><path fill="#ffffff" d="M208 0H332.1c12.7 0 24.9 5.1 33.9 14.1l67.9 67.9c9 9 14.1 21.2 14.1
33.9V336c0 26.5-21.5 48-48 48H208c-26.5 0-48-21.5-48-48V48c0-26.5 21.5-48 48-48zM48 128h80v64H64V448H256V416h64v48c0
26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V176c0-26.5 21.5-48 48-48z"/></svg>"""
FA_COPY_LIGHT = FA_COPY.replace("#ffffff", "#0d53b1")
FA_DOWNLOAD = """<svg xmlns="http://www.w3.org/2000/svg" height="1rem" width="1rem" viewBox="0 0 512 512"><!--!Font
Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright
2024 Fonticons, Inc.--><path fill="#ffffff" d="M288 32c0-17.7-14.3-32-32-32s-32 14.3-32
32V274.7l-73.4-73.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l128 128c12.5 12.5 32.8 12.5 45.3
0l128-128c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L288 274.7V32zM64 352c-35.3 0-64 28.7-64 64v32c0 35.3 28.7 64
64 64H448c35.3 0 64-28.7 64-64V416c0-35.3-28.7-64-64-64H346.5l-45.3 45.3c-25 25-65.5 25-90.5 0L165.5 352H64zm368
56a24 24 0 1 1 0 48 24 24 0 1 1 0-48z"/></svg>"""
[docs]
def timestamp_to_local_tz(dt_tz):
"""Converts a datetime to the local timezone"""
return dt_tz.astimezone(tz=None)
[docs]
class WidgetBase(ABC):
"""Generates HTML/Javascript for the GoFigr Jupyter widget shown under each figure"""
def __init__(self):
"""
:param revision: gf.Revision instance
"""
self._logo_b64 = None
[docs]
def get_logo_b64(self):
"""Reads the GoFigr logo and returns it as a base64 string"""
if self._logo_b64 is not None:
return self._logo_b64
self._logo_b64 = read_resource_b64("gofigr.resources", "logo_small.png")
return self._logo_b64
[docs]
class RevisionWidgetBase(WidgetBase, ABC):
"""Generates HTML/Javascript for the GoFigr Jupyter widget shown under each figure"""
def __init__(self, revision):
"""
:param revision: gf.Revision instance
"""
super().__init__()
self.revision = revision
self.id = f"gf-widget-{str(uuid4())}"
self.alert_id = f"{self.id}-alert"
[docs]
def get_download_link(self, label, watermark, image_format="png"):
"""Generates HTML for the download link"""
matches = [img for img in self.revision.image_data
if img.format and img.format.lower() == image_format.lower() and img.is_watermarked == watermark]
if len(matches) == 0:
return ""
else:
img = matches[0]
return f"""<a download="{self.revision.figure.name}.{image_format}"
style="{DOWNLOAD_BUTTON_STYLE}"
href="data:image/{image_format};base64,{b64encode(img.data).decode('ascii')}">
{label}</a>"""
def _render_template(self, name, mapping):
"""Renders a JavaScript template with the given variable mappings"""
template = read_resource_text("gofigr.resources", name).replace("\n", "")
for key, value in mapping.items():
template = template.replace(key, value)
template = template.replace("\"", "'")
return template
[docs]
def get_copy_link(self, label, watermark, image_format="png"):
"""Generates HTML/JS for the copy image button"""
matches = [img for img in self.revision.image_data
if img.format and img.format.lower() == image_format.lower() and img.is_watermarked == watermark]
if len(matches) == 0:
return ""
img = matches[0]
onclick = self._render_template("copy_image.js",
{"_ALERT_ID_": self.alert_id,
"_ERROR_MESSAGE_": "Unable to copy image. "
"Are you running over HTTPS and/or localhost?",
"_SUCCESS_MESSAGE_": "Image copied to clipboard!",
"_IMAGE_B64_": b64encode(img.data).decode('ascii')})
return f"""<button onclick="{onclick}" style="{COPY_BUTTON_STYLE}">{label}</button>"""
[docs]
def get_text_copy_link(self, label, text):
"""Generates HTML/JS for the copy link button"""
onclick = read_resource_text("gofigr.resources", "copy_text.js").replace("\n", "")
onclick = onclick.replace("_TEXT_", text)
onclick = onclick.replace("_ALERT_ID_", self.alert_id)
onclick = onclick.replace("_SUCCESS_MESSAGE_", "Link copied to clipboard!")
onclick = onclick.replace("_ERROR_MESSAGE_", "Unable to copy link. Make sure your Jupyter instance is "
"running over HTTPS and/or on localhost.")
onclick = onclick.replace("\"", "'")
return f"""<span onclick="{onclick}" style="cursor: pointer; color: #0d53b1; ">{label}</span>"""
[docs]
class DetailedWidget(RevisionWidgetBase):
"""Generates HTML/Javascript for the GoFigr Jupyter widget shown under each figure"""
[docs]
def show(self):
"""Renders this widget in Jupyter by generating the HTML/JS & calling display()"""
logo_b64 = self.get_logo_b64()
copy_url = self.get_text_copy_link(f"<span style='margin-left: 0.5rem;'>"
f"{self.revision.figure.name}</span>"
f"<span style='margin-left: 0.25rem; margin-right: 0.25rem';>"
f"{FA_COPY_LIGHT}</span>",
self.revision.revision_url)
logo_html = self.get_text_copy_link(f"""<img src="data:image;base64,{logo_b64}" alt="GoFigr.io logo"
style='width: 3rem; height: 3rem'/>""", self.revision.revision_url)
return display(HTML(f"""
<div style="{WIDGET_STYLE}">
<div style="{ROW_STYLE + "margin-bottom: 0.5rem"}">
<span>Successfully published {copy_url}
on {timestamp_to_local_tz(self.revision.created_on).strftime("%x at %X")}.</span>
<span style="margin-left: 0.25rem;">
Revision size: {humanize.naturalsize(self.revision.size_bytes)}</span>
</div>
<div style="{ROW_STYLE}">
<!-- Logo -->
{logo_html}
<!-- View on GoFigr -->
<a href='{self.revision.revision_url}' target="_blank" style="{VIEW_BUTTON_STYLE}">{FA_VIEW}
<span> View on GoFigr</span></a>
<!-- Download -->
{self.get_download_link(FA_DOWNLOAD + "<span> Download</span>", True, "png")}
{self.get_download_link(FA_DOWNLOAD + "<span> Download (no watermark)</span>", False, "png")}
<!-- Copy -->
{self.get_copy_link(FA_COPY + "<span> Copy</span>", True, "png")}
{self.get_copy_link(FA_COPY + "<span> Copy (no watermark)</span>", False, "png")}
</div>
<div id={self.alert_id} style="{ROW_STYLE + MESSAGE_STYLE}">
</div>
</div>"""))
[docs]
class CompactWidget(RevisionWidgetBase):
"""Generates a compact GoFigr widget"""
[docs]
def show(self):
"""Renders this widget in Jupyter by generating the HTML/JS & calling display()"""
logo_b64 = self.get_logo_b64()
logo_html = self.get_text_copy_link(f"""<img src="data:image;base64,{logo_b64}" alt="GoFigr.io logo"
style='width: 2rem; height: 2rem'/>""", self.revision.revision_url)
return display(HTML(f"""
<div style="{COMPACT_WIDGET_STYLE}">
<div style="{ROW_STYLE + "margin-bottom: 0.0rem"}">
<!-- Logo -->
{logo_html}
<!-- View on GoFigr -->
<a href='{self.revision.revision_url}' target="_blank" style="{VIEW_BUTTON_STYLE}">{FA_VIEW}
<span> View on GoFigr</span></a>
<!-- Download -->
{self.get_download_link(FA_DOWNLOAD + "<span> Download</span>", True, "png")}
{self.get_download_link(FA_DOWNLOAD + "<span> Download (no watermark)</span>", False, "png")}
<!-- Copy -->
{self.get_copy_link(FA_COPY + "<span> Copy</span>", True, "png")}
{self.get_copy_link(FA_COPY + "<span> Copy (no watermark)</span>", False, "png")}
</div>
<div id={self.alert_id} style="{ROW_STYLE + MESSAGE_STYLE}">
</div>
</div>"""))
[docs]
class MinimalWidget(RevisionWidgetBase):
"""Generates a compact GoFigr widget"""
[docs]
def show(self):
"""Renders this widget in Jupyter by generating the HTML/JS & calling display()"""
logo_b64 = self.get_logo_b64()
logo_html = self.get_text_copy_link(f"""<img src="data:image;base64,{logo_b64}" alt="GoFigr.io logo"
style='width: 2rem; height: 2rem; margin-right: 0.5rem;'/>""", self.revision.revision_url)
return display(HTML(f"""
<div style="{COMPACT_WIDGET_STYLE}">
<div style="{ROW_STYLE + "margin-bottom: 0.0rem"}">
<!-- Logo -->
{logo_html}
<!-- View on GoFigr -->
<a href='{self.revision.revision_url}' target="_blank" style="margin-top: auto; margin-bottom: auto;">View on GoFigr</a>
</div>
<div id={self.alert_id} style="{ROW_STYLE + MESSAGE_STYLE}">
</div>
</div>"""))
[docs]
class StartupWidget(WidgetBase):
"""Generates a GoFigr startup widget"""
def __init__(self, extension):
super().__init__()
self.extension = extension
[docs]
def get_link(self, obj):
"""Gets the app link to a GoFigr object"""
return f"""<a style="margin-left: 0.25em" href={obj.app_url}>{obj.name}</a>"""
[docs]
def show(self):
"""Renders this widget in Jupyter by generating the HTML/JS & calling display()"""
self.extension.startup_widget_shown = True
logo_b64 = self.get_logo_b64()
logo_html = f"""<img src="data:image;base64,{logo_b64}" alt="GoFigr.io logo"
style='width: 2rem; height: 2rem; margin-right: 0.5rem;'/>"""
return display(HTML(f"""
<div style="{WIDGET_STYLE}">
<div style="{ROW_STYLE + "margin-bottom: 0.0rem"}">
<!-- Logo -->
{logo_html}
<div style="margin-top: auto; margin-bottom: auto;">
<span style="font-weight: bold">GoFigr active</span>.
Workspace: {self.get_link(self.extension.publisher.workspace)}.
Analysis: {self.get_link(self.extension.publisher.analysis)}.
Autopublish: {self.extension.auto_publish}.
</div>
</div>
</div>"""))
[docs]
class LiteStartupWidget(WidgetBase):
"""Generates a GoFigr startup widget"""
def __init__(self, extension):
super().__init__()
self.extension = extension
[docs]
def show(self):
"""Renders this widget in Jupyter by generating the HTML/JS & calling display()"""
logo_b64 = self.get_logo_b64()
logo_html = f"""<img src="data:image;base64,{logo_b64}" alt="GoFigr.io logo"
style='width: 2rem; height: 2rem; margin-right: 0.5rem;'/>"""
meta = NotebookMetadataAnnotator().parse_metadata(error=False)
if meta is None:
notebook_name = "N/A"
else:
notebook_name = meta.get(NOTEBOOK_NAME, "N/A")
return display(HTML(f"""
<div style="{WIDGET_STYLE}">
<div style="{ROW_STYLE + "margin-bottom: 0.0rem"}">
<!-- Logo -->
{logo_html}
<div style="margin-top: auto; margin-bottom: auto;">
<span style="font-weight: bold">GoFigr Lite active. Notebook: </span>
<code>{notebook_name}</code>.
</div>
</div>
<div style="{ROW_STYLE + "margin-bottom: 0.0rem; margin-top: 1.0rem"}">
<span>GoFigr Lite adds annotation to your figures but does not publish them to our servers. To experience
full functionality, consider using the full GoFigr extension. See
<a href="https://gofigr.io/gofigr-lite/">this page</a> for details.
</span>
</div>
</div>"""))
[docs]
class AssetWidget(WidgetBase):
"""Generates a GoFigr startup widget"""
def __init__(self, rev):
super().__init__()
self.rev = rev
[docs]
def get_link(self, obj):
"""Gets the app link to a GoFigr object"""
if not obj.asset.name:
obj.asset.fetch()
return f"""<a style="margin-left: 0.25em" href={obj.app_url}>{obj.asset.name}</a>"""
[docs]
def show(self):
"""Renders this widget in Jupyter by generating the HTML/JS & calling display()"""
logo_b64 = self.get_logo_b64()
logo_html = f"""<img src="data:image;base64,{logo_b64}" alt="GoFigr.io logo"
style='width: 2rem; height: 2rem; margin-right: 0.5rem;'/>"""
return display(HTML(f"""
<div style="{WIDGET_STYLE}">
<div style="{ROW_STYLE + "margin-bottom: 0.0rem"}">
<!-- Logo -->
{logo_html}
<div style="margin-top: auto; margin-bottom: auto;">
<span style="font-weight: bold">Asset: </span>
{self.get_link(self.rev)} {"(new revision)" if self.rev.is_new_revision else "(existing revision)"}.
</div>
</div>
</div>"""))