From b423a3e88f0dacd576c87dd90777f0c098ec389f Mon Sep 17 00:00:00 2001 From: Michael Trosper Date: Tue, 11 Aug 2026 23:05:53 -0600 Subject: [PATCH] Updated Reduction Tooltips, Problems Filter, Etc... --- .../svgs/visualizationCategories.js | 33 +++++ .../hooks/ProblemFilters/facetOptions.js | 24 ++++ components/pageblocks/ProblemRowReact.js | 4 + components/pageblocks/ReduceToRowReact.js | 20 ++- components/pageblocks/VisualizeRowReact.js | 8 +- components/redux/index.js | 8 +- components/widgets/PopoverTooltipClick.js | 17 +-- components/widgets/ProblemFilterMenu.js | 120 +++++++++++++++--- 8 files changed, 196 insertions(+), 38 deletions(-) create mode 100644 components/Visualization/svgs/visualizationCategories.js diff --git a/components/Visualization/svgs/visualizationCategories.js b/components/Visualization/svgs/visualizationCategories.js new file mode 100644 index 0000000..1f8db3d --- /dev/null +++ b/components/Visualization/svgs/visualizationCategories.js @@ -0,0 +1,33 @@ +// Simplified visualization-type categories for display (issue #378) and filtering +// (issue #379). Maps the wire-value visualizationType vocabulary +// (visualizationTypes.json) down to a small set of human-facing categories so a +// tooltip reads "Graph" instead of "GraphD3"/"GraphLaTeX", and so a problem can be +// filtered by "which kind of visualization" without the rendering-technology +// distinction (D3 vs. LaTeX vs. Q.js) leaking into the UI. +// +// Purely a display/filter-grouping concern -- does NOT change the wire value itself, +// which stays GraphD3/GraphLaTeX/etc. on the API and in Visualizations.js's renderer +// registry (see PopoverTooltipClick's toolTip prop / useProblemFilters' matching, +// which both still key off the raw wire value). +const VISUALIZATION_TYPE_CATEGORIES = { + GraphD3: "Graph", + GraphLaTeX: "Graph", + BooleanSatisfiability: "Boolean", + SetD3: "Set", + QuantumCircuitD3: "Circuit", + QuantumCircuitQjs: "Circuit", + DynamicTable: "Table", + PumpSchedule: "Schedule", +}; + +/** + * The simplified display category for a visualizationType wire value, e.g. + * "GraphD3" -> "Graph". Falls back to the raw value itself for anything not in the + * map (e.g. "Unimplemented", or a newly-declared type not yet categorized here) so + * it's never silently dropped from the UI. + */ +export function visualizationTypeCategory(type) { + return (type && VISUALIZATION_TYPE_CATEGORIES[type]) || type; +} + +export default VISUALIZATION_TYPE_CATEGORIES; diff --git a/components/hooks/ProblemFilters/facetOptions.js b/components/hooks/ProblemFilters/facetOptions.js index 529015d..94ee4d3 100644 --- a/components/hooks/ProblemFilters/facetOptions.js +++ b/components/hooks/ProblemFilters/facetOptions.js @@ -13,3 +13,27 @@ export function buildFacetOptions(problemIndex, pickValues) { .sort((a, b) => a[0].localeCompare(b[0])) .map(([key, count]) => ({ key, label: key, count })); } + +/** + * Same option-building as `buildFacetOptions`, but grouped under a coarser display + * category (e.g. visualizationType "GraphD3"/"GraphLaTeX" grouped under "Graph", + * issue #378/#379). The underlying option `key` stays the raw value -- selecting it + * still toggles the raw value into filter state, so matching logic elsewhere + * (`useProblemFilters`) is untouched; `categorize` only changes how options are + * grouped/labeled for display. + * + * @returns `[{category, options: [{key, label, count}]}]`, categories and options + * both sorted alphabetically. + */ +export function buildGroupedFacetOptions(problemIndex, pickValues, categorize) { + const flat = buildFacetOptions(problemIndex, pickValues); + const groups = new Map(); + for (const option of flat) { + const category = categorize(option.key) || option.key; + if (!groups.has(category)) groups.set(category, []); + groups.get(category).push(option); + } + return [...groups.entries()] + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([category, options]) => ({ category, options })); +} diff --git a/components/pageblocks/ProblemRowReact.js b/components/pageblocks/ProblemRowReact.js index 92f053f..724ed7f 100644 --- a/components/pageblocks/ProblemRowReact.js +++ b/components/pageblocks/ProblemRowReact.js @@ -46,6 +46,8 @@ export default function ProblemRowReact({ url, problemName, setProblemName, prob setSelectedComplexityClasses, selectedSolverComplexityBuckets, setSelectedSolverComplexityBuckets, + selectedVisualizationTypes, + setSelectedVisualizationTypes, filteredProblems, clearFilters, } = useProblemFilters(problemIndex, reductionGraph); @@ -214,6 +216,8 @@ export default function ProblemRowReact({ url, problemName, setProblemName, prob setSelectedComplexityClasses={setSelectedComplexityClasses} selectedSolverComplexityBuckets={selectedSolverComplexityBuckets} setSelectedSolverComplexityBuckets={setSelectedSolverComplexityBuckets} + selectedVisualizationTypes={selectedVisualizationTypes} + setSelectedVisualizationTypes={setSelectedVisualizationTypes} clearFilters={clearFilters} />{" "} diff --git a/components/pageblocks/ReduceToRowReact.js b/components/pageblocks/ReduceToRowReact.js index 7a8d770..3ec6f7f 100644 --- a/components/pageblocks/ReduceToRowReact.js +++ b/components/pageblocks/ReduceToRowReact.js @@ -28,12 +28,21 @@ const ACCORDION_FORM_TWO = { placeHolder: "Select Reduction" } const REDUCE_BUTTON = { buttonText: "Reduce" } const CARD = { cardBodyText: "Reduce To:", cardHeaderText: "Reduce" } const TOOLTIP1 = { header: "Reduce To Problem", formalDef: "Choose a problem to reduce your original problem to to see information about it", info: "" } -const TOOLTIP2 = { header: "Reduction Type", formalDef: "Choose a type of reduction to see information about it", info: "" } +const TOOLTIP2 = { + header: "Reduction Type", + formalDef: "Choose a type of reduction to see information about it", + info: "", + reductionType: "", + complexity: "", + complexityBucket: "", +} const THEME = { colors: { grey: "#424242", orange: "#d4441c", white: "#ffffff" } } -// ReductionCost describes output-size blowup relative to input size -- the -// closest thing reductions have to a Big-O figure (reductions themselves -// don't have a runtime complexity class the way problems/solvers do). +// ReductionCost describes output-size blowup relative to input size, a +// separate axis from ReductionComplexityBucket (runtime, shown below as +// "Complexity bucket") and the free-text `complexity` Big-O string (issue +// #376) -- a reduction declares all three independently, see +// Interfaces/ReductionCost.cs / ReductionComplexityBucket.cs in the API repo. const REDUCTION_COST_LABELS = { Linear: "Linear (O(n))", Quadratic: "Quadratic (O(n²))", @@ -161,6 +170,9 @@ export default function ReduceToRowReact({ label: "Reduction cost", value: REDUCTION_COST_LABELS[reducerInfo.cost] || reducerInfo.cost || "Unclassified", }, + { label: "Reduction type", value: reducerInfo.reductionType || "Unclassified" }, + { label: "Complexity bucket", value: reducerInfo.complexityBucket || "Unclassified" }, + { label: "Big-O", value: reducerInfo.complexity || "Not yet determined" }, ], // separate Source line source: reducerInfo.source, diff --git a/components/pageblocks/VisualizeRowReact.js b/components/pageblocks/VisualizeRowReact.js index 0f48268..222ef35 100644 --- a/components/pageblocks/VisualizeRowReact.js +++ b/components/pageblocks/VisualizeRowReact.js @@ -26,6 +26,7 @@ import Link from "next/link"; // <-- IMPORTANT for Quantum button import PopoverTooltipClick from "../widgets/PopoverTooltipClick"; import SearchBarExtensible from "../widgets/SearchBarExtensible"; +import { visualizationTypeCategory } from "../Visualization/svgs/visualizationCategories"; import { requestProblemGenericInstance, @@ -341,7 +342,12 @@ export default function VisualizeRowReact({ formalDef: visualizationInfo.visualizationDefinition ?? "", info: visualizationInfo.info ?? visualizationInfo.description ?? "", classification: [ - { label: "Visualization type", value: visualizationInfo.visualizationType || "Unclassified" }, + { + label: "Visualization type", + value: visualizationInfo.visualizationType + ? visualizationTypeCategory(visualizationInfo.visualizationType) + : "Unclassified", + }, ], source: visualizationInfo.source, credit: diff --git a/components/redux/index.js b/components/redux/index.js index a77d9d9..ae2f73b 100644 --- a/components/redux/index.js +++ b/components/redux/index.js @@ -294,7 +294,13 @@ export async function requestReducedInstance(url, reduction, instance) { * @returns `undefined` on failure and logs the error. */ export async function requestReductionInfo(url, apiCall) { - return await fetchJson(`${url}${apiCall}/info`, () => `${apiCall} INFO REQUEST FAILED`); + // Reductions are served by the same generic ProblemProvider/info?interface= + // endpoint as problems/solvers/verifiers (see requestInfo above) -- this used to + // request `${apiCall}/info` directly, a route that doesn't exist on the API and + // 404s every time, silently leaving reducerInfo as {} (empty) and every reduction + // tooltip field (cost/reductionType/complexityBucket/complexity) on its fallback + // text no matter what the backend actually has classified. + return await requestInfo(url, apiCall); } /** diff --git a/components/widgets/PopoverTooltipClick.js b/components/widgets/PopoverTooltipClick.js index 3ad7505..94a4b5b 100644 --- a/components/widgets/PopoverTooltipClick.js +++ b/components/widgets/PopoverTooltipClick.js @@ -53,11 +53,9 @@ function PopoverTooltipClick({ toolTip = {} }) { {t.formalDef && t.isMathDef ? ( - {t.formalDef} - + ) : t.formalDef ? ( {t.formalDef} @@ -89,14 +87,7 @@ function PopoverTooltipClick({ toolTip = {} }) { ) : null} {Array.isArray(t.classification) && t.classification.length > 0 ? ( - + {t.classification.map(({ label, value }) => ( {label}: {value} diff --git a/components/widgets/ProblemFilterMenu.js b/components/widgets/ProblemFilterMenu.js index 6f2dfd2..bd7ae07 100644 --- a/components/widgets/ProblemFilterMenu.js +++ b/components/widgets/ProblemFilterMenu.js @@ -12,7 +12,8 @@ import { Typography, } from "@mui/material"; import FilterListIcon from "@mui/icons-material/FilterList"; -import { buildFacetOptions } from "../hooks/ProblemFilters/facetOptions"; +import { buildFacetOptions, buildGroupedFacetOptions } from "../hooks/ProblemFilters/facetOptions"; +import { visualizationTypeCategory } from "../Visualization/svgs/visualizationCategories"; function toggle(set, key) { const next = new Set(set); @@ -24,6 +25,26 @@ function toggle(set, key) { return next; } +function FacetCheckbox({ optionKey, label, count, selected, onChange }) { + return ( + onChange(toggle(selected, optionKey))} + /> + } + label={ + + {label} ({count}) + + } + /> + ); +} + function FacetGroup({ title, options, selected, onChange }) { return ( @@ -37,20 +58,13 @@ function FacetGroup({ title, options, selected, onChange }) { ) : ( options.map(({ key, label, count }) => ( - onChange(toggle(selected, key))} - /> - } - label={ - - {label} ({count}) - - } + optionKey={key} + label={label} + count={count} + selected={selected} + onChange={onChange} /> )) )} @@ -59,12 +73,54 @@ function FacetGroup({ title, options, selected, onChange }) { ); } +// Same as FacetGroup, but options are pre-grouped under a coarser display category +// (see buildGroupedFacetOptions) -- each category gets its own subheading, and its +// options render underneath. Selecting a checkbox still toggles the raw option key, +// same as FacetGroup. +function GroupedFacetGroup({ title, groups, selected, onChange }) { + return ( + + + {title} + + {groups.length === 0 ? ( + + No values available + + ) : ( + groups.map(({ category, options }) => ( + + + {category} + + + {options.map(({ key, label, count }) => ( + + ))} + + + )) + )} + + ); +} + /** * Filter button + popover for the problem dropdown -- lets the user restrict - * which problems appear by complexity class and/or solver Big-O bucket - * (worst-case complexity of any solver the problem has). Facet option lists - * are derived from `problemIndex` (see `useProblemIndex`), so only values - * actually present show up. + * which problems appear by complexity class, solver Big-O bucket (worst-case + * complexity of any solver the problem has), and/or visualization type (#379). + * Facet option lists are derived from `problemIndex` (see `useProblemIndex`), so + * only values actually present show up. */ export default function ProblemFilterMenu({ problemIndex, @@ -72,6 +128,8 @@ export default function ProblemFilterMenu({ setSelectedComplexityClasses, selectedSolverComplexityBuckets, setSelectedSolverComplexityBuckets, + selectedVisualizationTypes, + setSelectedVisualizationTypes, clearFilters, }) { const [anchorEl, setAnchorEl] = useState(null); @@ -82,8 +140,23 @@ export default function ProblemFilterMenu({ problemIndex, (tags) => tags.solverComplexityBuckets, ); + // Grouped by the #378 simplified categories (e.g. GraphD3/GraphLaTeX -> "Graph") + // for display; the underlying checkbox options are still the raw visualizationType + // wire values, so selecting one still matches `tags.visualizationTypes` exactly + // like the other facets -- see buildGroupedFacetOptions' own comment. + const visualizationTypeGroups = buildGroupedFacetOptions( + problemIndex, + (tags) => tags.visualizationTypes, + visualizationTypeCategory, + ); - const activeCount = selectedComplexityClasses.size + selectedSolverComplexityBuckets.size; + // #375: every facet actually offered below must count toward the badge and + // "Clear filters" enablement -- a facet selection that isn't counted here is + // effectively invisible/unclearable even though it's still filtering results. + const activeCount = + selectedComplexityClasses.size + + selectedSolverComplexityBuckets.size + + selectedVisualizationTypes.size; return ( <> @@ -118,6 +191,15 @@ export default function ProblemFilterMenu({ onChange={setSelectedSolverComplexityBuckets} /> + + + +