Skip to content
Merged
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
21 changes: 21 additions & 0 deletions docs/concepts/visualization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,27 @@ Configure your visualization

All of the above visualization functions share parameters to customize the visualization (e.g., hide legend, hide inputs). Learn more by reviewing the API reference for `Driver.display_all_functions() <https://hamilton.apache.org/reference/drivers/Driver/#hamilton.driver.Driver.display_all_functions>`_; parameters should apply to all other visualizations.

Custom node labels with display_name
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Use the ``@tag`` decorator with ``display_name`` to show human-readable labels in visualizations while keeping valid Python identifiers as function names. This is useful for creating presentation-ready diagrams or adding business-friendly names:

.. code-block:: python

from hamilton.function_modifiers import tag

@tag(display_name="Parse Raw JSON")
def parse_raw_json(raw_data: str) -> dict:
return json.loads(raw_data)

@tag(display_name="Transform to DataFrame")
def transform_to_df(parse_raw_json: dict) -> pd.DataFrame:
return pd.DataFrame(parse_raw_json)

When visualized, nodes will display "Parse Raw JSON" and "Transform to DataFrame" instead of their function names. This keeps your code Pythonic while making visualizations more readable for stakeholders.

Note that ``display_name`` only affects visualization labels - the actual node names used in code and execution remain the function names.

.. _custom-visualization-style:

Apply custom style
Expand Down
25 changes: 25 additions & 0 deletions docs/reference/decorators/tag.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,31 @@ available outputs for specific tag matches. E.g.
output = dr.execute(desired_outputs)


**Using display_name for visualization**

You can use the special ``display_name`` tag to provide a human-readable name for nodes in graphviz visualizations.
This allows you to show user-friendly names in DAG diagrams while keeping valid Python identifiers as function names.

.. code-block:: python

import pandas as pd
from hamilton.function_modifiers import tag

@tag(display_name="Customer Lifetime Value")
def customer_ltv(purchases: pd.DataFrame, tenure: pd.Series) -> pd.Series:
"""Calculate customer lifetime value."""
return purchases.sum() * tenure

When you visualize the DAG using ``dr.display_all_functions()``, the node will display "Customer Lifetime Value"
instead of "customer_ltv". This is useful for:

- Creating presentation-ready diagrams for stakeholders
- Adding business-friendly names for technical functions
- Making visualizations more readable for non-technical audiences

Note that ``display_name`` only affects visualization - the actual node name used in code remains the function name.


----

**Reference Documentation**
Expand Down
Binary file modified examples/hello_world/my_dag.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 4 additions & 0 deletions examples/hello_world/my_functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

import pandas as pd

from hamilton.function_modifiers import tag

"""
Notes:
1. This file is used for all the [ray|dask|spark]/hello_world examples.
Expand All @@ -25,11 +27,13 @@
"""


@tag(display_name="Rolling 3-Week Average Spend")
def avg_3wk_spend(spend: pd.Series) -> pd.Series:
"""Rolling 3 week average spend."""
return spend.rolling(3).mean()


@tag(display_name="Cost Per Signup")
def spend_per_signup(spend: pd.Series, signups: pd.Series) -> pd.Series:
"""The cost per signup in relation to spend."""
return spend / signups
Expand Down
39 changes: 31 additions & 8 deletions hamilton/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -283,34 +283,57 @@ def _get_node_label(
name and type but values can be overridden. Overriding is currently
used for materializers since `type_` is stored in n.tags.

If a node has a 'display_name' tag, it will be used as the label
instead of the node name. This allows human-readable names in
visualizations while keeping Python-valid identifiers as node names.
See: https://github.com/apache/hamilton/issues/1413

ref: https://graphviz.org/doc/info/shapes.html#html
"""
name = n.name if name is None else name
# Determine display name: explicit name param > display_name tag > node.name
if name is not None:
display_name = name
elif n.tags.get("display_name"):
display_name = n.tags["display_name"]
# Handle case where display_name is a list (use first element)
if isinstance(display_name, list):
display_name = display_name[0] if display_name else n.name
else:
display_name = n.name

if type_string is None:
type_string = get_type_as_string(n.type) if get_type_as_string(n.type) else ""

# We need to ensure that name and type string are HTML-escaped
# strings to avoid syntax errors. This is particular important
# because config *values* are passed through this function
# strings to avoid syntax errors. This is particularly important
# because config *values* and display_name tags are passed through this function
# see issue: https://github.com/apache/hamilton/issues/1200
# see graphviz ref: https://graphviz.org/doc/info/shapes.html#html
if len(type_string) > MAX_STRING_LENGTH:
type_string = type_string[:MAX_STRING_LENGTH] + "[...]"

escaped_display_name = html.escape(display_name, quote=True)
escaped_type_string = html.escape(type_string, quote=True)
return f"<<b>{name}</b><br /><br /><i>{escaped_type_string}</i>>"
return f"<<b>{escaped_display_name}</b><br /><br /><i>{escaped_type_string}</i>>"

def _get_input_label(input_nodes: FrozenSet[node.Node]) -> str:
"""Get a graphviz HTML-like node label formatted aspyer a table.
"""Get a graphviz HTML-like node label formatted as a table.
Each row is a different input node with one column containing
the name and the other the type.
the name (or display_name if present) and the other the type.
ref: https://graphviz.org/doc/info/shapes.html#html
"""
rows = []
for dep in input_nodes:
name = dep.name
# Use display_name tag if present, otherwise use node name
display_name = dep.tags.get("display_name", dep.name)
# Handle case where display_name is a list (use first element)
if isinstance(display_name, list):
display_name = display_name[0] if display_name else dep.name
type_string = get_type_as_string(dep.type) if get_type_as_string(dep.type) else ""
rows.append(f"<tr><td>{name}</td><td>{type_string}</td></tr>")
# HTML escape for security
escaped_display_name = html.escape(display_name, quote=True)
escaped_type_string = html.escape(type_string, quote=True)
rows.append(f"<tr><td>{escaped_display_name}</td><td>{escaped_type_string}</td></tr>")
return f"<<table border=\"0\">{''.join(rows)}</table>>"

def _get_node_type(n: node.Node) -> str:
Expand Down
52 changes: 52 additions & 0 deletions tests/resources/display_name_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Test module for display_name tag support in graphviz visualization.

See: https://github.com/apache/hamilton/issues/1413
"""

from hamilton.function_modifiers import tag


def input_a() -> int:
"""A simple input node without display_name."""
return 1


@tag(display_name="My Custom Display Name")
def node_with_display_name(input_a: int) -> int:
"""A node with a custom display name for visualization."""
return input_a + 1


@tag(display_name='Special <Characters> & "Quotes"')
def node_with_special_chars(input_a: int) -> int:
"""A node with special HTML characters that need escaping."""
return input_a * 2


@tag(owner="data-science")
def node_without_display_name(input_a: int) -> int:
"""A node with other tags but no display_name."""
return input_a + 10


@tag(display_name="Final Output Node", owner="analytics")
def output_node(node_with_display_name: int, node_without_display_name: int) -> int:
"""A node with display_name and other tags."""
return node_with_display_name + node_without_display_name
34 changes: 34 additions & 0 deletions tests/resources/display_name_list_functions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.

"""Test module for display_name tag with list values (edge case).

See: https://github.com/apache/hamilton/issues/1413
"""

from hamilton.function_modifiers import tag


def input_value() -> int:
"""A simple input node."""
return 1


@tag(display_name=["First Name", "Second Name"])
def node_with_list_display_name(input_value: int) -> int:
"""A node with display_name as a list - edge case testing."""
return input_value + 1
Loading
Loading