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 @@ + + Batstats icon + Stylised bat embracing analytics bars and line chart + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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) => (