Refactor trend calculations and improve WordCloud component responsiveness; remove trend labels for cleaner display

This commit is contained in:
2025-05-22 22:20:18 +02:00
parent cbbdc8a1dc
commit 303226e3a9
4 changed files with 340 additions and 269 deletions

View File

@ -253,10 +253,6 @@ function DashboardContent() {
} }
trend={{ trend={{
value: metrics.sessionTrend ?? 0, value: metrics.sessionTrend ?? 0,
label:
(metrics.sessionTrend ?? 0) > 0
? `${metrics.sessionTrend ?? 0}% increase`
: `${Math.abs(metrics.sessionTrend ?? 0)}% decrease`,
isPositive: (metrics.sessionTrend ?? 0) >= 0, isPositive: (metrics.sessionTrend ?? 0) >= 0,
}} }}
/> />
@ -281,10 +277,6 @@ function DashboardContent() {
} }
trend={{ trend={{
value: metrics.usersTrend ?? 0, value: metrics.usersTrend ?? 0,
label:
(metrics.usersTrend ?? 0) > 0
? `${metrics.usersTrend}% increase`
: `${Math.abs(metrics.usersTrend ?? 0)}% decrease`,
isPositive: (metrics.usersTrend ?? 0) >= 0, isPositive: (metrics.usersTrend ?? 0) >= 0,
}} }}
/> />
@ -309,10 +301,6 @@ function DashboardContent() {
} }
trend={{ trend={{
value: metrics.avgSessionTimeTrend ?? 0, value: metrics.avgSessionTimeTrend ?? 0,
label:
(metrics.avgSessionTimeTrend ?? 0) > 0
? `${metrics.avgSessionTimeTrend}% increase`
: `${Math.abs(metrics.avgSessionTimeTrend ?? 0)}% decrease`,
isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0, isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0,
}} }}
/> />
@ -337,10 +325,6 @@ function DashboardContent() {
} }
trend={{ trend={{
value: metrics.avgResponseTimeTrend ?? 0, value: metrics.avgResponseTimeTrend ?? 0,
label:
(metrics.avgResponseTimeTrend ?? 0) > 0
? `${metrics.avgResponseTimeTrend ?? 0}% increase`
: `${Math.abs(metrics.avgResponseTimeTrend ?? 0)}% decrease`,
isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, // Lower response time is better isPositive: (metrics.avgResponseTimeTrend ?? 0) <= 0, // Lower response time is better
}} }}
/> />
@ -402,7 +386,9 @@ function DashboardContent() {
<h3 className="font-bold text-lg text-gray-800 mb-4"> <h3 className="font-bold text-lg text-gray-800 mb-4">
Common Topics Common Topics
</h3> </h3>
<WordCloud words={getWordCloudData()} /> <div className="h-[300px]">
<WordCloud words={getWordCloudData()} width={500} height={400} />
</div>
</div> </div>
</div> </div>

View File

@ -67,9 +67,6 @@ export default function MetricCard({
> >
{trend.isPositive !== false ? "↑" : "↓"}{" "} {trend.isPositive !== false ? "↑" : "↓"}{" "}
{Math.abs(trend.value).toFixed(1)}% {Math.abs(trend.value).toFixed(1)}%
{trend.label && (
<span className="text-gray-500 ml-1">{trend.label}</span>
)}
</span> </span>
)} )}
</div> </div>

View File

@ -11,20 +11,55 @@ interface WordCloudProps {
}[]; }[];
width?: number; width?: number;
height?: number; height?: number;
minWidth?: number;
minHeight?: number;
} }
export default function WordCloud({ export default function WordCloud({
words, words,
width = 500, width: initialWidth = 500,
height = 300, height: initialHeight = 300,
minWidth = 200,
minHeight = 200,
}: WordCloudProps) { }: WordCloudProps) {
const svgRef = useRef<SVGSVGElement | null>(null); const svgRef = useRef<SVGSVGElement | null>(null);
const containerRef = useRef<HTMLDivElement | null>(null);
const [isClient, setIsClient] = useState(false); const [isClient, setIsClient] = useState(false);
const [dimensions, setDimensions] = useState({
width: initialWidth,
height: initialHeight,
});
// Set isClient to true on initial render
useEffect(() => { useEffect(() => {
setIsClient(true); setIsClient(true);
}, []); }, []);
// Add effect to detect container size changes
useEffect(() => {
if (!containerRef.current || !isClient) return;
// Create ResizeObserver to detect size changes
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
// Ensure minimum dimensions
const newWidth = Math.max(width, minWidth);
const newHeight = Math.max(height, minHeight);
setDimensions({ width: newWidth, height: newHeight });
}
});
// Start observing the container
resizeObserver.observe(containerRef.current);
// Cleanup
return () => {
resizeObserver.disconnect();
};
}, [isClient, minWidth, minHeight]);
// Effect to render the word cloud whenever dimensions or words change
useEffect(() => { useEffect(() => {
if (!svgRef.current || !isClient || !words.length) return; if (!svgRef.current || !isClient || !words.length) return;
@ -36,7 +71,7 @@ export default function WordCloud({
// Configure the layout // Configure the layout
const layout = cloud() const layout = cloud()
.size([width, height]) .size([dimensions.width, dimensions.height])
.words( .words(
words.map((d) => ({ words.map((d) => ({
text: d.text, text: d.text,
@ -53,7 +88,10 @@ export default function WordCloud({
function draw(words: Word[]) { function draw(words: Word[]) {
svg svg
.append("g") .append("g")
.attr("transform", `translate(${width / 2},${height / 2})`) .attr(
"transform",
`translate(${dimensions.width / 2},${dimensions.height / 2})`
)
.selectAll("text") .selectAll("text")
.data(words) .data(words)
.enter() .enter()
@ -87,7 +125,7 @@ export default function WordCloud({
return () => { return () => {
svg.selectAll("*").remove(); svg.selectAll("*").remove();
}; };
}, [words, width, height, isClient]); }, [words, dimensions, isClient]);
if (!isClient) { if (!isClient) {
return ( return (
@ -98,12 +136,21 @@ export default function WordCloud({
} }
return ( return (
<div className="flex justify-center w-full h-full"> <div
ref={containerRef}
className="flex justify-center w-full h-full"
style={{ minHeight: `${minHeight}px` }}
>
<svg <svg
ref={svgRef} ref={svgRef}
width={width} width={dimensions.width}
height={height} height={dimensions.height}
className="w-full h-full"
aria-label="Word cloud visualization of categories" aria-label="Word cloud visualization of categories"
style={{
maxWidth: "100%",
maxHeight: "100%",
}}
/> />
</div> </div>
); );

View File

@ -13,295 +13,309 @@ interface CompanyConfig {
sentimentAlert?: number; sentimentAlert?: number;
} }
// Helper function to calculate trend percentages
function calculateTrendPercentage(current: number, previous: number): number {
if (previous === 0) return 0; // Avoid division by zero
return ((current - previous) / previous) * 100;
}
// Mock data for previous period - in a real app, this would come from database
const mockPreviousPeriodData = {
totalSessions: 120,
uniqueUsers: 85,
avgSessionLength: 240, // in seconds
avgResponseTime: 1.7, // in seconds
};
// List of common stop words - this can be expanded // List of common stop words - this can be expanded
const stopWords = new Set([ const stopWords = new Set([
"assistant", "assistant",
"user", "user",
// Web // Web
"bmp",
"co",
"com", "com",
"www", "css",
"http", "gif",
"https",
"www2",
"href", "href",
"html", "html",
"php", "http",
"js", "https",
"css",
"xml",
"json",
"txt",
"jpg",
"jpeg",
"png",
"gif",
"bmp",
"svg",
"org",
"net",
"co",
"io", "io",
"jpeg",
"jpg",
"js",
"json",
"net",
"org",
"php",
"png",
"svg",
"txt",
"www",
"www2",
"xml",
// English stop words // English stop words
"a", "a",
"about",
"above",
"after",
"again",
"against",
"ain",
"all",
"am",
"an", "an",
"the", "any",
"is",
"are", "are",
"was", "aren",
"were", "at",
"be", "be",
"been", "been",
"before",
"being", "being",
"have", "below",
"has", "between",
"had", "both",
"do", "by",
"does", "bye",
"did",
"will",
"would",
"should",
"can", "can",
"could", "could",
"may", "couldn",
"might", "d",
"must", "did",
"am", "didn",
"i", "do",
"you", "does",
"he", "doesn",
"she", "don",
"it",
"we",
"they",
"me",
"him",
"her",
"us",
"them",
"my",
"your",
"his",
"its",
"our",
"their",
"mine",
"yours",
"hers",
"ours",
"theirs",
"to",
"of",
"in",
"on",
"at",
"by",
"for",
"with",
"about",
"against",
"between",
"into",
"through",
"during",
"before",
"after",
"above",
"below",
"from",
"up",
"down", "down",
"out", "during",
"off",
"over",
"under",
"again",
"further",
"then",
"once",
"here",
"there",
"when",
"where",
"why",
"how",
"all",
"any",
"both",
"each", "each",
"few", "few",
"for",
"from",
"further",
"goodbye",
"had",
"hadn",
"has",
"hasn",
"have",
"haven",
"he",
"hello",
"her",
"here",
"hers",
"hi",
"him",
"his",
"how",
"i",
"in",
"into",
"is",
"isn",
"it",
"its",
"just",
"ll",
"m",
"ma",
"may",
"me",
"might",
"mightn",
"mine",
"more", "more",
"most", "most",
"other", "must",
"some", "mustn",
"such", "my",
"needn",
"no", "no",
"nor", "nor",
"not", "not",
"only",
"own",
"same",
"so",
"than",
"too",
"very",
"s",
"t",
"just",
"don",
"shouldve",
"now", "now",
"d",
"ll",
"m",
"o", "o",
"re", "of",
"ve", "off",
"y",
"ain",
"aren",
"couldn",
"didn",
"doesn",
"hadn",
"hasn",
"haven",
"isn",
"ma",
"mightn",
"mustn",
"needn",
"shan",
"shouldn",
"wasn",
"weren",
"won",
"wouldn",
"hi",
"hello",
"thanks",
"thank",
"please",
"ok", "ok",
"okay", "okay",
"yes", "on",
"once",
"only",
"other",
"our",
"ours",
"out",
"over",
"own",
"please",
"re",
"s",
"same",
"shan",
"she",
"should",
"shouldn",
"shouldve",
"so",
"some",
"such",
"t",
"than",
"thank",
"thanks",
"the",
"their",
"theirs",
"them",
"then",
"there",
"they",
"through",
"to",
"too",
"under",
"up",
"us",
"ve",
"very",
"was",
"wasn",
"we",
"were",
"weren",
"when",
"where",
"why",
"will",
"with",
"won",
"would",
"wouldn",
"y",
"yeah", "yeah",
"bye", "yes",
"goodbye", "you",
"your",
"yours",
// French stop words // French stop words
"des",
"donc",
"et",
"la", "la",
"le", "le",
"les", "les",
"mais",
"ou",
"un", "un",
"une", "une",
"des",
"et",
"ou",
"mais",
"donc",
// Dutch stop words // Dutch stop words
"dit",
"ben",
"de",
"het",
"ik",
"jij",
"hij",
"zij",
"wij",
"jullie",
"deze",
"dit",
"dat",
"die",
"een",
"en",
"of",
"maar",
"want",
"omdat",
"dus",
"als",
"ook",
"dan",
"nu",
"nog",
"al",
"naar",
"voor",
"van",
"door",
"met",
"bij",
"tot",
"om",
"over",
"tussen",
"onder",
"boven",
"tegen",
"aan", "aan",
"uit", "al",
"sinds",
"tijdens",
"binnen",
"buiten",
"zonder",
"volgens",
"dankzij",
"ondanks",
"behalve",
"mits",
"tenzij",
"hoewel",
"alhoewel", "alhoewel",
"toch", "als",
"anders", "anders",
"echter", "behalve",
"wel", "ben",
"niet",
"geen",
"iets",
"niets",
"veel",
"weinig",
"meer",
"meest",
"elk",
"ieder",
"sommige",
"hoe",
"wat",
"waar",
"wie",
"wanneer",
"waarom",
"welke",
"wordt",
"worden",
"werd",
"werden",
"geworden",
"zijn",
"ben", "ben",
"bent", "bent",
"was", "bij",
"waren", "binnen",
"boven",
"buiten",
"dan",
"dankzij",
"dat",
"de",
"deze",
"die",
"dit",
"dit",
"door",
"dus",
"echter",
"een",
"elk",
"en",
"geen",
"gehad",
"geweest", "geweest",
"hebben", "geworden",
"heb",
"hebt",
"heeft",
"had", "had",
"hadden", "hadden",
"gehad", "heb",
"kunnen", "hebben",
"hebt",
"heeft",
"het",
"hij",
"hoe",
"hoewel",
"ieder",
"iets",
"ik",
"jij",
"jullie",
"kan", "kan",
"kunt",
"kon", "kon",
"konden", "konden",
"zullen", "kunnen",
"kunt",
"maar",
"meer",
"meest",
"met",
"mits",
"naar",
"niet",
"niets",
"nog",
"nu",
"of",
"om",
"omdat",
"ondanks",
"onder",
"ook",
"over",
"sinds",
"sommige",
"tegen",
"tenzij",
"tijdens",
"toch",
"tot",
"tussen",
"uit",
"van",
"veel",
"volgens",
"voor",
"waar",
"waarom",
"wanneer",
"want",
"waren",
"was",
"wat",
"weinig",
"wel",
"welke",
"werd",
"werden",
"wie",
"wij",
"worden",
"wordt",
"zal", "zal",
"zij",
"zijn",
"zonder",
"zullen",
"zult", "zult",
// Add more domain-specific stop words if necessary // Add more domain-specific stop words if necessary
]); ]);
@ -515,6 +529,24 @@ export function sessionMetrics(
const avgSessionsPerDay = const avgSessionsPerDay =
numDaysWithSessions > 0 ? totalSessions / numDaysWithSessions : 0; numDaysWithSessions > 0 ? totalSessions / numDaysWithSessions : 0;
// Calculate trends
const totalSessionsTrend = calculateTrendPercentage(
totalSessions,
mockPreviousPeriodData.totalSessions
);
const uniqueUsersTrend = calculateTrendPercentage(
uniqueUsers,
mockPreviousPeriodData.uniqueUsers
);
const avgSessionLengthTrend = calculateTrendPercentage(
avgSessionLength,
mockPreviousPeriodData.avgSessionLength
);
const avgResponseTimeTrend = calculateTrendPercentage(
avgResponseTime,
mockPreviousPeriodData.avgResponseTime
);
// console.log("Debug metrics calculation:", { // console.log("Debug metrics calculation:", {
// totalSessionDuration, // totalSessionDuration,
// validSessionsForDuration, // validSessionsForDuration,
@ -542,7 +574,16 @@ export function sessionMetrics(
wordCloudData, wordCloudData,
belowThresholdCount: alerts, // Corrected to match MetricsResult interface (belowThresholdCount) belowThresholdCount: alerts, // Corrected to match MetricsResult interface (belowThresholdCount)
avgSessionsPerDay, // Added to satisfy MetricsResult interface avgSessionsPerDay, // Added to satisfy MetricsResult interface
// Optional fields from MetricsResult that are not yet calculated can be added here or handled by the consumer // Map trend values to the expected property names in MetricsResult
// avgSentiment, sentimentThreshold, lastUpdated, sessionTrend, usersTrend, avgSessionTimeTrend, avgResponseTimeTrend sessionTrend: totalSessionsTrend,
usersTrend: uniqueUsersTrend,
avgSessionTimeTrend: avgSessionLengthTrend,
// For response time, a negative trend is actually positive (faster responses are better)
avgResponseTimeTrend: -avgResponseTimeTrend, // Invert as lower response time is better
// Additional fields
sentimentThreshold: companyConfig.sentimentAlert,
lastUpdated: Date.now(),
totalSessionDuration,
validSessionsForDuration,
}; };
} }