mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 09:32:08 +01:00
Uses `Intl.DisplayNames` to display localized language names in the language pie chart, enhancing user experience and readability. Also converts country and language values from the CSV data to ISO codes for standardization and improved data handling. Adds tooltip to display ISO language code.
315 lines
8.1 KiB
TypeScript
315 lines
8.1 KiB
TypeScript
"use client";
|
|
import { useEffect, useRef } from "react";
|
|
import Chart from "chart.js/auto";
|
|
|
|
interface SessionsData {
|
|
[date: string]: number;
|
|
}
|
|
|
|
interface CategoriesData {
|
|
[category: string]: number;
|
|
}
|
|
|
|
interface LanguageData {
|
|
[language: string]: number;
|
|
}
|
|
|
|
interface SessionsLineChartProps {
|
|
sessionsPerDay: SessionsData;
|
|
}
|
|
|
|
interface CategoriesBarChartProps {
|
|
categories: CategoriesData;
|
|
}
|
|
|
|
interface LanguagePieChartProps {
|
|
languages: LanguageData;
|
|
}
|
|
|
|
interface SentimentChartProps {
|
|
sentimentData: {
|
|
positive: number;
|
|
neutral: number;
|
|
negative: number;
|
|
};
|
|
}
|
|
|
|
interface TokenUsageChartProps {
|
|
tokenData: {
|
|
labels: string[];
|
|
values: number[];
|
|
costs: number[];
|
|
};
|
|
}
|
|
|
|
// Basic line and bar chart for metrics. Extend as needed.
|
|
export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
useEffect(() => {
|
|
if (!ref.current || !sessionsPerDay) return;
|
|
const ctx = ref.current.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: "line",
|
|
data: {
|
|
labels: Object.keys(sessionsPerDay),
|
|
datasets: [
|
|
{
|
|
label: "Sessions",
|
|
data: Object.values(sessionsPerDay),
|
|
borderColor: "rgb(59, 130, 246)",
|
|
backgroundColor: "rgba(59, 130, 246, 0.1)",
|
|
borderWidth: 2,
|
|
tension: 0.3,
|
|
fill: true,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: { y: { beginAtZero: true } },
|
|
},
|
|
});
|
|
return () => chart.destroy();
|
|
}, [sessionsPerDay]);
|
|
return <canvas ref={ref} height={180} />;
|
|
}
|
|
|
|
export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
|
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
useEffect(() => {
|
|
if (!ref.current || !categories) return;
|
|
const ctx = ref.current.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: "bar",
|
|
data: {
|
|
labels: Object.keys(categories),
|
|
datasets: [
|
|
{
|
|
label: "Categories",
|
|
data: Object.values(categories),
|
|
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
|
borderWidth: 1,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: { y: { beginAtZero: true } },
|
|
},
|
|
});
|
|
return () => chart.destroy();
|
|
}, [categories]);
|
|
return <canvas ref={ref} height={180} />;
|
|
}
|
|
|
|
export function SentimentChart({ sentimentData }: SentimentChartProps) {
|
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
useEffect(() => {
|
|
if (!ref.current || !sentimentData) return;
|
|
const ctx = ref.current.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: "doughnut",
|
|
data: {
|
|
labels: ["Positive", "Neutral", "Negative"],
|
|
datasets: [
|
|
{
|
|
data: [
|
|
sentimentData.positive,
|
|
sentimentData.neutral,
|
|
sentimentData.negative,
|
|
],
|
|
backgroundColor: [
|
|
"rgba(34, 197, 94, 0.8)", // green
|
|
"rgba(249, 115, 22, 0.8)", // orange
|
|
"rgba(239, 68, 68, 0.8)", // red
|
|
],
|
|
borderWidth: 1,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: "right",
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 20,
|
|
},
|
|
},
|
|
},
|
|
cutout: "65%",
|
|
},
|
|
});
|
|
return () => chart.destroy();
|
|
}, [sentimentData]);
|
|
return <canvas ref={ref} height={180} />;
|
|
}
|
|
|
|
export function LanguagePieChart({ languages }: LanguagePieChartProps) {
|
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
useEffect(() => {
|
|
if (!ref.current || !languages) return;
|
|
const ctx = ref.current.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
// Get top 5 languages, combine others
|
|
const entries = Object.entries(languages);
|
|
let topLanguages = entries.sort((a, b) => b[1] - a[1]).slice(0, 5);
|
|
|
|
// Sum the count of all other languages
|
|
const otherCount = entries
|
|
.slice(5)
|
|
.reduce((sum, [, count]) => sum + count, 0);
|
|
if (otherCount > 0) {
|
|
topLanguages.push(["Other", otherCount]);
|
|
}
|
|
|
|
// Use Intl.DisplayNames to get localized language names from ISO codes
|
|
const languageDisplayNames = new Intl.DisplayNames(["en"], {
|
|
type: "language",
|
|
});
|
|
|
|
// Store original ISO codes for tooltip
|
|
const isoCodes = topLanguages.map(([lang]) => lang);
|
|
|
|
const labels = topLanguages.map(([lang], index) => {
|
|
// Check if this is a valid ISO 639-1 language code
|
|
if (lang && lang !== "Other" && /^[a-z]{2}$/.test(lang)) {
|
|
try {
|
|
return languageDisplayNames.of(lang);
|
|
} catch (e) {
|
|
return lang; // Fallback to code if display name can't be resolved
|
|
}
|
|
}
|
|
return lang; // Return original string for "Other" or invalid codes
|
|
});
|
|
|
|
const data = topLanguages.map(([, count]) => count);
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: "pie",
|
|
data: {
|
|
labels,
|
|
datasets: [
|
|
{
|
|
data,
|
|
backgroundColor: [
|
|
"rgba(59, 130, 246, 0.8)",
|
|
"rgba(16, 185, 129, 0.8)",
|
|
"rgba(249, 115, 22, 0.8)",
|
|
"rgba(236, 72, 153, 0.8)",
|
|
"rgba(139, 92, 246, 0.8)",
|
|
"rgba(107, 114, 128, 0.8)",
|
|
],
|
|
borderWidth: 1,
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: "right",
|
|
labels: {
|
|
usePointStyle: true,
|
|
padding: 20,
|
|
},
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function (context) {
|
|
const label = context.label || "";
|
|
const value = context.formattedValue || "";
|
|
const index = context.dataIndex;
|
|
const isoCode = isoCodes[index];
|
|
|
|
// Only show ISO code if it's not "Other" and it's a valid 2-letter code
|
|
if (
|
|
isoCode &&
|
|
isoCode !== "Other" &&
|
|
/^[a-z]{2}$/.test(isoCode)
|
|
) {
|
|
return `${label} (${isoCode.toUpperCase()}): ${value}`;
|
|
}
|
|
|
|
return `${label}: ${value}`;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return () => chart.destroy();
|
|
}, [languages]);
|
|
return <canvas ref={ref} height={180} />;
|
|
}
|
|
|
|
export function TokenUsageChart({ tokenData }: TokenUsageChartProps) {
|
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
|
useEffect(() => {
|
|
if (!ref.current || !tokenData) return;
|
|
const ctx = ref.current.getContext("2d");
|
|
if (!ctx) return;
|
|
|
|
const chart = new Chart(ctx, {
|
|
type: "bar",
|
|
data: {
|
|
labels: tokenData.labels,
|
|
datasets: [
|
|
{
|
|
label: "Tokens",
|
|
data: tokenData.values,
|
|
backgroundColor: "rgba(59, 130, 246, 0.7)",
|
|
borderWidth: 1,
|
|
yAxisID: "y",
|
|
},
|
|
{
|
|
label: "Cost (EUR)",
|
|
data: tokenData.costs,
|
|
backgroundColor: "rgba(16, 185, 129, 0.7)",
|
|
borderWidth: 1,
|
|
type: "line",
|
|
yAxisID: "y1",
|
|
},
|
|
],
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: true } },
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
position: "left",
|
|
title: {
|
|
display: true,
|
|
text: "Token Count",
|
|
},
|
|
},
|
|
y1: {
|
|
beginAtZero: true,
|
|
position: "right",
|
|
grid: {
|
|
drawOnChartArea: false,
|
|
},
|
|
title: {
|
|
display: true,
|
|
text: "Cost (EUR)",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
return () => chart.destroy();
|
|
}, [tokenData]);
|
|
return <canvas ref={ref} height={180} />;
|
|
}
|