Skip to content

Commit

Permalink
Add new graph renderer with HTML (beta)
Browse files Browse the repository at this point in the history
  • Loading branch information
Godefroy committed Sep 26, 2024
1 parent b38238a commit f2f0fc1
Show file tree
Hide file tree
Showing 27 changed files with 732 additions and 157 deletions.
4 changes: 2 additions & 2 deletions packages/webapp/src/features/circle/pages/CirclesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Title } from '@/common/atoms/Title'
import { useElementSize } from '@/common/hooks/useElementSize'
import useOverflowHidden from '@/common/hooks/useOverflowHidden'
import useQueryParams from '@/common/hooks/useQueryParams'
import CirclesCanvasGraph from '@/graph/CirclesCanvasGraph'
import CirclesHTMLGraph from '@/graph/CirclesHTMLGraph'
import CirclesSVGGraph from '@/graph/CirclesSVGGraph'
import { GraphProvider } from '@/graph/contexts/GraphContext'
import useCirclesEvents from '@/graph/hooks/useGraphEvents'
Expand Down Expand Up @@ -133,7 +133,7 @@ export default function CirclesPage() {
circles &&
boxSize &&
(beta ? (
<CirclesCanvasGraph
<CirclesHTMLGraph
key={view + colorMode}
view={view}
circles={circles}
Expand Down
7 changes: 5 additions & 2 deletions packages/webapp/src/features/graph/CirclesCanvasGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { CirclesGraph } from './graphs/CirclesGraph'
import useCirclesGraph, { CirclesGraphProps } from './hooks/useCirclesGraph'
import useRenderer from './hooks/useRenderer'
import { CanvasRenderer } from './renderers/canvas/CanvasRenderer'

// Force reset with fast refresh
// @refresh reset
Expand All @@ -10,10 +12,11 @@ export default forwardRef<CirclesGraph | undefined, CirclesGraphProps>(
const canvasRef = useRef<HTMLCanvasElement>(null)

// Instanciate graph
const graphRef = useCirclesGraph(canvasRef, props)
const graph = useCirclesGraph(canvasRef, props)
useRenderer(graph, (graph) => new CanvasRenderer(graph))

// Expose ref
useImperativeHandle(ref, () => graphRef.current)
useImperativeHandle(ref, () => graph)

return <canvas ref={canvasRef} />
}
Expand Down
60 changes: 60 additions & 0 deletions packages/webapp/src/features/graph/CirclesHTMLGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Box } from '@chakra-ui/react'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { CirclesGraph } from './graphs/CirclesGraph'
import useCirclesGraph, { CirclesGraphProps } from './hooks/useCirclesGraph'
import CirclesTitles from './renderers/html/CirclesTitles'
import Nodes from './renderers/html/Nodes'
import { Panzoom } from './renderers/html/Panzoom'

// Force reset with fast refresh
// @refresh reset

export default forwardRef<CirclesGraph | undefined, CirclesGraphProps>(
function CirclesHTMLGraph(props, ref) {
const containerRef = useRef<HTMLDivElement>(null)

// Instanciate graph
const graph = useCirclesGraph(containerRef, props)

// Expose ref
useImperativeHandle(ref, () => graph)

const focusWidth =
props.width - (props.focusCrop?.left || 0) - (props.focusCrop?.right || 0)

const focusHeight =
props.height -
(props.focusCrop?.top || 0) -
(props.focusCrop?.bottom || 0)

const graphMinSize = Math.min(focusWidth, focusHeight)

// Click outside => unselect circle
const handleClickOutside = (event: React.MouseEvent<HTMLDivElement>) => {
if (event.target === containerRef.current) {
props.events?.onClickOutside?.()
}
}

return (
<Box
ref={containerRef}
width={props.width}
height={props.height}
style={
{
'--graph-min-size': graphMinSize,
} as React.CSSProperties
}
onClick={handleClickOutside}
>
{graph && (
<Panzoom graph={graph}>
<Nodes graph={graph} />
<CirclesTitles graph={graph} />
</Panzoom>
)}
</Box>
)
}
)
12 changes: 9 additions & 3 deletions packages/webapp/src/features/graph/CirclesSVGGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useColorMode } from '@chakra-ui/react'
import React, { forwardRef, useImperativeHandle, useRef } from 'react'
import { nanoid } from 'nanoid'
import React, { forwardRef, useImperativeHandle, useMemo, useRef } from 'react'
import { CirclesGraph } from './graphs/CirclesGraph'
import useCirclesGraph, { CirclesGraphProps } from './hooks/useCirclesGraph'
import useRenderer from './hooks/useRenderer'
import { StyledSVG } from './renderers/svg/StyledSVG'
import { SVGRenderer } from './renderers/svg/SVGRenderer'

// Force reset with fast refresh
// @refresh reset
Expand All @@ -11,15 +14,18 @@ export default forwardRef<CirclesGraph | undefined, CirclesGraphProps>(
function CirclesSVGGraph(props, ref) {
const { colorMode } = useColorMode()
const svgRef = useRef<SVGSVGElement>(null)
const id = useMemo(() => nanoid(), [])

// Instanciate graph
const graphRef = useCirclesGraph(svgRef, props)
const graph = useCirclesGraph(svgRef, props)
useRenderer(graph, (graph) => new SVGRenderer(graph))

// Expose ref
useImperativeHandle(ref, () => graphRef.current)
useImperativeHandle(ref, () => graph)

return (
<StyledSVG
id={id}
ref={svgRef}
view={props.view}
width={props.width}
Expand Down
71 changes: 31 additions & 40 deletions packages/webapp/src/features/graph/graphs/CirclesGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import { Participant } from '@rolebase/shared/model/member'
import { textEllipsis } from '@utils/textEllipsis'
import * as d3 from 'd3'
import { HierarchyNode } from 'd3'
import Renderer from '../renderers/Renderer'
import { CanvasRenderer } from '../renderers/canvas/CanvasRenderer'
import { SVGRenderer } from '../renderers/svg/SVGRenderer'
import settings from '../settings'
import { Data, GraphParams, NodeType, RootElement } from '../types'
import { Graph } from './Graph'
Expand All @@ -21,31 +18,16 @@ export interface CircleData extends CircleFullFragment {
}

export abstract class CirclesGraph extends Graph<CircleFullFragment[]> {
public renderer: Renderer
public origCircles: CircleFullFragment[] = []

constructor(
public element: RootElement,
element: RootElement,
public params: GraphParams
) {
super(element, params)

// Instanciate renderer
const tagName = element.tagName.toLowerCase()
if (tagName === 'canvas') {
this.renderer = new CanvasRenderer(this)
} else if (tagName === 'svg') {
this.renderer = new SVGRenderer(this)
} else {
throw new Error(
`Graph: Element tag name must be "canvas" or "svg", got "${element.tagName}"`
)
}
}

destroy() {
this.renderer.destroy()

// @ts-ignore
this.element = undefined
// @ts-ignore
Expand Down Expand Up @@ -180,26 +162,33 @@ export abstract class CirclesGraph extends Graph<CircleFullFragment[]> {
.sum((d) => d.value || 0)
.sort(this.packSorting)

return d3
.pack<Data>()
.radius(() => settings.memberValue)
.padding((d) => {
// Circle
if (d.data.type === NodeType.Circle) {
const hasSubCircles = d.data.children?.some(
(c) => c.type === NodeType.Circle
)
if (!hasSubCircles) return settings.padding.circleWithoutSubCircle
const multipleChildren = (d.data.children?.length || 0) > 1
return multipleChildren
? settings.padding.circleWithSubCircles
: settings.padding.circleWithSingleSubCircle
} else if (d.data.type === NodeType.MembersCircle) {
// Members Circle
return settings.padding.membersCircle
}
return 0
})(hierarchyNode)
return (
d3
.pack<Data>()
.radius(() => settings.memberValue)
.padding((d) => {
// Circle
if (d.data.type === NodeType.Circle) {
const hasSubCircles = d.data.children?.some(
(c) => c.type === NodeType.Circle
)
if (!hasSubCircles) return settings.padding.circleWithoutSubCircle
const multipleChildren = (d.data.children?.length || 0) > 1
return multipleChildren
? settings.padding.circleWithSubCircles
: settings.padding.circleWithSingleSubCircle
} else if (d.data.type === NodeType.MembersCircle) {
// Members Circle
return settings.padding.membersCircle
}
return 0
})(hierarchyNode)

// Sort by depth and Y, then raise
.sort((a, b) =>
a.depth === b.depth ? a.y - b.y : a.depth < b.depth ? -1 : 1
)
)
}

updateData(circles: CircleFullFragment[]) {
Expand Down Expand Up @@ -235,6 +224,8 @@ export abstract class CirclesGraph extends Graph<CircleFullFragment[]> {
)
}

this.emit('nodesData', nodesMap.slice(1))
// Save and dispatch nodes data
this.nodes = nodesMap.slice(1)
this.emit('nodesData', this.nodes)
}
}
Loading

0 comments on commit f2f0fc1

Please sign in to comment.