Skip to content

Commit

Permalink
feat: topic criteria table transposes rows & columns
Browse files Browse the repository at this point in the history
* feat: topic criteria table transposes rows & columns

* chore: feeling emo, might delete
  • Loading branch information
alextilot authored Mar 3, 2023
1 parent 3b7468a commit 4f5e971
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css } from "@emotion/react";
import styled from "@emotion/styled";
import { IconButton } from "@mui/material";
import { Button, IconButton } from "@mui/material";

import { AddNodeButton } from "../AddNodeButton/AddNodeButton";
import { nodeWidth } from "../EditableNode/EditableNode.styles";
Expand Down Expand Up @@ -60,12 +60,11 @@ const tableDivOptions = {
};

export const TableDiv = styled("div", tableDivOptions)<TableDivProps>`
left: 50%;
transform: translateX(-50%);
position: relative;
${({ numberOfColumns }) => {
return css`
left: 50%;
transform: translateX(-50%);
position: relative;
// Want to have table grow with columns, so we hardcode a total-width of table, but, if column widths
// are hardcoded as well, doing so will create a horizontal scrollbar whenever too many rows results in a vertical scrollbar.
// Therefore, the total-width will have a little extra space so that nodes are still >= min-width
Expand All @@ -85,7 +84,6 @@ const options = {

export const PositionedAddNodeButton = styled(AddNodeButton, options)<AddNodeButtonProps>`
position: absolute;
${({ position }) => {
if (position === "column") {
return css`
Expand All @@ -102,3 +100,9 @@ export const PositionedAddNodeButton = styled(AddNodeButton, options)<AddNodeBut
}
}}
`;

export const StyledTransposeTableButton = styled(Button)`
position: absolute;
top: 0;
left: 0;
`;
74 changes: 41 additions & 33 deletions web/src/modules/topic/components/CriteriaTable/CriteriaTable.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
import { Global } from "@emotion/react";
import { Cancel } from "@mui/icons-material";
import { Cancel, PivotTableChart } from "@mui/icons-material";
import { Typography } from "@mui/material";
import MaterialReactTable, { type MRT_ColumnDef } from "material-react-table";
import { useState } from "react";

import { closeTable } from "../../store/actions";
import { useCriterionSolutionEdges, useNode, useNodeChildren } from "../../store/nodeHooks";
import { problemDiagramId } from "../../store/store";
import { Edge, Node } from "../../utils/diagram";
import { getConnectingEdge } from "../../utils/edge";
import { EdgeCell } from "../EdgeCell/EdgeCell";
import { EditableNode } from "../EditableNode/EditableNode";
import { NodeCell } from "../NodeCell/NodeCell";
import {
PositionedAddNodeButton,
PositionedCloseButton,
StyledTransposeTableButton,
TableDiv,
TitleDiv,
tableStyles,
} from "./CriteriaTable.styles";

const getCriterionSolutionEdge = (criterion: Node, solution: Node, edges: Edge[]) => {
const edge = edges.find((edge) => edge.source === criterion.id && edge.target === solution.id);
if (!edge) throw new Error(`No edge found between ${criterion.id} and ${solution.id}`);

return edge;
};

type RowData = Record<string, Node | Edge>;

// returns structure:
Expand All @@ -33,38 +29,37 @@ type RowData = Record<string, Node | Edge>;
// [solution_id: string]: Edge,
// }
// but not sure how to type this because index signatures require _all_ properties to match the specified type
const buildRows = (
criteria: Node[],
solutions: Node[],
criterionSolutionEdges: Edge[]
): RowData[] => {
return criteria.map((criterion) => {

const buildRows = (rowNodes: Node[], columnNodes: Node[], edges: Edge[]): RowData[] => {
return rowNodes.map((rowNode) => {
return {
criterion,
rowNode,
...Object.fromEntries(
solutions.map((solution) => [
solution.id,
getCriterionSolutionEdge(criterion, solution, criterionSolutionEdges),
columnNodes.map((columnNode) => [
columnNode.id,
getConnectingEdge(rowNode, columnNode, edges),
])
),
};
});
};

const buildColumns = (solutions: Node[]): MRT_ColumnDef<RowData>[] => {
const buildColumns = (columnNodes: Node[]): MRT_ColumnDef<RowData>[] => {
return [
{
accessorKey: "criterion.data.label", // this determines how cols should sort/filter
accessorKey: "rowNode.data.label", // this determines how cols should sort/filter
header: "", // corner column header, and solutions are along the top
Cell: ({ row }) => <NodeCell node={row.original.criterion as Node} />,
Cell: ({ row }) => {
return <NodeCell node={row.original.rowNode as Node} />;
},
},
...solutions.map(
(solution) =>
...columnNodes.map(
(columnNode) =>
({
accessorKey: `${solution.id}.data.score`, // we'll sort/filter edges by their score for now I guess
accessorKey: `${columnNode.id}.data.score`, // we'll sort/filter edges by their score for now I guess
header: "Score",
Header: <NodeCell node={solution} />,
Cell: ({ row }) => <EdgeCell edge={row.original[solution.id] as Edge} />,
Header: <NodeCell node={columnNode} />,
Cell: ({ row }) => <EdgeCell edge={row.original[columnNode.id] as Edge} />,
} as MRT_ColumnDef<RowData>)
),
];
Expand All @@ -75,6 +70,7 @@ interface Props {
}

export const CriteriaTable = ({ problemNodeId }: Props) => {
const [useSolutionsForColumns, setUseSolutionsForColumns] = useState<boolean>(true);
const problemNode = useNode(problemNodeId, problemDiagramId);
const nodeChildren = useNodeChildren(problemNodeId, problemDiagramId);
const criterionSolutionEdges = useCriterionSolutionEdges(problemNodeId, problemDiagramId);
Expand All @@ -84,8 +80,11 @@ export const CriteriaTable = ({ problemNodeId }: Props) => {
const criteria = nodeChildren.filter((node) => node.type === "criterion");
const solutions = nodeChildren.filter((node) => node.type === "solution");

const rowData = buildRows(criteria, solutions, criterionSolutionEdges);
const columns = buildColumns(solutions);
const rowNodes = useSolutionsForColumns ? criteria : solutions;
const columnNodes = useSolutionsForColumns ? solutions : criteria;

const rowData = buildRows(rowNodes, columnNodes, criterionSolutionEdges);
const columnData = buildColumns(columnNodes);

return (
<>
Expand All @@ -98,11 +97,11 @@ export const CriteriaTable = ({ problemNodeId }: Props) => {
<EditableNode node={problemNode} />
</TitleDiv>

<TableDiv numberOfColumns={columns.length}>
<TableDiv numberOfColumns={columnData.length}>
{/* Hard to tell if material-react-table is worth using because the cells are all custom components. */}
{/* It's doubtful that we'll use sorting/filtering... but pinning and re-ordering may come in handy. */}
<MaterialReactTable
columns={columns}
columns={columnData}
data={rowData}
enableColumnActions={false}
enablePagination={false}
Expand All @@ -112,19 +111,28 @@ export const CriteriaTable = ({ problemNodeId }: Props) => {
muiTablePaperProps={{
className: "criteria-table-paper",
}}
initialState={{ columnPinning: { left: ["criterion.data.label"] } }}
initialState={{ columnPinning: { left: ["rowNode.data.label"] } }}
/>

<StyledTransposeTableButton
size="small"
variant="contained"
color="neutral"
onClick={() => setUseSolutionsForColumns(!useSolutionsForColumns)}
>
<PivotTableChart />
</StyledTransposeTableButton>

<PositionedAddNodeButton
position="column"
position={useSolutionsForColumns ? "column" : "row"}
fromNodeId={problemNodeId}
as="child"
toNodeType="criterion"
relation="criterion for"
/>

<PositionedAddNodeButton
position="row"
position={useSolutionsForColumns ? "row" : "column"}
fromNodeId={problemNodeId}
as="child"
toNodeType="solution"
Expand Down
11 changes: 11 additions & 0 deletions web/src/modules/topic/utils/edge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,14 @@ export const parentNode = (edge: Edge, diagram: Diagram) => {
export const childNode = (edge: Edge, diagram: Diagram) => {
return findScorable(diagram, edge.target, "node");
};

export const getConnectingEdge = (node1: Node, node2: Node, edges: Edge[]) => {
const edge = edges.find(
(edge) =>
(edge.source === node1.id && edge.target === node2.id) ||
(edge.source === node2.id && edge.target === node1.id)
);
if (!edge) throw new Error(`No edge found between ${node1.id} and ${node2.id}`);

return edge;
};

0 comments on commit 4f5e971

Please sign in to comment.