@sequencemedia/graphviz-react
Version:
React component for displaying Graphviz graphs
201 lines (168 loc) • 3.98 kB
JSX
import React, {
useState,
useEffect,
useCallback
} from 'react'
import PropTypes from 'prop-types'
import classnames from 'classnames'
import {
graphviz
} from 'd3-graphviz'
import debug from 'debug'
const DEFAULT_OPTIONS = {
useWorker: false
}
function handleEntries (entries) {
for (const entry of entries) {
if (hasEntryTarget(entry)) {
const target = getEntryTarget(entry)
if (target instanceof Element) {
const svg = target.querySelector('svg')
if (svg instanceof SVGElement) {
const {
contentRect: {
width,
height
}
} = entry
svg.setAttribute('width', width + 'px')
svg.setAttribute('height', height + 'px')
}
}
}
}
}
function DEFAULT_HANDLE_EVENT () {
//
}
export function hasEventTarget ({ target }) {
return (target instanceof Element)
}
export function getEventTarget ({ target }) {
if (target instanceof Element) return target
return null
}
export function hasEntryTarget ({ target }) {
return (target instanceof Element)
}
export function getEntryTarget ({ target }) {
if (target instanceof Element) return target
return null
}
/**
* @param {React.RefObject<any>} ref
* @returns {ref is React.RefObject<HTMLElement>}
*/
export function hasCurrent (ref = { current: null }) {
const {
current // = null
} = ref
return (current instanceof Element)
}
/**
* @param {React.RefObject<HTMLElement>} ref
* @returns {HTMLElement}
*/
export function getCurrent ({ current }) {
return current
}
const resizeObserver = new ResizeObserver(handleEntries)
const log = debug('@sequencemedia/graphviz-react')
export default function GraphvizReact ({
graphRef: ref,
dot,
className,
options = DEFAULT_OPTIONS,
onStart = DEFAULT_HANDLE_EVENT,
onRenderStart = DEFAULT_HANDLE_EVENT,
onRenderEnd = DEFAULT_HANDLE_EVENT,
onRenderDot = DEFAULT_HANDLE_EVENT,
onEnd = DEFAULT_HANDLE_EVENT,
onClick = DEFAULT_HANDLE_EVENT
}) {
log('GraphvizReact')
const [
eventEmitter,
setEventEmitter
] = useState(null)
useEffect(() => {
const {
fit = false
} = options
if (fit) {
if (hasCurrent(ref)) {
const current = getCurrent(ref)
resizeObserver.observe(current)
return () => {
resizeObserver.unobserve(current)
}
}
}
})
useEffect(() => {
if (hasCurrent(ref)) {
const current = getCurrent(ref)
const OPTIONS = {
...DEFAULT_OPTIONS,
...options
}
const eventEmitter = (
graphviz(current, OPTIONS)
.renderDot(dot, onRenderDot)
)
setEventEmitter(eventEmitter)
}
}, [dot, options])
useEffect(() => {
if (eventEmitter) {
eventEmitter
.on('start', onStart)
.on('renderStart', onRenderStart)
.on('renderEnd', onRenderEnd)
.on('end', onEnd)
}
}, [
eventEmitter,
onStart,
onRenderStart,
onRenderEnd,
onEnd
])
const handleClick = useCallback(function handleClick (event) {
if (hasEventTarget(event)) {
const target = getEventTarget(event)
if (target instanceof Element) {
if (hasCurrent(ref)) {
const current = getCurrent(ref) ?? { contains () { return false } }
if (current.contains(target)) onClick(event)
}
}
}
}, [
dot,
options,
onClick
])
log(ref)
return (
<div
className={classnames('graphviz', className)}
onClick={handleClick}
ref={ref}
/>
)
}
GraphvizReact.propTypes = {
graphRef: PropTypes.shape({
current: PropTypes.shape()
}),
dot: PropTypes.string.isRequired,
className: PropTypes.string,
options: PropTypes.shape(),
onClick: PropTypes.func,
onStart: PropTypes.func,
onRenderStart: PropTypes.func,
onRenderEnd: PropTypes.func,
onRenderDot: PropTypes.func,
onEnd: PropTypes.func
}