Join us at FabCon Atlanta from March 16 - 20, 2026, for the ultimate Fabric, Power BI, AI and SQL community-led event. Save $200 with code FABCOMM.
Register now!The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now! Learn more
I have a power bi dashboard that is hosted in a wrapper portal built on .net or some technology. So that dashboard can be accessed only authorized users.
The dashboard is about some surveys. I have qualitative comments (free text) by survey respondents as part of data. Based on the user selections on various slicer filters like gender, event, country, bu, dept, etc, the sub-set of comments (free text) are summarized and a short summary text output is shown. This summarization and display of text is done using a Python script. All these are working as expected. However, when the dashboard is hosted on wrapper portal, it errors on this visual.
I also found some reference wherein it states Python or R visuals do not render on embedded reports:
it suggests to pre-compute the results and load the data for visualization. Given my scenario, it would be hard to pre-compute the comments summary as the user would select any permutation and combination of filters/slicers.
Any other alternate solutions would be highly appreciated.
Regards,
Prasad
Solved! Go to Solution.
Python (and R) visuals are not supported in embedded scenarios. They render in Desktop (and sometimes in the Service UI), but when you host the report inside a .NET “wrapper” (Power BI Embedded / iframe / app owns data), the Service will not execute that Python script for security/runtime reasons — so the visual fails. That’s expected behavior, not a bug you can fix with settings.
What to do instead :
1. Precompute + slice
If you can reduce the degrees of freedom, precompute summaries at a reasonable grain (e.g., Country × BU × Dept × Event) and let the report filter those results. You don’t need “every permutation”, just the combinations your business actually uses.
2. Move summarization outside Power BI
Keep the report interactive, but do the summarization in an external service:
Send current filter context (selected values) to an API
API returns the summary text
Display it in Power BI using a custom visual (HTML/React) or a Power BI visual that can call an endpoint
This is the only way to keep “any combination of slicers” dynamic in embedded.
3. Use a Text Analytics pipeline upstream
If you’re on Fabric / Azure, you can store embeddings/topics or “key phrases” per comment and summarize based on filtered retrieval (much lighter than re-summarizing raw text every time). The report then aggregates precomputed signals.
4. If you must stay purely in Power BI Embedded
Then you cannot use Python/R visuals. You’ll need to replace the Python visual with:
a standard visual based on precomputed tables, or
a certified custom visual that doesn’t require Python execution in the client.
Hi @prasaddn
Apologies for misreading your original post and by going through other replies you had made. I believe that I understand your situation now, so I’ve deleted my previous comment.
If I understand correctly, you’re basically looking for an alternative solution for python to use an API as a data source that works like DirectQuery. If that’s the case, please have a look at this article:
Hi @kushanNa In my other reply I have posted my Python Script which works as expected. However, it wont support when the dashboard is hosted / embedded on .NET wrapper. Hence, I am looking for alternate ways to summarize the survey comments, like some custom visual or script that supports in .NET wrapper.
Hi @prasaddn
To be honest, since I am not an app or web developer, I am not very familiar with .NET wrappers.
However, I can suggest a solution for you to try if it suites you. Power BI Embedded Analytics can detect slicer selections.
I did a test and I have written the following blog to demonstrate how you can capture slicer selections using Power BI Embedded Analytics with HTML and Javascript;
https://community.fabric.microsoft.com/t5/Power-BI-Community-Blog/Real-Time-Cross-Report-Filtering-U...
Once you capture the slicer selections-> you can send them to your summarization logic using an API->store them in a data source where DirectQuery is enabled, and then pull that data into your visual.
Hi @prasaddn
Since pre-computing every permutation of filters isn't feasible for free-text comments, you need to move the Python logic "up" the stack
Solution 1: Move Logic to the .NET Wrapper (Pro-Code)
Since you are already using a .NET portal, you can handle the summarization outside of Power BI.
The Workflow:
Use the Power BI Client SDK in your .NET app to capture the state of the slicers.
Use a DAX Query via the REST API (Datasets - Execute Queries) to fetch only the filtered subset of comments.
Pass those comments to a Python Azure Function (where you can reuse your existing script).
Display the result in a standard HTML <div> or text box in your portal, positioned next to the report.
Solution 2: Power Automate + AI Builder (Low-Code)
If you want to keep the result inside the report area, you can replace the Python visual with a Power Automate visual.
The Workflow:
Add the Power Automate for Power BI visual to your report.
Pass the "Comments" field into the visual data bucket.
Create a flow that triggers on button click, sends the filtered data to Azure OpenAI or AI Builder, and returns the summary to a "Results" table.
Benefit: This avoids the sandbox limitations because the "Visual" is just a button, and the actual processing happens in the cloud.
Solution 3: The "HTML Content" Visual (Fastest Implementation)
You can use the HTML Content custom visual (available in AppSource) to display dynamic text that standard visuals can't handle.
Instead of Python, you can use a Calculation Group or a complex DAX measure to concatenate the strings (if the summarization logic is simple).
If the logic must be Python, you can use Power Query to call a web API during the refresh, though this will not be "real-time" for slicer changes.
If this architectural change helps you bypass the Python visual limitation in your portal, please mark this as an "Accepted Solution" to help others!
Best regards,
Vishwanath
Dear @AshokKunwar , Firstly thank you for providing 3 different approaches. I need to also call out that I am new to Power BI.
Solution 1: Move Logic to the .NET Wrapper (Pro-Code)
I don't have Power BI Client SDK and the .NET portal is also developed by some other team, and we use them to host our dashboard. so literally, I dont have access to edit or modify anything in the portal. I am also not familiar with using Azure functions.
Solution 2: Power Automate + AI Builder (Low-Code)
Like I mentioned, do not have exposure or access to Azure or AI functions or token based model.
Solution 3: The "HTML Content" Visual (Fastest Implementation). I was thinking we may have some custom visual that could take input as set of text comments, and then return some summarize text. However, I am not sure if this can be done with calcaluation groups as it involves understanding all the comments and creating short summary.
Please advise if the attached python script here, can be converted in a DAX function or some script that works with HTML Content custom visual and in .NET environment?
apologies as it is getting a long post:
====================
import matplotlib.pyplot as plt
import re
from collections import Counter
import textwrap
# ============================================================
# CONFIG
# ============================================================
EVENT_COL = "Event Name"
TEXT_COL = "Any suggestions for us to improve your experience?"
MAX_THEMES = 5 # show at most this many themes
MIN_COUNT_RAW = 4 # min frequency for unmapped raw words (very strict)
RAW_THEMES_ON = True # set False if you ONLY want curated themes from label_map
# ============================================================
# ============================================================
# STEP 1 — Extract event-name tokens so event names never become themes
# ============================================================
def tokenize_event_name(name: str):
if not isinstance(name, str):
return []
name = name.lower()
name = re.sub(r"[^a-z0-9\s]", " ", name)
return [t for t in name.split() if len(t) > 2]
event_tokens = set()
if EVENT_COL in dataset.columns:
for ev in dataset[EVENT_COL].dropna().unique():
event_tokens.update(tokenize_event_name(ev))
# ============================================================
# STEP 2 — Robust stopword + junk dictionary
# ============================================================
# Base English stopwords (NLTK-style)
base_stopwords = {
"i","me","my","myself","we","our","ours","ourselves","you","your","yours","yourself","yourselves",
"he","him","his","himself","she","her","hers","herself","it","its","itself",
"they","them","their","theirs","themselves",
"what","which","who","whom","this","that","these","those",
"am","is","are","was","were","be","been","being",
"have","has","had","having","do","does","did","doing",
"a","an","the","and","but","if","or","because","as","until","while",
"of","at","by","for","with","about","against","between","into","through","during",
"before","after","above","below","to","from","up","down","in","out","on","off","over","under",
"again","further","then","once","here","there","when","where","why","how",
"all","any","both","each","few","more","most","other","some","such",
"no","nor","not","only","own","same","so","than","too","very",
"can","will","just","don","should","now","also","please","include","better"
}
# Extra junk/structural words specific to suggestions context
extra_junk = {
# Generic praise/fillers – not themes
"good","great","nice","awesome","amazing","excellent","fantastic","superb","wonderful","best",
"well","fine","ok","okay","super","memorable","enjoyable","satisfying",
# Politeness / thanks
"thank","thanks","thankyou","thanks!","thanks.","thanks!!","thanks!!!","thanks🙏","thankyou🙏",
# Question echo / filler words
"suggestions","suggestion","improve","improved","improvement","improvements","experience",
"feedback","inputs","input","point","points","comment","comments","share","shared","sharing",
# Event-generic nouns
"event","events","session","sessions","meeting","meetings","program","programs",
"show","shows","function","functions","day","days","today","yesterday","tomorrow",
"evening","morning","afternoon",
# Generic verbs & vague stuff
"got","had","made","did","doing","done","make","makes","take","took","taken",
"see","saw","seen","want","wanted","would","could","should","hope","hoped","hoping",
"looking","look","felt","feel","feeling","enjoy","enjoyed","liked","like","love","loved",
"appreciate","appreciated","helped","helpful","managed","arranged","organized","organised",
"planning","planned","coordinate","coordinated","support","supported","supportive",
# Short conversational fillers
"yeah","yes","yep","okie","okay","hmm","hmmm","hahaha","haha","lol",
# Emojis / junk tokens
"👍","👏","😊","🙏","😀","😄","😂","😁","🙂","😉","😇","🎉","🎊","💯","❤️","♥","😁😁","😂😂",
# Known junk/structural words you do not want as themes
"camp", # often part of event name like "Blood Donation Camp"
}
# Combine base stopwords, custom junk, and event tokens
stopwords = base_stopwords | extra_junk | event_tokens
# ============================================================
# STEP 3 — Tokenizer with strong filtering
# ============================================================
def clean_tokens(text: str):
if not isinstance(text, str):
return []
text = text.lower()
text = re.sub(r"[^a-z0-9\s]", " ", text)
tokens = []
for t in text.split():
if not t:
continue
t = t.strip()
# skip pure numbers
if t.isdigit():
continue
# skip very short tokens
if len(t) <= 2:
continue
# skip stopwords and event-name pieces
if t in stopwords or t in event_tokens:
continue
tokens.append(t)
return tokens
# ============================================================
# STEP 4 — Theme dictionary (label_map) tuned for improvements
# ============================================================
label_map = {
# Crowd / queue / flow
"crowd": "crowd & queues",
"crowded": "crowd & queues",
"queue": "crowd & queues",
"queues": "crowd & queues",
"line": "crowd & queues",
"lines": "crowd & queues",
"entry": "crowd & queues",
"exit": "crowd & queues",
"flow": "crowd & queues",
# Time / schedule / duration
"timing": "time management",
"delay": "time management",
"delays": "time management",
"late": "time management",
"earlier": "time management",
"duration": "time management",
"schedule": "time management",
"reschedule": "time management",
# Venue / seating / facilities
"venue": "venue & facilities",
"hall": "venue & facilities",
"auditorium": "venue & facilities",
"location": "venue & facilities",
"space": "seating & space",
"seating": "seating & space",
"seat": "seating & space",
"seats": "seating & space",
"chairs": "seating & space",
"chair": "seating & space",
"fan": "venue & facilities",
"fans": "venue & facilities",
"ac": "venue & facilities",
"air": "venue & facilities",
"sound": "sound & technical",
"audio": "sound & technical",
"mic": "sound & technical",
"speaker": "sound & technical",
"speakers": "sound & technical",
"light": "lighting & ambience",
"lights": "lighting & ambience",
"lighting": "lighting & ambience",
# Food / snacks / water
"snack": "snacks & refreshments",
"snacks": "snacks & refreshments",
"refreshment": "snacks & refreshments",
"refreshments": "snacks & refreshments",
"food": "snacks & refreshments",
"lunch": "snacks & refreshments",
"dinner": "snacks & refreshments",
"water": "snacks & refreshments",
"bottle": "snacks & refreshments",
"bottles": "snacks & refreshments",
# Registration / process / communication
"registration": "registration & process",
"register": "registration & process",
"forms": "registration & process",
"form": "registration & process",
"process": "registration & process",
"procedure": "registration & process",
"instructions": "communication & information",
"information": "communication & information",
"info": "communication & information",
"communication": "communication & information",
"update": "communication & information",
"updates": "communication & information",
"mail": "communication & information",
"email": "communication & information",
"whatsapp": "communication & information",
"notification": "communication & information",
"notifications": "communication & information",
# Parking / transport / access
"parking": "parking & access",
"park": "parking & access",
"entrypoint": "parking & access",
"gate": "parking & access",
"traffic": "parking & access",
# Kids & family
"kids": "kids & family activities",
"children": "kids & family activities",
"family": "kids & family activities",
"families": "kids & family activities",
"booth": "kids & family activities",
"photo": "kids & family activities",
"photos": "kids & family activities",
"photobooth": "kids & family activities",
"games": "kids & family activities", # kids game stalls etc.
"game": "kids & family activities",
# Frequency / future events
"frequency": "event frequency & cadence",
"regular": "event frequency & cadence",
"regularly": "event frequency & cadence",
"often": "event frequency & cadence",
"monthly": "event frequency & cadence",
"yearly": "event frequency & cadence",
"quarterly": "event frequency & cadence",
# Content / activities / engagement
"content": "content & activities",
"activities": "content & activities",
"activity": "content & activities",
"session": "content & activities",
"sessions": "content & activities",
"variety": "content & activities",
"options": "content & activities",
"options": "content & activities",
# Safety / hygiene
"safety": "safety & hygiene",
"safe": "safety & hygiene",
"hygiene": "safety & hygiene",
"cleanliness": "safety & hygiene",
"cleaning": "safety & hygiene",
"washroom": "safety & hygiene",
"washrooms": "safety & hygiene",
"toilet": "safety & hygiene",
"toilets": "safety & hygiene",
# Staffing / volunteers
"volunteers": "staffing & volunteers",
"volunteer": "staffing & volunteers",
"staff": "staffing & volunteers",
"helpdesk": "staffing & volunteers",
}
# ============================================================
# STEP 5 — Summariser
# ============================================================
def summarize_group(texts):
tokens = []
for t in texts:
tokens.extend(clean_tokens(t))
if not tokens:
return ""
counts = Counter(tokens)
# 1) Aggregate mapped themes
theme_counts = Counter()
for word, c in counts.items():
if word in label_map:
theme_counts[label_map[word]] += c
# 2) Optional: Add very strong unmapped words as raw themes
if RAW_THEMES_ON:
for word, c in counts.items():
if word in label_map:
continue
if c < MIN_COUNT_RAW:
continue
# avoid obvious verbs/adverbs
if word.endswith(("ed", "ing", "ly", "s")):
continue
if len(word) <= 3:
continue
if word in event_tokens:
continue
theme_counts[word] += c
if not theme_counts:
return ""
top_themes = [theme for theme, _ in theme_counts.most_common(MAX_THEMES)]
if not top_themes:
return ""
if len(top_themes) == 1:
return f"Participants most frequently suggested improvements in {top_themes[0]}."
elif len(top_themes) == 2:
return (
f"Participants most frequently suggested improvements in "
f"{top_themes[0]} and {top_themes[1]}."
)
else:
return (
"Participants most frequently suggested improvements in "
+ ", ".join(top_themes[:-1])
+ f", and {top_themes[-1]}."
)
def wrap_text(text, width=80):
return "\n".join(textwrap.wrap(text, width))
# ============================================================
# APPLY TO CURRENT FILTERED DATA
# ============================================================
if TEXT_COL not in dataset.columns:
summary_text = f"Column '{TEXT_COL}' not found in current visual."
else:
texts = dataset[TEXT_COL].dropna()
summary_text = summarize_group(texts)
if not summary_text:
summary_text = "No clear recurring improvement themes found for the current selection."
wrapped = wrap_text(summary_text)
# ============================================================
# DRAW VISUAL — robust against clipping
# ============================================================
fig_w, fig_h, dpi = 20, 4, 120
fontsize = 40
# Optional: smarter wrapping based on figure + font size
def dynamic_wrap(text, fig_width_in=fig_w, dpi=dpi, font_size=fontsize, char_width_px=0.6):
px_width = fig_width_in * dpi
approx_char_px = font_size * char_width_px
width_chars = max(30, int(px_width / approx_char_px))
return "\n".join(textwrap.wrap(text, width=width_chars))
wrapped = dynamic_wrap(summary_text)
fig = plt.figure(figsize=(fig_w, fig_h), dpi=dpi)
# Place text in FIGURE coordinates to avoid axes clipping
fig.text(0.5, 0.5, wrapped,
ha="center", va="center",
fontsize=fontsize, fontweight='bold',
color='darkblue',
wrap=True,
bbox=dict(boxstyle='round,pad=0.5', facecolor='white', alpha=0.95, edgecolor='white'))
plt.tight_layout(pad=2.0)
plt.show()
====================
Hi @kushanNa , given the challenges I have with report embedded in .NET portal, and not being able to pre-process, the solution you provided is definitely helpful.
I need to see and keep updating the ThemeMap table. And, this will improve the summarization. Out of so many different alternate solutions I explored and seen from expertise here, at least for my situation, this is most applicable solution. It does not mean the solutions others proposed is wrong, it was just not feasible in my business scenario.
Hi @prasaddn ,
Given the constraints of a .NET embedded scenario and the inability to preprocess data, maintaining and periodically updating the ThemeMap table is a practical and supported approach. It gives you a way to manage and improve the summarization logic directly within the Power BI model, without depending on unsupported Python visuals or external services.
As you rightly pointed out, while the other options discussed are technically valid, this approach aligns best with your current business needs and platform limitations.
Hii @prasaddn
Python (and R) visuals are not supported in Power BI Embedded when using a .NET wrapper with “Embed for your customers (app owns data)”, so the visual works in Desktop/Service but fails in the embedded portal. This is a platform limitation, not a coding issue. To resolve it, move the Python logic outside Power BI (for example, run the text summarization in Azure Functions, Databricks, or Fabric and store the results) and only display the output using standard Power BI visuals, or use a custom visual that calls a backend API for dynamic summaries.
thank you for the response @rohit1991 . Yes, the only alternate I heard is pre-computing. However, it is not feasible with my current scenario. would be keen to know if you are aware of any such free custom visual that calls a backend API for dynamic summaries and also works in a .NET wrapper environment.
Hii @prasaddn
There is no free, out-of-the-box Power BI custom visual that can securely call an external backend API and render dynamic text summaries in an Embed for your customers (.NET / app-owns-data) scenario. Due to sandboxing and security restrictions, custom visuals cannot freely make arbitrary API calls unless they are purpose-built and certified, and most AppSource visuals don’t support this pattern. In practice, the supported approaches are to build your own custom visual that calls a controlled API endpoint, or move the summarization logic outside Power BI (Azure Function/Fabric/Databricks) and load the results into the model for display using standard visuals.
Thank you again @rohit1991 for sharing your expertise on this. Let me see what best I can do. The last option is to pre-compute and summarize comments for various dimension of filter selections. Thank you again for your time.
Hi @prasaddn ,
Thanks for confirming. Given the current limitations with Power BI Embedded, pre computing or periodically refreshing the summarized results outside Power BI and then displaying them using standard visuals is the most reliable and supported approach in a .NET embedded setup. Glad the discussion helped bring clarity on the available options.
The Power BI Data Visualization World Championships is back! Get ahead of the game and start preparing now!
| User | Count |
|---|---|
| 40 | |
| 37 | |
| 33 | |
| 29 | |
| 27 |
| User | Count |
|---|---|
| 133 | |
| 104 | |
| 61 | |
| 59 | |
| 55 |