Forum Discussion
Python Visual rendering issue on .Net wrapper
- 7 months ago
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()
====================
- prasaddn7 months agoFrequent Visitor
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.
- v-sshirivolu7 months agoCommunity Support
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.