Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions components/Visualization/svgs/visualizationCategories.js
Original file line number Diff line number Diff line change
@@ -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;
24 changes: 24 additions & 0 deletions components/hooks/ProblemFilters/facetOptions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
}
4 changes: 4 additions & 0 deletions components/pageblocks/ProblemRowReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ export default function ProblemRowReact({ url, problemName, setProblemName, prob
setSelectedComplexityClasses,
selectedSolverComplexityBuckets,
setSelectedSolverComplexityBuckets,
selectedVisualizationTypes,
setSelectedVisualizationTypes,
filteredProblems,
clearFilters,
} = useProblemFilters(problemIndex, reductionGraph);
Expand Down Expand Up @@ -214,6 +216,8 @@ export default function ProblemRowReact({ url, problemName, setProblemName, prob
setSelectedComplexityClasses={setSelectedComplexityClasses}
selectedSolverComplexityBuckets={selectedSolverComplexityBuckets}
setSelectedSolverComplexityBuckets={setSelectedSolverComplexityBuckets}
selectedVisualizationTypes={selectedVisualizationTypes}
setSelectedVisualizationTypes={setSelectedVisualizationTypes}
clearFilters={clearFilters}
/>{" "}
<PopoverTooltipClick toolTip={tip} />
Expand Down
20 changes: 16 additions & 4 deletions components/pageblocks/ReduceToRowReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -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²))",
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion components/pageblocks/VisualizeRowReact.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
8 changes: 7 additions & 1 deletion components/redux/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

/**
Expand Down
17 changes: 4 additions & 13 deletions components/widgets/PopoverTooltipClick.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,17 @@ function PopoverTooltipClick({ toolTip = {} }) {

<Box sx={{ px: 2, py: 1.5, maxWidth: 480 }}>
{t.formalDef && t.isMathDef ? (
<Box
<Typography
variant="body2"
sx={{
p: '8px 10px',
borderRadius: 1,
bgcolor: 'rgba(0,0,0,0.06)',
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, monospace',
fontSize: '0.85em',
whiteSpace: 'pre-wrap',
mb: 1.5,
}}
>
{t.formalDef}
</Box>
</Typography>
) : t.formalDef ? (
<Typography variant="body2" sx={{ mb: 1.5 }}>
{t.formalDef}
Expand All @@ -89,14 +87,7 @@ function PopoverTooltipClick({ toolTip = {} }) {
) : null}

{Array.isArray(t.classification) && t.classification.length > 0 ? (
<Box
sx={{
mb: 1.5,
p: '6px 10px',
borderRadius: 1,
bgcolor: 'rgba(0,0,0,0.04)',
}}
>
<Box sx={{ mb: 1.5 }}>
{t.classification.map(({ label, value }) => (
<Typography key={label} variant="body2" sx={{ lineHeight: 1.35 }}>
<strong>{label}:</strong> {value}
Expand Down
120 changes: 101 additions & 19 deletions components/widgets/ProblemFilterMenu.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -24,6 +25,26 @@ function toggle(set, key) {
return next;
}

function FacetCheckbox({ optionKey, label, count, selected, onChange }) {
return (
<FormControlLabel
key={optionKey}
control={
<Checkbox
size="small"
checked={selected.has(optionKey)}
onChange={() => onChange(toggle(selected, optionKey))}
/>
}
label={
<Typography variant="body2">
{label} <Box component="span" sx={{ color: "text.secondary" }}>({count})</Box>
</Typography>
}
/>
);
}

function FacetGroup({ title, options, selected, onChange }) {
return (
<Box sx={{ mb: 1.5 }}>
Expand All @@ -37,20 +58,13 @@ function FacetGroup({ title, options, selected, onChange }) {
</Typography>
) : (
options.map(({ key, label, count }) => (
<FormControlLabel
<FacetCheckbox
key={key}
control={
<Checkbox
size="small"
checked={selected.has(key)}
onChange={() => onChange(toggle(selected, key))}
/>
}
label={
<Typography variant="body2">
{label} <Box component="span" sx={{ color: "text.secondary" }}>({count})</Box>
</Typography>
}
optionKey={key}
label={label}
count={count}
selected={selected}
onChange={onChange}
/>
))
)}
Expand All @@ -59,19 +73,63 @@ 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 (
<Box sx={{ mb: 1.5 }}>
<Typography variant="subtitle2" fontWeight={700} sx={{ mb: 0.5 }}>
{title}
</Typography>
{groups.length === 0 ? (
<Typography variant="body2" sx={{ fontStyle: "italic", color: "text.secondary" }}>
No values available
</Typography>
) : (
groups.map(({ category, options }) => (
<Box key={category} sx={{ mb: 0.75, ml: 0.5 }}>
<Typography
variant="caption"
sx={{ color: "text.secondary", fontWeight: 600, display: "block" }}
>
{category}
</Typography>
<FormGroup>
{options.map(({ key, label, count }) => (
<FacetCheckbox
key={key}
optionKey={key}
label={label}
count={count}
selected={selected}
onChange={onChange}
/>
))}
</FormGroup>
</Box>
))
)}
</Box>
);
}

/**
* 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,
selectedComplexityClasses,
setSelectedComplexityClasses,
selectedSolverComplexityBuckets,
setSelectedSolverComplexityBuckets,
selectedVisualizationTypes,
setSelectedVisualizationTypes,
clearFilters,
}) {
const [anchorEl, setAnchorEl] = useState(null);
Expand All @@ -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 (
<>
Expand Down Expand Up @@ -118,6 +191,15 @@ export default function ProblemFilterMenu({
onChange={setSelectedSolverComplexityBuckets}
/>

<Divider sx={{ my: 1.5 }} />

<GroupedFacetGroup
title="Visualization type"
groups={visualizationTypeGroups}
selected={selectedVisualizationTypes}
onChange={setSelectedVisualizationTypes}
/>

<Button size="small" onClick={clearFilters} disabled={activeCount === 0}>
Clear filters
</Button>
Expand Down
Loading