Decks
Decks lets you display customized data visualizations from within your task code. Decks are rendered as HTML and appear right in the Union.ai UI when you run your workflow.
Decks is an opt-in feature; to enable it, set enable_deck
to True
in the task parameters.
To begin, import the dependencies:
import union
from flytekit.deck.renderer import MarkdownRenderer
from sklearn.decomposition import PCA
We create a new deck named pca
and render Markdown content along with a
PCA plot.
Now, declare the required dependnecies in an ImageSpec
:
custom_image = union.ImageSpec(
packages=[
"flytekitplugins-deck-standard",
"markdown",
"pandas",
"pillow",
"plotly",
"pyarrow",
"scikit-learn",
"ydata_profiling",
],
builder="union",
)
Next, we define the task that will construct the figure and create the Deck:
@union.task(enable_deck=True, container_image=custom_image)
def pca_plot():
iris_df = px.data.iris()
X = iris_df[["sepal_length", "sepal_width", "petal_length", "petal_width"]]
pca = PCA(n_components=3)
components = pca.fit_transform(X)
total_var = pca.explained_variance_ratio_.sum() * 100
fig = px.scatter_3d(
components,
x=0,
y=1,
z=2,
color=iris_df["species"],
title=f"Total Explained Variance: {total_var:.2f}%",
labels={"0": "PC 1", "1": "PC 2", "2": "PC 3"},
)
main_deck = union.Deck("pca", MarkdownRenderer().to_html("### Principal Component Analysis"))
main_deck.append(plotly.io.to_html(fig))
Note the usage of append
to append the Plotly figure to the Markdown deck.
The following is the expected output containing the path to the deck.html
file:
{"asctime": "2023-07-11 13:16:04,558", "name": "flytekit", "levelname": "INFO", "message": "pca_plot task creates flyte deck html to file:///var/folders/6f/xcgm46ds59j7g__gfxmkgdf80000gn/T/flyte-0_8qfjdd/sandbox/local_flytekit/c085853af5a175edb17b11cd338cbd61/deck.html"}
Once you execute this task on the Union.ai instance, you can access the deck by going to the task view and clicking the Deck button:
Deck tabs
Each Deck has a minimum of three tabs: input, output and default. The input and output tabs are used to render the input and output data of the task, while the default deck can be used to creta cusom renderings such as line plots, scatter plots, Markdown text, etc. Additionally, you can create other tabs as well.
Deck renderers
Frame profiling renderer
The frame profiling render creates a profile report from a Pandas DataFrame.
import union
import pandas as pd
from flytekitplugins.deck.renderer import FrameProfilingRenderer
@union.task(enable_deck=True, container_image=custom_image)
def frame_renderer() -> None:
df = pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]})
union.Deck("Frame Renderer", FrameProfilingRenderer().to_html(df=df))
Top-frame renderer
The top-fram renderer renders a DataFrame as an HTML table.
import union
from typing import Annotated
from flytekit.deck import TopFrameRenderer
@union.task(enable_deck=True, container_image=custom_image)
def top_frame_renderer() -> Annotated[pd.DataFrame, TopFrameRenderer(1)]:
return pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]})
Markdown renderer
The Markdown renderer converts a Markdown string into HTML.
import union
from flytekit.deck import MarkdownRenderer
@union.task(enable_deck=True, container_image=custom_image)
def markdown_renderer() -> None:
union.current_context().default_deck.append(
MarkdownRenderer().to_html("You can install flytekit using this command: ```import flytekit```")
)
Box renderer
The box renderer groups rows of a DataFrame together into a box-and-whisker mark to visualize their distribution.
Each box extends from the first quartile (Q1) to the third quartile (Q3). The median (Q2) is indicated by a line within the box. Typically, the whiskers extend to the edges of the box, plus or minus 1.5 times the interquartile range (IQR: Q3-Q1).
import union
from flytekitplugins.deck.renderer import BoxRenderer
@union.task(enable_deck=True, container_image=custom_image)
def box_renderer() -> None:
iris_df = px.data.iris()
union.Deck("Box Plot", BoxRenderer("sepal_length").to_html(iris_df))
Image renderer
The image renderer converts a FlyteFile
or PIL.Image.Image
object into an HTML displayable image,
where the image data is encoded as a base64 string.
import union
from flytekitplugins.deck.renderer import ImageRenderer
@union.task(enable_deck=True, container_image=custom_image)
def image_renderer(image: union.FlyteFile) -> None:
union.Deck("Image Renderer", ImageRenderer().to_html(image_src=image))
@union.workflow
def image_renderer_wf(image: union.FlyteFile = "https://bit.ly/3KZ95q4",) -> None:
image_renderer(image=image)
Table renderer
The table renderer converts a Pandas DataFrame into an HTML table.
import union
from flytekitplugins.deck.renderer import TableRenderer
@union.task(enable_deck=True, container_image=custom_image)
def table_renderer() -> None:
union.Deck(
"Table Renderer",
TableRenderer().to_html(df=pd.DataFrame(data={"col1": [1, 2], "col2": [3, 4]}), table_width=50),
)
Custom renderers
YOU can also create your own cusome renderer. A renderer is essentially a class with a to_html method. Here we create custom renderer that summarizes the data from a Pandas DataFRame instead of showing raw values.
class DataFrameSummaryRenderer:
def to_html(self, df: pd.DataFrame) -> str:
assert isinstance(df, pd.DataFrame)
return df.describe().to_html()
Then we can use the Annotated type to override the default renderer of the pandas.DataFrame type:
try:
from typing import Annotated
except ImportError:
from typing_extensions import Annotated
@task(enable_deck=True)
def iris_data(
sample_frac: Optional[float] = None,
random_state: Optional[int] = None,
) -> Annotated[pd.DataFrame, DataFrameSummaryRenderer()]:
data = px.data.iris()
if sample_frac is not None:
data = data.sample(frac=sample_frac, random_state=random_state)
md_text = (
"# Iris Dataset\n"
"This task loads the iris dataset using the `plotly` package."
)
flytekit.current_context().default_deck.append(MarkdownRenderer().to_html(md_text))
flytekit.Deck("box plot", BoxRenderer("sepal_length").to_html(data))
return data
Streaming Decks
You can stream a Deck directly using Deck.publish()
:
import union
@task(enable_deck=True)
def t_deck():
union.Deck.publish()
This will create a live deck that where you can click the refresh button and see the Deck update until the task succeeds.
When the task fails, you can also see the deck in the UI.