add: relation graph
This commit is contained in:
parent
59f9dd11eb
commit
f30504e22f
|
|
@ -0,0 +1,6 @@
|
|||
<svg t="1712475235724" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6084"
|
||||
width="16" height="16">
|
||||
<path
|
||||
d="M896.244622 765.400781c-32.538127 0-62.261685 12.153822-84.820885 32.154322l-212.542629-190.40988a222.180398 222.180398 0 0 0 39.74513-127.33794c0-35.267407-8.145193-69.340754-23.497389-100.045146l139.363827-114.416508a156.507113 156.507113 0 1 0-23.75326-32.154322l-136.890417 112.369548a226.658122 226.658122 0 0 0-20.725465-23.881194 222.308333 222.308333 0 0 0-158.212913-65.54535 222.095108 222.095108 0 0 0-133.819978 44.43608L218.658377 219.71549a127.807035 127.807035 0 1 0-32.580772 23.113584l65.246835 84.43708a222.180398 222.180398 0 0 0-60.044146 152.498484c0 58.850086 22.601845 114.331218 63.75426 156.379179L200.278387 710.729904a127.807035 127.807035 0 1 0 29.510333 27.292794l55.609067-75.780148a222.137753 222.137753 0 0 0 129.512834 41.237706c59.702986 0 115.909083-23.284165 158.170268-65.502705l0.127935-0.17058 212.798499 190.623105a127.807035 127.807035 0 1 0 110.194654-63.029295z m-28.742723-701.595374c51.429858 0 93.264593 41.79209 93.264593 93.221948s-41.79209 93.264593-93.264593 93.264593A93.307238 93.307238 0 0 1 774.279951 157.027355c0-51.429858 41.79209-93.221948 93.221948-93.221948zM65.520218 128.327276A63.967485 63.967485 0 0 1 129.359768 64.487726a63.967485 63.967485 0 0 1 63.92484 63.882195 63.967485 63.967485 0 0 1-63.92484 63.92484 63.967485 63.967485 0 0 1-63.83955-63.967485z m63.882195 752.641428A63.967485 63.967485 0 0 1 65.520218 817.043864a63.967485 63.967485 0 0 1 63.882195-63.882195 63.967485 63.967485 0 0 1 63.92484 63.882195 63.967485 63.967485 0 0 1-63.92484 63.92484z m285.593498-241.370643a159.961357 159.961357 0 0 1-159.790777-159.790778 159.961357 159.961357 0 0 1 159.790777-159.748132 159.961357 159.961357 0 0 1 159.748132 159.748132 159.961357 159.961357 0 0 1-159.748132 159.748133zM896.244622 957.047366a63.967485 63.967485 0 0 1-63.92484-63.882195 63.967485 63.967485 0 0 1 63.92484-63.92484 63.967485 63.967485 0 0 1 63.882195 63.92484 63.967485 63.967485 0 0 1-63.882195 63.882195z"
|
||||
fill="#e8af59" p-id="6085"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
<svg t="1712475235724" class="icon" viewBox="0 0 1025 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="6084"
|
||||
width="20" height="20">
|
||||
<path
|
||||
d="M896.244622 765.400781c-32.538127 0-62.261685 12.153822-84.820885 32.154322l-212.542629-190.40988a222.180398 222.180398 0 0 0 39.74513-127.33794c0-35.267407-8.145193-69.340754-23.497389-100.045146l139.363827-114.416508a156.507113 156.507113 0 1 0-23.75326-32.154322l-136.890417 112.369548a226.658122 226.658122 0 0 0-20.725465-23.881194 222.308333 222.308333 0 0 0-158.212913-65.54535 222.095108 222.095108 0 0 0-133.819978 44.43608L218.658377 219.71549a127.807035 127.807035 0 1 0-32.580772 23.113584l65.246835 84.43708a222.180398 222.180398 0 0 0-60.044146 152.498484c0 58.850086 22.601845 114.331218 63.75426 156.379179L200.278387 710.729904a127.807035 127.807035 0 1 0 29.510333 27.292794l55.609067-75.780148a222.137753 222.137753 0 0 0 129.512834 41.237706c59.702986 0 115.909083-23.284165 158.170268-65.502705l0.127935-0.17058 212.798499 190.623105a127.807035 127.807035 0 1 0 110.194654-63.029295z m-28.742723-701.595374c51.429858 0 93.264593 41.79209 93.264593 93.221948s-41.79209 93.264593-93.264593 93.264593A93.307238 93.307238 0 0 1 774.279951 157.027355c0-51.429858 41.79209-93.221948 93.221948-93.221948zM65.520218 128.327276A63.967485 63.967485 0 0 1 129.359768 64.487726a63.967485 63.967485 0 0 1 63.92484 63.882195 63.967485 63.967485 0 0 1-63.92484 63.92484 63.967485 63.967485 0 0 1-63.83955-63.967485z m63.882195 752.641428A63.967485 63.967485 0 0 1 65.520218 817.043864a63.967485 63.967485 0 0 1 63.882195-63.882195 63.967485 63.967485 0 0 1 63.92484 63.882195 63.967485 63.967485 0 0 1-63.92484 63.92484z m285.593498-241.370643a159.961357 159.961357 0 0 1-159.790777-159.790778 159.961357 159.961357 0 0 1 159.790777-159.748132 159.961357 159.961357 0 0 1 159.748132 159.748132 159.961357 159.961357 0 0 1-159.748132 159.748133zM896.244622 957.047366a63.967485 63.967485 0 0 1-63.92484-63.882195 63.967485 63.967485 0 0 1 63.92484-63.92484 63.967485 63.967485 0 0 1 63.882195 63.92484 63.967485 63.967485 0 0 1-63.882195 63.882195z"
|
||||
fill="#e8af59" p-id="6085"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,210 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Force-Directed Tree</title>
|
||||
<script src="chrome://__addonRef__/content/lib/js/d3.v7.min.js"></script>
|
||||
<link rel="stylesheet" href="chrome://zotero-platform/content/zotero.css" />
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
overflow: hidden;
|
||||
background: var(--material-background);
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.node:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--text-color: black;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-color: white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
window.addEventListener("message", (ev) => {
|
||||
if (ev.data.type === "render") {
|
||||
render(ev.data.graph);
|
||||
}
|
||||
});
|
||||
function render(data) {
|
||||
// Define the data for the nodes and links
|
||||
// Specify the dimensions of the chart.
|
||||
var width = window.innerWidth;
|
||||
var height = window.innerHeight;
|
||||
// 2: 100; 22: 50
|
||||
function distance() {
|
||||
if (data.nodes.length > 22) {
|
||||
return 50;
|
||||
}
|
||||
return 105 - 2.5 * data.nodes.length;
|
||||
}
|
||||
|
||||
// Specify the color scale.
|
||||
const color = d3.scaleOrdinal([1, 2], ["grey", "#e8af59"]);
|
||||
|
||||
// The force simulation mutates links and nodes, so create a copy
|
||||
// so that re-evaluating this cell produces the same result.
|
||||
const links = data.links.map((d) => ({ ...d }));
|
||||
const nodes = data.nodes.map((d) => ({ ...d }));
|
||||
|
||||
// Create a simulation with several forces.
|
||||
const simulation = d3
|
||||
.forceSimulation(nodes)
|
||||
.force(
|
||||
"link",
|
||||
d3
|
||||
.forceLink(links)
|
||||
.id((d) => d.id)
|
||||
.distance(distance()),
|
||||
)
|
||||
.force("charge", d3.forceManyBody().strength(-400))
|
||||
.force("x", d3.forceX())
|
||||
.force("y", d3.forceY());
|
||||
|
||||
// Create the SVG container.
|
||||
const svg = d3
|
||||
.create("svg")
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", [-width / 2, -height / 2, width, height])
|
||||
.attr("style", "max-width: 100%; height: auto;");
|
||||
|
||||
// Add a line for each link, and a circle for each node.
|
||||
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("g") // Append a 'g' element for each node
|
||||
.attr("class", "node"); // Assign a class for styling if needed
|
||||
|
||||
node
|
||||
.append("circle")
|
||||
.attr("r", 7)
|
||||
.attr("fill", (d) => color(d.group));
|
||||
|
||||
node
|
||||
.append("text")
|
||||
.attr("x", 0) // Center the text horizontally
|
||||
.attr("y", 18) // Position the text below the circle
|
||||
.attr("text-anchor", "middle") // Ensure the text is centered
|
||||
.attr("fill", "var(--text-color)")
|
||||
.attr("stroke", "none")
|
||||
.text((d) => d.title);
|
||||
|
||||
node.append("title").text((d) => d.title);
|
||||
|
||||
// Add a drag behavior.
|
||||
node.call(
|
||||
d3
|
||||
.drag()
|
||||
.on("start", dragstarted)
|
||||
.on("drag", dragged)
|
||||
.on("end", dragended),
|
||||
);
|
||||
|
||||
// Set the position attributes of links and nodes each time the simulation ticks.
|
||||
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("transform", (d) => `translate(${d.x},${d.y})`);
|
||||
});
|
||||
|
||||
// Reheat the simulation when drag starts, and fix the subject position.
|
||||
function dragstarted(event) {
|
||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||
event.subject.fx = event.subject.x;
|
||||
event.subject.fy = event.subject.y;
|
||||
}
|
||||
|
||||
// Update the subject (dragged node) position during drag.
|
||||
function dragged(event) {
|
||||
event.subject.fx = event.x;
|
||||
event.subject.fy = event.y;
|
||||
}
|
||||
|
||||
// Restore the target alpha so the simulation cools after dragging ends.
|
||||
// Unfix the subject position now that it’s no longer being dragged.
|
||||
function dragended(event) {
|
||||
if (!event.active) simulation.alphaTarget(0);
|
||||
event.subject.fx = null;
|
||||
event.subject.fy = null;
|
||||
}
|
||||
|
||||
// Add the hover interaction
|
||||
node
|
||||
.on("mouseover", function (event, d) {
|
||||
// Enlarge the node circle
|
||||
d3.select(this)
|
||||
.select("circle")
|
||||
.transition()
|
||||
.duration(200)
|
||||
.attr("r", 10); // New, larger radius
|
||||
d3.select(".tooltip").style("display", "").html(d.title);
|
||||
})
|
||||
.on("mouseout", function (event, d) {
|
||||
// Shrink the node circle back to original size
|
||||
d3.select(this)
|
||||
.select("circle")
|
||||
.transition()
|
||||
.duration(500)
|
||||
.attr("r", 7); // Original radius
|
||||
d3.select(".tooltip").style("display", "none");
|
||||
})
|
||||
.on("click", function (event, d) {
|
||||
window.postMessage(
|
||||
{ type: "openNote", isShift: !!event.shiftKey, id: d.id },
|
||||
"*",
|
||||
);
|
||||
});
|
||||
|
||||
document.body.replaceChildren(
|
||||
svg.node(),
|
||||
d3
|
||||
.create("div")
|
||||
.attr("class", "tooltip")
|
||||
.style("display", "none")
|
||||
.node(),
|
||||
);
|
||||
}
|
||||
|
||||
d3.select(window).on("resize", function () {
|
||||
width = window.innerWidth;
|
||||
height = window.innerHeight;
|
||||
d3.select(document.querySelector("svg"))
|
||||
.attr("width", width)
|
||||
.attr("height", height)
|
||||
.attr("viewBox", [-width / 2, -height / 2, width, height]);
|
||||
});
|
||||
</script>
|
||||
</head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
#bn-relation-graph {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
note-relation-header =
|
||||
.label = Relation Graph
|
||||
note-relation-sidenav =
|
||||
.tooltiptext = Relation Graph
|
||||
note-relation-refresh =
|
||||
.tooltiptext = Refresh
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
note-relation-header =
|
||||
.label = Relation Graph
|
||||
note-relation-sidenav =
|
||||
.tooltiptext = Relation Graph
|
||||
note-relation-refresh =
|
||||
.tooltiptext = Refresh
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
note-relation-header =
|
||||
.label = Relation Graph
|
||||
note-relation-sidenav =
|
||||
.tooltiptext = Relation Graph
|
||||
note-relation-refresh =
|
||||
.tooltiptext = Refresh
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
note-relation-header =
|
||||
.label = Relation Graph
|
||||
note-relation-sidenav =
|
||||
.tooltiptext = Relation Graph
|
||||
note-relation-refresh =
|
||||
.tooltiptext = Refresh
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
note-relation-header =
|
||||
.label = 关系图
|
||||
note-relation-sidenav =
|
||||
.tooltiptext = 关系图
|
||||
note-relation-refresh =
|
||||
.tooltiptext = 刷新
|
||||
|
|
@ -5,9 +5,9 @@ const buildDir = "build";
|
|||
|
||||
export async function main() {
|
||||
await build({
|
||||
entryPoints: ["./src/extras/*.ts"],
|
||||
entryPoints: ["./src/extras/*.*"],
|
||||
outdir: path.join(buildDir, "addon/chrome/content/scripts"),
|
||||
bundle: true,
|
||||
target: ["firefox102"],
|
||||
target: ["firefox115"],
|
||||
}).catch(() => exit(1));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,7 @@ export const esbuildOptions = {
|
|||
__env__: `"${env.NODE_ENV}"`,
|
||||
},
|
||||
bundle: true,
|
||||
target: "firefox102",
|
||||
target: "firefox115",
|
||||
outfile: path.join(
|
||||
buildDir,
|
||||
`addon/chrome/content/scripts/${config.addonRef}.js`,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ export class NoteDetails extends ItemDetails {
|
|||
|
||||
init() {
|
||||
MozXULElement.insertFTLIfNeeded(`${config.addonRef}-notePreview.ftl`);
|
||||
MozXULElement.insertFTLIfNeeded(`${config.addonRef}-noteRelation.ftl`);
|
||||
super.init();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import { initSyncList } from "./modules/sync/api";
|
|||
import { patchViewItems } from "./modules/viewItems";
|
||||
import { onUpdateRelated } from "./modules/relatedNotes";
|
||||
import { getFocusedWindow } from "./utils/window";
|
||||
import { registerNoteRelation } from "./modules/workspace/relation";
|
||||
|
||||
async function onStartup() {
|
||||
await Promise.all([
|
||||
|
|
@ -58,6 +59,8 @@ async function onStartup() {
|
|||
|
||||
registerReaderAnnotationButton();
|
||||
|
||||
registerNoteRelation();
|
||||
|
||||
initSyncList();
|
||||
|
||||
setSyncing();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
import { config } from "../../../package.json";
|
||||
import { slice } from "../../utils/str";
|
||||
import { waitUtilAsync } from "../../utils/wait";
|
||||
|
||||
export function registerNoteRelation() {
|
||||
const key = Zotero.ItemPaneManager.registerSection({
|
||||
paneID: `bn-note-relation`,
|
||||
pluginID: config.addonID,
|
||||
header: {
|
||||
icon: `chrome://${config.addonRef}/content/icons/relation-16.svg`,
|
||||
l10nID: `${config.addonRef}-note-relation-header`,
|
||||
},
|
||||
sidenav: {
|
||||
icon: `chrome://${config.addonRef}/content/icons/relation-20.svg`,
|
||||
l10nID: `${config.addonRef}-note-relation-sidenav`,
|
||||
},
|
||||
bodyXHTML: `
|
||||
<linkset>
|
||||
<html:link
|
||||
rel="stylesheet"
|
||||
href="chrome://${config.addonRef}/content/styles/relation.css"
|
||||
></html:link>
|
||||
</linkset>
|
||||
<iframe
|
||||
src="chrome://${config.addonRef}/content/relationGraph.html"
|
||||
id="bn-relation-graph"
|
||||
></iframe>`,
|
||||
sectionButtons: [
|
||||
{
|
||||
type: "refreshGraph",
|
||||
icon: "chrome://zotero/skin/16/universal/sync.svg",
|
||||
l10nID: `${config.addonRef}-note-relation-refresh`,
|
||||
onClick: ({ body, item }) => {
|
||||
refresh(body, item);
|
||||
},
|
||||
},
|
||||
],
|
||||
onInit({ body }) {
|
||||
body
|
||||
.querySelector("iframe")!
|
||||
.contentWindow?.addEventListener("message", (ev) => {
|
||||
if (ev.data.type === "openNote") {
|
||||
addon.hooks.onOpenNote(
|
||||
ev.data.id,
|
||||
ev.data.isShift ? "window" : "tab",
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
onItemChange: ({ body, setEnabled }) => {
|
||||
if (body.closest("bn-workspace") as HTMLElement | undefined) {
|
||||
setEnabled(true);
|
||||
return;
|
||||
}
|
||||
setEnabled(false);
|
||||
},
|
||||
onRender: () => {},
|
||||
onAsyncRender: async ({ body, item }) => {
|
||||
await refresh(body, item);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function refresh(body: HTMLElement, item: Zotero.Item) {
|
||||
const data = await getRelationData(item);
|
||||
await waitUtilAsync(
|
||||
() =>
|
||||
body.querySelector("iframe")!.contentDocument?.readyState === "complete",
|
||||
);
|
||||
body.querySelector("iframe")!.contentWindow?.postMessage(
|
||||
{
|
||||
type: "render",
|
||||
graph: data,
|
||||
},
|
||||
"*",
|
||||
);
|
||||
}
|
||||
|
||||
async function getRelationData(note: Zotero.Item) {
|
||||
if (!note) return;
|
||||
const currentContent = note.getNote();
|
||||
const currentLink = addon.api.convert.note2link(note);
|
||||
const currentTitle = slice(note.getNoteTitle(), 15);
|
||||
const { detectedIDSet, currentIDSet } =
|
||||
await addon.api.related.getRelatedNoteIds(note.id);
|
||||
if (!areSetsEqual(detectedIDSet, currentIDSet)) {
|
||||
await addon.api.related.updateRelatedNotes(note.id);
|
||||
}
|
||||
const items = Zotero.Items.get(Array.from(detectedIDSet));
|
||||
|
||||
const nodes = [];
|
||||
const links = [];
|
||||
for (const item of items) {
|
||||
const compareContent = item.getNote();
|
||||
const compareLink = addon.api.convert.note2link(item);
|
||||
const compareTitle = slice(item.getNoteTitle(), 15);
|
||||
|
||||
if (currentLink && compareContent.includes(currentLink)) {
|
||||
links.push({
|
||||
source: item.id,
|
||||
target: note.id,
|
||||
value: 1,
|
||||
});
|
||||
}
|
||||
if (compareLink && currentContent.includes(compareLink)) {
|
||||
links.push({
|
||||
source: note.id,
|
||||
target: item.id,
|
||||
value: 1,
|
||||
});
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: item.id,
|
||||
title: compareTitle,
|
||||
group: 2,
|
||||
});
|
||||
}
|
||||
|
||||
nodes.push({
|
||||
id: note.id,
|
||||
title: currentTitle,
|
||||
group: 1,
|
||||
});
|
||||
|
||||
return { nodes, links };
|
||||
}
|
||||
|
||||
function areSetsEqual(set1: Set<any>, set2: Set<any>): boolean {
|
||||
if (set1.size !== set2.size) {
|
||||
return false;
|
||||
}
|
||||
for (const item of set1) {
|
||||
if (!set2.has(item)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Loading…
Reference in New Issue