From ac59f6ffa11edf82388995efd9e4e239dc23e986 Mon Sep 17 00:00:00 2001 From: Ross Keenan Date: Mon, 2 Aug 2021 22:30:44 +0200 Subject: [PATCH] feat(Vis View): :sparkles: Functional Vis Modal! --- .vscode/settings.json | 3 +- src/MatrixView.ts | 142 +--------------------------------------- src/VisModal.ts | 149 ++++++++++++++++++++++++++++++++++++++++++ src/VisView.ts | 140 ++++++++++++++++++++++++++++++++------- src/interfaces.ts | 15 +++++ src/main.ts | 5 ++ 6 files changed, 289 insertions(+), 165 deletions(-) create mode 100644 src/VisModal.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 2877156a..6d2773f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "Grid View", "Juggl", "List/Matrix View", - "Stats View" + "Stats View", + "Vis View" ] } \ No newline at end of file diff --git a/src/MatrixView.ts b/src/MatrixView.ts index 32eab40f..518cf424 100644 --- a/src/MatrixView.ts +++ b/src/MatrixView.ts @@ -14,6 +14,7 @@ import type { BreadcrumbsSettings, internalLinkObj, SquareProps, + d3Graph, } from "src/interfaces"; import type BreadcrumbsPlugin from "src/main"; import { closeImpliedLinks, debug } from "src/sharedFunctions"; @@ -183,26 +184,6 @@ export default class MatrixView extends ItemView { return pathsArr; } - dfsAdjList(g: Graph, startNode: string): AdjListItem[] { - const queue: string[] = [startNode]; - const adjList: AdjListItem[] = []; - - let i = 0; - while (queue.length && i < 1000) { - i++; - - const currNode = queue.shift(); - const newNodes = (g.successors(currNode) ?? []) as string[]; - - newNodes.forEach((succ) => { - const next: AdjListItem = { name: currNode, parentId: succ }; - queue.unshift(succ); - adjList.push(next); - }); - } - return adjList; - } - createIndex( // Gotta give it a starting index. This allows it to work for the global index feat index: string, @@ -268,127 +249,6 @@ export default class MatrixView extends ItemView { const currFile = this.app.workspace.getActiveFile(); const settings = this.plugin.settings; - const closedParents = closeImpliedLinks(gParents, gChildren); - // const dijkstraPaths = graphlib.alg.dijkstra( - // closedParents, - // currFile.basename - // ); - - const adjList: AdjListItem[] = this.dfsAdjList( - closedParents, - currFile.basename - ); - console.log({ adjList }); - - const noDoubles = adjList.filter( - (thing, index, self) => - index === - self.findIndex( - (t) => t.name === thing.name && t?.parentId === thing?.parentId - ) - ); - console.log({ noDoubles }); - console.time("tree"); - const hierarchy = createTreeHierarchy(noDoubles, { excludeParent: true }); - console.timeEnd("tree"); - console.log({ hierarchy }); - - const d3GraphDiv = this.contentEl.createDiv({ cls: "d3-graph" }); - - const width = 450; - const height = 450; - - const tree = (data) => { - const root = d3 - .hierarchy(data) - .sort( - (a, b) => - d3.descending(a.height, b.height) || - d3.ascending(a.data.name, b.data.name) - ); - root.dx = 10; - root.dy = width / (root.height + 1); - return d3.cluster().nodeSize([root.dx, root.dy])(root); - }; - - const pack = (data) => - d3 - .pack() - .size([width - 2, height - 2]) - .padding(3)( - d3 - .hierarchy(data) - .sum((d) => d.value) - .sort((a, b) => b.value - a.value) - ); - - const root = pack(adjList); - - const svg = d3 - .create("svg") - .attr("viewBox", [0, 0, width, height]) - .style("font", "10px sans-serif") - .attr("text-anchor", "middle"); - - svg - .append("filter") - .append("feDropShadow") - .attr("flood-opacity", 0.3) - .attr("dx", 0) - .attr("dy", 1); - - const node = svg - .selectAll("g") - .data(d3.group(root.descendants(), (d) => d.height)) - .join("g") - .selectAll("g") - .data((d) => d[1]) - .join("g") - .attr("transform", (d) => `translate(${d.x + 1},${d.y + 1})`); - - node - .append("circle") - .attr("r", (d) => d.r) - .attr("fill", (d) => color(d.height)); - - const leaf = node.filter((d) => !d.children); - - // leaf.select("circle").attr("id", (d) => (d.leafUid = DOM.uid("leaf")).id); - - leaf - .append("clipPath") - // .attr("id", (d) => (d.clipUid = DOM.uid("clip")).id) - .append("use") - .attr("xlink:href", (d) => d.leafUid.href); - - leaf - .append("text") - .attr("clip-path", (d) => d.clipUid) - .selectAll("tspan") - .data((d) => d.data.name.split(/(?=[A-Z][a-z])|\s+/g)) - .join("tspan") - .attr("x", 0) - .attr("y", (d, i, nodes) => `${i - nodes.length / 2 + 0.8}em`) - .text((d) => d); - - node.append("title").text( - (d) => - `${d - .ancestors() - .map((d) => d.data.name) - .reverse() - .join("/")}\n${format(d.value)}` - ); - - // for (const [node, path] of Object.entries(dijkstraPaths)) { - // const item: AdjListItem = { id: node }; - // const predecessor: string | undefined = path.predecessor; - // if (predecessor) { - // item.parentId = predecessor; - // } - // adjList.push(item); - // } - const viewToggleButton = this.contentEl.createEl("button", { text: this.matrixQ ? "List" : "Matrix", }); diff --git a/src/VisModal.ts b/src/VisModal.ts new file mode 100644 index 00000000..60cf88c7 --- /dev/null +++ b/src/VisModal.ts @@ -0,0 +1,149 @@ +import * as d3 from "d3"; +import type { Graph } from "graphlib"; +import { createTreeHierarchy } from "hierarchy-js"; +import { App, Modal } from "obsidian"; +import type { AdjListItem, d3Graph } from "src/interfaces"; +import type BreadcrumbsPlugin from "src/main"; +import { closeImpliedLinks } from "src/sharedFunctions"; + +export class VisModal extends Modal { + plugin: BreadcrumbsPlugin; + + constructor(app: App, plugin: BreadcrumbsPlugin) { + super(app); + this.plugin = plugin; + } + + onOpen() { + // ANCHOR Setup Modal layout + let { contentEl } = this; + contentEl.createEl("h1", { text: "Hey" }); + + function dfsAdjList(g: Graph, startNode: string): AdjListItem[] { + const queue: string[] = [startNode]; + const adjList: AdjListItem[] = []; + + let i = 0; + while (queue.length && i < 1000) { + i++; + + const currNode = queue.shift(); + const newNodes = (g.successors(currNode) ?? []) as string[]; + + newNodes.forEach((succ) => { + const next: AdjListItem = { name: currNode, parentId: succ }; + queue.unshift(succ); + adjList.push(next); + }); + } + return adjList; + } + + function graphlibToD3(g: Graph): d3Graph { + const d3Graph: d3Graph = { nodes: [], links: [] }; + const edgeIDs = {}; + + g.nodes().forEach((node, i) => { + d3Graph.nodes.push({ id: i, name: node }); + edgeIDs[node] = i; + }); + g.edges().forEach((edge) => { + d3Graph.links.push({ + source: edgeIDs[edge.v], + target: edgeIDs[edge.w], + }); + }); + return d3Graph; + } + + contentEl.empty(); + const { gParents, gSiblings, gChildren } = this.plugin.currGraphs; + const currFile = this.app.workspace.getActiveFile(); + + const closedParents = closeImpliedLinks(gParents, gChildren); + + const adjList: AdjListItem[] = dfsAdjList(closedParents, currFile.basename); + console.log({ adjList }); + + const noDoubles = adjList.filter( + (thing, index, self) => + index === + self.findIndex( + (t) => t.name === thing.name && t?.parentId === thing?.parentId + ) + ); + console.log({ noDoubles }); + console.time("tree"); + const hierarchy = createTreeHierarchy(noDoubles, { + id: "name", + excludeParent: true, + }); + console.timeEnd("tree"); + console.log({ hierarchy }); + + const data = graphlibToD3(closedParents); + const d3GraphDiv = contentEl.createDiv({ + cls: "d3-graph", + }); + + const width = 1000; + const height = 1000; + + contentEl.style.width = `${Math.round(screen.width / 1.5)}px`; + contentEl.style.height = `${Math.round(screen.height / 1.3)}px`; + + const links = data.links.map((d) => Object.create(d)); + const nodes = data.nodes.map((d) => Object.create(d)); + + const simulation = d3 + .forceSimulation(nodes) + .force( + "link", + d3.forceLink(links).id((d) => d.id) + ) + .force("charge", d3.forceManyBody()) + .force("center", d3.forceCenter(width / 2, height / 2)); + + const svg = d3 + .select(".d3-graph") + .append("svg") + .attr("height", Math.round(screen.height / 1.3)) + .attr("width", Math.round(screen.width / 1.3)); + + const link = svg + .append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line") + .attr("stroke-width", (d) => Math.sqrt(d.value)); + + const node = svg + .append("g") + .attr("stroke", "#fff") + .attr("stroke-width", 1.5) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("r", 5); + // .attr("fill", color) + // .call(drag(simulation)); + + node.append("title").text((d) => d.id); + + simulation.on("tick", () => { + link + .attr("x1", (d) => d.source.x) + .attr("y1", (d) => d.source.y) + .attr("x2", (d) => d.target.x) + .attr("y2", (d) => d.target.y); + + node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); + }); + } + + onClose() { + this.contentEl.empty(); + } +} diff --git a/src/VisView.ts b/src/VisView.ts index bf8973e5..2cefc8b4 100644 --- a/src/VisView.ts +++ b/src/VisView.ts @@ -1,15 +1,12 @@ import * as d3 from "d3"; -import { createTreeHierarchy } from "hierarchy-js"; - import type { Graph } from "graphlib"; +import { createTreeHierarchy } from "hierarchy-js"; +import { cloneDeep } from "lodash"; import { ItemView, WorkspaceLeaf } from "obsidian"; -import type { d3Tree } from "src/interfaces"; -import { - DATAVIEW_INDEX_DELAY, - VIEW_TYPE_BREADCRUMBS_STATS, -} from "src/constants"; +import { closeImpliedLinks } from "src/sharedFunctions"; +import { DATAVIEW_INDEX_DELAY } from "src/constants"; +import type { AdjListItem, BreadcrumbsSettings, d3Graph } from "src/interfaces"; import type BreadcrumbsPlugin from "src/main"; -import Stats from "./Components/Stats.svelte"; export default class StatsView extends ItemView { private plugin: BreadcrumbsPlugin; @@ -73,26 +70,123 @@ export default class StatsView extends ItemView { return pathsArr; } - - - toTree(g: Graph): d3Tree { - const topLevelNodes = g.sinks(); - - const tree: d3Tree = { name: "top", children: [] }; - if (topLevelNodes.length) { - topLevelNodes.forEach((node) => { - const dfsPaths = this.dfsAllPaths(g, node); - dfsPaths.forEach((path) => { - path.forEach((step) => { - const child: d3Tree = { name: step }; - tree.children.push(); - }); - }); + dfsAdjList(g: Graph, startNode: string): AdjListItem[] { + const queue: string[] = [startNode]; + const adjList: AdjListItem[] = []; + + let i = 0; + while (queue.length && i < 1000) { + i++; + + const currNode = queue.shift(); + const newNodes = (g.successors(currNode) ?? []) as string[]; + + newNodes.forEach((succ) => { + const next: AdjListItem = { name: currNode, parentId: succ }; + queue.unshift(succ); + adjList.push(next); }); } + return adjList; + } + + graphlibToD3(g: Graph): d3Graph { + const d3Graph: d3Graph = { nodes: [], links: [] }; + const edgeIDs = {}; + + g.nodes().forEach((node, i) => { + d3Graph.nodes.push({ id: i, name: node }); + edgeIDs[node] = i; + }); + g.edges().forEach((edge) => { + d3Graph.links.push({ source: edgeIDs[edge.v], target: edgeIDs[edge.w] }); + }); + return d3Graph; } async draw(): Promise { this.contentEl.empty(); + const { gParents, gSiblings, gChildren } = this.plugin.currGraphs; + const currFile = this.app.workspace.getActiveFile(); + const settings = this.plugin.settings; + + const closedParents = closeImpliedLinks(gParents, gChildren); + + const adjList: AdjListItem[] = this.dfsAdjList( + closedParents, + currFile.basename + ); + console.log({ adjList }); + + const noDoubles = adjList.filter( + (thing, index, self) => + index === + self.findIndex( + (t) => t.name === thing.name && t?.parentId === thing?.parentId + ) + ); + console.log({ noDoubles }); + console.time("tree"); + const hierarchy = createTreeHierarchy(noDoubles, { + id: "name", + excludeParent: true, + }); + console.timeEnd("tree"); + console.log({ hierarchy }); + + const data = this.graphlibToD3(closedParents); + const d3GraphDiv = this.contentEl.createDiv({ + cls: "d3-graph", + attr: { height: "1000px" }, + }); + + const width = 1000; + const height = 1000; + + const links = data.links.map((d) => Object.create(d)); + const nodes = data.nodes.map((d) => Object.create(d)); + + const simulation = d3 + .forceSimulation(nodes) + .force( + "link", + d3.forceLink(links).id((d) => d.id) + ) + .force("charge", d3.forceManyBody()) + .force("center", d3.forceCenter(width / 2, height / 2)); + + const svg = d3.select(".d3-graph").append("svg"); + + const link = svg + .append("g") + .attr("stroke", "#999") + .attr("stroke-opacity", 0.6) + .selectAll("line") + .data(links) + .join("line") + .attr("stroke-width", (d) => Math.sqrt(d.value)); + + const node = svg + .append("g") + .attr("stroke", "#fff") + .attr("stroke-width", 1.5) + .selectAll("circle") + .data(nodes) + .join("circle") + .attr("r", 5); + // .attr("fill", color) + // .call(drag(simulation)); + + node.append("title").text((d) => d.id); + + simulation.on("tick", () => { + link + .attr("x1", (d) => d.source.x) + .attr("y1", (d) => d.source.y) + .attr("x2", (d) => d.target.x) + .attr("y2", (d) => d.target.y); + + node.attr("cx", (d) => d.x).attr("cy", (d) => d.y); + }); } } diff --git a/src/interfaces.ts b/src/interfaces.ts index 34c0df72..bee3391b 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -90,3 +90,18 @@ export interface AdjListItem { name: string; parentId?: string; } + +export interface d3Node { + id: number; + name: string; +} + +export interface d3Link { + source: number; + target: number; +} + +export interface d3Graph { + nodes: d3Node[]; + links: d3Link[]; +} diff --git a/src/main.ts b/src/main.ts index 1be5a49e..5dff8fc9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -25,6 +25,7 @@ import { } from "src/sharedFunctions"; import TrailGrid from "./Components/TrailGrid.svelte"; import TrailPath from "./Components/TrailPath.svelte"; +import { VisModal } from "src/VisModal"; const DEFAULT_SETTINGS: BreadcrumbsSettings = { parentFieldName: "parent", @@ -161,6 +162,10 @@ export default class BreadcrumbsPlugin extends Plugin { }, }); + this.addRibbonIcon("dice", "Make Adjacency Matrix", () => + new VisModal(this.app, this).open() + ); + this.addSettingTab(new BreadcrumbsSettingTab(this.app, this)); }