top of page

Using Microsoft Fabric Notebooks to Render an SVG Measure From a Semantic Model

  • Jihwan Kim
  • 2 hours ago
  • 6 min read

In this writing, I’d like to share the small experiment I did recently: querying an SVG measure from a semantic model using a Fabric Notebook, scaling it with Python, and displaying it directly in the notebook output.

The idea started from a question from my manager: “If the semantic model produces SVG via DAX, can we render it outside Power BI and reuse it somewhere else?”

Once I tried it, new use cases started to appear.



The Data Model I Used


Below is the sample semantic model I used for this test.


ree

ree

DEFINE
	/// svg image kpi card
	FUNCTION _fn_svg_kpi_card = (_lastrefresh: expr, _salesmeasure: expr, _visitorsmeasure: expr) =>
		/* ===== Canvas ===== */
		VAR _w = 600
		VAR _h = 780
		VAR _pad = 24
		VAR _gap = 20

		/* Card sizes */
		VAR _c1H = 150
		VAR _c2H = 240
		VAR _c3H = 240
		VAR _cardW = _w - (2 * _pad)
		VAR _x = _pad
		VAR _y1 = _pad
		VAR _y2 = _y1 + _c1H + _gap
		VAR _y3 = _y2 + _c2H + _gap

		/* ===== Colors & fonts ===== */
		VAR _stroke = "#d9d9d9"
		VAR _titleBlue = "#1a73e8"
		VAR _txt = "#222222"
		VAR _font = "Segoe UI"

		/* ===== Measures as variables (replace with yours) ===== */
		VAR _LastRefresh = _lastrefresh -- datetime measure
		VAR _Sales = _salesmeasure -- numeric measure
		VAR _Visitors = _visitorsmeasure -- numeric measure

		/* Formats */
		VAR _dtFmt = FORMAT(
			_LastRefresh,
			"mm/dd/yyyy h:mm:ss AM/PM"
		)
		VAR _numFmt = "#,0"

		/* ===== Card 1: Last data refresh ===== */
		VAR _card1 =
		"<g>" &
		"<rect x='" & _x & "' y='" & _y1 & "' width='" & _cardW & "' height='" & _c1H &
		"' rx='6' ry='6' fill='white' stroke='" & _stroke & "'/>" &
		"<text x='" & (_x + 20) & "' y='" & (_y1 + 40) &
		"' font-family='" & _font & "' font-size='26' fill='" & _titleBlue & "'>Last data refresh date time</text>" &
		"<text x='" & (_x + 20) & "' y='" & (_y1 + 80) &
		"' font-family='" & _font & "' font-size='22' fill='" & _txt & "'>" & _dtFmt & "</text>" &
		"</g>"

		/* ===== Card 2: Sales ===== */
		VAR _card2 =
		"<g>" &
		"<rect x='" & _x & "' y='" & _y2 & "' width='" & _cardW & "' height='" & _c2H &
		"' rx='6' ry='6' fill='white' stroke='" & _stroke & "'/>" &
		"<text x='" & (_x + 20) & "' y='" & (_y2 + 44) &
		"' font-family='" & _font & "' font-size='26' font-weight='600' fill='" & _titleBlue & "'>Sales</text>" &
		"<text x='" & (_x + 20) & "' y='" & (_y2 + 120) &
		"' font-family='" & _font & "' font-size='56' font-weight='600' fill='" & _txt & "'>" &
		FORMAT(
			_Sales,
			_numFmt
		) & "</text>" &
		"</g>"

		/* ===== Card 3: Visitors ===== */
		VAR _card3 =
		"<g>" &
		"<rect x='" & _x & "' y='" & _y3 & "' width='" & _cardW & "' height='" & _c3H &
		"' rx='6' ry='6' fill='white' stroke='" & _stroke & "'/>" &
		"<text x='" & (_x + 20) & "' y='" & (_y3 + 44) &
		"' font-family='" & _font & "' font-size='26' font-weight='600' fill='" & _titleBlue & "'>Visitors</text>" &
		"<text x='" & (_x + 20) & "' y='" & (_y3 + 120) &
		"' font-family='" & _font & "' font-size='56' font-weight='600' fill='" & _txt & "'>" &
		FORMAT(
			_Visitors,
			_numFmt
		) & "</text>" &
		"</g>"

		/* ===== Compose SVG ===== */
		VAR _svg =
		"data:image/svg+xml;utf8," &
		"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 " & _w & " " & _h &
		"' preserveAspectRatio='xMinYMin meet'>" &
		"<rect width='100%' height='100%' fill='white'/>" &
		_card1 & _card2 & _card3 &
		"</svg>"

		RETURN
			_svg

It’s a straightforward model, but it contains:

  • One main fact table (factinternetsales) and several dimensions (dim_date, dimproduct, dimsalesterritory)

  • A utility table (last_data_refresh)

  • And most importantly: a DAX measure that uses User Defined Function and returns an SVG string (SVG KPI Panel)

This measure is fully authored in the semantic model and returns a data:image/svg+xml object.


I published both the report and the semantic model into an F4 capacity workspace named SVG.


ree



Running a Fabric Notebook Against the Model


After publishing, I created a Fabric Notebook and wrote the following Python code using semPy:


ree

from sempy import fabric
import pandas as pd
from IPython.display import SVG, display

# ---------------------------------------------
# SVG SIZE CONTROL
# ---------------------------------------------
# Option A: force half-size rendering
scale = 0.5

# Option B: or manually set width / height (uncomment to override scale)
# svg_width = 300
# svg_height = 390
# ---------------------------------------------


# Always return a TABLE, not a SET
dax_query = """
    EVALUATE
    {[SVG KPI Panel]}
"""

# Query the SVG from the semantic model
result = fabric.evaluate_dax(
    workspace="SVG",
    dataset="svg",
    dax_string=dax_query
)

# Convert directly — result is already row-oriented
df = pd.DataFrame(result)

# Identify SVG column
col = df.columns[0]

# Extract SVG markup
svg_raw = df.iloc[0][col]

# Remove prefix
clean_svg = svg_raw.replace("data:image/svg+xml;utf8,", "")

# ---------------------------------------------
# Apply SVG scaling
# ---------------------------------------------

# Inject width/height directly into the <svg> tag
# Original viewBox is: 0 0 600 780 → we scale from these values
scaled_width = int(600 * scale)
scaled_height = int(780 * scale)

# If user explicitly defined width/height variables, override
try:
    scaled_width = svg_width
    scaled_height = svg_height
except NameError:
    pass

# Update SVG tag: add width and height attributes
# (If already exists, replace; if not, insert)
import re

clean_svg_scaled = re.sub(
    r"<svg([^>]*)>",
    rf"<svg\1 width='{scaled_width}' height='{scaled_height}'>",
    clean_svg
)

# ---------------------------------------------
# Render SVG
# ---------------------------------------------
display(SVG(clean_svg_scaled))

What this does:

  1. Runs a DAX query against the semantic model using fabric.evaluate_dax()

    Reference: What is semantic link? - Microsoft Fabric | Microsoft Learn

  2. Extracts the SVG string from the DAX result.

  3. Cleans the prefix data: image/svg+xml;utf8,.

  4. Injects new width/height attributes so I can resize the SVG.

  5. Renders it directly inside the notebook using IPython.display.SVG.


And the result looks exactly like the rendered SVG in Power BI, because it is the same DAX measure.


ree



So… What Can We Do With This?


Once I saw the SVG rendering properly in the notebook, several interesting use cases appeared immediately.


1. Automated Image Generation

If my semantic model can output SVG images through DAX, a Fabric Notebook can:

  • Execute the DAX

  • Render the image

  • Export it as a file and save it in Lakehouse

This becomes a reusable image generation pipeline.


# ---------------------------------------------------------
# FABRIC Notebook: save it as html in Lakehouse
# ---------------------------------------------------------
from sempy import fabric
import pandas as pd
from datetime import datetime
import re
from notebookutils import mssparkutils

# ---------------------------------------------------------
# SETTINGS
# ---------------------------------------------------------
WORKSPACE = "SVG"
DATASET   = "svg"
MEASURE   = "[SVG KPI Panel]"

# Fabric-compliant path (must start with Files/)
EXPORT_FOLDER = "Files/svg_output"

timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename_html = f"kpi_panel_{timestamp}.html"
export_path_html = f"{EXPORT_FOLDER}/{filename_html}"

scale = 0.5

# ---------------------------------------------------------
# 1. RUN DAX QUERY
# ---------------------------------------------------------
dax_query = f"""
EVALUATE
{{ {MEASURE} }}
"""

result = fabric.evaluate_dax(
    workspace=WORKSPACE,
    dataset=DATASET,
    dax_string=dax_query
)

df = pd.DataFrame(result)
col = df.columns[0]
svg_raw = df.iloc[0][col]

# Strip Power BI data URL prefix
clean_svg = svg_raw.replace("data:image/svg+xml;utf8,", "")

# ---------------------------------------------------------
# 2. SCALE SVG
# ---------------------------------------------------------
orig_width, orig_height = 600, 780
scaled_width  = int(orig_width * scale)
scaled_height = int(orig_height * scale)

clean_svg_scaled = re.sub(
    r"<svg([^>]*)>",
    rf"<svg\1 width='{scaled_width}' height='{scaled_height}'>",
    clean_svg
)

# ---------------------------------------------------------
# 3. HTML WRAPPER
# ---------------------------------------------------------
html_doc = f"""<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>SVG Export</title>
</head>
<body>
{clean_svg_scaled}
</body>
</html>"""

# ---------------------------------------------------------
# 4. WRITE TO FABRIC LAKEHOUSE (mssparkutils.fs)
# ---------------------------------------------------------
# Create directory if not exists
mssparkutils.fs.mkdirs(EXPORT_FOLDER)

# Write HTML
mssparkutils.fs.put(export_path_html, html_doc, overwrite=True)

print("File written to:", export_path_html)
print("Folder contents:")
print(mssparkutils.fs.ls(EXPORT_FOLDER))

ree


2. Embeddable KPI Badges

SVG output can be embedded in:

  • SharePoint intranet pages

  • Teams tabs

  • HTML dashboards

  • Internal applications

  • Email summaries (as inline SVG or attachments)

My semantic model becomes a design engine.



3. Automating SVG-Based Reports

Your DAX measure might generate:

  • Monthly KPI cards

  • Trend graphs

  • Mini dashboards

  • Status indicators

If I automate the notebook, these visuals can update daily without anyone opening Power BI Desktop or the Service.



Can This Refresh Daily?


Yes. Fabric Notebooks support scheduling.

If scheduled:

  • semPy will re-run the DAX query

  • The SVG will regenerate

  • My output file, URL, or embedded asset will update automatically

As long as the notebook writes the updated SVG to a stable location, whatever embeds it (Teams, SharePoint, a website, etc.) will also refresh.

This means you can create auto-updating KPI images backed by a Power BI semantic model.



Why This Matters


This experiment showed me something powerful:

The semantic model is no longer confined to Power BI. With SemPy, it becomes a service I can query dynamically inside Fabric.

Power BI Semantic Models + SemPy + scheduling = a full pipeline that can produce analytics artifacts without opening Power BI Desktop.



Closing Thoughts


Using SemPy to query an SVG measure turned out to be a surprisingly flexible approach. Once I saw the image render inside the notebook, I realized this workflow can support automation, embedding, and even micro-reporting outside Power BI.

I hope this helps having fun in exploring Power BI semantic models with SemPy, and new ways to use Fabric notebooks as part of BI automation toolkit.

Comments


bottom of page