mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 08:32:09 +01:00
Refactor trend calculations and improve WordCloud component responsiveness; remove trend labels for cleaner display
This commit is contained in:
@ -253,10 +253,6 @@ function DashboardContent() {
|
||||
}
|
||||
trend={{
|
||||
value: metrics.sessionTrend ?? 0,
|
||||
label:
|
||||
(metrics.sessionTrend ?? 0) > 0
|
||||
? `${metrics.sessionTrend ?? 0}% increase`
|
||||
: `${Math.abs(metrics.sessionTrend ?? 0)}% decrease`,
|
||||
isPositive: (metrics.sessionTrend ?? 0) >= 0,
|
||||
}}
|
||||
/>
|
||||
@ -281,10 +277,6 @@ function DashboardContent() {
|
||||
}
|
||||
trend={{
|
||||
value: metrics.usersTrend ?? 0,
|
||||
label:
|
||||
(metrics.usersTrend ?? 0) > 0
|
||||
? `${metrics.usersTrend}% increase`
|
||||
: `${Math.abs(metrics.usersTrend ?? 0)}% decrease`,
|
||||
isPositive: (metrics.usersTrend ?? 0) >= 0,
|
||||
}}
|
||||
/>
|
||||
@ -309,10 +301,6 @@ function DashboardContent() {
|
||||
}
|
||||
trend={{
|
||||
value: metrics.avgSessionTimeTrend ?? 0,
|
||||
label:
|
||||
(metrics.avgSessionTimeTrend ?? 0) > 0
|
||||
? `${metrics.avgSessionTimeTrend}% increase`
|
||||
: `${Math.abs(metrics.avgSessionTimeTrend ?? 0)}% decrease`,
|
||||
isPositive: (metrics.avgSessionTimeTrend ?? 0) >= 0,
|
||||
}}
|
||||
/>
|
||||
@ -337,10 +325,6 @@ function DashboardContent() {
|
||||
}
|
||||
trend={{
|
||||
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
|
||||
}}
|
||||
/>
|
||||
@ -402,7 +386,9 @@ function DashboardContent() {
|
||||
<h3 className="font-bold text-lg text-gray-800 mb-4">
|
||||
Common Topics
|
||||
</h3>
|
||||
<WordCloud words={getWordCloudData()} />
|
||||
<div className="h-[300px]">
|
||||
<WordCloud words={getWordCloudData()} width={500} height={400} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -67,9 +67,6 @@ export default function MetricCard({
|
||||
>
|
||||
{trend.isPositive !== false ? "↑" : "↓"}{" "}
|
||||
{Math.abs(trend.value).toFixed(1)}%
|
||||
{trend.label && (
|
||||
<span className="text-gray-500 ml-1">{trend.label}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -11,20 +11,55 @@ interface WordCloudProps {
|
||||
}[];
|
||||
width?: number;
|
||||
height?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
}
|
||||
|
||||
export default function WordCloud({
|
||||
words,
|
||||
width = 500,
|
||||
height = 300,
|
||||
width: initialWidth = 500,
|
||||
height: initialHeight = 300,
|
||||
minWidth = 200,
|
||||
minHeight = 200,
|
||||
}: WordCloudProps) {
|
||||
const svgRef = useRef<SVGSVGElement | null>(null);
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: initialWidth,
|
||||
height: initialHeight,
|
||||
});
|
||||
|
||||
// Set isClient to true on initial render
|
||||
useEffect(() => {
|
||||
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(() => {
|
||||
if (!svgRef.current || !isClient || !words.length) return;
|
||||
|
||||
@ -36,7 +71,7 @@ export default function WordCloud({
|
||||
|
||||
// Configure the layout
|
||||
const layout = cloud()
|
||||
.size([width, height])
|
||||
.size([dimensions.width, dimensions.height])
|
||||
.words(
|
||||
words.map((d) => ({
|
||||
text: d.text,
|
||||
@ -53,7 +88,10 @@ export default function WordCloud({
|
||||
function draw(words: Word[]) {
|
||||
svg
|
||||
.append("g")
|
||||
.attr("transform", `translate(${width / 2},${height / 2})`)
|
||||
.attr(
|
||||
"transform",
|
||||
`translate(${dimensions.width / 2},${dimensions.height / 2})`
|
||||
)
|
||||
.selectAll("text")
|
||||
.data(words)
|
||||
.enter()
|
||||
@ -87,7 +125,7 @@ export default function WordCloud({
|
||||
return () => {
|
||||
svg.selectAll("*").remove();
|
||||
};
|
||||
}, [words, width, height, isClient]);
|
||||
}, [words, dimensions, isClient]);
|
||||
|
||||
if (!isClient) {
|
||||
return (
|
||||
@ -98,12 +136,21 @@ export default function WordCloud({
|
||||
}
|
||||
|
||||
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
|
||||
ref={svgRef}
|
||||
width={width}
|
||||
height={height}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
className="w-full h-full"
|
||||
aria-label="Word cloud visualization of categories"
|
||||
style={{
|
||||
maxWidth: "100%",
|
||||
maxHeight: "100%",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
523
lib/metrics.ts
523
lib/metrics.ts
@ -13,295 +13,309 @@ interface CompanyConfig {
|
||||
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
|
||||
const stopWords = new Set([
|
||||
"assistant",
|
||||
"user",
|
||||
// Web
|
||||
"bmp",
|
||||
"co",
|
||||
"com",
|
||||
"www",
|
||||
"http",
|
||||
"https",
|
||||
"www2",
|
||||
"css",
|
||||
"gif",
|
||||
"href",
|
||||
"html",
|
||||
"php",
|
||||
"js",
|
||||
"css",
|
||||
"xml",
|
||||
"json",
|
||||
"txt",
|
||||
"jpg",
|
||||
"jpeg",
|
||||
"png",
|
||||
"gif",
|
||||
"bmp",
|
||||
"svg",
|
||||
"org",
|
||||
"net",
|
||||
"co",
|
||||
"http",
|
||||
"https",
|
||||
"io",
|
||||
"jpeg",
|
||||
"jpg",
|
||||
"js",
|
||||
"json",
|
||||
"net",
|
||||
"org",
|
||||
"php",
|
||||
"png",
|
||||
"svg",
|
||||
"txt",
|
||||
"www",
|
||||
"www2",
|
||||
"xml",
|
||||
// English stop words
|
||||
"a",
|
||||
"about",
|
||||
"above",
|
||||
"after",
|
||||
"again",
|
||||
"against",
|
||||
"ain",
|
||||
"all",
|
||||
"am",
|
||||
"an",
|
||||
"the",
|
||||
"is",
|
||||
"any",
|
||||
"are",
|
||||
"was",
|
||||
"were",
|
||||
"aren",
|
||||
"at",
|
||||
"be",
|
||||
"been",
|
||||
"before",
|
||||
"being",
|
||||
"have",
|
||||
"has",
|
||||
"had",
|
||||
"do",
|
||||
"does",
|
||||
"did",
|
||||
"will",
|
||||
"would",
|
||||
"should",
|
||||
"below",
|
||||
"between",
|
||||
"both",
|
||||
"by",
|
||||
"bye",
|
||||
"can",
|
||||
"could",
|
||||
"may",
|
||||
"might",
|
||||
"must",
|
||||
"am",
|
||||
"i",
|
||||
"you",
|
||||
"he",
|
||||
"she",
|
||||
"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",
|
||||
"couldn",
|
||||
"d",
|
||||
"did",
|
||||
"didn",
|
||||
"do",
|
||||
"does",
|
||||
"doesn",
|
||||
"don",
|
||||
"down",
|
||||
"out",
|
||||
"off",
|
||||
"over",
|
||||
"under",
|
||||
"again",
|
||||
"further",
|
||||
"then",
|
||||
"once",
|
||||
"here",
|
||||
"there",
|
||||
"when",
|
||||
"where",
|
||||
"why",
|
||||
"how",
|
||||
"all",
|
||||
"any",
|
||||
"both",
|
||||
"during",
|
||||
"each",
|
||||
"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",
|
||||
"most",
|
||||
"other",
|
||||
"some",
|
||||
"such",
|
||||
"must",
|
||||
"mustn",
|
||||
"my",
|
||||
"needn",
|
||||
"no",
|
||||
"nor",
|
||||
"not",
|
||||
"only",
|
||||
"own",
|
||||
"same",
|
||||
"so",
|
||||
"than",
|
||||
"too",
|
||||
"very",
|
||||
"s",
|
||||
"t",
|
||||
"just",
|
||||
"don",
|
||||
"shouldve",
|
||||
"now",
|
||||
"d",
|
||||
"ll",
|
||||
"m",
|
||||
"o",
|
||||
"re",
|
||||
"ve",
|
||||
"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",
|
||||
"of",
|
||||
"off",
|
||||
"ok",
|
||||
"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",
|
||||
"bye",
|
||||
"goodbye",
|
||||
"yes",
|
||||
"you",
|
||||
"your",
|
||||
"yours",
|
||||
// French stop words
|
||||
"des",
|
||||
"donc",
|
||||
"et",
|
||||
"la",
|
||||
"le",
|
||||
"les",
|
||||
"mais",
|
||||
"ou",
|
||||
"un",
|
||||
"une",
|
||||
"des",
|
||||
"et",
|
||||
"ou",
|
||||
"mais",
|
||||
"donc",
|
||||
// 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",
|
||||
"uit",
|
||||
"sinds",
|
||||
"tijdens",
|
||||
"binnen",
|
||||
"buiten",
|
||||
"zonder",
|
||||
"volgens",
|
||||
"dankzij",
|
||||
"ondanks",
|
||||
"behalve",
|
||||
"mits",
|
||||
"tenzij",
|
||||
"hoewel",
|
||||
"al",
|
||||
"alhoewel",
|
||||
"toch",
|
||||
"als",
|
||||
"anders",
|
||||
"echter",
|
||||
"wel",
|
||||
"niet",
|
||||
"geen",
|
||||
"iets",
|
||||
"niets",
|
||||
"veel",
|
||||
"weinig",
|
||||
"meer",
|
||||
"meest",
|
||||
"elk",
|
||||
"ieder",
|
||||
"sommige",
|
||||
"hoe",
|
||||
"wat",
|
||||
"waar",
|
||||
"wie",
|
||||
"wanneer",
|
||||
"waarom",
|
||||
"welke",
|
||||
"wordt",
|
||||
"worden",
|
||||
"werd",
|
||||
"werden",
|
||||
"geworden",
|
||||
"zijn",
|
||||
"behalve",
|
||||
"ben",
|
||||
"ben",
|
||||
"bent",
|
||||
"was",
|
||||
"waren",
|
||||
"bij",
|
||||
"binnen",
|
||||
"boven",
|
||||
"buiten",
|
||||
"dan",
|
||||
"dankzij",
|
||||
"dat",
|
||||
"de",
|
||||
"deze",
|
||||
"die",
|
||||
"dit",
|
||||
"dit",
|
||||
"door",
|
||||
"dus",
|
||||
"echter",
|
||||
"een",
|
||||
"elk",
|
||||
"en",
|
||||
"geen",
|
||||
"gehad",
|
||||
"geweest",
|
||||
"hebben",
|
||||
"heb",
|
||||
"hebt",
|
||||
"heeft",
|
||||
"geworden",
|
||||
"had",
|
||||
"hadden",
|
||||
"gehad",
|
||||
"kunnen",
|
||||
"heb",
|
||||
"hebben",
|
||||
"hebt",
|
||||
"heeft",
|
||||
"het",
|
||||
"hij",
|
||||
"hoe",
|
||||
"hoewel",
|
||||
"ieder",
|
||||
"iets",
|
||||
"ik",
|
||||
"jij",
|
||||
"jullie",
|
||||
"kan",
|
||||
"kunt",
|
||||
"kon",
|
||||
"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",
|
||||
"zij",
|
||||
"zijn",
|
||||
"zonder",
|
||||
"zullen",
|
||||
"zult",
|
||||
// Add more domain-specific stop words if necessary
|
||||
]);
|
||||
@ -515,6 +529,24 @@ export function sessionMetrics(
|
||||
const avgSessionsPerDay =
|
||||
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:", {
|
||||
// totalSessionDuration,
|
||||
// validSessionsForDuration,
|
||||
@ -542,7 +574,16 @@ export function sessionMetrics(
|
||||
wordCloudData,
|
||||
belowThresholdCount: alerts, // Corrected to match MetricsResult interface (belowThresholdCount)
|
||||
avgSessionsPerDay, // Added to satisfy MetricsResult interface
|
||||
// Optional fields from MetricsResult that are not yet calculated can be added here or handled by the consumer
|
||||
// avgSentiment, sentimentThreshold, lastUpdated, sessionTrend, usersTrend, avgSessionTimeTrend, avgResponseTimeTrend
|
||||
// Map trend values to the expected property names in MetricsResult
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user