Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ root = true
end_of_line = lf
insert_final_newline = true

[*.{js,cjs,mjs,json,ts,cjs,mts,jsx,tsx}]
[*.{css,js,cjs,mjs,json,ts,cjs,mts,jsx,tsx}]
charset = utf-8
indent_style = tab
indent_size = 2
Expand Down
2 changes: 2 additions & 0 deletions .storybook/preview.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { Preview } from "@storybook/react";

import './stories.css'

const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
Expand Down
7 changes: 7 additions & 0 deletions .storybook/stories.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.even {
fill: red;
}

.odd {
stroke-dasharray: 1.5 0.5;
}
150 changes: 74 additions & 76 deletions rollup.config.prod.mjs
Original file line number Diff line number Diff line change
@@ -1,89 +1,87 @@
import { defineConfig } from 'rollup'
import pluginTs from '@rollup/plugin-typescript'
import dts from 'rollup-plugin-dts'
import pluginTs from '@rollup/plugin-typescript'
import terser from '@rollup/plugin-terser'

const input = 'src/BeautifulTree.tsx'
const external = ['react', 'react-dom', 'react/jsx-runtime']
const globals = {
react: 'React',
'react-dom': 'ReactDOM',
'react-dom': 'ReactDOM',
'react/jsx-runtime': 'jsxRuntime',
}

export default defineConfig([
{
input,
output: [
{
file: 'dist/beautiful-tree.cjs',
format: 'cjs',
globals,
sourcemap: true
},
{
file: 'dist/beautiful-tree.mjs',
format: 'es',
globals,
sourcemap: true
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.iife.js',
format: 'iife',
globals,
sourcemap: true
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.umd.js',
format: 'umd',
globals,
sourcemap: true,
},
],
external,
plugins: [pluginTs()]
},
{
input,
output: [
{
file: 'dist/beautiful-tree.min.cjs',
format: 'cjs',
globals,
sourcemap: true,
},
{
file: 'dist/beautiful-tree.min.mjs',
format: 'es',
globals,
sourcemap: true,
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.min.iife.js',
format: 'iife',
globals,
sourcemap: true,
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.min.umd.js',
format: 'umd',
globals,
sourcemap: true,
},
],
external,
plugins: [pluginTs(), terser()]
},
{
input,
output: [
{ file: 'dist/beautiful-tree.d.ts' },
],
external,
plugins: [dts()]
},
{
input,
output: [
{
file: 'dist/beautiful-tree.cjs',
format: 'cjs',
globals,
sourcemap: true,
},
{
file: 'dist/beautiful-tree.mjs',
format: 'es',
globals,
sourcemap: true,
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.iife.js',
format: 'iife',
globals,
sourcemap: true,
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.umd.js',
format: 'umd',
globals,
sourcemap: true,
},
],
external,
plugins: [pluginTs()],
},
{
input,
output: [
{
file: 'dist/beautiful-tree.min.cjs',
format: 'cjs',
globals,
sourcemap: true,
},
{
file: 'dist/beautiful-tree.min.mjs',
format: 'es',
globals,
sourcemap: true,
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.min.iife.js',
format: 'iife',
globals,
sourcemap: true,
},
{
name: 'BeautifulTree',
file: 'dist/beautiful-tree.min.umd.js',
format: 'umd',
globals,
sourcemap: true,
},
],
external,
plugins: [pluginTs(), terser()],
},
{
input,
output: [{ file: 'dist/beautiful-tree.d.ts' }],
external,
plugins: [dts()],
},
])
53 changes: 35 additions & 18 deletions src/BeautifulTree.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { edgesIterator, postOrderIterator } from './traversal'
import type { Tree } from './types'
import type { WrappedTreeWithLayout } from './layouts'
export { computeLeftShiftLayout, computeCenter3Layout } from './layouts'
export { computeNaiveLayout, computeSmartLayout } from './layouts'

export type CssClassesGetter = (
data?: Readonly<Record<string, unknown>> | undefined,
) => string[]

export interface BeautifulTreeProps {
readonly id: string
Expand All @@ -14,19 +18,31 @@ export interface BeautifulTreeProps {
readonly computeLayout: (
tree: Readonly<Tree>,
) => Readonly<WrappedTreeWithLayout>
readonly getNodeClass?: CssClassesGetter | undefined
readonly getEdgeClass?: CssClassesGetter | undefined
}

function runClassesGetter(
classesGetter?: CssClassesGetter | undefined,
data?: Readonly<Record<string, unknown>> | undefined,
): string {
const cssClasses = classesGetter?.(data) ?? []
return cssClasses.length === 0 ? '' : ` ${cssClasses.join(' ')}`
}

export function BeautifulTree({
id,
svgProps,
tree,
computeLayout,
getNodeClass: nodeClassesInferrer,
getEdgeClass: edgeClassesInferrer,
}: Readonly<BeautifulTreeProps>): JSX.Element {
const { tree: treeWithLayout, maxX, maxY } = computeLayout(tree)
const { tree: treeWithLayout, mX, mY } = computeLayout(tree)
const { width, height, sizeUnit = 'px' } = svgProps

const xCoef = width / (maxX + 2)
const yCoef = height / (maxY + 2)
const xCoef = width / (mX + 2)
const yCoef = height / (mY + 2)
const maxNodeWidth = xCoef * 0.25
const maxNodeHeight = yCoef * 0.25
const maxNodeRadius = Math.min(maxNodeWidth, maxNodeHeight)
Expand All @@ -42,34 +58,35 @@ export function BeautifulTree({
}}
className={'beautiful-tree-react'}
>
<style>{`
line { stroke: black; }
circle { stroke: black; fill: white; }
`}</style>
{/* TODO: introduce edge "styles" (straight, cornered, curved..., plus CSS styles) */}
{[...edgesIterator(treeWithLayout)].map((edge, idx) => {
<style>{'line{stroke:black;}circle{stroke:black;fill:white;}'}</style>
{Array.from(edgesIterator(treeWithLayout), (edge, idx) => {
return (
<line
key={`${id}-edge-${idx}`}
className={'beautiful-tree-edge'}
className={`beautiful-tree-edge${runClassesGetter(
edgeClassesInferrer,
edge.eData,
)}`}
x1={(edge.start.x + 1) * xCoef}
y1={(edge.start.y + 1) * yCoef}
x2={(edge.end.x + 1) * xCoef}
y2={(edge.end.y + 1) * yCoef}
/>
)
})}
{[...postOrderIterator(treeWithLayout)].map((node, idx) => {
const aX = node.meta.pos.x
const aY = node.meta.pos.y
{Array.from(postOrderIterator(treeWithLayout), (node, idx) => {
const nm = node.meta
return (
<circle
key={`${id}-node-${idx}`}
className={`beautiful-tree-node${
node.meta.isRoot ? ' beautiful-tree-root' : ''
}${node.meta.isLeaf ? ' beautiful-tree-leaf' : ''}`}
cx={(aX + 1) * xCoef}
cy={(aY + 1) * yCoef}
nm.isRoot ? ' beautiful-tree-root' : ''
}${nm.isLeaf ? ' beautiful-tree-leaf' : ''}${runClassesGetter(
nodeClassesInferrer,
node.data,
)}`}
cx={(nm.pos.x + 1) * xCoef}
cy={(nm.pos.y + 1) * yCoef}
r={maxNodeRadius}
/>
)
Expand Down
Loading