Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Split drawing component #31

Merged
merged 1 commit into from
Apr 2, 2024
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
116 changes: 4 additions & 112 deletions src/components/drawing.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { clsx } from "clsx";
import { nanoid } from "nanoid";

import { DrawingMode, Graph, Point, RubberBand } from "./graph";
import { Edge, GraphData, Node } from "../type";
import { ProbabilitySelector, percentage, reduceToSum } from "./probability-selector";

import { DragIcon } from "./drawing/drag-icon";
import { NodeModal } from "./drawing/node-modal";

import SelectIcon from "../assets/select-icon.svg";
import AddNodeIcon from "../assets/add-node-icon.svg";
Expand All @@ -13,116 +15,6 @@ import DeleteIcon from "../assets/delete-icon.svg";

import "./drawing.scss";

interface NodeModalProps {
node?: Node,
graph: GraphData;
onChange: (id: string, newNode: Node, newEdge: Edge[]) => void,
onCancel: () => void
}

export const NodeModal = ({node, graph, onChange, onCancel}: NodeModalProps) => {
const [label, setLabel] = useState(node?.label || "");
const [exactPercentages, _setExactPercentages] = useState<number[]>([]);

const setExactPercentages = useCallback((percentages: number[]) => {
// make sure percentages add up to exactly 100%
const allButLastSum = reduceToSum(percentages.slice(0, -1));
percentages[percentages.length - 1] = 100 - allButLastSum;
_setExactPercentages(percentages);
}, [_setExactPercentages]);

const fromEdges = useMemo(() => {
return node ? graph.edges.filter(e => e.from === node.id) : [];
}, [node, graph.edges]);

const edgeLabels = useMemo(() => {
const labels = graph.nodes.reduce<Record<string,string>>((acc, cur) => {
acc[cur.id] = cur.label;
return acc;
}, {});
return fromEdges.map(e => labels[e.to]);
}, [graph.nodes, fromEdges]);

const edgeValues = useMemo(() => fromEdges.map(e => e.value), [fromEdges]);
const sum = useMemo(() => reduceToSum(edgeValues), [edgeValues]);

useEffect(() => {
const percentages = edgeValues.map(edgeValue => percentage(edgeValue / sum));
setExactPercentages(percentages);
}, [edgeValues, sum, setExactPercentages]);

useEffect(() => {
setLabel(node?.label || "");
}, [node, setLabel]);

const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (node) {
let valueIndex = 0;
const newEdges = graph.edges.map(edge => {
if (edge.from === node.id) {
const value = sum * exactPercentages[valueIndex++];
return {...edge, value};
} else {
return edge;
}
});
onChange(node.id, {...node, label: label.trim()}, newEdges);
}
}, [label, node, exactPercentages, graph.edges, sum, onChange]);

const handleChangeLabel = (e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
};

if (!node) {
return null;
}

return (
<>
<div className="nodeModalBackground" />
<div className="nodeModal">
<div className="nodeModalContent">
<div className="nodeModalTitle">Update State</div>
<form onSubmit={handleSubmit}>
<input type="text" value={label} onChange={handleChangeLabel} autoFocus={true} />
<ProbabilitySelector
exactPercentages={exactPercentages}
edgeLabels={edgeLabels}
onChange={setExactPercentages}
/>
<div className="nodeModalButtons">
<button type="submit">Save</button>
<button onClick={onCancel}>Cancel</button>
</div>
</form>
</div>
</div>
</>
);
};

export const DragIcon = ({drawingMode}: {drawingMode: DrawingMode}) => {
const [style, setStyle] = useState<React.CSSProperties>({});

useEffect(() => {
const mouseHandler = (e: MouseEvent) => setStyle({left: e.clientX - 20, top: e.clientY - 20});
window.addEventListener("mousemove", mouseHandler);
return () => window.removeEventListener("mousemove", mouseHandler);
}, []);

if (drawingMode === "addEdge") {
return <div className="dragIcon" style={style}><AddEdgeIcon /></div>;
}
if (drawingMode === "addNode") {
return <div className="dragIcon" style={style}><AddNodeIcon /></div>;
}
if (drawingMode === "delete") {
return <div className="dragIcon" style={style}><DeleteIcon /></div>;
}
return null;
};

interface Props {
highlightNode?: Node,
Expand Down
26 changes: 26 additions & 0 deletions src/components/drawing/drag-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, { useEffect, useState } from "react";
import { DrawingMode } from "../graph";
import AddNodeIcon from "../../assets/add-node-icon.svg";
import AddEdgeIcon from "../../assets/add-edge-icon.svg";
import DeleteIcon from "../../assets/delete-icon.svg";

export const DragIcon = ({ drawingMode }: { drawingMode: DrawingMode; }) => {
const [style, setStyle] = useState<React.CSSProperties>({});

useEffect(() => {
const mouseHandler = (e: MouseEvent) => setStyle({ left: e.clientX - 20, top: e.clientY - 20 });
window.addEventListener("mousemove", mouseHandler);
return () => window.removeEventListener("mousemove", mouseHandler);
}, []);

if (drawingMode === "addEdge") {
return <div className="dragIcon" style={style}><AddEdgeIcon /></div>;
}
if (drawingMode === "addNode") {
return <div className="dragIcon" style={style}><AddNodeIcon /></div>;
}
if (drawingMode === "delete") {
return <div className="dragIcon" style={style}><DeleteIcon /></div>;
}
return null;
};
92 changes: 92 additions & 0 deletions src/components/drawing/node-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import React, { useCallback, useEffect, useMemo, useState } from "react";
import { ProbabilitySelector, percentage, reduceToSum } from "./probability-selector";
import { Node, Edge, GraphData } from "../../type";

export interface NodeModalProps {
node?: Node,
graph: GraphData;
onChange: (id: string, newNode: Node, newEdge: Edge[]) => void,
onCancel: () => void
}

export const NodeModal = ({ node, graph, onChange, onCancel }: NodeModalProps) => {
const [label, setLabel] = useState(node?.label || "");
const [exactPercentages, _setExactPercentages] = useState<number[]>([]);

const setExactPercentages = useCallback((percentages: number[]) => {
// make sure percentages add up to exactly 100%
const allButLastSum = reduceToSum(percentages.slice(0, -1));
percentages[percentages.length - 1] = 100 - allButLastSum;
_setExactPercentages(percentages);
}, [_setExactPercentages]);

const fromEdges = useMemo(() => {
return node ? graph.edges.filter(e => e.from === node.id) : [];
}, [node, graph.edges]);

const edgeLabels = useMemo(() => {
const labels = graph.nodes.reduce<Record<string, string>>((acc, cur) => {
acc[cur.id] = cur.label;
return acc;
}, {});
return fromEdges.map(e => labels[e.to]);
}, [graph.nodes, fromEdges]);

const edgeValues = useMemo(() => fromEdges.map(e => e.value), [fromEdges]);
const sum = useMemo(() => reduceToSum(edgeValues), [edgeValues]);

useEffect(() => {
const percentages = edgeValues.map(edgeValue => percentage(edgeValue / sum));
setExactPercentages(percentages);
}, [edgeValues, sum, setExactPercentages]);

useEffect(() => {
setLabel(node?.label || "");
}, [node, setLabel]);

const handleSubmit = useCallback((e: React.FormEvent) => {
e.preventDefault();
if (node) {
let valueIndex = 0;
const newEdges = graph.edges.map(edge => {
if (edge.from === node.id) {
const value = sum * exactPercentages[valueIndex++];
return { ...edge, value };
} else {
return edge;
}
});
onChange(node.id, { ...node, label: label.trim() }, newEdges);
}
}, [label, node, exactPercentages, graph.edges, sum, onChange]);

const handleChangeLabel = (e: React.ChangeEvent<HTMLInputElement>) => {
setLabel(e.target.value);
};

if (!node) {
return null;
}

return (
<>
<div className="nodeModalBackground" />
<div className="nodeModal">
<div className="nodeModalContent">
<div className="nodeModalTitle">Update State</div>
<form onSubmit={handleSubmit}>
<input type="text" value={label} onChange={handleChangeLabel} autoFocus={true} />
<ProbabilitySelector
exactPercentages={exactPercentages}
edgeLabels={edgeLabels}
onChange={setExactPercentages} />
<div className="nodeModalButtons">
<button type="submit">Save</button>
<button onClick={onCancel}>Cancel</button>
</div>
</form>
</div>
</div>
</>
);
};
Loading