note-graph
Advanced tools
Comparing version 0.1.0 to 0.1.1
@@ -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 @@ |
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" | ||
} | ||
} |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
74284
16
1539
3