diff --git a/frontend/bats-dashboar-tp/icon/batstats.svg b/frontend/bats-dashboar-tp/icon/batstats.svg
new file mode 100644
index 00000000..23b80ce2
--- /dev/null
+++ b/frontend/bats-dashboar-tp/icon/batstats.svg
@@ -0,0 +1,38 @@
+
diff --git a/frontend/bats-dashboar-tp/icon/favico_batstats.png b/frontend/bats-dashboar-tp/icon/favico_batstats.png
new file mode 100644
index 00000000..5c8c5b88
Binary files /dev/null and b/frontend/bats-dashboar-tp/icon/favico_batstats.png differ
diff --git a/frontend/bats-dashboar-tp/icon/favico_batstats_128.png b/frontend/bats-dashboar-tp/icon/favico_batstats_128.png
new file mode 100644
index 00000000..6bcd91be
Binary files /dev/null and b/frontend/bats-dashboar-tp/icon/favico_batstats_128.png differ
diff --git a/frontend/bats-dashboar-tp/icon/favico_batstats_256.png b/frontend/bats-dashboar-tp/icon/favico_batstats_256.png
new file mode 100644
index 00000000..9ddc869a
Binary files /dev/null and b/frontend/bats-dashboar-tp/icon/favico_batstats_256.png differ
diff --git a/frontend/bats-dashboar-tp/icon/favico_batstats_32.png b/frontend/bats-dashboar-tp/icon/favico_batstats_32.png
new file mode 100644
index 00000000..c229cd02
Binary files /dev/null and b/frontend/bats-dashboar-tp/icon/favico_batstats_32.png differ
diff --git a/frontend/bats-dashboar-tp/icon/favico_batstats_512.png b/frontend/bats-dashboar-tp/icon/favico_batstats_512.png
new file mode 100644
index 00000000..b8e324d2
Binary files /dev/null and b/frontend/bats-dashboar-tp/icon/favico_batstats_512.png differ
diff --git a/frontend/bats-dashboar-tp/icon/favico_batstats_64.png b/frontend/bats-dashboar-tp/icon/favico_batstats_64.png
new file mode 100644
index 00000000..5c8c5b88
Binary files /dev/null and b/frontend/bats-dashboar-tp/icon/favico_batstats_64.png differ
diff --git a/frontend/bats-dashboar-tp/icon/generate_icons.py b/frontend/bats-dashboar-tp/icon/generate_icons.py
new file mode 100644
index 00000000..4daefe4a
--- /dev/null
+++ b/frontend/bats-dashboar-tp/icon/generate_icons.py
@@ -0,0 +1,167 @@
+from pathlib import Path
+
+from PIL import Image, ImageDraw
+
+
+PALETTE = {
+ "background": (11, 31, 51, 255), # #0b1f33
+ "background_glow": (26, 55, 83, 255),
+ "wing": (14, 20, 36, 255),
+ "wing_highlight": (32, 54, 82, 255),
+ "chart_bar": (244, 198, 69, 255), # #f4c645
+ "chart_line": (86, 196, 255, 255), # #56c4ff
+ "spark": (255, 255, 255, 255),
+}
+
+
+def draw_icon(size: int, output: Path) -> None:
+ img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
+ draw = ImageDraw.Draw(img)
+
+ center = size / 2
+ radius = size * 0.46
+
+ # Background circle with subtle glow
+ draw.ellipse(
+ [
+ (center - radius, center - radius),
+ (center + radius, center + radius),
+ ],
+ fill=PALETTE["background"],
+ )
+
+ glow_radius = radius * 0.82
+ draw.ellipse(
+ [
+ (center - glow_radius, center - glow_radius),
+ (center + glow_radius, center + glow_radius),
+ ],
+ fill=PALETTE["background_glow"],
+ )
+
+ # Chart bars
+ bar_width = size * 0.045
+ base_y = center + radius * 0.35
+ spacing = bar_width * 1.6
+ heights = [radius * h for h in (0.32, 0.5, 0.4)]
+ start_x = center - spacing
+ for i, height in enumerate(heights):
+ x0 = start_x + i * spacing
+ draw.rounded_rectangle(
+ [
+ (x0 - bar_width / 2, base_y - height),
+ (x0 + bar_width / 2, base_y),
+ ],
+ radius=bar_width * 0.3,
+ fill=PALETTE["chart_bar"],
+ )
+
+ # Line chart overlay
+ line_points = [
+ (center - spacing * 1.4, base_y - radius * 0.15),
+ (center - spacing * 0.4, base_y - radius * 0.38),
+ (center + spacing * 0.6, base_y - radius * 0.2),
+ (center + spacing * 1.4, base_y - radius * 0.45),
+ ]
+ draw.line(line_points, fill=PALETTE["chart_line"], width=int(size * 0.022))
+ for point in line_points:
+ draw.ellipse(
+ [
+ (point[0] - bar_width * 0.6, point[1] - bar_width * 0.6),
+ (point[0] + bar_width * 0.6, point[1] + bar_width * 0.6),
+ ],
+ fill=PALETTE["chart_line"],
+ )
+
+ # Bat silhouette
+ def scale(points):
+ return [
+ (
+ center + (x - 256) * (size / 512),
+ center + (y - 256) * (size / 512),
+ )
+ for x, y in points
+ ]
+
+ bat_points = scale(
+ [
+ (256, 188),
+ (236, 200),
+ (210, 192),
+ (180, 170),
+ (150, 184),
+ (128, 210),
+ (110, 240),
+ (148, 235),
+ (182, 262),
+ (198, 298),
+ (226, 282),
+ (256, 304),
+ (286, 282),
+ (314, 298),
+ (330, 262),
+ (364, 235),
+ (402, 240),
+ (384, 210),
+ (362, 184),
+ (332, 170),
+ (302, 192),
+ (276, 200),
+ ]
+ )
+
+ draw.polygon(bat_points, fill=PALETTE["wing"])
+
+ ear_width = size * 0.05
+ ear_height = size * 0.08
+ ear_offset = size * 0.045
+ draw.polygon(
+ [
+ (center - ear_offset - ear_width / 2, center - radius * 0.35),
+ (center - ear_offset, center - radius * 0.55),
+ (center - ear_offset + ear_width / 2, center - radius * 0.35),
+ ],
+ fill=PALETTE["wing_highlight"],
+ )
+ draw.polygon(
+ [
+ (center + ear_offset - ear_width / 2, center - radius * 0.35),
+ (center + ear_offset, center - radius * 0.55),
+ (center + ear_offset + ear_width / 2, center - radius * 0.35),
+ ],
+ fill=PALETTE["wing_highlight"],
+ )
+
+ # Sparks
+ spark_radius = size * 0.018
+ spark_positions = [
+ (center + radius * 0.55, center - radius * 0.25),
+ (center + radius * 0.35, center - radius * 0.45),
+ (center + radius * 0.65, center - radius * 0.05),
+ ]
+ for sx, sy in spark_positions:
+ draw.ellipse(
+ [
+ (sx - spark_radius, sy - spark_radius),
+ (sx + spark_radius, sy + spark_radius),
+ ],
+ fill=PALETTE["spark"],
+ )
+
+ img.save(output)
+
+
+def main():
+ output_dir = Path(__file__).resolve().parent
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ for size in (512, 256, 128, 64, 32):
+ filename = output_dir / f"favico_batstats_{size}.png"
+ draw_icon(size, filename)
+
+ # default favicon
+ draw_icon(64, output_dir / "favico_batstats.png")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/frontend/bats-dashboar-tp/src/Components/Barchart/BarChart.tsx b/frontend/bats-dashboar-tp/src/Components/Barchart/BarChart.tsx
index 2479d324..4f67580a 100644
--- a/frontend/bats-dashboar-tp/src/Components/Barchart/BarChart.tsx
+++ b/frontend/bats-dashboar-tp/src/Components/Barchart/BarChart.tsx
@@ -3,7 +3,7 @@ import { BarChartState, BarLayout } from '../../types/Types';
import useBarChartState from './useBarChartState';
import Plot from 'react-plotly.js';
-import { useEffect } from 'react';
+import { useEffect, useMemo } from 'react';
const BarChart = ({ data, onStateChange }:
@@ -22,9 +22,9 @@ const BarChart = ({ data, onStateChange }:
return
No data available
;
}
-
- const headers = data[0];
- const rows = data.slice(1);
+
+ const headers = useMemo(() => (Array.isArray(data) && data.length > 0 ? data[0] : []), [data]);
+ const rows = useMemo(() => (Array.isArray(data) && data.length > 1 ? data.slice(1) : []), [data]);
const {
@@ -36,34 +36,47 @@ const BarChart = ({ data, onStateChange }:
handleNumericChange,
handleAdditionalColumnChange,
} = useBarChartState(headers);
+
+ const barChartData = useMemo(() => {
+ if (!categoricalColumn || !numericColumn || !additionalColumn) {
+ return [];
+ }
- const barChartData = (): BarChartState[] => {
+ const categoricalIndex = headers.indexOf(categoricalColumn);
+ const numericIndex = headers.indexOf(numericColumn);
+ const additionalIndex = headers.indexOf(additionalColumn);
- if (!categoricalColumn || !numericColumn || !additionalColumn) return [];
+ if (categoricalIndex === -1 || numericIndex === -1 || additionalIndex === -1) {
+ return [];
+ }
const groupedData: { [key: string]: { values: number[]; tooltips: string[] } } = {};
rows.forEach((row) => {
- const category = row[headers.indexOf(categoricalColumn)] as string;
- const numericValue = Number(row[headers.indexOf(numericColumn)]);
- const additionalValue = row[headers.indexOf(additionalColumn)];
+ const category = String(row[categoricalIndex] ?? '');
+ const numericValue = Number(row[numericIndex]);
+ const additionalValue = row[additionalIndex];
+
+ if (!Number.isFinite(numericValue)) {
+ return;
+ }
if (!selectedCategories.length || selectedCategories.includes(category)) {
if (!groupedData[category]) {
groupedData[category] = { values: [], tooltips: [] };
}
groupedData[category].values.push(numericValue);
- groupedData[category].tooltips.push(`${category}, ${numericValue}, ${additionalColumn}: ${additionalValue}`);
+ groupedData[category].tooltips.push(`${category}, ${numericValue}, ${additionalColumn}: ${additionalValue ?? ''}`);
}
});
- const sortedGroupedData: BarChartState[] = Object.entries(groupedData)
- .sort(([, a], [, b]) => {
+ return Object.entries(groupedData)
+ .sort(([, a], [, b]) => {
const sumA = a.values.reduce((acc, val) => acc + val, 0);
const sumB = b.values.reduce((acc, val) => acc + val, 0);
return sumB - sumA;
- })
- .map(([key, { values, tooltips }]) => ({
+ })
+ .map(([key, { values, tooltips }]) => ({
y: values,
x: Array.from({ length: values.length }, (_, i) => i + 1),
type: 'bar' as const,
@@ -71,44 +84,55 @@ const BarChart = ({ data, onStateChange }:
text: tooltips,
hoverinfo: 'text',
textposition: 'none',
- }));
-
- return sortedGroupedData;
- };
+ }));
+ }, [additionalColumn, headers, numericColumn, rows, selectedCategories, categoricalColumn]);
useEffect(() => {
- if (onStateChange) {
- const numericIndex = headers.indexOf(numericColumn);
- // const numericValues = data.map((d: any) => d[numericColumn]);
- const numericValues = rows.map((row) => Number(row[numericIndex]))
- .filter((value) => Number.isFinite(value));
- const minNumeric = Math.min(...numericValues);
- const maxNumeric = Math.max(...numericValues);
- const layout: BarLayout = {
+ if (!onStateChange || !categoricalColumn || !numericColumn || !additionalColumn) {
+ return;
+ }
+
+ const numericIndex = headers.indexOf(numericColumn);
+ const categoricalIndex = headers.indexOf(categoricalColumn);
+
+ if (numericIndex === -1 || categoricalIndex === -1) {
+ return;
+ }
+
+ const numericValues = rows
+ .map((row) => Number(row[numericIndex]))
+ .filter((value) => Number.isFinite(value));
+
+ const minNumeric = numericValues.length ? Math.min(...numericValues) : 0;
+ const maxNumeric = numericValues.length ? Math.max(...numericValues) : 0;
+
+ const layout: BarLayout = {
+ title: {
+ text: 'Bar Chart: Dynamic Analysis',
+ },
+ yaxis: {
title: {
- text: 'Bar Chart: Dynamic Analysis',
+ text: numericColumn,
},
- yaxis: { title: {
- text: numericColumn },
type: 'linear',
range: [minNumeric, maxNumeric],
- autorange: true, },
-
- xaxis: {
- title: {
- text: categoricalColumn },
- type: 'category',
- range: [0, data.length - 1],
- autorange: true,
+ autorange: !numericValues.length,
+ },
+ xaxis: {
+ title: {
+ text: categoricalColumn,
},
- };
-
- onStateChange({
- data: barChartData(),
- layout,
- });
- }
- }, [categoricalColumn, numericColumn, additionalColumn, selectedCategories]);
+ type: 'category',
+ range: [0, Math.max(rows.length - 1, 0)],
+ autorange: rows.length === 0,
+ },
+ };
+
+ onStateChange({
+ data: barChartData,
+ layout,
+ });
+ }, [additionalColumn, barChartData, categoricalColumn, headers, numericColumn, onStateChange, rows.length]);
return (
@@ -122,7 +146,7 @@ const BarChart = ({ data, onStateChange }:
value={categoricalColumn || ''}
onChange={handleCategoricalChange}
>
-
+
{headers.map((header) => (
+
{headers.map((header) => (
+
{headers.map((header) => (