Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Feature: backgroundGrid #513

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
31 changes: 31 additions & 0 deletions demo/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class App extends Component {
this.setLargeTree = this.setLargeTree.bind(this);
this.setOrientation = this.setOrientation.bind(this);
this.setPathFunc = this.setPathFunc.bind(this);
this.setBackgroundGrid = this.setBackgroundGrid.bind(this);
this.handleChange = this.handleChange.bind(this);
this.handleFloatChange = this.handleFloatChange.bind(this);
this.toggleCollapsible = this.toggleCollapsible.bind(this);
Expand Down Expand Up @@ -157,6 +158,10 @@ class App extends Component {
this.setState({ pathFunc });
}

setBackgroundGrid(backgroundGrid) {
this.setState({ backgroundGrid });
}

handleChange(evt) {
const target = evt.target;
const parsedIntValue = parseInt(target.value, 10);
Expand Down Expand Up @@ -389,6 +394,31 @@ class App extends Component {
</button>
</div>

<div className="prop-container">
<h4 className="prop">Background Grid</h4>
<button
type="button"
className="btn btn-controls btn-block"
onClick={() => this.setBackgroundGrid(undefined)}
>
{'None'}
</button>
<button
type="button"
className="btn btn-controls btn-block"
onClick={() => this.setBackgroundGrid({type: 'dot'})}
>
{'Dot'}
</button>
<button
type="button"
className="btn btn-controls btn-block"
onClick={() => this.setBackgroundGrid({type: 'line'})}
>
{'Line'}
</button>
</div>

<div className="prop-container">
<label className="prop" htmlFor="customNodeElement">
Custom Node Element
Expand Down Expand Up @@ -707,6 +737,7 @@ class App extends Component {
onLinkMouseOut={(...args) => {
console.log('onLinkMouseOut', args);
}}
backgroundGrid={this.state.backgroundGrid}
/>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,4 @@
"typedoc": "^0.19.2",
"typescript": "^4.9.4"
}
}
}
81 changes: 81 additions & 0 deletions src/Tree/BackgroundGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import React, { ReactElement } from "react"

export type BackgroundGrid = {
type: "dot" | "line" | "custom",
thickness?: number,
color?: string,
gridCellSize?: {width: number, height: number},
gridCellFunc?: (options?: BackgroundGrid) => ReactElement
}

interface BackgroundGridProps extends BackgroundGrid{
patternInstanceRef: string //a unique className for d3zoom to specify
}

/**
* helper function to assign default values to `thickness`, `color` and `gridCellSize`, which are required by rendering/zooming bgGrid
*/
export const getDefaultBackgroundGridParam = (param: BackgroundGrid | BackgroundGridProps) => {
if (param === undefined) return undefined;
const {
thickness = 2,
color = "#bbb",
gridCellSize = { width: 24, height: 24 },
} = param;

return {
thickness,
color,
gridCellSize,
...param
};
}

const BackgroundGrid = (props: BackgroundGridProps) => {

const param = getDefaultBackgroundGridParam(props);
const {
type,
thickness,
color,
gridCellSize,
gridCellFunc
} = param;

return <>
<pattern
id="bgPattern"
className={`rd3t-pattern ${props.patternInstanceRef}`}
patternUnits="userSpaceOnUse"
width={gridCellSize.width}
height={gridCellSize.height}
>
{
type === "dot"
? <rect
width={thickness}
height={thickness}
rx={thickness}
fill={color}
/>
: null
}
{
type === "line"
? <>
<line strokeWidth={thickness} stroke={color} x1="0" y1="0" x2={gridCellSize.width} y2="0" opacity="1"/>
<line strokeWidth={thickness} stroke={color} x1="0" y1="0" x2="0" y2={gridCellSize.height} opacity="1"/>
</>
: null
}
{
type === "custom" && gridCellFunc
? gridCellFunc(param)
: null
}
</pattern>
<rect fill="url(#bgPattern)" width="100%" height="100%" id='bgPatternContainer'/>
</>
}

export default BackgroundGrid;
32 changes: 31 additions & 1 deletion src/Tree/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Link from '../Link/index.js';
import { TreeNodeDatum, Point, RawNodeDatum } from '../types/common.js';
import { TreeLinkEventCallback, TreeNodeEventCallback, TreeProps } from './types.js';
import globalCss from '../globalCss.js';
import BackgroundGrid, { getDefaultBackgroundGridParam } from './backgroundGrid.js';

type TreeState = {
dataRef: TreeProps['data'];
Expand Down Expand Up @@ -56,6 +57,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
dimensions: undefined,
centeringTransitionDuration: 800,
dataKey: undefined,
backgroundGrid: undefined,
};

state: TreeState = {
Expand All @@ -74,6 +76,7 @@ class Tree extends React.Component<TreeProps, TreeState> {

svgInstanceRef = `rd3t-svg-${uuidv4()}`;
gInstanceRef = `rd3t-g-${uuidv4()}`;
patternInstanceRef = `rd3t-pattern-${uuidv4()}`;

static getDerivedStateFromProps(nextProps: TreeProps, prevState: TreeState) {
let derivedState: Partial<TreeState> = null;
Expand Down Expand Up @@ -113,7 +116,8 @@ class Tree extends React.Component<TreeProps, TreeState> {
this.props.zoomable !== prevProps.zoomable ||
this.props.draggable !== prevProps.draggable ||
this.props.zoom !== prevProps.zoom ||
this.props.enableLegacyTransitions !== prevProps.enableLegacyTransitions
this.props.enableLegacyTransitions !== prevProps.enableLegacyTransitions ||
this.props.backgroundGrid !== prevProps.backgroundGrid
) {
// If zoom-specific props change -> rebind listener with new values.
// Or: rebind zoom listeners to new DOM nodes in case legacy transitions were enabled/disabled.
Expand Down Expand Up @@ -152,6 +156,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
const { zoomable, scaleExtent, translate, zoom, onUpdate, hasInteractiveNodes } = props;
const svg = select(`.${this.svgInstanceRef}`);
const g = select(`.${this.gInstanceRef}`);
const pattern = select(`.${this.patternInstanceRef}`);

// Sets initial offset, so that first pan and zoom does not jump back to default [0,0] coords.
// @ts-ignore
Expand All @@ -165,6 +170,7 @@ class Tree extends React.Component<TreeProps, TreeState> {
return (
event.target.classList.contains(this.svgInstanceRef) ||
event.target.classList.contains(this.gInstanceRef) ||
event.target.id === 'bgPatternContainer' ||
event.shiftKey
);
}
Expand All @@ -179,6 +185,22 @@ class Tree extends React.Component<TreeProps, TreeState> {
}

g.attr('transform', event.transform);

// gridCellSize is required by zooming
const bgGrid = getDefaultBackgroundGridParam(this.props.backgroundGrid);
// apply zoom effect onto bgGrid only if specified
if (bgGrid) {
pattern
.attr('x', event.transform.x)
.attr('y', event.transform.y)
.attr('width', bgGrid.gridCellSize.width * event.transform.k)
.attr('height', bgGrid.gridCellSize.height * event.transform.k)

pattern
.selectAll('*')
.attr('transform',`scale(${event.transform.k})`)
}

if (typeof onUpdate === 'function') {
// This callback is magically called not only on "zoom", but on "drag", as well,
// even though event.type == "zoom".
Expand Down Expand Up @@ -557,6 +579,14 @@ class Tree extends React.Component<TreeProps, TreeState> {
width="100%"
height="100%"
>
{
this.props.backgroundGrid
? <BackgroundGrid
{...this.props.backgroundGrid}
patternInstanceRef={this.patternInstanceRef}
/>
: null
}
<TransitionGroupWrapper
enableLegacyTransitions={enableLegacyTransitions}
component="g"
Expand Down
112 changes: 112 additions & 0 deletions src/Tree/tests/BackgroundGrid.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import React from 'react'
import { render } from 'enzyme'
import BackgroundGrid from "../backgroundGrid"

describe('<BackgroundGrid />', () => {
it('renders dot grid elements', () => {
const wrapper = render(<svg>
<BackgroundGrid type="dot" patternInstanceRef="testingRef"/>
</svg>);

const pattern = wrapper.find('.testingRef');
const dot = wrapper.find('.testingRef rect');
const bgRect = wrapper.find('#bgPatternContainer');

expect(dot.length).toBe(1);
expect(pattern.length).toBe(1);
expect(bgRect.length).toBe(1);
})

it('renders line grid elements', () => {
const wrapper = render(<svg>
<BackgroundGrid type="line" patternInstanceRef="testingRef"/>
</svg>);

const pattern = wrapper.find('.testingRef');
const lines = wrapper.find('.testingRef line');
const bgRect = wrapper.find('#bgPatternContainer');

expect(lines.length).toBe(2);
expect(pattern.length).toBe(1);
expect(bgRect.length).toBe(1);
})

it('applies backgroundGrid options to dot grid when specified', () => {
const wrapper = render(<svg>
<BackgroundGrid
type="dot"
color="red"
thickness={12}
gridCellSize={{width: 200, height: 400}}
patternInstanceRef="testingRef"
/>
</svg>);

const pattern = wrapper.find('.testingRef');
const dot = wrapper.find('.testingRef rect');

expect(pattern[0].attribs.width).toBe('200');
expect(pattern[0].attribs.height).toBe('400');
expect(dot[0].attribs.fill).toBe('red');
expect(dot[0].attribs.width).toBe('12');
expect(dot[0].attribs.height).toBe('12');
expect(dot[0].attribs.rx).toBe('12');
})

it('applies backgroundGrid options to line grid when specified', () => {
const wrapper = render(<svg>
<BackgroundGrid
type="line"
color="red"
thickness={12}
gridCellSize={{width: 200, height: 400}}
patternInstanceRef="testingRef"
/>
</svg>);

const pattern = wrapper.find('.testingRef');
const lines = wrapper.find('.testingRef line');

expect(pattern[0].attribs.width).toBe('200');
expect(pattern[0].attribs.height).toBe('400');
expect(lines[0].attribs.stroke).toBe('red');
expect(lines[0].attribs['stroke-width']).toBe('12');
expect(lines[0].attribs.x2).toBe('200');
expect(lines[1].attribs.stroke).toBe('red');
expect(lines[1].attribs['stroke-width']).toBe('12');
expect(lines[1].attribs.y2).toBe('400');
})

it('renders custom gridCellFunc when specified', () => {
const wrapper = render(<svg>
<BackgroundGrid
type="custom"
color="red"
thickness={12}
gridCellSize={{width: 200, height: 400}}
gridCellFunc={(options) => {
return <circle
r={options.gridCellSize.width/2}
cx={options.gridCellSize.width/2}
cy={options.gridCellSize.height/2}
stroke={options.color}
fill='none'
strokeWidth={options.thickness}
/>
}}
patternInstanceRef="testingRef"
/>
</svg>);

const pattern = wrapper.find('.testingRef');
const circle = wrapper.find('.testingRef circle');

expect(circle.length).toBe(1);
expect(pattern.length).toBe(1);
expect(circle[0].attribs.r).toBe('100');
expect(circle[0].attribs.cx).toBe('100');
expect(circle[0].attribs.cy).toBe('200');
expect(circle[0].attribs.stroke).toBe('red');
expect(circle[0].attribs['stroke-width']).toBe('12');
})
})
19 changes: 17 additions & 2 deletions src/Tree/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
RenderCustomNodeElementFn,
TreeNodeDatum,
} from '../types/common.js';
import { BackgroundGrid } from './backgroundGrid';

export type TreeNodeEventCallback = (
node: HierarchyPointNode<TreeNodeDatum>,
Expand Down Expand Up @@ -200,12 +201,12 @@ export interface TreeProps {
*/
zoomable?: boolean;

/**
/**
* Toggles ability to drag the Tree.
*
* {@link Tree.defaultProps.draggable | Default value}
*/
draggable?: boolean;
draggable?: boolean;

/**
* A floating point number to set the initial zoom level. It is constrained by `scaleExtent`.
Expand Down Expand Up @@ -319,4 +320,18 @@ export interface TreeProps {
* {@link Tree.defaultProps.dataKey | Default value}
*/
dataKey?: string;

/**
* Sets a background grid using svg <pattern> and and a background <rect fill="url(#grid-pattern)"/>
* there's 2 default type of grid: `dot` and `line`
* - `color`: color of each line / dot
* - `thickness`: thickness of each line / radius of the each dot
* - `gridCellSize`: the space take place by each dot / cross / customRenderFunction
*
* `type: custom` allows customizing content inside <pattern><pattern/> by
* passing `gridCellFunc` a function that return a ReactElement(svg elements)
*
* {@link Tree.defaultProps.backgroundGrid | Default value}
*/
backgroundGrid?: BackgroundGrid;
}