diff --git a/website-nextjs/src/components/admin/benchmarks/BenchmarkSummaryTable.tsx b/website-nextjs/src/components/admin/benchmarks/BenchmarkSummaryTable.tsx index 9264dccf..ff3e2a84 100644 --- a/website-nextjs/src/components/admin/benchmarks/BenchmarkSummaryTable.tsx +++ b/website-nextjs/src/components/admin/benchmarks/BenchmarkSummaryTable.tsx @@ -7,6 +7,10 @@ const BenchmarkSummaryTable = () => { return state.results.metaData; }); + const nOfProblems = [ + "Total number of benchmark problems", + "Total number of benchmark size instances", + ]; const availableModels = useSelector((state: { results: IResultState }) => { return state.results.availableModels; }); @@ -51,13 +55,20 @@ const BenchmarkSummaryTable = () => { const sectorsMap = new Map(); const milpFeaturesMap = new Map(); const timeHorizonsMap = new Map(); + const realSizesMap = new Map(); + const nOfProblemsMap = new Map(); function updateData(data: Map, key: string) { data.set(key, (data.get(key) || 0) + 1); } - Object.keys(metaData).forEach((key) => { if (metaData[key].modelName === model) { + // Number of problems + updateData(nOfProblemsMap, "totalNOfDiffProblems"); + metaData[key].sizes.forEach(() => { + updateData(nOfProblemsMap, "multipleSizes"); + }); + availableTechniques.forEach((technique) => { if (metaData[key].technique === technique) { updateData(techniquesMap, technique); @@ -83,8 +94,17 @@ const BenchmarkSummaryTable = () => { updateData(timeHorizonsMap, timeHorizon as string); } }); + if (metaData[key].sizes.some((instance) => instance.size === "R")) { + if (metaData[key].technique === "MILP") { + updateData(realSizesMap, "milp" as string); + } + updateData(realSizesMap, "real" as string); + } else { + updateData(realSizesMap, "other" as string); + } } }); + if (timeHorizonsMap.size === 0) { timeHorizonsMap.set("single", -1); timeHorizonsMap.set("multi", -1); @@ -96,13 +116,14 @@ const BenchmarkSummaryTable = () => { milpFeatures: milpFeaturesMap, timeHorizons: timeHorizonsMap, sectors: sectorsMap, + realSizes: realSizesMap, + nOfProblems: nOfProblemsMap, }; }); return (
-

Model Distribution Matrix

@@ -118,30 +139,86 @@ const BenchmarkSummaryTable = () => { {model} ))} + {/* N. of problem */} + {nOfProblems.map((nOfProblem, nOfProblemIdx) => ( + + {nOfProblemIdx === 0 && ( + + )} + + + {summary.map((s, sIdx) => ( + + ))} + + + ))} {/* Technique */} {availableTechniques.map((technique, techniqueIdx) => ( - - + )} + {summary.map((s, sIdx) => ( ))} + ))} {/* Kind of Problem */} @@ -151,20 +228,32 @@ const BenchmarkSummaryTable = () => { key={kindOfProblemIdx} className="border-b odd:bg-[#BFD8C71A] odd:bg-opacity-10" > - - + )} + {summary.map((s, sIdx) => ( ))} + ), )} @@ -174,22 +263,38 @@ const BenchmarkSummaryTable = () => { key={timeHorizonIdx} className="border-b odd:bg-[#BFD8C71A] odd:bg-opacity-10" > - - + )} + {summary.map((s, sIdx) => ( ))} + ))} {/* MILP Features */} @@ -198,20 +303,82 @@ const BenchmarkSummaryTable = () => { key={milpFeatureIdx} className="border-b odd:bg-[#BFD8C71A] odd:bg-opacity-10" > - - + )} + {summary.map((s, sIdx) => ( ))} + + + ))} + {/* Size Features */} + {["Real(MILP)", "Other"].map((size, sizeIdx) => ( + + {sizeIdx === 0 && ( + + )} + + {summary.map((s, sIdx) => ( + + ))} + ))} diff --git a/website-nextjs/src/components/admin/benchmarks/RowChart.tsx b/website-nextjs/src/components/admin/benchmarks/RowChart.tsx new file mode 100644 index 00000000..9aa5b287 --- /dev/null +++ b/website-nextjs/src/components/admin/benchmarks/RowChart.tsx @@ -0,0 +1,199 @@ +import D3BarChart from "@/components/shared/D3BarChart"; +import D3StackedBarChart from "@/components/shared/D3StackedBarChart"; +import { IResultState } from "@/types/state"; +import { getChartColor } from "@/utils/chart"; +import React, { useMemo } from "react"; +import { useSelector } from "react-redux"; + +const RowChart = () => { + const metaData = useSelector((state: { results: IResultState }) => { + return state.results.metaData; + }); + + const availableModels = useSelector((state: { results: IResultState }) => { + return state.results.availableModels; + }); + + const availableTechniques = useSelector( + (state: { results: IResultState }) => { + return state.results.availableTechniques; + }, + ); + + const availableKindOfProblems = useSelector( + (state: { results: IResultState }) => { + return state.results.availableKindOfProblems; + }, + ); + + const availableSectors = useSelector((state: { results: IResultState }) => { + return state.results.availableSectors; + }); + + const availableMilpFeatures = useMemo(() => { + return Array.from( + new Set(Object.keys(metaData).map((key) => metaData[key].milpFeatures)), + ); + }, [metaData]); + + const availabletimeHorizons = ["single", "multi"]; + const summary = availableModels.map((model) => { + const techniquesMap = new Map(); + const kindOfProblemsMap = new Map(); + const sectorsMap = new Map(); + const milpFeaturesMap = new Map(); + const timeHorizonsMap = new Map(); + const realSizesMap = new Map(); + const nOfProblemsMap = new Map(); + + function updateData(data: Map, key: string) { + data.set(key, (data.get(key) || 0) + 1); + } + Object.keys(metaData).forEach((key) => { + if (metaData[key].modelName === model) { + // Number of problems + updateData(nOfProblemsMap, "totalNOfDiffProblems"); + metaData[key].sizes.forEach(() => { + updateData(nOfProblemsMap, "multipleSizes"); + }); + + availableTechniques.forEach((technique) => { + if (metaData[key].technique === technique) { + updateData(techniquesMap, technique); + } + }); + availableKindOfProblems.forEach((kindOfProblem) => { + if (metaData[key].kindOfProblem === kindOfProblem) { + updateData(kindOfProblemsMap, kindOfProblem); + } + }); + availableSectors.forEach((sector) => { + if (metaData[key].sectors === sector) { + updateData(sectorsMap, sector); + } + }); + availableMilpFeatures.forEach((milpFeature) => { + if (metaData[key].milpFeatures === milpFeature) { + updateData(milpFeaturesMap, milpFeature as string); + } + }); + availabletimeHorizons.forEach((timeHorizon) => { + if (metaData[key].timeHorizon.toLowerCase().includes(timeHorizon)) { + updateData(timeHorizonsMap, timeHorizon as string); + } + }); + if (metaData[key].sizes.some((instance) => instance.size === "R")) { + if (metaData[key].technique === "MILP") { + updateData(realSizesMap, "milp" as string); + } + updateData(realSizesMap, "real" as string); + } else { + updateData(realSizesMap, "other" as string); + } + } + }); + + if (timeHorizonsMap.size === 0) { + timeHorizonsMap.set("single", -1); + timeHorizonsMap.set("multi", -1); + } + return { + modelName: model, + techniques: techniquesMap, + kindOfProblems: kindOfProblemsMap, + milpFeatures: milpFeaturesMap, + timeHorizons: timeHorizonsMap, + sectors: sectorsMap, + realSizes: realSizesMap, + nOfProblems: nOfProblemsMap, + }; + }); + + const techniquesChartData = summary.map((data) => ({ + modelName: data.modelName, + LP: data.techniques.get("LP") || 0, + MILP: data.techniques.get("MILP") || 0, + })); + + const timeHorizonsChartData = summary.map((data) => ({ + modelName: data.modelName, + single: data.timeHorizons.get("single") || 0, + multi: data.timeHorizons.get("multi") || 0, + })); + const availableProblemSizes = useSelector( + (state: { results: IResultState }) => state.results.availableProblemSizes, + ); + const sizeData = useMemo(() => { + const sizeData = availableProblemSizes.map((size) => { + return { + size, + value: 0, + category: size, + group: size, + }; + }); + Object.keys(metaData).forEach((key) => { + metaData[key].sizes.forEach((s) => { + const data = sizeData.find((sd) => sd.size === s.size); + if (data) { + data.value += 1; + } + }); + }); + return sizeData; + }, [metaData, availableProblemSizes]); + + return ( +
+
+ Number of benchmark instances +
+
+
+ +
+
+ +
+
+ { + acc[d.size] = getChartColor(idx); + return acc; + }, + {} as Record, + )} + tooltipFormat={(d) => `${d.group}: ${d.value}`} + yAxisLabel="" + title="By Size" + /> +
+
+
+ ); +}; + +export default RowChart; diff --git a/website-nextjs/src/components/shared/D3BarChart.tsx b/website-nextjs/src/components/shared/D3BarChart.tsx new file mode 100644 index 00000000..2a95fd8a --- /dev/null +++ b/website-nextjs/src/components/shared/D3BarChart.tsx @@ -0,0 +1,220 @@ +import { useEffect, useRef } from "react"; +import * as d3 from "d3"; + +interface DataPoint { + category: string; + group: string; + value: number; +} + +interface ID3BarChart { + title: string; + height?: number; + className?: string; + data: DataPoint[]; + colors?: Record; + tooltipFormat?: (d: DataPoint) => string; + yAxisLabel?: string; + xAxisLabel?: string; +} + +const D3BarChart = ({ + title, + height = 200, + className = "", + data = [], + colors = {}, + tooltipFormat, + yAxisLabel = "", + xAxisLabel = "", +}: ID3BarChart) => { + const containerRef = useRef(null); + const svgRef = useRef(null); + + useEffect(() => { + const width = containerRef.current?.clientWidth || 400; + const margin = { top: 20, right: 10, bottom: 40, left: 40 }; + + // Clear previous SVG + d3.select(svgRef.current).selectAll("*").remove(); + + const svg = d3 + .select(svgRef.current) + .attr("width", width) + .attr("height", height) + .style("background", "white") + .style("overflow", "visible"); + + const tooltip = d3 + .select("body") + .append("div") + .style("position", "absolute") + .style("background", "white") + .style("border", "1px solid #ccc") + .style("border-radius", "5px") + .style("padding", "8px") + .style("font-size", "12px") + .style("color", "#333") + .style("box-shadow", "0px 4px 6px rgba(0, 0, 0, 0.1)") + .style("pointer-events", "none") + .style("opacity", 0); + + // Scales + const xScale = d3 + .scaleBand() + .domain(Array.from(new Set(data.map((d) => d.category)))) + .range([margin.left + 20, width - margin.right]) + .padding(0.2); + + const yScale = d3 + .scaleLinear() + .domain([0, (d3.max(data, (d) => d.value) ?? 0) + 1]) + .range([height - margin.bottom, margin.top]); + + // Axes + const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); + const yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); + + // Add axes + svg + .append("g") + .attr("transform", `translate(0,${height - margin.bottom})`) + .call(xAxis) + .call((g) => { + g.selectAll(".domain").attr("display", "none"); + g.selectAll(".tick line").attr("display", "none"); + g.selectAll("text").attr("fill", "#A1A9BC"); + }); + + svg + .append("g") + .attr("transform", `translate(${margin.left},0)`) + .call(yAxis) + .call((g) => { + g.selectAll(".domain").attr("display", "none"); + g.selectAll(".tick line").attr("display", "none"); + g.selectAll("text").attr("fill", "#A1A9BC"); + }); + + // Group data by group + const groupedData = d3.group(data, (d) => d.group); + + // Create bars for each group + groupedData.forEach((values, group, groups) => { + const groupIndex = Array.from(groups.keys()).indexOf(group); + const barWidth = xScale.bandwidth(); + + svg + .selectAll(`.bar-${group}`) + .data(values) + .join("rect") + .attr("class", `bar-${group}`) + .attr("x", (d) => xScale(d.category) ?? 0) + .attr("y", (d) => yScale(d.value)) + .attr("width", barWidth) + .attr("height", (d) => height - margin.bottom - yScale(d.value)) + .attr( + "fill", + colors[group] || `hsl(${(groupIndex * 360) / groups.size}, 70%, 50%)`, + ) + .on("mouseover", (event, d) => { + tooltip + .style("opacity", 1) + .html( + tooltipFormat?.(d) || + `${d.group}
+ ${d.category}: ${d.value}`, + ) + .style("left", `${event.pageX + 10}px`) + .style("top", `${event.pageY - 30}px`); + }) + .on("mousemove", (event) => { + tooltip + .style("left", `${event.pageX + 10}px`) + .style("top", `${event.pageY - 30}px`); + }) + .on("mouseout", () => { + tooltip.style("opacity", 0); + }); + }); + + // Add grid lines + const grid = (g: d3.Selection) => + g + .attr("stroke", "currentColor") + .attr("stroke-opacity", 0.1) + .call((g) => + g + .append("g") + .selectAll("line") + .data(yScale.ticks()) + .join("line") + .attr("y1", (d) => 0.5 + yScale(d)) + .attr("y2", (d) => 0.5 + yScale(d)) + .attr("x1", margin.left) + .attr("x2", width - margin.right) + .attr("stroke-dasharray", "4,4"), + ); + svg.append("g").call(grid); + + // Add axis labels + svg + .append("text") + .attr("x", width / 2) + .attr("y", height - 5) + .attr("text-anchor", "middle") + .attr("fill", "#8C8C8C") + .text(xAxisLabel); + + svg + .append("text") + .attr("transform", "rotate(-90)") + .attr("x", -height / 2) + .attr("y", 15) + .attr("text-anchor", "middle") + .attr("fill", "#8C8C8C") + .text(yAxisLabel); + + return () => { + tooltip.remove(); + }; + }, [data, height, colors, tooltipFormat, xAxisLabel, yAxisLabel]); + + return ( +
+
+ {title} +
+ {/* Legend */} +
+ {Array.from(new Set(data.map((d) => d.group))).map((group) => ( +
+ d.group))).indexOf( + group, + ) * + 360) / + new Set(data.map((d) => d.group)).size + }, 70%, 50%)`, + }} + /> + {group} +
+ ))} +
+
+ +
+
+ ); +}; + +export default D3BarChart; diff --git a/website-nextjs/src/components/shared/D3StackedBarChart.tsx b/website-nextjs/src/components/shared/D3StackedBarChart.tsx new file mode 100644 index 00000000..b3357eb9 --- /dev/null +++ b/website-nextjs/src/components/shared/D3StackedBarChart.tsx @@ -0,0 +1,195 @@ +import { useEffect, useRef } from "react"; +import * as d3 from "d3"; +import { CircleIcon } from "@/assets/icons"; +import { ID3StackedBarChart } from "@/types/chart"; + +const D3StackedBarChart = ({ + title, + height = 200, + className = "", + data = [], + categoryKey, + xAxisTooltipFormat, + colors, + xAxisLabel = "Category", + yAxisLabel = "Value", + rotateXAxisLabels = false, + showXaxisLabel = true, +}: ID3StackedBarChart) => { + const containerRef = useRef(null); + const svgRef = useRef(null); + + useEffect(() => { + if (!data.length) return; + + const width = containerRef.current?.clientWidth || 400; + const margin = { top: 20, right: 10, bottom: 40, left: 40 }; + + // Clear previous SVG + d3.select(svgRef.current).selectAll("*").remove(); + + const svg = d3 + .select(svgRef.current) + .attr("width", width) + .attr("height", height) + .style("background", "white") + .style("overflow", "visible"); + + // Get keys for stacking (excluding category key) + const keys = Object.keys(data[0]).filter((key) => key !== categoryKey); + + // Stack the data + const stack = d3.stack().keys(keys); + const stackedData = stack(data as Iterable<{ [key: string]: number }>); + + // Scales + const xScale = d3 + .scaleBand() + .domain(data.map((d) => d[categoryKey].toString())) + .range([margin.left, width - margin.right]) + .padding(0.1); + + const yScale = d3 + .scaleLinear() + .domain([ + 0, + d3.max(stackedData[stackedData.length - 1], (d) => d[1]) || 0, + ]) + .range([height - margin.bottom, margin.top]); + + // Tooltip + const tooltip = d3 + .select("body") + .append("div") + .style("position", "absolute") + .style("background", "white") + .style("border", "1px solid #ccc") + .style("border-radius", "5px") + .style("padding", "8px") + .style("font-size", "12px") + .style("pointer-events", "none") + .style("opacity", 0); + + // Create bars + svg + .selectAll("g.stack") + .data(stackedData) + .join("g") + .attr("class", "stack") + .attr("fill", (d) => colors[d.key]) + .selectAll("rect") + .data((d) => d) + .join("rect") + .attr("x", (d) => xScale(d.data[categoryKey].toString()) || 0) + .attr("y", (d) => yScale(d[1])) + .attr("height", (d) => yScale(d[0]) - yScale(d[1])) + .attr("width", xScale.bandwidth()) + .on("mouseover", (event, d) => { + const key = ( + d3.select(event.target.parentNode).datum() as { key: string } + ).key; + const value = d.data[key]; + tooltip + .style("opacity", 1) + .html( + `${xAxisLabel}: ${d.data[categoryKey]}
+ Category: ${key}
+ Value: ${ + xAxisTooltipFormat ? xAxisTooltipFormat(value) : value + }`, + ) + .style("left", `${event.pageX + 10}px`) + .style("top", `${event.pageY - 30}px`); + }) + .on("mouseout", () => tooltip.style("opacity", 0)); + + // Add axes + const xAxis = d3.axisBottom(xScale).tickSizeOuter(0); + const yAxis = d3.axisLeft(yScale).ticks(6).tickSizeOuter(0); + // X-axis + svg + .append("g") + .attr("transform", `translate(0,${height - margin.bottom})`) + .call(xAxis) + .call((g) => { + g.selectAll(".domain").attr("display", "none"); + g.selectAll(".tick line").attr("display", "none"); + g.selectAll("text") + .attr("fill", "#A1A9BC") + .style("text-anchor", rotateXAxisLabels ? "end" : "middle") + .attr("transform", rotateXAxisLabels ? "rotate(-45)" : "rotate(0)"); + }); + + // Y-axis + svg + .append("g") + .attr("transform", `translate(${margin.left},0)`) + .call(yAxis) + .call((g) => { + g.selectAll(".domain").attr("display", "none"); + g.selectAll(".tick line").attr("display", "none"); + g.selectAll("text").attr("fill", "#A1A9BC"); + }); + if (showXaxisLabel) { + // Update axis labels + svg + .append("text") + .attr("x", width / 2) + .attr("y", height - 5) + .attr("text-anchor", "middle") + .attr("fill", "#8C8C8C") + .style("font-size", "12px") + .text(xAxisLabel); + } + svg + .append("text") + .attr("x", -height / 2) + .attr("y", margin.left - 60) + .attr("fill", "#8C8C8C") + .attr("transform", "rotate(-90)") + .attr("text-anchor", "middle") + .style("font-size", "12px") + .text(yAxisLabel); + + return () => { + tooltip.remove(); + }; + }, [ + data, + height, + colors, + xAxisTooltipFormat, + title, + xAxisLabel, + yAxisLabel, + categoryKey, + rotateXAxisLabels, + ]); + + return ( +
+
+ {title} +
+
+ {Object.keys(colors).map((solverKey) => ( +
+ + {solverKey} +
+ ))} +
+
+ +
+
+ ); +}; + +export default D3StackedBarChart; diff --git a/website-nextjs/src/constants/path.ts b/website-nextjs/src/constants/path.ts index 3a9cd534..fb2ebc48 100644 --- a/website-nextjs/src/constants/path.ts +++ b/website-nextjs/src/constants/path.ts @@ -7,6 +7,7 @@ export const PATH_DASHBOARD = { list: `/${BASE_PATH}/benchmark-details`, one: `/${BASE_PATH}/benchmark-details/{name}`, }, + benchmarkSummary: `/${BASE_PATH}/benchmark-summary`, compareSolvers: `/${BASE_PATH}/compare-solvers`, solvers: `/${BASE_PATH}/solvers`, performanceHistory: `/${BASE_PATH}/performance-history`, diff --git a/website-nextjs/src/pages/dashboard/benchmark-details.tsx b/website-nextjs/src/pages/dashboard/benchmark-details.tsx index 15bd0474..7b8354b6 100644 --- a/website-nextjs/src/pages/dashboard/benchmark-details.tsx +++ b/website-nextjs/src/pages/dashboard/benchmark-details.tsx @@ -1,10 +1,9 @@ import { useSelector } from "react-redux"; // local -import DetailSection from "@/components/admin/DetailSection"; import { AdminHeader, Footer, Navbar } from "@/components/shared"; import Head from "next/head"; import BenchmarkTableResult from "@/components/admin/benchmark-detail/BenchmarkTableResult"; -import { ArrowIcon, HomeIcon } from "@/assets/icons"; +import { ArrowIcon, ArrowUpIcon, HomeIcon } from "@/assets/icons"; import { PATH_DASHBOARD } from "@/constants/path"; import Link from "next/link"; import BenchmarkDetailFilterSection from "@/components/admin/benchmark-detail/BenchmarkDetailFilterSection"; @@ -12,6 +11,7 @@ import { IFilterState, IResultState } from "@/types/state"; import { useMemo, useState, useEffect } from "react"; import { useRouter } from "next/router"; import { IFilterBenchmarkDetails } from "@/types/benchmark"; +import RowChart from "@/components/admin/benchmarks/RowChart"; const PageBenchmarkDetail = () => { const router = useRouter(); @@ -205,7 +205,32 @@ const PageBenchmarkDetail = () => { {/* Content */} - +
+
Benchmarks
+

+ On this page you can see details of all the benchmarks on our + platform, including their source and download links. +

+
+ +
+
+ Summary of Benchmark Set +
+ + See more details + + +
+ +
+
+ List of All Benchmarks +
+
{ availableModels={availableModels} availableProblemSizes={availableProblemSizes} /> -
-
Benchmarks
-

- On this page you can see details of all the benchmarks on our - platform, including their source and download links. -

-
diff --git a/website-nextjs/src/pages/dashboard/benchmark-summary.tsx b/website-nextjs/src/pages/dashboard/benchmark-summary.tsx new file mode 100644 index 00000000..8deb3b56 --- /dev/null +++ b/website-nextjs/src/pages/dashboard/benchmark-summary.tsx @@ -0,0 +1,198 @@ +import { useSelector } from "react-redux"; +import { AdminHeader, Footer, Navbar } from "@/components/shared"; +import Head from "next/head"; +import { ArrowIcon, HomeIcon } from "@/assets/icons"; +import { PATH_DASHBOARD } from "@/constants/path"; +import Link from "next/link"; +import { IFilterState, IResultState } from "@/types/state"; +import { useState, useEffect } from "react"; +import { useRouter } from "next/router"; +import { IFilterBenchmarkDetails } from "@/types/benchmark"; +import BenchmarkSummaryTable from "@/components/admin/benchmarks/BenchmarkSummaryTable"; + +const PageBenchmarkDetail = () => { + const router = useRouter(); + const isNavExpanded = useSelector( + (state: { theme: { isNavExpanded: boolean } }) => state.theme.isNavExpanded, + ); + + const fullMetaData = useSelector((state: { results: IResultState }) => { + return state.results.fullMetaData; + }); + + const problemSizeResult: { [key: string]: string } = {}; + Object.keys(fullMetaData).forEach((metaDataKey) => { + fullMetaData[metaDataKey]?.sizes?.forEach((s) => { + problemSizeResult[`${metaDataKey}'-'${s.name}`] = s.size; + }); + }); + + const uniqueValues = { + sectors: new Set(), + techniques: new Set(), + kindOfProblems: new Set(), + models: new Set(), + }; + + Object.keys(fullMetaData).forEach((key) => { + const { sectors, technique, kindOfProblem, modelName } = fullMetaData[key]; + uniqueValues.sectors.add(sectors); + uniqueValues.techniques.add(technique); + uniqueValues.kindOfProblems.add(kindOfProblem); + uniqueValues.models.add(modelName); + }); + + const availableSectors = Array.from(uniqueValues.sectors); + const availableTechniques = Array.from(uniqueValues.techniques); + const availableKindOfProblems = Array.from(uniqueValues.kindOfProblems); + const availableModels = Array.from(uniqueValues.models); + const availableProblemSizes = Array.from( + new Set( + Object.keys(problemSizeResult).map((key) => problemSizeResult[key]), + ), + ); + + const encodeValue = (value: string) => { + return encodeURIComponent(value); + }; + + const decodeValue = (value: string) => { + return decodeURIComponent(value); + }; + + const parseUrlParams = () => { + const filters: Partial = {}; + + [ + "sectors", + "technique", + "kindOfProblem", + "modelName", + "problemSize", + ].forEach((key) => { + const value = router.query[key]; + if (typeof value === "string") { + // @ts-expect-error Type inference issues with dynamic keys + filters[key as keyof IFilterState] = value + ? value.split(";").map(decodeValue) + : []; + } + }); + + return filters; + }; + + const [isInit, setIsInit] = useState(false); + const [localFilters, setLocalFilters] = useState({ + sectors: availableSectors, + technique: availableTechniques, + kindOfProblem: availableKindOfProblems, + modelName: availableModels, + problemSize: availableProblemSizes, + }); + + useEffect(() => { + if (!router.isReady) return; + + const urlFilters = parseUrlParams(); + + if (Object.keys(urlFilters).length > 0) { + setLocalFilters((prevFilters) => ({ + ...prevFilters, + ...urlFilters, + })); + } else { + setLocalFilters({ + sectors: availableSectors, + technique: availableTechniques, + kindOfProblem: availableKindOfProblems, + modelName: availableModels, + problemSize: availableProblemSizes, + }); + } + setIsInit(true); + }, [router.isReady, fullMetaData]); + + // Update URL when filters change + useEffect(() => { + if (!isInit) return; + + const queryParams = new URLSearchParams(); + Object.entries(localFilters).forEach(([key, values]) => { + if ( + Array.isArray(values) && + values.length > 0 && + values.length < + (key === "sectors" + ? availableSectors.length + : key === "technique" + ? availableTechniques.length + : key === "kindOfProblem" + ? availableKindOfProblems.length + : key === "modelName" + ? availableModels.length + : key === "problemSize" + ? availableProblemSizes.length + : 0) + ) { + queryParams.set(key, values.map(encodeValue).join(";")); + } + }); + + router.replace( + { + pathname: router.pathname, + query: Object.fromEntries(queryParams), + }, + undefined, + { shallow: true }, + ); + }, [localFilters, isInit]); + + return ( + <> + + Benchmark Detail + +
+ +
+ +
+
+ + + + + + + + Benchmark Details + + + + + Feature Distribution + +
+
+
+
+
+ Distribution of Model Features in Benchmark Set +
+
+ {/* Content */} + +
+
+
+ + ); +}; + +export default PageBenchmarkDetail; diff --git a/website-nextjs/src/types/chart.ts b/website-nextjs/src/types/chart.ts new file mode 100644 index 00000000..339f2c1b --- /dev/null +++ b/website-nextjs/src/types/chart.ts @@ -0,0 +1,17 @@ +export interface StackedBarData { + [key: string]: number | string; +} + +export interface ID3StackedBarChart { + title: string; + height?: number; + className?: string; + data: StackedBarData[]; + categoryKey: string; // key to use for x-axis + xAxisTooltipFormat?: (value: number | string) => string; + colors: Record; + xAxisLabel?: string; + yAxisLabel?: string; + rotateXAxisLabels?: boolean; + showXaxisLabel?: boolean; +}
Total
+ N. of Problems + + {nOfProblem} + + {nOfProblemIdx === 0 + ? s.nOfProblems.get("totalNOfDiffProblems") || 0 + : s.nOfProblems.get("multipleSizes")} + + {nOfProblemIdx === 0 + ? summary.reduce( + (acc, curr) => + acc + + (curr.nOfProblems.get("totalNOfDiffProblems") || 0), + 0, + ) + : summary.reduce( + (acc, curr) => + acc + (curr.nOfProblems.get("multipleSizes") || 0), + 0, + )} +
- {techniqueIdx === 0 ? "Technique" : ""} - + {techniqueIdx === 0 && ( + + Technique + {technique} {s.techniques.get(technique) || 0} + {summary.reduce( + (acc, curr) => + acc + (curr.techniques.get(technique) || 0), + 0, + )} +
- {kindOfProblemIdx === 0 ? "Kind Of Problem" : ""} - + {kindOfProblemIdx === 0 && ( + + Kind Of Problem + {kindOfProblem} {s.kindOfProblems.get(kindOfProblem) || 0} + {summary.reduce( + (acc, curr) => + acc + (curr.kindOfProblems.get(kindOfProblem) || 0), + 0, + )} +
- {timeHorizonIdx === 0 ? "Time Horizon" : ""} - + {timeHorizonIdx === 0 && ( + + Time Horizon + {getTimeHorizonLabel(timeHorizon)} {s.timeHorizons.get(timeHorizon) == -1 ? "N/A" : s.timeHorizons.get(timeHorizon) || 0} + {summary.reduce((acc, curr) => { + // If the value is -1, then it is N/A + const a = acc == -1 ? 0 : acc || 0; + const b = + curr.timeHorizons.get(timeHorizon) == -1 + ? 0 + : curr.timeHorizons.get(timeHorizon) || 0; + return a + b; + }, 0)} +
- {milpFeatureIdx === 0 ? "MILP Feature" : ""} - + {milpFeatureIdx === 0 && ( + + MILP Features + {milpFeature || "-"} {s.milpFeatures.get(milpFeature as string) || 0} + {summary.reduce( + (acc, curr) => + acc + + (curr.milpFeatures.get(milpFeature as string) || 0), + 0, + )} +
+ Size + + {size || "-"} + + {" "} + {size === "Other" + ? s.realSizes.get("other") || 0 + : `${s.realSizes.get("real") || 0} (${ + s.realSizes.get("milp") || 0 + })`} + + {size === "Other" + ? summary.reduce( + (acc, curr) => + acc + (curr.realSizes.get("other") || 0), + 0, + ) + : `${summary.reduce( + (acc, curr) => + acc + (curr.realSizes.get("real") || 0), + 0, + )} (${summary.reduce( + (acc, curr) => + acc + (curr.realSizes.get("milp") || 0), + 0, + )})`} +