markmap-lib
Version:
Visualize your Markdown as mindmaps with Markmap
373 lines (332 loc) • 12.3 kB
JavaScript
;
var _interopRequireWildcard = require("@babel/runtime/helpers/interopRequireWildcard");
exports.__esModule = true;
exports.markmap = markmap;
exports.loadPlugins = loadPlugins;
exports.plugins = exports.Markmap = void 0;
var d3 = _interopRequireWildcard(require("d3"));
var _d3Flextree = require("d3-flextree");
var _util = require("./util");
var plugins = _interopRequireWildcard(require("./plugins"));
exports.plugins = plugins;
var _hook = require("./util/hook");
function linkWidth(nodeData) {
const data = nodeData.data;
return Math.max(6 - 2 * data.d, 1.5);
}
function adjustSpacing(tree, spacing) {
(0, _util.walkTree)(tree, (d, next) => {
d.ySizeInner = d.ySize - spacing;
d.y += spacing;
next();
}, 'children');
}
class Markmap {
constructor(svg, opts) {
this.options = void 0;
this.state = void 0;
this.svg = void 0;
this.styleNode = void 0;
this.g = void 0;
this.zoom = void 0;
['handleZoom', 'handleClick'].forEach(key => {
this[key] = this[key].bind(this);
});
this.svg = svg.datum ? svg : d3.select(svg);
this.styleNode = this.svg.append('style');
this.zoom = d3.zoom().on('zoom', this.handleZoom);
this.options = Object.assign({
duration: 500,
nodeFont: '300 16px/20px sans-serif',
nodeMinHeight: 16,
spacingVertical: 5,
spacingHorizontal: 80,
autoFit: false,
fitRatio: 0.95,
color: (colorFn => node => colorFn(node.p.i))(d3.scaleOrdinal(d3.schemeCategory10)),
paddingX: 8
}, opts);
this.state = {
id: this.options.id || (0, _util.getId)()
};
this.g = this.svg.append('g').attr('class', `${this.state.id}-g`);
this.updateStyle();
this.svg.call(this.zoom);
}
getStyleContent() {
const {
style,
nodeFont
} = this.options;
const {
id
} = this.state;
const extraStyle = typeof style === 'function' ? style(id) : '';
const styleText = `\
.${id} a { color: #0097e6; }
.${id} a:hover { color: #00a8ff; }
.${id}-g > path { fill: none; }
.${id}-fo > div { font: ${nodeFont}; white-space: nowrap; }
.${id}-fo code { padding: .2em .4em; font-size: calc(1em - 2px); color: #555; background-color: #f0f0f0; border-radius: 2px; }
.${id}-fo del { text-decoration: line-through; }
.${id}-fo em { font-style: italic; }
.${id}-fo strong { font-weight: 500; }
.${id}-fo pre { margin: 0; }
.${id}-fo pre[class*=language-] { padding: 0; }
.${id}-g > g { cursor: pointer; }
${extraStyle}
`;
return styleText;
}
updateStyle() {
this.svg.attr('class', (0, _util.addClass)(this.svg.attr('class'), this.state.id));
this.styleNode.text(this.getStyleContent());
}
handleZoom() {
const {
transform
} = d3.event;
this.g.attr('transform', transform);
}
handleClick(d) {
var _data$p;
const {
data
} = d;
data.p = Object.assign(Object.assign({}, data.p), {}, {
f: !((_data$p = data.p) == null ? void 0 : _data$p.f)
});
this.renderData(d.data);
}
initializeData(node) {
let i = 0;
const {
nodeFont,
color,
nodeMinHeight
} = this.options;
const {
id
} = this.state;
const container = document.createElement('div');
const containerClass = `${id}-container`;
container.className = (0, _util.addClass)(container.className, `${id}-fo`, containerClass);
const style = document.createElement('style');
style.textContent = `
${this.getStyleContent()}
.${containerClass} {
position: absolute;
width: 0;
height: 0;
top: -100px;
left: -100px;
overflow: hidden;
font: ${nodeFont};
}
.${containerClass} > div {
display: inline-block;
}
`;
document.body.append(style, container);
(0, _util.walkTree)(node, (item, next) => {
var _item$c;
item.c = (_item$c = item.c) == null ? void 0 : _item$c.map(child => Object.assign({}, child));
i += 1;
const el = document.createElement('div');
el.innerHTML = item.v;
container.append(el);
item.p = Object.assign(Object.assign({}, item.p), {}, {
// unique ID
i,
el
});
color(item); // preload colors
next();
});
const nodes = (0, _util.arrayFrom)(container.childNodes);
this.constructor.transformHtml.call(this, nodes);
(0, _util.walkTree)(node, (item, next, parent) => {
var _parent$p;
const rect = item.p.el.getBoundingClientRect();
item.v = item.p.el.innerHTML;
item.p.s = [Math.ceil(rect.width), Math.max(Math.ceil(rect.height), nodeMinHeight)]; // TODO keep keys for unchanged objects
// unique key, should be based on content
item.p.k = `${(parent == null ? void 0 : (_parent$p = parent.p) == null ? void 0 : _parent$p.i) || ''}.${item.p.i}:${item.v}`;
next();
});
container.remove();
style.remove();
}
setOptions(opts) {
Object.assign(this.options, opts);
}
setData(data, opts) {
if (!data) data = Object.assign({}, this.state.data);
this.state.data = data;
this.initializeData(data);
if (opts) this.setOptions(opts);
this.renderData();
}
renderData(originData) {
var _origin$data$p$x, _origin$data$p$y;
if (!this.state.data) return;
const {
spacingHorizontal,
paddingX,
spacingVertical,
autoFit,
color
} = this.options;
const {
id
} = this.state;
const layout = (0, _d3Flextree.flextree)().children(d => {
var _d$p;
return !((_d$p = d.p) == null ? void 0 : _d$p.f) && d.c;
}).nodeSize(d => {
const [width, height] = d.data.p.s;
return [height, width + (width ? paddingX * 2 : 0) + spacingHorizontal];
}).spacing((a, b) => {
return a.parent === b.parent ? spacingVertical : spacingVertical * 2;
});
const tree = layout.hierarchy(this.state.data);
layout(tree);
adjustSpacing(tree, spacingHorizontal);
const descendants = tree.descendants().reverse();
const links = tree.links();
const linkShape = d3.linkHorizontal();
const minX = d3.min(descendants, d => d.x - d.xSize / 2);
const maxX = d3.max(descendants, d => d.x + d.xSize / 2);
const minY = d3.min(descendants, d => d.y);
const maxY = d3.max(descendants, d => d.y + d.ySizeInner);
Object.assign(this.state, {
minX,
maxX,
minY,
maxY
});
if (autoFit) this.fit();
const origin = originData && descendants.find(item => item.data === originData) || tree;
const x0 = (_origin$data$p$x = origin.data.p.x0) != null ? _origin$data$p$x : origin.x;
const y0 = (_origin$data$p$y = origin.data.p.y0) != null ? _origin$data$p$y : origin.y; // Update the nodes
const node = this.g.selectAll((0, _util.childSelector)('g')).data(descendants, d => d.data.p.k);
const nodeEnter = node.enter().append('g').attr('transform', d => `translate(${y0 + origin.ySizeInner - d.ySizeInner},${x0 + origin.xSize / 2 - d.xSize})`).on('click', this.handleClick);
const nodeExit = this.transition(node.exit());
nodeExit.select('rect').attr('width', 0).attr('x', d => d.ySizeInner);
nodeExit.select('foreignObject').style('opacity', 0);
nodeExit.attr('transform', d => `translate(${origin.y + origin.ySizeInner - d.ySizeInner},${origin.x + origin.xSize / 2 - d.xSize})`).remove();
const nodeMerge = node.merge(nodeEnter);
this.transition(nodeMerge).attr('transform', d => `translate(${d.y},${d.x - d.xSize / 2})`);
const rect = nodeMerge.selectAll((0, _util.childSelector)('rect')).data(d => [d], d => d.data.p.k).join(enter => {
return enter.append('rect').attr('x', d => d.ySizeInner).attr('y', d => d.xSize - linkWidth(d) / 2).attr('width', 0).attr('height', linkWidth);
}, update => update, exit => exit.remove());
this.transition(rect).attr('x', -1).attr('width', d => d.ySizeInner + 2).attr('fill', d => color(d.data));
const circle = nodeMerge.selectAll((0, _util.childSelector)('circle')).data(d => d.data.c ? [d] : [], d => d.data.p.k).join(enter => {
return enter.append('circle').attr('stroke-width', '1.5').attr('cx', d => d.ySizeInner).attr('cy', d => d.xSize).attr('r', 0);
}, update => update, exit => exit.remove());
this.transition(circle).attr('r', 6).attr('stroke', d => color(d.data)).attr('fill', d => {
var _d$data$p;
return ((_d$data$p = d.data.p) == null ? void 0 : _d$data$p.f) ? color(d.data) : '#fff';
});
const foreignObject = nodeMerge.selectAll((0, _util.childSelector)('foreignObject')).data(d => [d], d => d.data.p.k).join(enter => {
const fo = enter.append('foreignObject').attr('class', `${id}-fo`).attr('x', paddingX).attr('y', 0).style('opacity', 0).attr('height', d => d.xSize);
fo.append('xhtml:div').select(function (d) {
const node = d.data.p.el.cloneNode(true);
this.replaceWith(node);
return node;
}).attr('xmlns', 'http://www.w3.org/1999/xhtml');
return fo;
}, update => update, exit => exit.remove()).attr('width', d => Math.max(0, d.ySizeInner - paddingX * 2));
this.transition(foreignObject).style('opacity', 1); // Update the links
const path = this.g.selectAll((0, _util.childSelector)('path')).data(links, d => d.target.data.p.k).join(enter => {
const source = [y0 + origin.ySizeInner, x0 + origin.xSize / 2];
return enter.insert('path', 'g').attr('d', linkShape({
source,
target: source
}));
}, update => update, exit => {
const source = [origin.y + origin.ySizeInner, origin.x + origin.xSize / 2];
return this.transition(exit).attr('d', linkShape({
source,
target: source
})).remove();
});
this.transition(path).attr('stroke', d => color(d.target.data)).attr('stroke-width', d => linkWidth(d.target)).attr('d', d => {
const source = [d.source.y + d.source.ySizeInner, d.source.x + d.source.xSize / 2];
const target = [d.target.y, d.target.x + d.target.xSize / 2];
return linkShape({
source,
target
});
});
descendants.forEach(d => {
d.data.p.x0 = d.x;
d.data.p.y0 = d.y;
});
}
transition(sel) {
const {
duration
} = this.options;
return sel.transition().duration(duration);
}
fit() {
const svgNode = this.svg.node();
const {
width: offsetWidth,
height: offsetHeight
} = svgNode.getBoundingClientRect();
const {
fitRatio
} = this.options;
const {
minX,
maxX,
minY,
maxY
} = this.state;
const naturalWidth = maxY - minY;
const naturalHeight = maxX - minX;
const scale = Math.min(offsetWidth / naturalWidth * fitRatio, offsetHeight / naturalHeight * fitRatio, 2);
const initialZoom = d3.zoomIdentity.translate((offsetWidth - naturalWidth * scale) / 2 - minY * scale, (offsetHeight - naturalHeight * scale) / 2 - minX * scale).scale(scale);
return this.transition(this.svg).call(this.zoom.transform, initialZoom).end();
}
rescale(scale) {
const svgNode = this.svg.node();
const {
width: offsetWidth,
height: offsetHeight
} = svgNode.getBoundingClientRect();
const halfWidth = offsetWidth / 2;
const halfHeight = offsetHeight / 2;
const transform = d3.zoomTransform(svgNode);
const newTransform = transform.translate((halfWidth - transform.x) * (1 - scale) / transform.k, (halfHeight - transform.y) * (1 - scale) / transform.k).scale(scale);
return this.transition(this.svg).call(this.zoom.transform, newTransform).end();
}
static create(svg, opts, data) {
const mm = new Markmap(svg, opts);
if (data) {
mm.setData(data);
mm.fit(); // always fit for the first render
}
return mm;
}
}
exports.Markmap = Markmap;
Markmap.transformHtml = new _hook.Hook();
function markmap(svg, data, opts) {
return Markmap.create(svg, opts, data);
}
async function loadPlugins(items, options) {
items = items.map(item => {
if (typeof item === 'string') {
const name = item;
item = plugins[name];
if (!item) {
console.warn(`[markmap] Unknown plugin: ${name}`);
}
}
return item;
}).filter(Boolean);
return (0, _util.initializePlugins)(Markmap, items, options);
}