Add directional arrows. Apply same face lift to time filtering
This commit is contained in:
@@ -73,7 +73,7 @@ export function Home() {
|
|||||||
href="https://github.com/WillJeynes/LLMsForDisinformationPrediction/"
|
href="https://github.com/WillJeynes/LLMsForDisinformationPrediction/"
|
||||||
className="block bg-white rounded-xl px-4 py-2 underline text-center w-64 m-1"
|
className="block bg-white rounded-xl px-4 py-2 underline text-center w-64 m-1"
|
||||||
>
|
>
|
||||||
Project Source Code
|
Site Source Code
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function VizSmallConnected() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fgRef.current) {
|
if (fgRef.current) {
|
||||||
fgRef.current.zoom(0.01, 0);
|
fgRef.current.zoom(0.05, 0);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -174,7 +174,8 @@ export function VizSmallConnected() {
|
|||||||
linkColor={() => "black"}
|
linkColor={() => "black"}
|
||||||
linkWidth={2.5}
|
linkWidth={2.5}
|
||||||
onNodeClick={(node) => setSelectedNode(node)}
|
onNodeClick={(node) => setSelectedNode(node)}
|
||||||
|
linkDirectionalArrowLength={100}
|
||||||
|
linkDirectionalArrowRelPos={0.9}
|
||||||
nodeCanvasObject={(node, ctx, globalScale) => {
|
nodeCanvasObject={(node, ctx, globalScale) => {
|
||||||
const label = node.label;
|
const label = node.label;
|
||||||
const fontSize = 16 + 32 * node.members.length;
|
const fontSize = 16 + 32 * node.members.length;
|
||||||
@@ -190,10 +191,10 @@ export function VizSmallConnected() {
|
|||||||
const x = node.x - width / 2;
|
const x = node.x - width / 2;
|
||||||
const y = node.y - height / 2;
|
const y = node.y - height / 2;
|
||||||
|
|
||||||
const radius = Math.min(10, fontSize * 0.6);
|
const radius = node.type.includes("claim") ? fontSize * 6 : 0;
|
||||||
|
|
||||||
// background
|
// background
|
||||||
ctx.fillStyle = node.type.includes("claim") ? "blue" : "green";
|
ctx.fillStyle = node.type.includes("claim") ? "DarkMagenta" : "green";
|
||||||
drawRoundedRect(ctx, x, y, width, height, radius);
|
drawRoundedRect(ctx, x, y, width, height, radius);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
|
|
||||||
@@ -227,10 +228,10 @@ export function VizSmallConnected() {
|
|||||||
<FloatingPanelStack>
|
<FloatingPanelStack>
|
||||||
<DetailsPanel selectedNode={selectedNode} data={data} />
|
<DetailsPanel selectedNode={selectedNode} data={data} />
|
||||||
<FloatingPanel title={"Key"}>
|
<FloatingPanel title={"Key"}>
|
||||||
<p>Trigger Event Cluster</p>
|
<p style={{backgroundColor: "green"}} className="text-white p-1 text-xl mb-3">Trigger Event Cluster</p>
|
||||||
<p>Claim Cluster</p>
|
<p style={{backgroundColor: "DarkMagenta"}} className="text-white p-1 text-xl rounded-3xl">Claim Cluster</p>
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
<FloatingPanel title={"Config"}>
|
<FloatingPanel title={"Config"} defaultOpen={false}>
|
||||||
<label>
|
<label>
|
||||||
Min connected graph size: <strong>{minGraphSize}</strong>
|
Min connected graph size: <strong>{minGraphSize}</strong>
|
||||||
</label>
|
</label>
|
||||||
@@ -243,11 +244,7 @@ export function VizSmallConnected() {
|
|||||||
onChange={(e) => setMinGraphSize(Number(e.target.value))}
|
onChange={(e) => setMinGraphSize(Number(e.target.value))}
|
||||||
/>
|
/>
|
||||||
</FloatingPanel>
|
</FloatingPanel>
|
||||||
|
|
||||||
</FloatingPanelStack>
|
</FloatingPanelStack>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,10 @@ import * as d3 from "d3-force-3d";
|
|||||||
|
|
||||||
import data from "./data_date.json";
|
import data from "./data_date.json";
|
||||||
import titlesData from "./titles_date.json";
|
import titlesData from "./titles_date.json";
|
||||||
|
import HomeButton from "./utils/HomeButton";
|
||||||
|
import { FloatingPanelStack } from "./utils/FloatingPanelStack";
|
||||||
|
import { DetailsPanel } from "./utils/DetailsPanel";
|
||||||
|
import { FloatingPanel } from "./utils/FloatingPanel";
|
||||||
|
|
||||||
function drawRoundedRect(ctx, x, y, width, height, radius) {
|
function drawRoundedRect(ctx, x, y, width, height, radius) {
|
||||||
const r = Math.min(radius, width / 2, height / 2);
|
const r = Math.min(radius, width / 2, height / 2);
|
||||||
@@ -136,6 +140,7 @@ export function VizTimeFilter() {
|
|||||||
const [selectedNode, setSelectedNode] = useState(null);
|
const [selectedNode, setSelectedNode] = useState(null);
|
||||||
const [inputDate, setInputDate] = useState(1682353753000); //some time in 2023
|
const [inputDate, setInputDate] = useState(1682353753000); //some time in 2023
|
||||||
const [showAll, setShowAll] = useState(false);
|
const [showAll, setShowAll] = useState(false);
|
||||||
|
const [filterLeeway, setFilterLeeway] = useState(6);
|
||||||
|
|
||||||
const parsedInputDate = useMemo(() => {
|
const parsedInputDate = useMemo(() => {
|
||||||
const d = new Date(inputDate);
|
const d = new Date(inputDate);
|
||||||
@@ -200,7 +205,7 @@ export function VizTimeFilter() {
|
|||||||
function isNodeHighlighted(node, referenceDate) {
|
function isNodeHighlighted(node, referenceDate) {
|
||||||
if (!referenceDate || !node.avgDate) return false;
|
if (!referenceDate || !node.avgDate) return false;
|
||||||
const diffMonths = Math.abs(referenceDate - node.avgDate) / (1000 * 60 * 60 * 24 * 30.44);
|
const diffMonths = Math.abs(referenceDate - node.avgDate) / (1000 * 60 * 60 * 24 * 30.44);
|
||||||
return diffMonths <= 6;
|
return diffMonths <= filterLeeway;
|
||||||
}
|
}
|
||||||
|
|
||||||
const highlightedNodeIds = useMemo(() => {
|
const highlightedNodeIds = useMemo(() => {
|
||||||
@@ -215,21 +220,24 @@ export function VizTimeFilter() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return set;
|
return set;
|
||||||
}, [graphData.nodes, parsedInputDate, showAll]);
|
}, [graphData.nodes, parsedInputDate, showAll, filterLeeway]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (fgRef.current) {
|
if (fgRef.current) {
|
||||||
fgRef.current.zoom(0.01, 0);
|
fgRef.current.zoom(0.01, 0);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<HomeButton />
|
||||||
<ForceGraph2D
|
<ForceGraph2D
|
||||||
ref={fgRef}
|
ref={fgRef}
|
||||||
graphData={graphData}
|
graphData={graphData}
|
||||||
nodeLabel={(node) => node.label}
|
nodeLabel={(node) => node.label}
|
||||||
nodeAutoColorBy="type"
|
nodeAutoColorBy="type"
|
||||||
|
linkDirectionalArrowLength={500}
|
||||||
|
linkDirectionalArrowRelPos={0.9}
|
||||||
linkColor={(link) => {
|
linkColor={(link) => {
|
||||||
const sourceId =
|
const sourceId =
|
||||||
typeof link.source === "object" ? link.source.id : link.source;
|
typeof link.source === "object" ? link.source.id : link.source;
|
||||||
@@ -241,7 +249,7 @@ export function VizTimeFilter() {
|
|||||||
highlightedNodeIds.has(sourceId) &&
|
highlightedNodeIds.has(sourceId) &&
|
||||||
highlightedNodeIds.has(targetId);
|
highlightedNodeIds.has(targetId);
|
||||||
|
|
||||||
return bothHighlighted ? "orange" : "rgba(0,0,0,0)";
|
return bothHighlighted ? "black" : "rgba(0,0,0,0)";
|
||||||
}}
|
}}
|
||||||
linkWidth={2.5}
|
linkWidth={2.5}
|
||||||
onNodeClick={(node) => setSelectedNode(node)}
|
onNodeClick={(node) => setSelectedNode(node)}
|
||||||
@@ -260,10 +268,10 @@ export function VizTimeFilter() {
|
|||||||
const x = node.x - width / 2;
|
const x = node.x - width / 2;
|
||||||
const y = node.y - height / 2;
|
const y = node.y - height / 2;
|
||||||
|
|
||||||
const radius = Math.min(10, fontSize * 0.6);
|
const radius = node.type.includes("claim") ? fontSize * 6 : 0;
|
||||||
|
|
||||||
ctx.fillStyle = node.type.includes("claim")
|
ctx.fillStyle = node.type.includes("claim")
|
||||||
? "blue"
|
? "DarkMagenta"
|
||||||
: "green"
|
: "green"
|
||||||
|
|
||||||
if (highlightedNodeIds.has(node.id)) {
|
if (highlightedNodeIds.has(node.id)) {
|
||||||
@@ -297,58 +305,37 @@ export function VizTimeFilter() {
|
|||||||
ctx.fill();
|
ctx.fill();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FloatingPanelStack bot="lg">
|
||||||
|
<DetailsPanel selectedNode={selectedNode} data={data} />
|
||||||
|
<FloatingPanel title={"Key"}>
|
||||||
|
<p style={{ backgroundColor: "green" }} className="text-white p-1 text-xl mb-3">Trigger Event Cluster</p>
|
||||||
|
<p style={{ backgroundColor: "DarkMagenta" }} className="text-white p-1 text-xl rounded-3xl">Claim Cluster</p>
|
||||||
|
</FloatingPanel>
|
||||||
|
<FloatingPanel title={"Config"} defaultOpen={false}>
|
||||||
|
|
||||||
|
<label className="flex">
|
||||||
|
<span className="mr-1 grow">Show all</span>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={showAll}
|
||||||
|
onChange={() => setShowAll(!showAll)}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<br />
|
||||||
|
<label className="flex">
|
||||||
|
<span className="mr-1 grow">Date Window (mos)</span>
|
||||||
|
<input
|
||||||
|
className="w-16 border-2"
|
||||||
|
type="number"
|
||||||
|
value={filterLeeway}
|
||||||
|
onChange={(e) => setFilterLeeway(Number(e.target.value))}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</FloatingPanel>
|
||||||
|
</FloatingPanelStack>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
style={{
|
className="fixed bottom-0 left-0 w-full bg-gray-600 p-3 z-10"
|
||||||
position: "absolute",
|
|
||||||
top: "10px",
|
|
||||||
right: "10px",
|
|
||||||
borderRadius: "3px",
|
|
||||||
backgroundColor: "gray",
|
|
||||||
padding: "20px",
|
|
||||||
maxWidth: "500px"
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p><a href="#home">Go Home</a></p>
|
|
||||||
<h2>Details</h2>
|
|
||||||
{selectedNode ? (
|
|
||||||
<div>
|
|
||||||
<p><strong>Title:</strong> {selectedNode.label}</p>
|
|
||||||
<p><strong>Date: </strong>{new Date(selectedNode.avgDate).toISOString().slice(0, 10)}</p>
|
|
||||||
{selectedNode.members && (
|
|
||||||
<div>
|
|
||||||
<p><strong>Members:</strong></p>
|
|
||||||
<ul>
|
|
||||||
{selectedNode.members.map((m) => {
|
|
||||||
const memberData =
|
|
||||||
data.claims.find((c) => c.id === m) ||
|
|
||||||
data.events.find((e) => e.id === m);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={m}>
|
|
||||||
{memberData ? memberData.text : m}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p>Click a node to see details</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: "fixed",
|
|
||||||
bottom: 0,
|
|
||||||
left: 0,
|
|
||||||
width: "100%",
|
|
||||||
background: "#222",
|
|
||||||
padding: "10px 20px",
|
|
||||||
boxSizing: "border-box",
|
|
||||||
zIndex: 10
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
@@ -356,34 +343,14 @@ export function VizTimeFilter() {
|
|||||||
max={timeRange.max}
|
max={timeRange.max}
|
||||||
value={inputDate}
|
value={inputDate}
|
||||||
onChange={(e) => setInputDate(new Number(e.target.value))}
|
onChange={(e) => setInputDate(new Number(e.target.value))}
|
||||||
style={{
|
className="w-full"
|
||||||
width: "100%"
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
color: "white",
|
|
||||||
fontSize: "12px",
|
|
||||||
marginTop: "5px",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{new Date(inputDate).toISOString().slice(0, 10)} (± 6 months window)
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<label style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
<span className="text-white">
|
||||||
<span>Show all</span>
|
{new Date(inputDate).toISOString().slice(0, 10)} (± {filterLeeway} month(s) window)
|
||||||
<input
|
</span>
|
||||||
type="checkbox"
|
|
||||||
checked={showAll}
|
|
||||||
onChange={() => setShowAll(!showAll)}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export function FloatingPanelStack({ children }) {
|
export function FloatingPanelStack({ children, bot = "sml" }) {
|
||||||
return (
|
const botSty = bot == "sml" ? "bottom-4" : "bottom-20"
|
||||||
<div className="fixed bottom-4 right-4 z-50 flex flex-col gap-2 items-end">
|
return (
|
||||||
{children}
|
<div className={"fixed right-4 z-50 flex flex-col gap-2 items-end " + botSty}>
|
||||||
</div>
|
{children}
|
||||||
);
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user