Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

note-graph

Package Overview
Dependencies
Maintainers
1
Versions
13
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

note-graph - npm Package Compare versions

Comparing version 0.1.0 to 0.1.1

dist/data-adapters.d.ts

7

CHANGELOG.md

@@ -5,2 +5,9 @@ # Changelog

### [0.1.1](https://github.com/hikerpig/note-graph/compare/v0.1.0...v0.1.1) (2020-12-04)
### Features
* replace microbundle with rollup and fix UMD bundle globals problem ([c8a898b](https://github.com/hikerpig/note-graph/commit/c8a898b05a6fac21a486aa08dd9d1fb3a3bbf172))
## [0.1.0](https://github.com/hikerpig/note-graph/compare/v0.0.3...v0.1.0) (2020-12-03)

@@ -7,0 +14,0 @@

1147

dist/note-graph.esm.js
import { rgb, hsl } from 'd3-color';
import { forceX, forceY, forceCollide } from 'd3-force';
import { scaleLinear } from 'd3-scale';
import { debounce } from 'throttle-debounce';
import ForceGraph from 'force-graph';

@@ -11,179 +10,300 @@

class NoteGraphModel {
constructor(notes) {
this.subscribers = [];
this.notes = notes;
this.updateCache();
}
constructor(notes) {
this.subscribers = [];
this.notes = notes;
this.updateCache();
}
updateCache() {
const nodes = [];
const links = [];
const nodeInfos = {};
const linkMap = new Map();
this.notes.forEach((note) => {
nodes.push({ id: note.id, data: { note } });
const nodeInfo = {
title: note.title,
linkIds: [],
neighbors: [],
};
if (note.linkTo) {
note.linkTo.forEach((linkedNodeId) => {
const link = {
id: this.formLinkId(note.id, linkedNodeId),
source: note.id,
target: linkedNodeId,
};
links.push(link);
linkMap.set(link.id, link);
nodeInfo.linkIds.push(link.id);
nodeInfo.neighbors.push(linkedNodeId);
});
}
if (note.referencedBy) {
note.referencedBy.forEach((refererId) => {
nodeInfo.linkIds.push(this.formLinkId(refererId, note.id));
nodeInfo.neighbors.push(refererId);
});
}
nodeInfos[note.id] = nodeInfo;
});
const cache = this.cache || {};
cache.nodeInfos = nodeInfos;
cache.links = links;
cache.linkMap = linkMap;
this.cache = cache;
}
getNodeInfoById(id) {
return this.cache.nodeInfos[id];
}
getLinkById(id) {
return this.cache.linkMap.get(id);
}
/**
* A link's id is a combination of source node and target node's id
*/
formLinkId(sourceId, targetId) {
return `${sourceId}-${targetId}`;
}
toGraphViewData() {
const vm = {
graphData: {
nodes: this.notes,
links: this.cache.links,
},
nodeInfos: this.cache.nodeInfos,
};
return vm;
}
publishChange() {
this.subscribers.forEach((subscriber) => {
subscriber(this);
});
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
return () => {
const pos = this.subscribers.indexOf(subscriber);
if (pos > -1) {
this.subscribers.splice(pos, 1);
}
};
}
}
updateCache() {
const links = [];
const nodeInfos = {};
const linkMap = new Map();
this.notes.forEach(note => {
const nodeInfo = {
title: note.title,
linkIds: [],
neighbors: []
};
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
if (note.linkTo) {
note.linkTo.forEach(linkedNodeId => {
const link = {
id: this.formLinkId(note.id, linkedNodeId),
source: note.id,
target: linkedNodeId
};
links.push(link);
linkMap.set(link.id, link);
nodeInfo.linkIds.push(link.id);
nodeInfo.neighbors.push(linkedNodeId);
});
}
/**
* Throttle execution of a function. Especially useful for rate limiting
* execution of handlers on events like resize and scroll.
*
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) are most useful.
* @param {boolean} [noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds while the
* throttled-function is being called. If noTrailing is false or unspecified, callback will be executed one final time
* after the last throttled-function call. (After the throttled-function has not been called for `delay` milliseconds,
* the internal counter is reset).
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, as-is,
* to `callback` when the throttled-function is executed.
* @param {boolean} [debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is false (at end),
* schedule `callback` to execute after `delay` ms.
*
* @returns {Function} A new, throttled, function.
*/
function throttle (delay, noTrailing, callback, debounceMode) {
/*
* After wrapper has stopped being called, this timeout ensures that
* `callback` is executed at the proper times in `throttle` and `end`
* debounce modes.
*/
var timeoutID;
var cancelled = false; // Keep track of the last time `callback` was executed.
if (note.referencedBy) {
note.referencedBy.forEach(refererId => {
nodeInfo.linkIds.push(this.formLinkId(refererId, note.id));
nodeInfo.neighbors.push(refererId);
});
}
var lastExec = 0; // Function to clear existing timeout
nodeInfos[note.id] = nodeInfo;
});
const cache = this.cache || {};
cache.nodeInfos = nodeInfos;
cache.links = links;
cache.linkMap = linkMap;
this.cache = cache;
}
function clearExistingTimeout() {
if (timeoutID) {
clearTimeout(timeoutID);
}
} // Function to cancel next exec
getNodeInfoById(id) {
return this.cache.nodeInfos[id];
}
getLinkById(id) {
return this.cache.linkMap.get(id);
function cancel() {
clearExistingTimeout();
cancelled = true;
} // `noTrailing` defaults to falsy.
if (typeof noTrailing !== 'boolean') {
debounceMode = callback;
callback = noTrailing;
noTrailing = undefined;
}
/**
* A link's id is a combination of source node and target node's id
/*
* The `wrapper` function encapsulates all of the throttling / debouncing
* functionality and when executed will limit the rate at which `callback`
* is executed.
*/
formLinkId(sourceId, targetId) {
return `${sourceId}-${targetId}`;
}
function wrapper() {
for (var _len = arguments.length, arguments_ = new Array(_len), _key = 0; _key < _len; _key++) {
arguments_[_key] = arguments[_key];
}
toGraphViewData() {
const vm = {
graphData: {
nodes: this.notes,
links: this.cache.links
},
nodeInfos: this.cache.nodeInfos
};
return vm;
}
var self = this;
var elapsed = Date.now() - lastExec;
publishChange() {
this.subscribers.forEach(subscriber => {
subscriber(this);
});
}
if (cancelled) {
return;
} // Execute `callback` and update the `lastExec` timestamp.
subscribe(subscriber) {
this.subscribers.push(subscriber);
return () => {
const pos = this.subscribers.indexOf(subscriber);
if (pos > -1) {
this.subscribers.splice(pos, 1);
}
};
}
function exec() {
lastExec = Date.now();
callback.apply(self, arguments_);
}
/*
* If `debounceMode` is true (at begin) this is used to clear the flag
* to allow future `callback` executions.
*/
}
const mergeObjects = function (target) {
var sources = [].slice.call(arguments, 1);
function clear() {
timeoutID = undefined;
}
if (!sources.length) {
return target;
}
if (debounceMode && !timeoutID) {
/*
* Since `wrapper` is being called for the first time and
* `debounceMode` is true (at begin), execute `callback`.
*/
exec();
}
const source = sources.shift();
clearExistingTimeout();
if (source === undefined) {
return target;
if (debounceMode === undefined && elapsed > delay) {
/*
* In throttle mode, if `delay` time has been exceeded, execute
* `callback`.
*/
exec();
} else if (noTrailing !== true) {
/*
* In trailing throttle mode, since `delay` time has not been
* exceeded, schedule `callback` to execute `delay` ms after most
* recent execution.
*
* If `debounceMode` is true (at begin), schedule `clear` to execute
* after `delay` ms.
*
* If `debounceMode` is false (at end), schedule `callback` to
* execute after `delay` ms.
*/
timeoutID = setTimeout(debounceMode ? clear : exec, debounceMode === undefined ? delay - elapsed : delay);
}
}
if (isMergebleObject(target) && isMergebleObject(source)) {
Object.keys(source).forEach(function (key) {
if (isMergebleObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
wrapper.cancel = cancel; // Return the wrapper function.
mergeObjects(target[key], source[key]);
} else {
target[key] = source[key];
}
});
}
return wrapper;
}
return mergeObjects(target, ...sources);
};
/* eslint-disable no-undefined */
/**
* Debounce execution of a function. Debouncing, unlike throttling,
* guarantees that a function is only executed a single time, either at the
* very beginning of a series of calls, or at the very end.
*
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) are most useful.
* @param {boolean} [atBegin] - Optional, defaults to false. If atBegin is false or unspecified, callback will only be executed `delay` milliseconds
* after the last debounced-function call. If atBegin is true, callback will be executed only at the first debounced-function call.
* (After the throttled-function has not been called for `delay` milliseconds, the internal counter is reset).
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, as-is,
* to `callback` when the debounced-function is executed.
*
* @returns {Function} A new, debounced function.
*/
const isObject = item => {
return item !== null && typeof item === 'object';
};
function debounce (delay, atBegin, callback) {
return callback === undefined ? throttle(delay, atBegin, false) : throttle(delay, callback, atBegin !== false);
}
const isMergebleObject = item => {
return isObject(item) && !Array.isArray(item);
var debounce_1 = debounce;
const mergeObjects = (target, ...sources) => {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (source === undefined) {
return target;
}
if (isMergebleObject(target) && isMergebleObject(source)) {
Object.keys(source).forEach(function (key) {
if (isMergebleObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
mergeObjects(target[key], source[key]);
}
else {
target[key] = source[key];
}
});
}
return mergeObjects(target, ...sources);
};
const isObject = (item) => {
return item !== null && typeof item === 'object';
};
const isMergebleObject = (item) => {
return isObject(item) && !Array.isArray(item);
};
function getColorOnContainer(container, name, fallback) {
return getComputedStyle(container).getPropertyValue(name) || fallback;
return getComputedStyle(container).getPropertyValue(name) || fallback;
}
function getDefaultColorOf(opts = {}) {
const container = opts.container || document.body;
const highlightedForeground = getColorOnContainer(container, '--notegraph-highlighted-foreground-color', '#f9c74f');
return {
background: getColorOnContainer(container, `--notegraph-background`, '#f7f7f7'),
fontSize: parseInt(getColorOnContainer(container, `--notegraph-font-size`, 12)),
highlightedForeground,
node: {
note: {
regular: getColorOnContainer(container, '--notegraph-note-color-regular', '#5f76e7')
},
unknown: getColorOnContainer(container, '--notegraph-unkown-node-color', '#f94144')
const container = opts.container || document.body;
const highlightedForeground = getColorOnContainer(container, '--notegraph-highlighted-foreground-color', '#f9c74f');
return {
background: getColorOnContainer(container, `--notegraph-background`, '#f7f7f7'),
fontSize: parseInt(getColorOnContainer(container, `--notegraph-font-size`, 12)),
highlightedForeground,
node: {
note: {
regular: getColorOnContainer(container, '--notegraph-note-color-regular', '#5f76e7'),
},
unknown: getColorOnContainer(container, '--notegraph-unkown-node-color', '#f94144'),
},
link: {
regular: getColorOnContainer(container, '--notegraph-link-color-regular', '#ccc'),
highlighted: getColorOnContainer(container, '--notegraph-link-color-highlighted', highlightedForeground),
},
hoverNodeLink: {
highlightedDirection: {
inbound: '#3078cd',
outbound: highlightedForeground,
},
},
};
}
const makeDrawWrapper = (ctx) => ({
circle: function (x, y, radius, color) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
return this;
},
link: {
regular: getColorOnContainer(container, '--notegraph-link-color-regular', '#ccc'),
highlighted: getColorOnContainer(container, '--notegraph-link-color-highlighted', highlightedForeground)
text: function (text, x, y, size, color) {
ctx.font = `${size}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = color;
ctx.fillText(text, x, y);
return this;
},
hoverNodeLink: {
highlightedDirection: {
inbound: '#3078cd',
outbound: highlightedForeground
}
}
};
}
const makeDrawWrapper = ctx => ({
circle: function (x, y, radius, color) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
return this;
},
text: function (text, x, y, size, color) {
ctx.font = `${size}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = color;
ctx.fillText(text, x, y);
return this;
}
});

@@ -194,409 +314,368 @@ /**

*/
class NoteGraphView {
constructor(opts) {
this.sizeScaler = scaleLinear().domain([0, 20]).range([1, 5]).clamp(true);
this.labelAlphaScaler = scaleLinear().domain([1.2, 2]).range([0, 1]).clamp(true);
this.interactionCallbacks = {};
this.hasInitialZoomToFit = false;
this.actions = {
selectNode(model, nodeId, isAppend) {
if (!isAppend) {
model.selectedNodes.clear();
constructor(opts) {
this.sizeScaler = scaleLinear()
.domain([0, 20])
.range([1, 5])
.clamp(true);
this.labelAlphaScaler = scaleLinear()
.domain([1.2, 2])
.range([0, 1])
.clamp(true);
this.interactionCallbacks = {};
this.hasInitialZoomToFit = false;
this.actions = {
selectNode(model, nodeId, isAppend) {
if (!isAppend) {
model.selectedNodes.clear();
}
if (nodeId != null) {
model.selectedNodes.add(nodeId);
}
},
highlightNode(model, nodeId) {
model.hoverNode = nodeId;
},
};
this.shouldDebugColor = false;
this.options = opts;
this.container = opts.container;
this.model = {
graphData: {
nodes: [],
links: [],
},
nodeInfos: {},
selectedNodes: new Set(),
focusNodes: new Set(),
focusLinks: new Set(),
hoverNode: null,
};
this.initStyle();
if (opts.graphModel) {
this.linkWithGraphModel(opts.graphModel);
if (!opts.lazyInitView) {
this.initView();
}
}
if (nodeId != null) {
model.selectedNodes.add(nodeId);
}
initStyle() {
if (!this.style) {
this.style = getDefaultColorOf({ container: this.container });
}
},
highlightNode(model, nodeId) {
model.hoverNode = nodeId;
}
};
this.shouldDebugColor = false;
this.options = opts;
this.container = opts.container;
this.model = {
graphData: {
nodes: [],
links: []
},
nodeInfos: {},
selectedNodes: new Set(),
focusNodes: new Set(),
focusLinks: new Set(),
hoverNode: null
};
this.initStyle();
if (opts.graphModel) {
this.linkWithGraphModel(opts.graphModel);
if (!opts.lazyInitView) {
this.initView();
}
mergeObjects(this.style, this.options.style);
}
}
initStyle() {
if (!this.style) {
this.style = getDefaultColorOf({
container: this.container
});
updateStyle(style) {
this.options.style = mergeObjects(this.options.style || {}, style);
this.initStyle();
this.refreshByStyle();
}
mergeObjects(this.style, this.options.style);
}
updateStyle(style) {
this.options.style = mergeObjects(this.options.style || {}, style);
this.initStyle();
this.refreshByStyle();
}
refreshByStyle() {
if (!this.forceGraph) return;
const getNodeColor = (nodeId, model) => {
const info = model.nodeInfos[nodeId];
const noteStyle = this.style.node.note;
const typeFill = this.style.node.note[info.type || 'regular'] || this.style.node.unknown;
if (this.shouldDebugColor) {
console.log('node fill', typeFill);
}
switch (this.getNodeState(nodeId, model)) {
case 'regular':
return {
fill: typeFill,
border: typeFill
};
case 'lessened':
let color = noteStyle.lessened;
if (!color) {
const c = hsl(typeFill);
c.opacity = 0.2;
color = c;
}
return {
fill: color,
border: color
};
case 'highlighted':
return {
fill: typeFill,
border: this.style.highlightedForeground
};
default:
throw new Error(`Unknown type for node ${nodeId}`);
}
};
this.forceGraph.backgroundColor(this.style.background).nodeCanvasObject((node, ctx, globalScale) => {
if (!node.id) return;
const info = this.model.nodeInfos[node.id];
const size = this.sizeScaler(info.neighbors ? info.neighbors.length : 1);
const {
fill,
border
} = getNodeColor(node.id, this.model);
const fontSize = this.style.fontSize / globalScale;
let textColor = rgb(fill);
const nodeState = this.getNodeState(node.id, this.model);
const alphaByDistance = this.labelAlphaScaler(globalScale);
textColor.opacity = nodeState === 'highlighted' ? 1 : nodeState === 'lessened' ? Math.min(0.2, alphaByDistance) : alphaByDistance;
const label = info.title;
makeDrawWrapper(ctx).circle(node.x, node.y, size + 0.5, border).circle(node.x, node.y, size, fill).text(label, node.x, node.y + size + 1, fontSize, textColor);
}).linkColor(link => {
return this.getLinkColor(link, this.model);
});
}
linkWithGraphModel(graphModel) {
if (this.currentDataModelEntry) {
this.currentDataModelEntry.unsub();
refreshByStyle() {
if (!this.forceGraph)
return;
const getNodeColor = (nodeId, model) => {
const info = model.nodeInfos[nodeId];
const noteStyle = this.style.node.note;
const typeFill = this.style.node.note[info.type || 'regular'] || this.style.node.unknown;
if (this.shouldDebugColor) {
console.log('node fill', typeFill);
}
switch (this.getNodeState(nodeId, model)) {
case 'regular':
return { fill: typeFill, border: typeFill };
case 'lessened':
let color = noteStyle.lessened;
if (!color) {
const c = hsl(typeFill);
c.opacity = 0.2;
color = c;
}
return { fill: color, border: color };
case 'highlighted':
return {
fill: typeFill,
border: this.style.highlightedForeground,
};
default:
throw new Error(`Unknown type for node ${nodeId}`);
}
};
this.forceGraph
.backgroundColor(this.style.background)
.nodeCanvasObject((node, ctx, globalScale) => {
if (!node.id)
return;
const info = this.model.nodeInfos[node.id];
const size = this.sizeScaler(info.neighbors ? info.neighbors.length : 1);
const { fill, border } = getNodeColor(node.id, this.model);
const fontSize = this.style.fontSize / globalScale;
let textColor = rgb(fill);
const nodeState = this.getNodeState(node.id, this.model);
const alphaByDistance = this.labelAlphaScaler(globalScale);
textColor.opacity =
nodeState === 'highlighted'
? 1
: nodeState === 'lessened'
? Math.min(0.2, alphaByDistance)
: alphaByDistance;
const label = info.title;
makeDrawWrapper(ctx)
.circle(node.x, node.y, size + 0.5, border)
.circle(node.x, node.y, size, fill)
.text(label, node.x, node.y + size + 1, fontSize, textColor);
})
.linkColor((link) => {
return this.getLinkColor(link, this.model);
});
}
this.updateViewData(graphModel.toGraphViewData());
const unsub = graphModel.subscribe(() => {
this.updateViewData(graphModel.toGraphViewData());
});
this.currentDataModelEntry = {
graphModel,
unsub
};
}
getColorOnContainer(name, fallback) {
return getComputedStyle(this.container).getPropertyValue(name) || fallback;
}
updateViewData(dataInput) {
Object.assign(this.model, dataInput);
if (dataInput.focusedNode) {
this.model.hoverNode = dataInput.focusedNode;
linkWithGraphModel(graphModel) {
if (this.currentDataModelEntry) {
this.currentDataModelEntry.unsub();
}
this.updateViewData(graphModel.toGraphViewData());
const unsub = graphModel.subscribe(() => {
this.updateViewData(graphModel.toGraphViewData());
});
this.currentDataModelEntry = {
graphModel,
unsub,
};
}
}
updateCanvasSize(size) {
if (!this.forceGraph) return;
if ('width' in size) {
this.forceGraph.width(size.width);
getColorOnContainer(name, fallback) {
return getComputedStyle(this.container).getPropertyValue(name) || fallback;
}
if ('height' in size) {
this.forceGraph.height(size.height);
updateViewData(dataInput) {
Object.assign(this.model, dataInput);
if (dataInput.focusedNode) {
this.model.hoverNode = dataInput.focusedNode;
}
}
}
initView() {
const {
options,
model,
style,
actions
} = this; // this runtime dependency may not be ready when this umd file excutes,
// so we will retrieve it from the global scope
const forceGraphFactory = ForceGraph || globalThis.ForceGraph;
const forceGraph = this.forceGraph || forceGraphFactory();
const width = options.width || window.innerWidth - this.container.offsetLeft - 20;
const height = options.height || window.innerHeight - this.container.offsetTop - 20; // const randomId = Math.floor(Math.random() * 1000)
// console.log('initView', randomId)
forceGraph(this.container).height(height).width(width).graphData(model.graphData).linkHoverPrecision(8).enableNodeDrag(!!options.enableNodeDrag).cooldownTime(200).d3Force('x', forceX()).d3Force('y', forceY()).d3Force('collide', forceCollide(forceGraph.nodeRelSize())).linkWidth(1).linkDirectionalParticles(1).linkDirectionalParticleWidth(link => this.getLinkState(link, model) === 'highlighted' ? 2 : 0).onEngineStop(() => {
if (!this.hasInitialZoomToFit) {
this.hasInitialZoomToFit = true;
forceGraph.zoomToFit(1000, 20);
}
}).onNodeHover(node => {
actions.highlightNode(this.model, node == null ? void 0 : node.id);
this.updateViewModeInteractiveState();
}).onNodeClick((node, event) => {
actions.selectNode(this.model, node.id, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('nodeClick', {
node,
event
});
}).onLinkClick((link, event) => {
this.fireInteraction('linkClick', {
link,
event
});
}).onBackgroundClick(event => {
actions.selectNode(this.model, null, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('backgroundClick', {
event
});
}).onBackgroundRightClick(event => {
forceGraph.zoomToFit(1000, 20);
this.fireInteraction('backgroundRightClick', {
event
});
});
if (options.enableSmartZooming !== false) {
this.initGraphSmartZooming(forceGraph);
updateCanvasSize(size) {
if (!this.forceGraph)
return;
if ('width' in size) {
this.forceGraph.width(size.width);
}
if ('height' in size) {
this.forceGraph.height(size.height);
}
}
this.forceGraph = forceGraph;
this.refreshByStyle();
}
initGraphSmartZooming(forceGraph) {
let isAdjustingZoom = false;
const debouncedZoomHandler = debounce(200, event => {
if (isAdjustingZoom) return;
const {
x: xb,
y: yb
} = this.forceGraph.getGraphBbox(); // x/y here is translate, k is scale
const {
k,
x,
y
} = event;
const scaledBoundL = k * xb[0];
const scaledBoundR = k * xb[1];
const scaledBoundT = k * yb[0];
const scaledBoundB = k * yb[1];
const graphCanvasW = this.forceGraph.width();
const graphCanvasH = this.forceGraph.height();
const oldCenter = this.forceGraph.centerAt();
const currentCenter = oldCenter; // TODO: this is more like the center before zoom, rather than current zooming one ?
let newCenterX;
let newCenterY; // should calculate proper center (because that's force-graph's only method...) to make the viewport fit the graphBbox
if (scaledBoundR + x < 0) {
// console.log('is out of right')
isAdjustingZoom = false;
newCenterX = xb[1];
} else if (scaledBoundL + x > graphCanvasW) {
// console.log('is out of left')
newCenterX = xb[0];
}
if (scaledBoundT + y > graphCanvasH) {
// is out of top
newCenterY = yb[0];
} else if (scaledBoundB + y < 0) {
// console.log('is out of bottom')
newCenterY = yb[1];
}
if (typeof newCenterX === 'number' || typeof newCenterY === 'number') {
// console.log('new centerX', newCenterX, newCenterY, 'old center', oldCenter)
this.forceGraph.centerAt(newCenterX !== undefined ? newCenterX : currentCenter.x, newCenterY !== undefined ? newCenterY : currentCenter.y, 2000);
}
});
forceGraph.onZoom(event => {
if (!this.hasInitialZoomToFit) return;
debouncedZoomHandler(event);
}).onZoomEnd(() => {
setTimeout(() => {
isAdjustingZoom = false;
}, 20);
});
}
getLinkNodeId(v) {
const t = typeof v;
return t === 'string' || t === 'number' ? v : v.id;
}
getNodeState(nodeId, model = this.model) {
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId ? 'highlighted' : model.focusNodes.size === 0 ? 'regular' : model.focusNodes.has(nodeId) ? 'regular' : 'lessened';
}
getLinkState(link, model = this.model) {
return model.focusNodes.size === 0 ? 'regular' : model.focusLinks.has(link.id) ? 'highlighted' : 'lessened';
}
getLinkColor(link, model) {
const style = this.style;
const linkStyle = style.link;
switch (this.getLinkState(link, model)) {
case 'regular':
return linkStyle.regular;
case 'highlighted':
// inbound/outbound link is a little bit different with hoverNode
let linkColorByDirection;
const hoverNodeLinkStyle = style.hoverNodeLink;
if (model.hoverNode === this.getLinkNodeId(link.source)) {
var _hoverNodeLinkStyle$h;
linkColorByDirection = (_hoverNodeLinkStyle$h = hoverNodeLinkStyle.highlightedDirection) == null ? void 0 : _hoverNodeLinkStyle$h.outbound;
} else if (model.hoverNode === this.getLinkNodeId(link.target)) {
var _hoverNodeLinkStyle$h2;
linkColorByDirection = (_hoverNodeLinkStyle$h2 = hoverNodeLinkStyle.highlightedDirection) == null ? void 0 : _hoverNodeLinkStyle$h2.inbound;
initView() {
const { options, model, style, actions } = this;
// this runtime dependency may not be ready when this umd file excutes,
// so we will retrieve it from the global scope
const forceGraphFactory = ForceGraph || globalThis.ForceGraph;
const forceGraph = this.forceGraph || forceGraphFactory();
const width = options.width || window.innerWidth - this.container.offsetLeft - 20;
const height = options.height || window.innerHeight - this.container.offsetTop - 20;
// const randomId = Math.floor(Math.random() * 1000)
// console.log('initView', randomId)
forceGraph(this.container)
.height(height)
.width(width)
.graphData(model.graphData)
.linkHoverPrecision(8)
.enableNodeDrag(!!options.enableNodeDrag)
.cooldownTime(200)
.d3Force('x', forceX())
.d3Force('y', forceY())
.d3Force('collide', forceCollide(forceGraph.nodeRelSize()))
.linkWidth(1)
.linkDirectionalParticles(1)
.linkDirectionalParticleWidth((link) => this.getLinkState(link, model) === 'highlighted' ? 2 : 0)
.onEngineStop(() => {
if (!this.hasInitialZoomToFit) {
this.hasInitialZoomToFit = true;
forceGraph.zoomToFit(1000, 20);
}
})
.onNodeHover((node) => {
actions.highlightNode(this.model, node?.id);
this.updateViewModeInteractiveState();
})
.onNodeClick((node, event) => {
actions.selectNode(this.model, node.id, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('nodeClick', { node, event });
})
.onLinkClick((link, event) => {
this.fireInteraction('linkClick', { link, event });
})
.onBackgroundClick((event) => {
actions.selectNode(this.model, null, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('backgroundClick', { event });
})
.onBackgroundRightClick((event) => {
forceGraph.zoomToFit(1000, 20);
this.fireInteraction('backgroundRightClick', { event });
});
if (options.enableSmartZooming !== false) {
this.initGraphSmartZooming(forceGraph);
}
return linkColorByDirection || linkStyle.highlighted || style.highlightedForeground;
case 'lessened':
let color = linkStyle.lessened;
if (!color) {
const c = hsl(style.node.note.lessened);
c.opacity = 0.2;
color = c;
this.forceGraph = forceGraph;
this.refreshByStyle();
}
initGraphSmartZooming(forceGraph) {
let isAdjustingZoom = false;
const debouncedZoomHandler = debounce_1(200, (event) => {
if (isAdjustingZoom)
return;
const { x: xb, y: yb } = this.forceGraph.getGraphBbox();
// x/y here is translate, k is scale
const { k, x, y } = event;
const scaledBoundL = k * xb[0];
const scaledBoundR = k * xb[1];
const scaledBoundT = k * yb[0];
const scaledBoundB = k * yb[1];
const graphCanvasW = this.forceGraph.width();
const graphCanvasH = this.forceGraph.height();
const oldCenter = this.forceGraph.centerAt();
const currentCenter = oldCenter; // TODO: this is more like the center before zoom, rather than current zooming one ?
let newCenterX;
let newCenterY;
// should calculate proper center (because that's force-graph's only method...) to make the viewport fit the graphBbox
if (scaledBoundR + x < 0) {
// console.log('is out of right')
isAdjustingZoom = false;
newCenterX = xb[1];
}
else if (scaledBoundL + x > graphCanvasW) {
// console.log('is out of left')
newCenterX = xb[0];
}
if (scaledBoundT + y > graphCanvasH) {
// is out of top
newCenterY = yb[0];
}
else if (scaledBoundB + y < 0) {
// console.log('is out of bottom')
newCenterY = yb[1];
}
if (typeof newCenterX === 'number' || typeof newCenterY === 'number') {
// console.log('new centerX', newCenterX, newCenterY, 'old center', oldCenter)
this.forceGraph.centerAt(newCenterX !== undefined ? newCenterX : currentCenter.x, newCenterY !== undefined ? newCenterY : currentCenter.y, 2000);
}
});
forceGraph
.onZoom((event) => {
if (!this.hasInitialZoomToFit)
return;
debouncedZoomHandler(event);
})
.onZoomEnd(() => {
setTimeout(() => {
isAdjustingZoom = false;
}, 20);
});
}
getLinkNodeId(v) {
const t = typeof v;
return t === 'string' || t === 'number' ? v : v.id;
}
getNodeState(nodeId, model = this.model) {
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId
? 'highlighted'
: model.focusNodes.size === 0
? 'regular'
: model.focusNodes.has(nodeId)
? 'regular'
: 'lessened';
}
getLinkState(link, model = this.model) {
return model.focusNodes.size === 0
? 'regular'
: model.focusLinks.has(link.id)
? 'highlighted'
: 'lessened';
}
getLinkColor(link, model) {
const style = this.style;
const linkStyle = style.link;
switch (this.getLinkState(link, model)) {
case 'regular':
return linkStyle.regular;
case 'highlighted':
// inbound/outbound link is a little bit different with hoverNode
let linkColorByDirection;
const hoverNodeLinkStyle = style.hoverNodeLink;
if (model.hoverNode === this.getLinkNodeId(link.source)) {
linkColorByDirection =
hoverNodeLinkStyle.highlightedDirection?.outbound;
}
else if (model.hoverNode === this.getLinkNodeId(link.target)) {
linkColorByDirection =
hoverNodeLinkStyle.highlightedDirection?.inbound;
}
return (linkColorByDirection ||
linkStyle.highlighted ||
style.highlightedForeground);
case 'lessened':
let color = linkStyle.lessened;
if (!color) {
const c = hsl(style.node.note.lessened);
c.opacity = 0.2;
color = c;
}
return color;
default:
throw new Error(`Unknown type for link ${link}`);
}
return color;
default:
throw new Error(`Unknown type for link ${link}`);
}
}
updateViewModeInteractiveState() {
const {
model
} = this; // compute highlighted elements
const focusNodes = new Set();
const focusLinks = new Set();
if (model.hoverNode) {
var _info$neighbors, _info$linkIds;
focusNodes.add(model.hoverNode);
const info = model.nodeInfos[model.hoverNode];
(_info$neighbors = info.neighbors) == null ? void 0 : _info$neighbors.forEach(neighborId => focusNodes.add(neighborId));
(_info$linkIds = info.linkIds) == null ? void 0 : _info$linkIds.forEach(link => focusLinks.add(link));
updateViewModeInteractiveState() {
const { model } = this;
// compute highlighted elements
const focusNodes = new Set();
const focusLinks = new Set();
if (model.hoverNode) {
focusNodes.add(model.hoverNode);
const info = model.nodeInfos[model.hoverNode];
info.neighbors?.forEach((neighborId) => focusNodes.add(neighborId));
info.linkIds?.forEach((link) => focusLinks.add(link));
}
if (model.selectedNodes) {
model.selectedNodes.forEach((nodeId) => {
focusNodes.add(nodeId);
const info = model.nodeInfos[nodeId];
info.neighbors?.forEach((neighborId) => focusNodes.add(neighborId));
info.linkIds?.forEach((link) => focusLinks.add(link));
});
}
model.focusNodes = focusNodes;
model.focusLinks = focusLinks;
}
if (model.selectedNodes) {
model.selectedNodes.forEach(nodeId => {
var _info$neighbors2, _info$linkIds2;
focusNodes.add(nodeId);
const info = model.nodeInfos[nodeId];
(_info$neighbors2 = info.neighbors) == null ? void 0 : _info$neighbors2.forEach(neighborId => focusNodes.add(neighborId));
(_info$linkIds2 = info.linkIds) == null ? void 0 : _info$linkIds2.forEach(link => focusLinks.add(link));
});
/**
* Select nodes to gain more initial attention
*/
setSelectedNodes(nodeIds, isAppend = false) {
if (!isAppend)
this.model.selectedNodes.clear();
nodeIds.forEach(nodeId => this.actions.selectNode(this.model, nodeId, true));
this.updateViewModeInteractiveState();
}
model.focusNodes = focusNodes;
model.focusLinks = focusLinks;
}
/**
* Select nodes to gain more initial attention
*/
setSelectedNodes(nodeIds, isAppend = false) {
if (!isAppend) this.model.selectedNodes.clear();
nodeIds.forEach(nodeId => this.actions.selectNode(this.model, nodeId, true));
this.updateViewModeInteractiveState();
}
onInteraction(name, cb) {
if (!this.interactionCallbacks[name]) this.interactionCallbacks[name] = [];
const callbackList = this.interactionCallbacks[name];
callbackList.push(cb);
return () => {
const pos = callbackList.indexOf(cb);
if (pos > -1) {
callbackList.splice(pos, 1);
}
};
}
fireInteraction(name, payload) {
const callbackList = this.interactionCallbacks[name];
if (callbackList) {
callbackList.forEach(cb => cb(payload));
onInteraction(name, cb) {
if (!this.interactionCallbacks[name])
this.interactionCallbacks[name] = [];
const callbackList = this.interactionCallbacks[name];
callbackList.push(cb);
return () => {
const pos = callbackList.indexOf(cb);
if (pos > -1) {
callbackList.splice(pos, 1);
}
};
}
}
dispose() {
if (this.forceGraph) {
this.forceGraph.pauseAnimation();
fireInteraction(name, payload) {
const callbackList = this.interactionCallbacks[name];
if (callbackList) {
callbackList.forEach((cb) => cb(payload));
}
}
}
dispose() {
if (this.forceGraph) {
this.forceGraph.pauseAnimation();
}
}
}
export { NoteGraphModel, NoteGraphView, getColorOnContainer, getDefaultColorOf };

@@ -1,9 +0,14 @@

function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
var d3Color = require('d3-color');
var d3Force = require('d3-force');
var d3Scale = require('d3-scale');
var throttleDebounce = require('throttle-debounce');
var ForceGraph = _interopDefault(require('force-graph'));
var ForceGraph = require('force-graph');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var ForceGraph__default = /*#__PURE__*/_interopDefaultLegacy(ForceGraph);
/**

@@ -13,179 +18,300 @@ * Can generate GraphViewModel by `toGraphViewModel`

class NoteGraphModel {
constructor(notes) {
this.subscribers = [];
this.notes = notes;
this.updateCache();
}
constructor(notes) {
this.subscribers = [];
this.notes = notes;
this.updateCache();
}
updateCache() {
const nodes = [];
const links = [];
const nodeInfos = {};
const linkMap = new Map();
this.notes.forEach((note) => {
nodes.push({ id: note.id, data: { note } });
const nodeInfo = {
title: note.title,
linkIds: [],
neighbors: [],
};
if (note.linkTo) {
note.linkTo.forEach((linkedNodeId) => {
const link = {
id: this.formLinkId(note.id, linkedNodeId),
source: note.id,
target: linkedNodeId,
};
links.push(link);
linkMap.set(link.id, link);
nodeInfo.linkIds.push(link.id);
nodeInfo.neighbors.push(linkedNodeId);
});
}
if (note.referencedBy) {
note.referencedBy.forEach((refererId) => {
nodeInfo.linkIds.push(this.formLinkId(refererId, note.id));
nodeInfo.neighbors.push(refererId);
});
}
nodeInfos[note.id] = nodeInfo;
});
const cache = this.cache || {};
cache.nodeInfos = nodeInfos;
cache.links = links;
cache.linkMap = linkMap;
this.cache = cache;
}
getNodeInfoById(id) {
return this.cache.nodeInfos[id];
}
getLinkById(id) {
return this.cache.linkMap.get(id);
}
/**
* A link's id is a combination of source node and target node's id
*/
formLinkId(sourceId, targetId) {
return `${sourceId}-${targetId}`;
}
toGraphViewData() {
const vm = {
graphData: {
nodes: this.notes,
links: this.cache.links,
},
nodeInfos: this.cache.nodeInfos,
};
return vm;
}
publishChange() {
this.subscribers.forEach((subscriber) => {
subscriber(this);
});
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
return () => {
const pos = this.subscribers.indexOf(subscriber);
if (pos > -1) {
this.subscribers.splice(pos, 1);
}
};
}
}
updateCache() {
const links = [];
const nodeInfos = {};
const linkMap = new Map();
this.notes.forEach(note => {
const nodeInfo = {
title: note.title,
linkIds: [],
neighbors: []
};
/* eslint-disable no-undefined,no-param-reassign,no-shadow */
if (note.linkTo) {
note.linkTo.forEach(linkedNodeId => {
const link = {
id: this.formLinkId(note.id, linkedNodeId),
source: note.id,
target: linkedNodeId
};
links.push(link);
linkMap.set(link.id, link);
nodeInfo.linkIds.push(link.id);
nodeInfo.neighbors.push(linkedNodeId);
});
}
/**
* Throttle execution of a function. Especially useful for rate limiting
* execution of handlers on events like resize and scroll.
*
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) are most useful.
* @param {boolean} [noTrailing] - Optional, defaults to false. If noTrailing is true, callback will only execute every `delay` milliseconds while the
* throttled-function is being called. If noTrailing is false or unspecified, callback will be executed one final time
* after the last throttled-function call. (After the throttled-function has not been called for `delay` milliseconds,
* the internal counter is reset).
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, as-is,
* to `callback` when the throttled-function is executed.
* @param {boolean} [debounceMode] - If `debounceMode` is true (at begin), schedule `clear` to execute after `delay` ms. If `debounceMode` is false (at end),
* schedule `callback` to execute after `delay` ms.
*
* @returns {Function} A new, throttled, function.
*/
function throttle (delay, noTrailing, callback, debounceMode) {
/*
* After wrapper has stopped being called, this timeout ensures that
* `callback` is executed at the proper times in `throttle` and `end`
* debounce modes.
*/
var timeoutID;
var cancelled = false; // Keep track of the last time `callback` was executed.
if (note.referencedBy) {
note.referencedBy.forEach(refererId => {
nodeInfo.linkIds.push(this.formLinkId(refererId, note.id));
nodeInfo.neighbors.push(refererId);
});
}
var lastExec = 0; // Function to clear existing timeout
nodeInfos[note.id] = nodeInfo;
});
const cache = this.cache || {};
cache.nodeInfos = nodeInfos;
cache.links = links;
cache.linkMap = linkMap;
this.cache = cache;
}
function clearExistingTimeout() {
if (timeoutID) {
clearTimeout(timeoutID);
}
} // Function to cancel next exec
getNodeInfoById(id) {
return this.cache.nodeInfos[id];
}
getLinkById(id) {
return this.cache.linkMap.get(id);
function cancel() {
clearExistingTimeout();
cancelled = true;
} // `noTrailing` defaults to falsy.
if (typeof noTrailing !== 'boolean') {
debounceMode = callback;
callback = noTrailing;
noTrailing = undefined;
}
/**
* A link's id is a combination of source node and target node's id
/*
* The `wrapper` function encapsulates all of the throttling / debouncing
* functionality and when executed will limit the rate at which `callback`
* is executed.
*/
formLinkId(sourceId, targetId) {
return `${sourceId}-${targetId}`;
}
function wrapper() {
for (var _len = arguments.length, arguments_ = new Array(_len), _key = 0; _key < _len; _key++) {
arguments_[_key] = arguments[_key];
}
toGraphViewData() {
const vm = {
graphData: {
nodes: this.notes,
links: this.cache.links
},
nodeInfos: this.cache.nodeInfos
};
return vm;
}
var self = this;
var elapsed = Date.now() - lastExec;
publishChange() {
this.subscribers.forEach(subscriber => {
subscriber(this);
});
}
if (cancelled) {
return;
} // Execute `callback` and update the `lastExec` timestamp.
subscribe(subscriber) {
this.subscribers.push(subscriber);
return () => {
const pos = this.subscribers.indexOf(subscriber);
if (pos > -1) {
this.subscribers.splice(pos, 1);
}
};
}
function exec() {
lastExec = Date.now();
callback.apply(self, arguments_);
}
/*
* If `debounceMode` is true (at begin) this is used to clear the flag
* to allow future `callback` executions.
*/
}
const mergeObjects = function (target) {
var sources = [].slice.call(arguments, 1);
function clear() {
timeoutID = undefined;
}
if (!sources.length) {
return target;
}
if (debounceMode && !timeoutID) {
/*
* Since `wrapper` is being called for the first time and
* `debounceMode` is true (at begin), execute `callback`.
*/
exec();
}
const source = sources.shift();
clearExistingTimeout();
if (source === undefined) {
return target;
if (debounceMode === undefined && elapsed > delay) {
/*
* In throttle mode, if `delay` time has been exceeded, execute
* `callback`.
*/
exec();
} else if (noTrailing !== true) {
/*
* In trailing throttle mode, since `delay` time has not been
* exceeded, schedule `callback` to execute `delay` ms after most
* recent execution.
*
* If `debounceMode` is true (at begin), schedule `clear` to execute
* after `delay` ms.
*
* If `debounceMode` is false (at end), schedule `callback` to
* execute after `delay` ms.
*/
timeoutID = setTimeout(debounceMode ? clear : exec, debounceMode === undefined ? delay - elapsed : delay);
}
}
if (isMergebleObject(target) && isMergebleObject(source)) {
Object.keys(source).forEach(function (key) {
if (isMergebleObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
wrapper.cancel = cancel; // Return the wrapper function.
mergeObjects(target[key], source[key]);
} else {
target[key] = source[key];
}
});
}
return wrapper;
}
return mergeObjects(target, ...sources);
};
/* eslint-disable no-undefined */
/**
* Debounce execution of a function. Debouncing, unlike throttling,
* guarantees that a function is only executed a single time, either at the
* very beginning of a series of calls, or at the very end.
*
* @param {number} delay - A zero-or-greater delay in milliseconds. For event callbacks, values around 100 or 250 (or even higher) are most useful.
* @param {boolean} [atBegin] - Optional, defaults to false. If atBegin is false or unspecified, callback will only be executed `delay` milliseconds
* after the last debounced-function call. If atBegin is true, callback will be executed only at the first debounced-function call.
* (After the throttled-function has not been called for `delay` milliseconds, the internal counter is reset).
* @param {Function} callback - A function to be executed after delay milliseconds. The `this` context and all arguments are passed through, as-is,
* to `callback` when the debounced-function is executed.
*
* @returns {Function} A new, debounced function.
*/
const isObject = item => {
return item !== null && typeof item === 'object';
};
function debounce (delay, atBegin, callback) {
return callback === undefined ? throttle(delay, atBegin, false) : throttle(delay, callback, atBegin !== false);
}
const isMergebleObject = item => {
return isObject(item) && !Array.isArray(item);
var debounce_1 = debounce;
const mergeObjects = (target, ...sources) => {
if (!sources.length) {
return target;
}
const source = sources.shift();
if (source === undefined) {
return target;
}
if (isMergebleObject(target) && isMergebleObject(source)) {
Object.keys(source).forEach(function (key) {
if (isMergebleObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
mergeObjects(target[key], source[key]);
}
else {
target[key] = source[key];
}
});
}
return mergeObjects(target, ...sources);
};
const isObject = (item) => {
return item !== null && typeof item === 'object';
};
const isMergebleObject = (item) => {
return isObject(item) && !Array.isArray(item);
};
function getColorOnContainer(container, name, fallback) {
return getComputedStyle(container).getPropertyValue(name) || fallback;
return getComputedStyle(container).getPropertyValue(name) || fallback;
}
function getDefaultColorOf(opts = {}) {
const container = opts.container || document.body;
const highlightedForeground = getColorOnContainer(container, '--notegraph-highlighted-foreground-color', '#f9c74f');
return {
background: getColorOnContainer(container, `--notegraph-background`, '#f7f7f7'),
fontSize: parseInt(getColorOnContainer(container, `--notegraph-font-size`, 12)),
highlightedForeground,
node: {
note: {
regular: getColorOnContainer(container, '--notegraph-note-color-regular', '#5f76e7')
},
unknown: getColorOnContainer(container, '--notegraph-unkown-node-color', '#f94144')
const container = opts.container || document.body;
const highlightedForeground = getColorOnContainer(container, '--notegraph-highlighted-foreground-color', '#f9c74f');
return {
background: getColorOnContainer(container, `--notegraph-background`, '#f7f7f7'),
fontSize: parseInt(getColorOnContainer(container, `--notegraph-font-size`, 12)),
highlightedForeground,
node: {
note: {
regular: getColorOnContainer(container, '--notegraph-note-color-regular', '#5f76e7'),
},
unknown: getColorOnContainer(container, '--notegraph-unkown-node-color', '#f94144'),
},
link: {
regular: getColorOnContainer(container, '--notegraph-link-color-regular', '#ccc'),
highlighted: getColorOnContainer(container, '--notegraph-link-color-highlighted', highlightedForeground),
},
hoverNodeLink: {
highlightedDirection: {
inbound: '#3078cd',
outbound: highlightedForeground,
},
},
};
}
const makeDrawWrapper = (ctx) => ({
circle: function (x, y, radius, color) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
return this;
},
link: {
regular: getColorOnContainer(container, '--notegraph-link-color-regular', '#ccc'),
highlighted: getColorOnContainer(container, '--notegraph-link-color-highlighted', highlightedForeground)
text: function (text, x, y, size, color) {
ctx.font = `${size}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = color;
ctx.fillText(text, x, y);
return this;
},
hoverNodeLink: {
highlightedDirection: {
inbound: '#3078cd',
outbound: highlightedForeground
}
}
};
}
const makeDrawWrapper = ctx => ({
circle: function (x, y, radius, color) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
return this;
},
text: function (text, x, y, size, color) {
ctx.font = `${size}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = color;
ctx.fillText(text, x, y);
return this;
}
});

@@ -196,407 +322,366 @@ /**

*/
class NoteGraphView {
constructor(opts) {
this.sizeScaler = d3Scale.scaleLinear().domain([0, 20]).range([1, 5]).clamp(true);
this.labelAlphaScaler = d3Scale.scaleLinear().domain([1.2, 2]).range([0, 1]).clamp(true);
this.interactionCallbacks = {};
this.hasInitialZoomToFit = false;
this.actions = {
selectNode(model, nodeId, isAppend) {
if (!isAppend) {
model.selectedNodes.clear();
constructor(opts) {
this.sizeScaler = d3Scale.scaleLinear()
.domain([0, 20])
.range([1, 5])
.clamp(true);
this.labelAlphaScaler = d3Scale.scaleLinear()
.domain([1.2, 2])
.range([0, 1])
.clamp(true);
this.interactionCallbacks = {};
this.hasInitialZoomToFit = false;
this.actions = {
selectNode(model, nodeId, isAppend) {
if (!isAppend) {
model.selectedNodes.clear();
}
if (nodeId != null) {
model.selectedNodes.add(nodeId);
}
},
highlightNode(model, nodeId) {
model.hoverNode = nodeId;
},
};
this.shouldDebugColor = false;
this.options = opts;
this.container = opts.container;
this.model = {
graphData: {
nodes: [],
links: [],
},
nodeInfos: {},
selectedNodes: new Set(),
focusNodes: new Set(),
focusLinks: new Set(),
hoverNode: null,
};
this.initStyle();
if (opts.graphModel) {
this.linkWithGraphModel(opts.graphModel);
if (!opts.lazyInitView) {
this.initView();
}
}
if (nodeId != null) {
model.selectedNodes.add(nodeId);
}
initStyle() {
if (!this.style) {
this.style = getDefaultColorOf({ container: this.container });
}
},
highlightNode(model, nodeId) {
model.hoverNode = nodeId;
}
};
this.shouldDebugColor = false;
this.options = opts;
this.container = opts.container;
this.model = {
graphData: {
nodes: [],
links: []
},
nodeInfos: {},
selectedNodes: new Set(),
focusNodes: new Set(),
focusLinks: new Set(),
hoverNode: null
};
this.initStyle();
if (opts.graphModel) {
this.linkWithGraphModel(opts.graphModel);
if (!opts.lazyInitView) {
this.initView();
}
mergeObjects(this.style, this.options.style);
}
}
initStyle() {
if (!this.style) {
this.style = getDefaultColorOf({
container: this.container
});
updateStyle(style) {
this.options.style = mergeObjects(this.options.style || {}, style);
this.initStyle();
this.refreshByStyle();
}
mergeObjects(this.style, this.options.style);
}
updateStyle(style) {
this.options.style = mergeObjects(this.options.style || {}, style);
this.initStyle();
this.refreshByStyle();
}
refreshByStyle() {
if (!this.forceGraph) return;
const getNodeColor = (nodeId, model) => {
const info = model.nodeInfos[nodeId];
const noteStyle = this.style.node.note;
const typeFill = this.style.node.note[info.type || 'regular'] || this.style.node.unknown;
if (this.shouldDebugColor) {
console.log('node fill', typeFill);
}
switch (this.getNodeState(nodeId, model)) {
case 'regular':
return {
fill: typeFill,
border: typeFill
};
case 'lessened':
let color = noteStyle.lessened;
if (!color) {
const c = d3Color.hsl(typeFill);
c.opacity = 0.2;
color = c;
}
return {
fill: color,
border: color
};
case 'highlighted':
return {
fill: typeFill,
border: this.style.highlightedForeground
};
default:
throw new Error(`Unknown type for node ${nodeId}`);
}
};
this.forceGraph.backgroundColor(this.style.background).nodeCanvasObject((node, ctx, globalScale) => {
if (!node.id) return;
const info = this.model.nodeInfos[node.id];
const size = this.sizeScaler(info.neighbors ? info.neighbors.length : 1);
const {
fill,
border
} = getNodeColor(node.id, this.model);
const fontSize = this.style.fontSize / globalScale;
let textColor = d3Color.rgb(fill);
const nodeState = this.getNodeState(node.id, this.model);
const alphaByDistance = this.labelAlphaScaler(globalScale);
textColor.opacity = nodeState === 'highlighted' ? 1 : nodeState === 'lessened' ? Math.min(0.2, alphaByDistance) : alphaByDistance;
const label = info.title;
makeDrawWrapper(ctx).circle(node.x, node.y, size + 0.5, border).circle(node.x, node.y, size, fill).text(label, node.x, node.y + size + 1, fontSize, textColor);
}).linkColor(link => {
return this.getLinkColor(link, this.model);
});
}
linkWithGraphModel(graphModel) {
if (this.currentDataModelEntry) {
this.currentDataModelEntry.unsub();
refreshByStyle() {
if (!this.forceGraph)
return;
const getNodeColor = (nodeId, model) => {
const info = model.nodeInfos[nodeId];
const noteStyle = this.style.node.note;
const typeFill = this.style.node.note[info.type || 'regular'] || this.style.node.unknown;
if (this.shouldDebugColor) {
console.log('node fill', typeFill);
}
switch (this.getNodeState(nodeId, model)) {
case 'regular':
return { fill: typeFill, border: typeFill };
case 'lessened':
let color = noteStyle.lessened;
if (!color) {
const c = d3Color.hsl(typeFill);
c.opacity = 0.2;
color = c;
}
return { fill: color, border: color };
case 'highlighted':
return {
fill: typeFill,
border: this.style.highlightedForeground,
};
default:
throw new Error(`Unknown type for node ${nodeId}`);
}
};
this.forceGraph
.backgroundColor(this.style.background)
.nodeCanvasObject((node, ctx, globalScale) => {
if (!node.id)
return;
const info = this.model.nodeInfos[node.id];
const size = this.sizeScaler(info.neighbors ? info.neighbors.length : 1);
const { fill, border } = getNodeColor(node.id, this.model);
const fontSize = this.style.fontSize / globalScale;
let textColor = d3Color.rgb(fill);
const nodeState = this.getNodeState(node.id, this.model);
const alphaByDistance = this.labelAlphaScaler(globalScale);
textColor.opacity =
nodeState === 'highlighted'
? 1
: nodeState === 'lessened'
? Math.min(0.2, alphaByDistance)
: alphaByDistance;
const label = info.title;
makeDrawWrapper(ctx)
.circle(node.x, node.y, size + 0.5, border)
.circle(node.x, node.y, size, fill)
.text(label, node.x, node.y + size + 1, fontSize, textColor);
})
.linkColor((link) => {
return this.getLinkColor(link, this.model);
});
}
this.updateViewData(graphModel.toGraphViewData());
const unsub = graphModel.subscribe(() => {
this.updateViewData(graphModel.toGraphViewData());
});
this.currentDataModelEntry = {
graphModel,
unsub
};
}
getColorOnContainer(name, fallback) {
return getComputedStyle(this.container).getPropertyValue(name) || fallback;
}
updateViewData(dataInput) {
Object.assign(this.model, dataInput);
if (dataInput.focusedNode) {
this.model.hoverNode = dataInput.focusedNode;
linkWithGraphModel(graphModel) {
if (this.currentDataModelEntry) {
this.currentDataModelEntry.unsub();
}
this.updateViewData(graphModel.toGraphViewData());
const unsub = graphModel.subscribe(() => {
this.updateViewData(graphModel.toGraphViewData());
});
this.currentDataModelEntry = {
graphModel,
unsub,
};
}
}
updateCanvasSize(size) {
if (!this.forceGraph) return;
if ('width' in size) {
this.forceGraph.width(size.width);
getColorOnContainer(name, fallback) {
return getComputedStyle(this.container).getPropertyValue(name) || fallback;
}
if ('height' in size) {
this.forceGraph.height(size.height);
updateViewData(dataInput) {
Object.assign(this.model, dataInput);
if (dataInput.focusedNode) {
this.model.hoverNode = dataInput.focusedNode;
}
}
}
initView() {
const {
options,
model,
style,
actions
} = this; // this runtime dependency may not be ready when this umd file excutes,
// so we will retrieve it from the global scope
const forceGraphFactory = ForceGraph || globalThis.ForceGraph;
const forceGraph = this.forceGraph || forceGraphFactory();
const width = options.width || window.innerWidth - this.container.offsetLeft - 20;
const height = options.height || window.innerHeight - this.container.offsetTop - 20; // const randomId = Math.floor(Math.random() * 1000)
// console.log('initView', randomId)
forceGraph(this.container).height(height).width(width).graphData(model.graphData).linkHoverPrecision(8).enableNodeDrag(!!options.enableNodeDrag).cooldownTime(200).d3Force('x', d3Force.forceX()).d3Force('y', d3Force.forceY()).d3Force('collide', d3Force.forceCollide(forceGraph.nodeRelSize())).linkWidth(1).linkDirectionalParticles(1).linkDirectionalParticleWidth(link => this.getLinkState(link, model) === 'highlighted' ? 2 : 0).onEngineStop(() => {
if (!this.hasInitialZoomToFit) {
this.hasInitialZoomToFit = true;
forceGraph.zoomToFit(1000, 20);
}
}).onNodeHover(node => {
actions.highlightNode(this.model, node == null ? void 0 : node.id);
this.updateViewModeInteractiveState();
}).onNodeClick((node, event) => {
actions.selectNode(this.model, node.id, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('nodeClick', {
node,
event
});
}).onLinkClick((link, event) => {
this.fireInteraction('linkClick', {
link,
event
});
}).onBackgroundClick(event => {
actions.selectNode(this.model, null, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('backgroundClick', {
event
});
}).onBackgroundRightClick(event => {
forceGraph.zoomToFit(1000, 20);
this.fireInteraction('backgroundRightClick', {
event
});
});
if (options.enableSmartZooming !== false) {
this.initGraphSmartZooming(forceGraph);
updateCanvasSize(size) {
if (!this.forceGraph)
return;
if ('width' in size) {
this.forceGraph.width(size.width);
}
if ('height' in size) {
this.forceGraph.height(size.height);
}
}
this.forceGraph = forceGraph;
this.refreshByStyle();
}
initGraphSmartZooming(forceGraph) {
let isAdjustingZoom = false;
const debouncedZoomHandler = throttleDebounce.debounce(200, event => {
if (isAdjustingZoom) return;
const {
x: xb,
y: yb
} = this.forceGraph.getGraphBbox(); // x/y here is translate, k is scale
const {
k,
x,
y
} = event;
const scaledBoundL = k * xb[0];
const scaledBoundR = k * xb[1];
const scaledBoundT = k * yb[0];
const scaledBoundB = k * yb[1];
const graphCanvasW = this.forceGraph.width();
const graphCanvasH = this.forceGraph.height();
const oldCenter = this.forceGraph.centerAt();
const currentCenter = oldCenter; // TODO: this is more like the center before zoom, rather than current zooming one ?
let newCenterX;
let newCenterY; // should calculate proper center (because that's force-graph's only method...) to make the viewport fit the graphBbox
if (scaledBoundR + x < 0) {
// console.log('is out of right')
isAdjustingZoom = false;
newCenterX = xb[1];
} else if (scaledBoundL + x > graphCanvasW) {
// console.log('is out of left')
newCenterX = xb[0];
}
if (scaledBoundT + y > graphCanvasH) {
// is out of top
newCenterY = yb[0];
} else if (scaledBoundB + y < 0) {
// console.log('is out of bottom')
newCenterY = yb[1];
}
if (typeof newCenterX === 'number' || typeof newCenterY === 'number') {
// console.log('new centerX', newCenterX, newCenterY, 'old center', oldCenter)
this.forceGraph.centerAt(newCenterX !== undefined ? newCenterX : currentCenter.x, newCenterY !== undefined ? newCenterY : currentCenter.y, 2000);
}
});
forceGraph.onZoom(event => {
if (!this.hasInitialZoomToFit) return;
debouncedZoomHandler(event);
}).onZoomEnd(() => {
setTimeout(() => {
isAdjustingZoom = false;
}, 20);
});
}
getLinkNodeId(v) {
const t = typeof v;
return t === 'string' || t === 'number' ? v : v.id;
}
getNodeState(nodeId, model = this.model) {
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId ? 'highlighted' : model.focusNodes.size === 0 ? 'regular' : model.focusNodes.has(nodeId) ? 'regular' : 'lessened';
}
getLinkState(link, model = this.model) {
return model.focusNodes.size === 0 ? 'regular' : model.focusLinks.has(link.id) ? 'highlighted' : 'lessened';
}
getLinkColor(link, model) {
const style = this.style;
const linkStyle = style.link;
switch (this.getLinkState(link, model)) {
case 'regular':
return linkStyle.regular;
case 'highlighted':
// inbound/outbound link is a little bit different with hoverNode
let linkColorByDirection;
const hoverNodeLinkStyle = style.hoverNodeLink;
if (model.hoverNode === this.getLinkNodeId(link.source)) {
var _hoverNodeLinkStyle$h;
linkColorByDirection = (_hoverNodeLinkStyle$h = hoverNodeLinkStyle.highlightedDirection) == null ? void 0 : _hoverNodeLinkStyle$h.outbound;
} else if (model.hoverNode === this.getLinkNodeId(link.target)) {
var _hoverNodeLinkStyle$h2;
linkColorByDirection = (_hoverNodeLinkStyle$h2 = hoverNodeLinkStyle.highlightedDirection) == null ? void 0 : _hoverNodeLinkStyle$h2.inbound;
initView() {
const { options, model, style, actions } = this;
// this runtime dependency may not be ready when this umd file excutes,
// so we will retrieve it from the global scope
const forceGraphFactory = ForceGraph__default['default'] || globalThis.ForceGraph;
const forceGraph = this.forceGraph || forceGraphFactory();
const width = options.width || window.innerWidth - this.container.offsetLeft - 20;
const height = options.height || window.innerHeight - this.container.offsetTop - 20;
// const randomId = Math.floor(Math.random() * 1000)
// console.log('initView', randomId)
forceGraph(this.container)
.height(height)
.width(width)
.graphData(model.graphData)
.linkHoverPrecision(8)
.enableNodeDrag(!!options.enableNodeDrag)
.cooldownTime(200)
.d3Force('x', d3Force.forceX())
.d3Force('y', d3Force.forceY())
.d3Force('collide', d3Force.forceCollide(forceGraph.nodeRelSize()))
.linkWidth(1)
.linkDirectionalParticles(1)
.linkDirectionalParticleWidth((link) => this.getLinkState(link, model) === 'highlighted' ? 2 : 0)
.onEngineStop(() => {
if (!this.hasInitialZoomToFit) {
this.hasInitialZoomToFit = true;
forceGraph.zoomToFit(1000, 20);
}
})
.onNodeHover((node) => {
actions.highlightNode(this.model, node?.id);
this.updateViewModeInteractiveState();
})
.onNodeClick((node, event) => {
actions.selectNode(this.model, node.id, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('nodeClick', { node, event });
})
.onLinkClick((link, event) => {
this.fireInteraction('linkClick', { link, event });
})
.onBackgroundClick((event) => {
actions.selectNode(this.model, null, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('backgroundClick', { event });
})
.onBackgroundRightClick((event) => {
forceGraph.zoomToFit(1000, 20);
this.fireInteraction('backgroundRightClick', { event });
});
if (options.enableSmartZooming !== false) {
this.initGraphSmartZooming(forceGraph);
}
return linkColorByDirection || linkStyle.highlighted || style.highlightedForeground;
case 'lessened':
let color = linkStyle.lessened;
if (!color) {
const c = d3Color.hsl(style.node.note.lessened);
c.opacity = 0.2;
color = c;
this.forceGraph = forceGraph;
this.refreshByStyle();
}
initGraphSmartZooming(forceGraph) {
let isAdjustingZoom = false;
const debouncedZoomHandler = debounce_1(200, (event) => {
if (isAdjustingZoom)
return;
const { x: xb, y: yb } = this.forceGraph.getGraphBbox();
// x/y here is translate, k is scale
const { k, x, y } = event;
const scaledBoundL = k * xb[0];
const scaledBoundR = k * xb[1];
const scaledBoundT = k * yb[0];
const scaledBoundB = k * yb[1];
const graphCanvasW = this.forceGraph.width();
const graphCanvasH = this.forceGraph.height();
const oldCenter = this.forceGraph.centerAt();
const currentCenter = oldCenter; // TODO: this is more like the center before zoom, rather than current zooming one ?
let newCenterX;
let newCenterY;
// should calculate proper center (because that's force-graph's only method...) to make the viewport fit the graphBbox
if (scaledBoundR + x < 0) {
// console.log('is out of right')
isAdjustingZoom = false;
newCenterX = xb[1];
}
else if (scaledBoundL + x > graphCanvasW) {
// console.log('is out of left')
newCenterX = xb[0];
}
if (scaledBoundT + y > graphCanvasH) {
// is out of top
newCenterY = yb[0];
}
else if (scaledBoundB + y < 0) {
// console.log('is out of bottom')
newCenterY = yb[1];
}
if (typeof newCenterX === 'number' || typeof newCenterY === 'number') {
// console.log('new centerX', newCenterX, newCenterY, 'old center', oldCenter)
this.forceGraph.centerAt(newCenterX !== undefined ? newCenterX : currentCenter.x, newCenterY !== undefined ? newCenterY : currentCenter.y, 2000);
}
});
forceGraph
.onZoom((event) => {
if (!this.hasInitialZoomToFit)
return;
debouncedZoomHandler(event);
})
.onZoomEnd(() => {
setTimeout(() => {
isAdjustingZoom = false;
}, 20);
});
}
getLinkNodeId(v) {
const t = typeof v;
return t === 'string' || t === 'number' ? v : v.id;
}
getNodeState(nodeId, model = this.model) {
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId
? 'highlighted'
: model.focusNodes.size === 0
? 'regular'
: model.focusNodes.has(nodeId)
? 'regular'
: 'lessened';
}
getLinkState(link, model = this.model) {
return model.focusNodes.size === 0
? 'regular'
: model.focusLinks.has(link.id)
? 'highlighted'
: 'lessened';
}
getLinkColor(link, model) {
const style = this.style;
const linkStyle = style.link;
switch (this.getLinkState(link, model)) {
case 'regular':
return linkStyle.regular;
case 'highlighted':
// inbound/outbound link is a little bit different with hoverNode
let linkColorByDirection;
const hoverNodeLinkStyle = style.hoverNodeLink;
if (model.hoverNode === this.getLinkNodeId(link.source)) {
linkColorByDirection =
hoverNodeLinkStyle.highlightedDirection?.outbound;
}
else if (model.hoverNode === this.getLinkNodeId(link.target)) {
linkColorByDirection =
hoverNodeLinkStyle.highlightedDirection?.inbound;
}
return (linkColorByDirection ||
linkStyle.highlighted ||
style.highlightedForeground);
case 'lessened':
let color = linkStyle.lessened;
if (!color) {
const c = d3Color.hsl(style.node.note.lessened);
c.opacity = 0.2;
color = c;
}
return color;
default:
throw new Error(`Unknown type for link ${link}`);
}
return color;
default:
throw new Error(`Unknown type for link ${link}`);
}
}
updateViewModeInteractiveState() {
const {
model
} = this; // compute highlighted elements
const focusNodes = new Set();
const focusLinks = new Set();
if (model.hoverNode) {
var _info$neighbors, _info$linkIds;
focusNodes.add(model.hoverNode);
const info = model.nodeInfos[model.hoverNode];
(_info$neighbors = info.neighbors) == null ? void 0 : _info$neighbors.forEach(neighborId => focusNodes.add(neighborId));
(_info$linkIds = info.linkIds) == null ? void 0 : _info$linkIds.forEach(link => focusLinks.add(link));
updateViewModeInteractiveState() {
const { model } = this;
// compute highlighted elements
const focusNodes = new Set();
const focusLinks = new Set();
if (model.hoverNode) {
focusNodes.add(model.hoverNode);
const info = model.nodeInfos[model.hoverNode];
info.neighbors?.forEach((neighborId) => focusNodes.add(neighborId));
info.linkIds?.forEach((link) => focusLinks.add(link));
}
if (model.selectedNodes) {
model.selectedNodes.forEach((nodeId) => {
focusNodes.add(nodeId);
const info = model.nodeInfos[nodeId];
info.neighbors?.forEach((neighborId) => focusNodes.add(neighborId));
info.linkIds?.forEach((link) => focusLinks.add(link));
});
}
model.focusNodes = focusNodes;
model.focusLinks = focusLinks;
}
if (model.selectedNodes) {
model.selectedNodes.forEach(nodeId => {
var _info$neighbors2, _info$linkIds2;
focusNodes.add(nodeId);
const info = model.nodeInfos[nodeId];
(_info$neighbors2 = info.neighbors) == null ? void 0 : _info$neighbors2.forEach(neighborId => focusNodes.add(neighborId));
(_info$linkIds2 = info.linkIds) == null ? void 0 : _info$linkIds2.forEach(link => focusLinks.add(link));
});
/**
* Select nodes to gain more initial attention
*/
setSelectedNodes(nodeIds, isAppend = false) {
if (!isAppend)
this.model.selectedNodes.clear();
nodeIds.forEach(nodeId => this.actions.selectNode(this.model, nodeId, true));
this.updateViewModeInteractiveState();
}
model.focusNodes = focusNodes;
model.focusLinks = focusLinks;
}
/**
* Select nodes to gain more initial attention
*/
setSelectedNodes(nodeIds, isAppend = false) {
if (!isAppend) this.model.selectedNodes.clear();
nodeIds.forEach(nodeId => this.actions.selectNode(this.model, nodeId, true));
this.updateViewModeInteractiveState();
}
onInteraction(name, cb) {
if (!this.interactionCallbacks[name]) this.interactionCallbacks[name] = [];
const callbackList = this.interactionCallbacks[name];
callbackList.push(cb);
return () => {
const pos = callbackList.indexOf(cb);
if (pos > -1) {
callbackList.splice(pos, 1);
}
};
}
fireInteraction(name, payload) {
const callbackList = this.interactionCallbacks[name];
if (callbackList) {
callbackList.forEach(cb => cb(payload));
onInteraction(name, cb) {
if (!this.interactionCallbacks[name])
this.interactionCallbacks[name] = [];
const callbackList = this.interactionCallbacks[name];
callbackList.push(cb);
return () => {
const pos = callbackList.indexOf(cb);
if (pos > -1) {
callbackList.splice(pos, 1);
}
};
}
}
dispose() {
if (this.forceGraph) {
this.forceGraph.pauseAnimation();
fireInteraction(name, payload) {
const callbackList = this.interactionCallbacks[name];
if (callbackList) {
callbackList.forEach((cb) => cb(payload));
}
}
}
dispose() {
if (this.forceGraph) {
this.forceGraph.pauseAnimation();
}
}
}

@@ -603,0 +688,0 @@

@@ -1,606 +0,4 @@

(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3-color'), require('d3-force'), require('d3-scale'), require('throttle-debounce'), require('force-graph')) :
typeof define === 'function' && define.amd ? define(['exports', 'd3-color', 'd3-force', 'd3-scale', 'throttle-debounce', 'force-graph'], factory) :
(global = global || self, factory(global.NOTE_GRAPH = {}, global.d3, global.d3, global.d3, global.throttleDebounce, global.forceGraph));
}(this, (function (exports, d3Color, d3Force, d3Scale, throttleDebounce, ForceGraph) {
ForceGraph = ForceGraph && Object.prototype.hasOwnProperty.call(ForceGraph, 'default') ? ForceGraph['default'] : ForceGraph;
/**
* Can generate GraphViewModel by `toGraphViewModel`
*/
class NoteGraphModel {
constructor(notes) {
this.subscribers = [];
this.notes = notes;
this.updateCache();
}
updateCache() {
const links = [];
const nodeInfos = {};
const linkMap = new Map();
this.notes.forEach(note => {
const nodeInfo = {
title: note.title,
linkIds: [],
neighbors: []
};
if (note.linkTo) {
note.linkTo.forEach(linkedNodeId => {
const link = {
id: this.formLinkId(note.id, linkedNodeId),
source: note.id,
target: linkedNodeId
};
links.push(link);
linkMap.set(link.id, link);
nodeInfo.linkIds.push(link.id);
nodeInfo.neighbors.push(linkedNodeId);
});
}
if (note.referencedBy) {
note.referencedBy.forEach(refererId => {
nodeInfo.linkIds.push(this.formLinkId(refererId, note.id));
nodeInfo.neighbors.push(refererId);
});
}
nodeInfos[note.id] = nodeInfo;
});
const cache = this.cache || {};
cache.nodeInfos = nodeInfos;
cache.links = links;
cache.linkMap = linkMap;
this.cache = cache;
}
getNodeInfoById(id) {
return this.cache.nodeInfos[id];
}
getLinkById(id) {
return this.cache.linkMap.get(id);
}
/**
* A link's id is a combination of source node and target node's id
*/
formLinkId(sourceId, targetId) {
return `${sourceId}-${targetId}`;
}
toGraphViewData() {
const vm = {
graphData: {
nodes: this.notes,
links: this.cache.links
},
nodeInfos: this.cache.nodeInfos
};
return vm;
}
publishChange() {
this.subscribers.forEach(subscriber => {
subscriber(this);
});
}
subscribe(subscriber) {
this.subscribers.push(subscriber);
return () => {
const pos = this.subscribers.indexOf(subscriber);
if (pos > -1) {
this.subscribers.splice(pos, 1);
}
};
}
}
const mergeObjects = function (target) {
var sources = [].slice.call(arguments, 1);
if (!sources.length) {
return target;
}
const source = sources.shift();
if (source === undefined) {
return target;
}
if (isMergebleObject(target) && isMergebleObject(source)) {
Object.keys(source).forEach(function (key) {
if (isMergebleObject(source[key])) {
if (!target[key]) {
target[key] = {};
}
mergeObjects(target[key], source[key]);
} else {
target[key] = source[key];
}
});
}
return mergeObjects(target, ...sources);
};
const isObject = item => {
return item !== null && typeof item === 'object';
};
const isMergebleObject = item => {
return isObject(item) && !Array.isArray(item);
};
function getColorOnContainer(container, name, fallback) {
return getComputedStyle(container).getPropertyValue(name) || fallback;
}
function getDefaultColorOf(opts = {}) {
const container = opts.container || document.body;
const highlightedForeground = getColorOnContainer(container, '--notegraph-highlighted-foreground-color', '#f9c74f');
return {
background: getColorOnContainer(container, `--notegraph-background`, '#f7f7f7'),
fontSize: parseInt(getColorOnContainer(container, `--notegraph-font-size`, 12)),
highlightedForeground,
node: {
note: {
regular: getColorOnContainer(container, '--notegraph-note-color-regular', '#5f76e7')
},
unknown: getColorOnContainer(container, '--notegraph-unkown-node-color', '#f94144')
},
link: {
regular: getColorOnContainer(container, '--notegraph-link-color-regular', '#ccc'),
highlighted: getColorOnContainer(container, '--notegraph-link-color-highlighted', highlightedForeground)
},
hoverNodeLink: {
highlightedDirection: {
inbound: '#3078cd',
outbound: highlightedForeground
}
}
};
}
const makeDrawWrapper = ctx => ({
circle: function (x, y, radius, color) {
ctx.beginPath();
ctx.arc(x, y, radius, 0, 2 * Math.PI, false);
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
return this;
},
text: function (text, x, y, size, color) {
ctx.font = `${size}px Sans-Serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
ctx.fillStyle = color;
ctx.fillText(text, x, y);
return this;
}
});
/**
* The view of the graph.
* Wraps a d3 force-graph inside
*/
class NoteGraphView {
constructor(opts) {
this.sizeScaler = d3Scale.scaleLinear().domain([0, 20]).range([1, 5]).clamp(true);
this.labelAlphaScaler = d3Scale.scaleLinear().domain([1.2, 2]).range([0, 1]).clamp(true);
this.interactionCallbacks = {};
this.hasInitialZoomToFit = false;
this.actions = {
selectNode(model, nodeId, isAppend) {
if (!isAppend) {
model.selectedNodes.clear();
}
if (nodeId != null) {
model.selectedNodes.add(nodeId);
}
},
highlightNode(model, nodeId) {
model.hoverNode = nodeId;
}
};
this.shouldDebugColor = false;
this.options = opts;
this.container = opts.container;
this.model = {
graphData: {
nodes: [],
links: []
},
nodeInfos: {},
selectedNodes: new Set(),
focusNodes: new Set(),
focusLinks: new Set(),
hoverNode: null
};
this.initStyle();
if (opts.graphModel) {
this.linkWithGraphModel(opts.graphModel);
if (!opts.lazyInitView) {
this.initView();
}
}
}
initStyle() {
if (!this.style) {
this.style = getDefaultColorOf({
container: this.container
});
}
mergeObjects(this.style, this.options.style);
}
updateStyle(style) {
this.options.style = mergeObjects(this.options.style || {}, style);
this.initStyle();
this.refreshByStyle();
}
refreshByStyle() {
if (!this.forceGraph) return;
const getNodeColor = (nodeId, model) => {
const info = model.nodeInfos[nodeId];
const noteStyle = this.style.node.note;
const typeFill = this.style.node.note[info.type || 'regular'] || this.style.node.unknown;
if (this.shouldDebugColor) {
console.log('node fill', typeFill);
}
switch (this.getNodeState(nodeId, model)) {
case 'regular':
return {
fill: typeFill,
border: typeFill
};
case 'lessened':
let color = noteStyle.lessened;
if (!color) {
const c = d3Color.hsl(typeFill);
c.opacity = 0.2;
color = c;
}
return {
fill: color,
border: color
};
case 'highlighted':
return {
fill: typeFill,
border: this.style.highlightedForeground
};
default:
throw new Error(`Unknown type for node ${nodeId}`);
}
};
this.forceGraph.backgroundColor(this.style.background).nodeCanvasObject((node, ctx, globalScale) => {
if (!node.id) return;
const info = this.model.nodeInfos[node.id];
const size = this.sizeScaler(info.neighbors ? info.neighbors.length : 1);
const {
fill,
border
} = getNodeColor(node.id, this.model);
const fontSize = this.style.fontSize / globalScale;
let textColor = d3Color.rgb(fill);
const nodeState = this.getNodeState(node.id, this.model);
const alphaByDistance = this.labelAlphaScaler(globalScale);
textColor.opacity = nodeState === 'highlighted' ? 1 : nodeState === 'lessened' ? Math.min(0.2, alphaByDistance) : alphaByDistance;
const label = info.title;
makeDrawWrapper(ctx).circle(node.x, node.y, size + 0.5, border).circle(node.x, node.y, size, fill).text(label, node.x, node.y + size + 1, fontSize, textColor);
}).linkColor(link => {
return this.getLinkColor(link, this.model);
});
}
linkWithGraphModel(graphModel) {
if (this.currentDataModelEntry) {
this.currentDataModelEntry.unsub();
}
this.updateViewData(graphModel.toGraphViewData());
const unsub = graphModel.subscribe(() => {
this.updateViewData(graphModel.toGraphViewData());
});
this.currentDataModelEntry = {
graphModel,
unsub
};
}
getColorOnContainer(name, fallback) {
return getComputedStyle(this.container).getPropertyValue(name) || fallback;
}
updateViewData(dataInput) {
Object.assign(this.model, dataInput);
if (dataInput.focusedNode) {
this.model.hoverNode = dataInput.focusedNode;
}
}
updateCanvasSize(size) {
if (!this.forceGraph) return;
if ('width' in size) {
this.forceGraph.width(size.width);
}
if ('height' in size) {
this.forceGraph.height(size.height);
}
}
initView() {
const {
options,
model,
style,
actions
} = this; // this runtime dependency may not be ready when this umd file excutes,
// so we will retrieve it from the global scope
const forceGraphFactory = ForceGraph || globalThis.ForceGraph;
const forceGraph = this.forceGraph || forceGraphFactory();
const width = options.width || window.innerWidth - this.container.offsetLeft - 20;
const height = options.height || window.innerHeight - this.container.offsetTop - 20; // const randomId = Math.floor(Math.random() * 1000)
// console.log('initView', randomId)
forceGraph(this.container).height(height).width(width).graphData(model.graphData).linkHoverPrecision(8).enableNodeDrag(!!options.enableNodeDrag).cooldownTime(200).d3Force('x', d3Force.forceX()).d3Force('y', d3Force.forceY()).d3Force('collide', d3Force.forceCollide(forceGraph.nodeRelSize())).linkWidth(1).linkDirectionalParticles(1).linkDirectionalParticleWidth(link => this.getLinkState(link, model) === 'highlighted' ? 2 : 0).onEngineStop(() => {
if (!this.hasInitialZoomToFit) {
this.hasInitialZoomToFit = true;
forceGraph.zoomToFit(1000, 20);
}
}).onNodeHover(node => {
actions.highlightNode(this.model, node == null ? void 0 : node.id);
this.updateViewModeInteractiveState();
}).onNodeClick((node, event) => {
actions.selectNode(this.model, node.id, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('nodeClick', {
node,
event
});
}).onLinkClick((link, event) => {
this.fireInteraction('linkClick', {
link,
event
});
}).onBackgroundClick(event => {
actions.selectNode(this.model, null, event.getModifierState('Shift'));
this.updateViewModeInteractiveState();
this.fireInteraction('backgroundClick', {
event
});
}).onBackgroundRightClick(event => {
forceGraph.zoomToFit(1000, 20);
this.fireInteraction('backgroundRightClick', {
event
});
});
if (options.enableSmartZooming !== false) {
this.initGraphSmartZooming(forceGraph);
}
this.forceGraph = forceGraph;
this.refreshByStyle();
}
initGraphSmartZooming(forceGraph) {
let isAdjustingZoom = false;
const debouncedZoomHandler = throttleDebounce.debounce(200, event => {
if (isAdjustingZoom) return;
const {
x: xb,
y: yb
} = this.forceGraph.getGraphBbox(); // x/y here is translate, k is scale
const {
k,
x,
y
} = event;
const scaledBoundL = k * xb[0];
const scaledBoundR = k * xb[1];
const scaledBoundT = k * yb[0];
const scaledBoundB = k * yb[1];
const graphCanvasW = this.forceGraph.width();
const graphCanvasH = this.forceGraph.height();
const oldCenter = this.forceGraph.centerAt();
const currentCenter = oldCenter; // TODO: this is more like the center before zoom, rather than current zooming one ?
let newCenterX;
let newCenterY; // should calculate proper center (because that's force-graph's only method...) to make the viewport fit the graphBbox
if (scaledBoundR + x < 0) {
// console.log('is out of right')
isAdjustingZoom = false;
newCenterX = xb[1];
} else if (scaledBoundL + x > graphCanvasW) {
// console.log('is out of left')
newCenterX = xb[0];
}
if (scaledBoundT + y > graphCanvasH) {
// is out of top
newCenterY = yb[0];
} else if (scaledBoundB + y < 0) {
// console.log('is out of bottom')
newCenterY = yb[1];
}
if (typeof newCenterX === 'number' || typeof newCenterY === 'number') {
// console.log('new centerX', newCenterX, newCenterY, 'old center', oldCenter)
this.forceGraph.centerAt(newCenterX !== undefined ? newCenterX : currentCenter.x, newCenterY !== undefined ? newCenterY : currentCenter.y, 2000);
}
});
forceGraph.onZoom(event => {
if (!this.hasInitialZoomToFit) return;
debouncedZoomHandler(event);
}).onZoomEnd(() => {
setTimeout(() => {
isAdjustingZoom = false;
}, 20);
});
}
getLinkNodeId(v) {
const t = typeof v;
return t === 'string' || t === 'number' ? v : v.id;
}
getNodeState(nodeId, model = this.model) {
return model.selectedNodes.has(nodeId) || model.hoverNode === nodeId ? 'highlighted' : model.focusNodes.size === 0 ? 'regular' : model.focusNodes.has(nodeId) ? 'regular' : 'lessened';
}
getLinkState(link, model = this.model) {
return model.focusNodes.size === 0 ? 'regular' : model.focusLinks.has(link.id) ? 'highlighted' : 'lessened';
}
getLinkColor(link, model) {
const style = this.style;
const linkStyle = style.link;
switch (this.getLinkState(link, model)) {
case 'regular':
return linkStyle.regular;
case 'highlighted':
// inbound/outbound link is a little bit different with hoverNode
let linkColorByDirection;
const hoverNodeLinkStyle = style.hoverNodeLink;
if (model.hoverNode === this.getLinkNodeId(link.source)) {
var _hoverNodeLinkStyle$h;
linkColorByDirection = (_hoverNodeLinkStyle$h = hoverNodeLinkStyle.highlightedDirection) == null ? void 0 : _hoverNodeLinkStyle$h.outbound;
} else if (model.hoverNode === this.getLinkNodeId(link.target)) {
var _hoverNodeLinkStyle$h2;
linkColorByDirection = (_hoverNodeLinkStyle$h2 = hoverNodeLinkStyle.highlightedDirection) == null ? void 0 : _hoverNodeLinkStyle$h2.inbound;
}
return linkColorByDirection || linkStyle.highlighted || style.highlightedForeground;
case 'lessened':
let color = linkStyle.lessened;
if (!color) {
const c = d3Color.hsl(style.node.note.lessened);
c.opacity = 0.2;
color = c;
}
return color;
default:
throw new Error(`Unknown type for link ${link}`);
}
}
updateViewModeInteractiveState() {
const {
model
} = this; // compute highlighted elements
const focusNodes = new Set();
const focusLinks = new Set();
if (model.hoverNode) {
var _info$neighbors, _info$linkIds;
focusNodes.add(model.hoverNode);
const info = model.nodeInfos[model.hoverNode];
(_info$neighbors = info.neighbors) == null ? void 0 : _info$neighbors.forEach(neighborId => focusNodes.add(neighborId));
(_info$linkIds = info.linkIds) == null ? void 0 : _info$linkIds.forEach(link => focusLinks.add(link));
}
if (model.selectedNodes) {
model.selectedNodes.forEach(nodeId => {
var _info$neighbors2, _info$linkIds2;
focusNodes.add(nodeId);
const info = model.nodeInfos[nodeId];
(_info$neighbors2 = info.neighbors) == null ? void 0 : _info$neighbors2.forEach(neighborId => focusNodes.add(neighborId));
(_info$linkIds2 = info.linkIds) == null ? void 0 : _info$linkIds2.forEach(link => focusLinks.add(link));
});
}
model.focusNodes = focusNodes;
model.focusLinks = focusLinks;
}
/**
* Select nodes to gain more initial attention
*/
setSelectedNodes(nodeIds, isAppend = false) {
if (!isAppend) this.model.selectedNodes.clear();
nodeIds.forEach(nodeId => this.actions.selectNode(this.model, nodeId, true));
this.updateViewModeInteractiveState();
}
onInteraction(name, cb) {
if (!this.interactionCallbacks[name]) this.interactionCallbacks[name] = [];
const callbackList = this.interactionCallbacks[name];
callbackList.push(cb);
return () => {
const pos = callbackList.indexOf(cb);
if (pos > -1) {
callbackList.splice(pos, 1);
}
};
}
fireInteraction(name, payload) {
const callbackList = this.interactionCallbacks[name];
if (callbackList) {
callbackList.forEach(cb => cb(payload));
}
}
dispose() {
if (this.forceGraph) {
this.forceGraph.pauseAnimation();
}
}
}
exports.NoteGraphModel = NoteGraphModel;
exports.NoteGraphView = NoteGraphView;
exports.getColorOnContainer = getColorOnContainer;
exports.getDefaultColorOf = getDefaultColorOf;
})));
/**
* @version 0.1.1
*/
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("d3-color"),require("d3-force"),require("d3-scale"),require("force-graph")):"function"==typeof define&&define.amd?define(["exports","d3-color","d3-force","d3-scale","force-graph"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).NOTE_GRAPH={},e.d3,e.d3,e.d3,e.ForceGraph)}(this,(function(e,t,i,o,n){"use strict";function r(e){return e&&"object"==typeof e&&"default"in e?e:{default:e}}var s=r(n);function h(e,t,i,o){var n,r=!1,s=0;function h(){n&&clearTimeout(n)}function a(){for(var a=arguments.length,d=new Array(a),l=0;l<a;l++)d[l]=arguments[l];var c=this,u=Date.now()-s;function f(){s=Date.now(),i.apply(c,d)}function g(){n=void 0}r||(o&&!n&&f(),h(),void 0===o&&u>e?f():!0!==t&&(n=setTimeout(o?g:f,void 0===o?e-u:e)))}return"boolean"!=typeof t&&(o=i,i=t,t=void 0),a.cancel=function(){h(),r=!0},a}var a=function(e,t,i){return void 0===i?h(e,t,!1):h(e,i,!1!==t)};const d=(e,...t)=>{if(!t.length)return e;const i=t.shift();return void 0===i?e:(l(e)&&l(i)&&Object.keys(i).forEach((function(t){l(i[t])?(e[t]||(e[t]={}),d(e[t],i[t])):e[t]=i[t]})),d(e,...t))},l=e=>(e=>null!==e&&"object"==typeof e)(e)&&!Array.isArray(e);function c(e,t,i){return getComputedStyle(e).getPropertyValue(t)||i}function u(e={}){const t=e.container||document.body,i=c(t,"--notegraph-highlighted-foreground-color","#f9c74f");return{background:c(t,"--notegraph-background","#f7f7f7"),fontSize:parseInt(c(t,"--notegraph-font-size",12)),highlightedForeground:i,node:{note:{regular:c(t,"--notegraph-note-color-regular","#5f76e7")},unknown:c(t,"--notegraph-unkown-node-color","#f94144")},link:{regular:c(t,"--notegraph-link-color-regular","#ccc"),highlighted:c(t,"--notegraph-link-color-highlighted",i)},hoverNodeLink:{highlightedDirection:{inbound:"#3078cd",outbound:i}}}}const f=e=>({circle:function(t,i,o,n){return e.beginPath(),e.arc(t,i,o,0,2*Math.PI,!1),e.fillStyle=n,e.fill(),e.closePath(),this},text:function(t,i,o,n,r){return e.font=`${n}px Sans-Serif`,e.textAlign="center",e.textBaseline="top",e.fillStyle=r,e.fillText(t,i,o),this}});e.NoteGraphModel=class{constructor(e){this.subscribers=[],this.notes=e,this.updateCache()}updateCache(){const e=[],t=[],i={},o=new Map;this.notes.forEach((n=>{e.push({id:n.id,data:{note:n}});const r={title:n.title,linkIds:[],neighbors:[]};n.linkTo&&n.linkTo.forEach((e=>{const i={id:this.formLinkId(n.id,e),source:n.id,target:e};t.push(i),o.set(i.id,i),r.linkIds.push(i.id),r.neighbors.push(e)})),n.referencedBy&&n.referencedBy.forEach((e=>{r.linkIds.push(this.formLinkId(e,n.id)),r.neighbors.push(e)})),i[n.id]=r}));const n=this.cache||{};n.nodeInfos=i,n.links=t,n.linkMap=o,this.cache=n}getNodeInfoById(e){return this.cache.nodeInfos[e]}getLinkById(e){return this.cache.linkMap.get(e)}formLinkId(e,t){return`${e}-${t}`}toGraphViewData(){return{graphData:{nodes:this.notes,links:this.cache.links},nodeInfos:this.cache.nodeInfos}}publishChange(){this.subscribers.forEach((e=>{e(this)}))}subscribe(e){return this.subscribers.push(e),()=>{const t=this.subscribers.indexOf(e);t>-1&&this.subscribers.splice(t,1)}}},e.NoteGraphView=class{constructor(e){this.sizeScaler=o.scaleLinear().domain([0,20]).range([1,5]).clamp(!0),this.labelAlphaScaler=o.scaleLinear().domain([1.2,2]).range([0,1]).clamp(!0),this.interactionCallbacks={},this.hasInitialZoomToFit=!1,this.actions={selectNode(e,t,i){i||e.selectedNodes.clear(),null!=t&&e.selectedNodes.add(t)},highlightNode(e,t){e.hoverNode=t}},this.shouldDebugColor=!1,this.options=e,this.container=e.container,this.model={graphData:{nodes:[],links:[]},nodeInfos:{},selectedNodes:new Set,focusNodes:new Set,focusLinks:new Set,hoverNode:null},this.initStyle(),e.graphModel&&(this.linkWithGraphModel(e.graphModel),e.lazyInitView||this.initView())}initStyle(){this.style||(this.style=u({container:this.container})),d(this.style,this.options.style)}updateStyle(e){this.options.style=d(this.options.style||{},e),this.initStyle(),this.refreshByStyle()}refreshByStyle(){if(!this.forceGraph)return;const e=(e,i)=>{const o=i.nodeInfos[e],n=this.style.node.note,r=this.style.node.note[o.type||"regular"]||this.style.node.unknown;switch(this.shouldDebugColor&&console.log("node fill",r),this.getNodeState(e,i)){case"regular":return{fill:r,border:r};case"lessened":let i=n.lessened;if(!i){const e=t.hsl(r);e.opacity=.2,i=e}return{fill:i,border:i};case"highlighted":return{fill:r,border:this.style.highlightedForeground};default:throw new Error(`Unknown type for node ${e}`)}};this.forceGraph.backgroundColor(this.style.background).nodeCanvasObject(((i,o,n)=>{if(!i.id)return;const r=this.model.nodeInfos[i.id],s=this.sizeScaler(r.neighbors?r.neighbors.length:1),{fill:h,border:a}=e(i.id,this.model),d=this.style.fontSize/n;let l=t.rgb(h);const c=this.getNodeState(i.id,this.model),u=this.labelAlphaScaler(n);l.opacity="highlighted"===c?1:"lessened"===c?Math.min(.2,u):u;const g=r.title;f(o).circle(i.x,i.y,s+.5,a).circle(i.x,i.y,s,h).text(g,i.x,i.y+s+1,d,l)})).linkColor((e=>this.getLinkColor(e,this.model)))}linkWithGraphModel(e){this.currentDataModelEntry&&this.currentDataModelEntry.unsub(),this.updateViewData(e.toGraphViewData());const t=e.subscribe((()=>{this.updateViewData(e.toGraphViewData())}));this.currentDataModelEntry={graphModel:e,unsub:t}}getColorOnContainer(e,t){return getComputedStyle(this.container).getPropertyValue(e)||t}updateViewData(e){Object.assign(this.model,e),e.focusedNode&&(this.model.hoverNode=e.focusedNode)}updateCanvasSize(e){this.forceGraph&&("width"in e&&this.forceGraph.width(e.width),"height"in e&&this.forceGraph.height(e.height))}initView(){const{options:e,model:t,style:o,actions:n}=this,r=s.default||globalThis.ForceGraph,h=this.forceGraph||r(),a=e.width||window.innerWidth-this.container.offsetLeft-20,d=e.height||window.innerHeight-this.container.offsetTop-20;h(this.container).height(d).width(a).graphData(t.graphData).linkHoverPrecision(8).enableNodeDrag(!!e.enableNodeDrag).cooldownTime(200).d3Force("x",i.forceX()).d3Force("y",i.forceY()).d3Force("collide",i.forceCollide(h.nodeRelSize())).linkWidth(1).linkDirectionalParticles(1).linkDirectionalParticleWidth((e=>"highlighted"===this.getLinkState(e,t)?2:0)).onEngineStop((()=>{this.hasInitialZoomToFit||(this.hasInitialZoomToFit=!0,h.zoomToFit(1e3,20))})).onNodeHover((e=>{n.highlightNode(this.model,e?.id),this.updateViewModeInteractiveState()})).onNodeClick(((e,t)=>{n.selectNode(this.model,e.id,t.getModifierState("Shift")),this.updateViewModeInteractiveState(),this.fireInteraction("nodeClick",{node:e,event:t})})).onLinkClick(((e,t)=>{this.fireInteraction("linkClick",{link:e,event:t})})).onBackgroundClick((e=>{n.selectNode(this.model,null,e.getModifierState("Shift")),this.updateViewModeInteractiveState(),this.fireInteraction("backgroundClick",{event:e})})).onBackgroundRightClick((e=>{h.zoomToFit(1e3,20),this.fireInteraction("backgroundRightClick",{event:e})})),!1!==e.enableSmartZooming&&this.initGraphSmartZooming(h),this.forceGraph=h,this.refreshByStyle()}initGraphSmartZooming(e){let t=!1;const i=a(200,(e=>{if(t)return;const{x:i,y:o}=this.forceGraph.getGraphBbox(),{k:n,x:r,y:s}=e,h=n*i[0],a=n*i[1],d=n*o[0],l=n*o[1],c=this.forceGraph.width(),u=this.forceGraph.height(),f=this.forceGraph.centerAt();let g,p;a+r<0?(t=!1,g=i[1]):h+r>c&&(g=i[0]),d+s>u?p=o[0]:l+s<0&&(p=o[1]),"number"!=typeof g&&"number"!=typeof p||this.forceGraph.centerAt(void 0!==g?g:f.x,void 0!==p?p:f.y,2e3)}));e.onZoom((e=>{this.hasInitialZoomToFit&&i(e)})).onZoomEnd((()=>{setTimeout((()=>{t=!1}),20)}))}getLinkNodeId(e){const t=typeof e;return"string"===t||"number"===t?e:e.id}getNodeState(e,t=this.model){return t.selectedNodes.has(e)||t.hoverNode===e?"highlighted":0===t.focusNodes.size||t.focusNodes.has(e)?"regular":"lessened"}getLinkState(e,t=this.model){return 0===t.focusNodes.size?"regular":t.focusLinks.has(e.id)?"highlighted":"lessened"}getLinkColor(e,i){const o=this.style,n=o.link;switch(this.getLinkState(e,i)){case"regular":return n.regular;case"highlighted":let r;const s=o.hoverNodeLink;return i.hoverNode===this.getLinkNodeId(e.source)?r=s.highlightedDirection?.outbound:i.hoverNode===this.getLinkNodeId(e.target)&&(r=s.highlightedDirection?.inbound),r||n.highlighted||o.highlightedForeground;case"lessened":let h=n.lessened;if(!h){const e=t.hsl(o.node.note.lessened);e.opacity=.2,h=e}return h;default:throw new Error(`Unknown type for link ${e}`)}}updateViewModeInteractiveState(){const{model:e}=this,t=new Set,i=new Set;if(e.hoverNode){t.add(e.hoverNode);const o=e.nodeInfos[e.hoverNode];o.neighbors?.forEach((e=>t.add(e))),o.linkIds?.forEach((e=>i.add(e)))}e.selectedNodes&&e.selectedNodes.forEach((o=>{t.add(o);const n=e.nodeInfos[o];n.neighbors?.forEach((e=>t.add(e))),n.linkIds?.forEach((e=>i.add(e)))})),e.focusNodes=t,e.focusLinks=i}setSelectedNodes(e,t=!1){t||this.model.selectedNodes.clear(),e.forEach((e=>this.actions.selectNode(this.model,e,!0))),this.updateViewModeInteractiveState()}onInteraction(e,t){this.interactionCallbacks[e]||(this.interactionCallbacks[e]=[]);const i=this.interactionCallbacks[e];return i.push(t),()=>{const e=i.indexOf(t);e>-1&&i.splice(e,1)}}fireInteraction(e,t){const i=this.interactionCallbacks[e];i&&i.forEach((e=>e(t)))}dispose(){this.forceGraph&&this.forceGraph.pauseAnimation()}},e.getColorOnContainer=c,e.getDefaultColorOf=u,Object.defineProperty(e,"__esModule",{value:!0})}));
{
"name": "note-graph",
"description": "a generic visualization tool designed to show the structure of the document space and the relations between each doc",
"version": "0.1.0",
"version": "0.1.1",
"scripts": {
"bootstrap": "lerna bootstrap",
"dev": "cd demo && yarn start",
"build": "microbundle --name NOTE_GRAPH --no-compress --no-sourcemap --globals d3-force=d3,d3-scale=d3,d3-color=d3",
"build": "rollup --config build/rollup.config.js",
"build-demo": "cd demo && yarn build",
"ci-build": "cd demo && yarn && yarn build",
"prepublishOnly": "yarn build",
"lint": "eslint src/**/*.ts"

@@ -15,3 +16,3 @@ },

"main": "dist/note-graph.js",
"umd:main": "dist/note-graph.umd.js",
"browser": "dist/note-graph.umd.js",
"module": "dist/note-graph.esm.js",

@@ -41,4 +42,6 @@ "files": [

"devDependencies": {
"@babel/core": "^7.12.9",
"@babel/preset-env": "^7.12.7",
"@babel/preset-typescript": "^7.12.7",
"@rollup/plugin-commonjs": "^17.0.0",
"@types/d3-color": "^2.0.1",

@@ -51,5 +54,8 @@ "@types/d3-force": "^2.1.0",

"lerna": "^3.22.1",
"microbundle": "^0.12.4",
"rollup": "^2.34.1",
"rollup-plugin-bundle-size": "^1.0.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.29.0",
"typescript": "4"
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc