├── examples ├── .env ├── public │ ├── robots.txt │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── App.test.js │ ├── index.css │ ├── index.js │ ├── assets │ │ └── avatar-personnel.svg │ ├── App.css │ ├── App.js │ ├── Tree.js │ └── serviceWorker.js ├── .gitignore ├── package.json └── README.md ├── src ├── vendor │ ├── base.js │ └── example.js ├── index.js ├── utils │ ├── index.js │ ├── collapse.js │ ├── helpers.js │ ├── wrapText.js │ └── covertImageToBase64.js ├── defs │ ├── defineAvatarClip.js │ ├── defineBorderRadius.js │ └── defineBoxShadow.js ├── chart │ ├── config.js │ ├── onParentClick.js │ ├── exportOrgChartImage.js │ ├── components │ │ ├── supervisorIcon.js │ │ └── iconLink.js │ ├── onClick.js │ ├── exportOrgChartPdf.js │ ├── renderLines.js │ ├── render.js │ └── index.js └── react │ └── org-chart.js ├── .npmignore ├── .gitignore ├── .prettierrc ├── now.json ├── .github └── workflows │ └── npmpublish.yml ├── webpack.config.js ├── package.json └── readme.md /examples/.env: -------------------------------------------------------------------------------- 1 | SKIP_PREFLIGHT_CHECK=true -------------------------------------------------------------------------------- /src/vendor/base.js: -------------------------------------------------------------------------------- 1 | require('d3') 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | yarn.lock 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | yarn.lock 4 | package-lock.json 5 | -------------------------------------------------------------------------------- /examples/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/vendor/example.js: -------------------------------------------------------------------------------- 1 | require('d3') 2 | require('faker') 3 | require('react') 4 | require('react-dom') 5 | -------------------------------------------------------------------------------- /examples/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicef/react-org-chart/HEAD/examples/public/favicon.ico -------------------------------------------------------------------------------- /examples/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicef/react-org-chart/HEAD/examples/public/logo192.png -------------------------------------------------------------------------------- /examples/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/unicef/react-org-chart/HEAD/examples/public/logo512.png -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "endOfLine": "lf", 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /now.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-org-chart", 3 | "type": "static", 4 | "files": ["examples", "dist"], 5 | "alias": "react-org-chart.now.sh" 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const OrgChart = require('./react/org-chart') 2 | const { init } = require('./chart') 3 | 4 | OrgChart.init = init 5 | 6 | module.exports = OrgChart 7 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | collapse: require('./collapse'), 3 | wrapText: require('./wrapText'), 4 | helpers: require('./helpers'), 5 | covertImageToBase64: require('./covertImageToBase64'), 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/collapse.js: -------------------------------------------------------------------------------- 1 | module.exports = function collapseNode(node) { 2 | // Check if this node has children 3 | if (node.children) { 4 | node._children = node.children 5 | node._children.forEach(collapseNode) 6 | node.children = null 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /examples/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/defs/defineAvatarClip.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = { 2 | borderRadius: 4, 3 | } 4 | 5 | module.exports = function defineAvatarClip(svg, id, config = {}) { 6 | config = { 7 | ...defaultConfig, 8 | ...config, 9 | } 10 | 11 | const defs = svg.append('svg:defs') 12 | 13 | defs 14 | .append('clipPath') 15 | .attr('id', id) 16 | .append('circle') 17 | .attr('cx', 70) 18 | .attr('cy', 32) 19 | .attr('r', 24) 20 | } 21 | -------------------------------------------------------------------------------- /examples/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | 7 | ReactDOM.render(, document.getElementById('root')); 8 | 9 | // If you want your app to work offline and load faster, you can change 10 | // unregister() to register() below. Note this comes with some pitfalls. 11 | // Learn more about service workers: https://bit.ly/CRA-PWA 12 | serviceWorker.unregister(); 13 | -------------------------------------------------------------------------------- /examples/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/defs/defineBorderRadius.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = { 2 | width: '100%', 3 | height: '100%', 4 | x: null, 5 | y: null, 6 | radius: 1 7 | } 8 | 9 | module.exports = function defineBorderRadius(svg, id, config = {}) { 10 | config = { 11 | ...defaultConfig, 12 | ...config 13 | } 14 | 15 | const defs = svg.append('svg:defs') 16 | const rectId = `${id}-rect` 17 | 18 | defs 19 | .append('rect') 20 | .attr('id', rectId) 21 | .attr('height', '100%') 22 | .attr('width', '100%') 23 | .attr('rx', config.radius) 24 | 25 | defs 26 | .append('clipPath') 27 | .attr('id', id) 28 | .append('use') 29 | .attr('xlink:href', '#' + rectId) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/npmpublish.yml: -------------------------------------------------------------------------------- 1 | name: Publish npm Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v1 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: 12 15 | - run: npm ci 16 | - run: npm test 17 | 18 | publish-npm: 19 | needs: build 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v1 23 | - uses: actions/setup-node@v1 24 | with: 25 | node-version: 12 26 | registry-url: https://registry.npmjs.org/ 27 | - run: npm ci 28 | - run: npm publish --access public 29 | env: 30 | NODE_AUTH_TOKEN: ${{secrets.npm_token}} 31 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const { resolve } = require('path') 3 | 4 | module.exports = { 5 | name: '@unicef/react-org-chart', 6 | devtool: 'source-map', 7 | entry: './src/index.js', 8 | output: { 9 | filename: 'index.js', 10 | devtoolLineToLine: true, 11 | sourceMapFilename: './index.js.map', 12 | pathinfo: true, 13 | path: resolve(__dirname, 'dist'), 14 | library: '@unicef/react-org-chart', 15 | libraryTarget: 'commonjs2', 16 | }, 17 | module: { 18 | rules: [ 19 | { 20 | test: /\.(js|jsx)$/, 21 | exclude: /node_modules/, 22 | use: ['babel-loader'], 23 | }, 24 | ], 25 | }, 26 | resolve: { 27 | extensions: ['*', '.js', '.jsx'], 28 | }, 29 | externals: { 30 | d3: { 31 | commonjs: 'd3', 32 | commonjs2: 'd3', 33 | amd: 'd3', 34 | root: '_', 35 | }, 36 | react: { 37 | commonjs: 'react', 38 | commonjs2: 'react', 39 | amd: 'react', 40 | root: '_', 41 | }, 42 | }, 43 | } 44 | -------------------------------------------------------------------------------- /examples/src/assets/avatar-personnel.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/defs/defineBoxShadow.js: -------------------------------------------------------------------------------- 1 | const defaultConfig = { 2 | width: '150%', 3 | height: '150%', 4 | x: 0, 5 | y: 2, 6 | blurRadius: 1, 7 | } 8 | 9 | module.exports = function defineBoxShadow(svg, id, config = {}) { 10 | config = { 11 | ...defaultConfig, 12 | ...config, 13 | } 14 | 15 | const filter = svg 16 | .append('svg:defs') 17 | .append('svg:filter') 18 | .attr('id', id) 19 | .attr('height', '150%') 20 | .attr('width', '150%') 21 | 22 | filter 23 | .append('svg:feGaussianBlur') 24 | .attr('in', 'SourceAlpha') 25 | .attr('stdDeviation', config.blurRadius) // stdDeviation is how much to blur 26 | .attr('result', 'blurOut') 27 | 28 | filter 29 | .append('svg:feOffset') 30 | .attr('in', 'blurOut') 31 | .attr('dx', config.x) 32 | .attr('dy', config.y) 33 | .attr('result', 'offsetOut') // how much to offset 34 | 35 | const feMerge = filter.append('feMerge') 36 | 37 | feMerge.append('feMergeNode').attr('in', 'offsetOut') 38 | feMerge.append('feMergeNode').attr('in', 'SourceGraphic') 39 | } 40 | -------------------------------------------------------------------------------- /src/chart/config.js: -------------------------------------------------------------------------------- 1 | const animationDuration = 350 2 | const shouldResize = true 3 | 4 | // Nodes 5 | const nodeWidth = 140 6 | const nodeHeight = 180 7 | const nodeSpacing = 12 8 | const nodePaddingX = 16 9 | const nodePaddingY = 16 10 | const avatarWidth = 48 11 | const nodeBorderRadius = 4 12 | const margin = { 13 | top: 20, 14 | right: 20, 15 | bottom: 20, 16 | left: 20, 17 | } 18 | 19 | // Lines 20 | const lineType = 'angle' 21 | const lineDepthY = 120 /* Height of the line for child nodes */ 22 | 23 | // Colors 24 | const backgroundColor = '#fff' 25 | const borderColor = '#c9c9c9' 26 | const nameColor = '#222d38' 27 | const titleColor = '#617080' 28 | const reportsColor = '#92A0AD' 29 | 30 | const config = { 31 | margin, 32 | animationDuration, 33 | nodeWidth, 34 | nodeHeight, 35 | nodeSpacing, 36 | nodePaddingX, 37 | nodePaddingY, 38 | nodeBorderRadius, 39 | avatarWidth, 40 | lineType, 41 | lineDepthY, 42 | backgroundColor, 43 | borderColor, 44 | nameColor, 45 | titleColor, 46 | reportsColor, 47 | shouldResize, 48 | } 49 | 50 | module.exports = config 51 | -------------------------------------------------------------------------------- /src/react/org-chart.js: -------------------------------------------------------------------------------- 1 | const { createElement, PureComponent } = require('react') 2 | const { init } = require('../chart') 3 | 4 | class OrgChart extends PureComponent { 5 | render() { 6 | const { id } = this.props 7 | 8 | return createElement('div', { 9 | id, 10 | }) 11 | } 12 | 13 | static defaultProps = { 14 | id: 'react-org-chart', 15 | downloadImageId: 'download-image', 16 | downloadPdfId: 'download-pdf', 17 | zoomInId: 'zoom-in', 18 | zoomOutId: 'zoom-out', 19 | zoomExtentId: 'zoom-extent', 20 | } 21 | 22 | componentDidMount() { 23 | const { 24 | id, 25 | downloadImageId, 26 | downloadPdfId, 27 | zoomInId, 28 | zoomOutId, 29 | zoomExtentId, 30 | tree, 31 | ...options 32 | } = this.props 33 | 34 | init({ 35 | id: `#${id}`, 36 | downloadImageId: `#${downloadImageId}`, 37 | downloadPdfId: `#${downloadPdfId}`, 38 | zoomInId: zoomInId, 39 | zoomOutId: zoomOutId, 40 | zoomExtentId: zoomExtentId, 41 | data: tree, 42 | ...options, 43 | }) 44 | } 45 | } 46 | 47 | module.exports = OrgChart 48 | -------------------------------------------------------------------------------- /examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "org-chart-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@unicef/react-org-chart": "file:..", 7 | "gh-pages": "^2.2.0", 8 | "react": "^16.12.0", 9 | "react-d3": "^0.4.0", 10 | "react-dom": "^16.12.0", 11 | "react-router-dom": "^5.1.2", 12 | "react-scripts": "^3.3.0" 13 | }, 14 | "homepage": "https://unicef.github.io/react-org-chart", 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/unicef/react-org-chart.git" 18 | }, 19 | "scripts": { 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "predeploy": "npm run build", 23 | "deploy": "gh-pages -d build", 24 | "test": "react-scripts test", 25 | "eject": "react-scripts eject" 26 | }, 27 | "eslintConfig": { 28 | "extends": "react-app" 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/helpers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | getTextForTitle, 3 | getTextForDepartment, 4 | getCursorForNode, 5 | } 6 | 7 | function getTextForTitle(datum) { 8 | if (!datum.person || !datum.person.totalReports) { 9 | return '' 10 | } 11 | 12 | const { 13 | person: { totalReports }, 14 | } = datum 15 | const pluralEnding = totalReports > 1 ? 's' : '' 16 | 17 | return `${totalReports} supervisee${pluralEnding}` 18 | } 19 | 20 | const departmentAbbrMap = { 21 | Marketing: 'mktg', 22 | Operations: 'ops', 23 | Growth: 'gwth', 24 | Branding: 'brand', 25 | Assurance: 'fin', 26 | Data: 'data', 27 | Design: 'design', 28 | Communications: 'comms', 29 | Product: 'prod', 30 | People: 'people', 31 | Sales: 'sales', 32 | } 33 | 34 | function getTextForDepartment(datum) { 35 | if (!datum.person.department) { 36 | return '' 37 | } 38 | 39 | const { department } = datum.person 40 | 41 | if (departmentAbbrMap[department]) { 42 | return departmentAbbrMap[department].toUpperCase() 43 | } 44 | 45 | return datum.person.department.substring(0, 3).toUpperCase() 46 | } 47 | 48 | function getCursorForNode(datum) { 49 | return datum.children || datum._children || datum.hasChild 50 | ? 'pointer' 51 | : 'default' 52 | } 53 | -------------------------------------------------------------------------------- /src/chart/onParentClick.js: -------------------------------------------------------------------------------- 1 | module.exports = onParentClick 2 | 3 | function onParentClick(configOnClick, children) { 4 | event.preventDefault() 5 | 6 | const { loadConfig } = configOnClick 7 | const config = loadConfig() 8 | const { loadParent } = config 9 | 10 | // If this person have `hasParent` is true, 11 | // attempt to load using the `loadParent` config function 12 | if (children.hasParent) { 13 | if (!loadParent) { 14 | console.error('react-org-chart.onClick: loadParent() not found in config') 15 | return 16 | } 17 | 18 | const result = loadParent(children) 19 | const handler = handleResult(config, children) 20 | 21 | // Check if the result is a promise and render the children 22 | if (result.then) { 23 | return result.then(handler) 24 | } else { 25 | return handler(result) 26 | } 27 | } 28 | } 29 | 30 | function handleResult(config, d) { 31 | const { render } = config 32 | 33 | return datum => { 34 | const children = datum.children.map(item => { 35 | if (item.id === d.id) { 36 | return { ...item, ...d } 37 | } else { 38 | return item 39 | } 40 | }) 41 | 42 | const result = { ...datum, children } 43 | 44 | // Pass in the newly rendered datum as the sourceNode 45 | // which tells the child nodes where to animate in from 46 | render({ 47 | ...config, 48 | treeData: { ...result, children, _children: null }, 49 | sourceNode: result, 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/utils/wrapText.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3') 2 | 3 | // One way of achieving text-wrapping capability in SVG 4 | module.exports = function wrapText(text, width) { 5 | if (text.length === 0) { 6 | return '' 7 | } 8 | 9 | let editedClass = '' 10 | 11 | text[0].forEach(textNode => { 12 | const text = d3.select(textNode) 13 | const x = text.attr('x') 14 | const y = text.attr('y') 15 | const dy = parseFloat(text.attr('dy')) 16 | const lineHeight = 1.1 17 | const words = text 18 | .text() 19 | .split(/\s+/) 20 | .reverse() 21 | 22 | let lineNumber = 0 23 | let word 24 | let line = [] 25 | let tspan = text 26 | .text(null) 27 | .append('tspan') 28 | .style('text-anchor', 'middle') 29 | .attr('x', x) 30 | .attr('y', y) 31 | .attr('dy', dy + 'em') 32 | 33 | while ((word = words.pop())) { 34 | line.push(word) 35 | tspan.text(line.join(' ')) 36 | 37 | if (tspan.node().getComputedTextLength() > width) { 38 | line.pop() 39 | tspan.text(line.join(' ')) 40 | line = [word] 41 | tspan = text 42 | .append('tspan') 43 | .style('text-anchor', 'middle') 44 | .attr('x', x) 45 | .attr('y', y) 46 | .attr('dy', ++lineNumber * lineHeight + dy + 'em') 47 | .text(word) 48 | } 49 | } 50 | 51 | if (!editedClass) { 52 | editedClass = text.attr('class').replace(' unedited', '') 53 | } 54 | 55 | text.attr('class', editedClass) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unicef/react-org-chart", 3 | "version": "0.3.4", 4 | "description": "Simple, high-performance react component for d3 org chart", 5 | "main": "dist/index.js", 6 | "directories": { 7 | "example": "src/examples" 8 | }, 9 | "files": [ 10 | "dist/index.js" 11 | ], 12 | "homepage": "https://unicef.github.io/react-org-chart", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/unicef/react-org-chart.git" 16 | }, 17 | "scripts": { 18 | "start": "webpack --watch --mode production", 19 | "build": "webpack --mode production", 20 | "clean": "rimraf dist", 21 | "deploy": "cd examples && npm run deploy" 22 | }, 23 | "keywords": [], 24 | "author": "UNICEF, based on work of Fouad Matin ", 25 | "license": "MIT", 26 | "devDependencies": { 27 | "babel-core": "^6.26.0", 28 | "babel-loader": "^7.1.4", 29 | "babel-preset-env": "^1.6.1", 30 | "babel-preset-stage-2": "^6.24.1", 31 | "faker": "^4.1.0", 32 | "glob": "^7.1.2", 33 | "lodash": "^4.17.5", 34 | "react": "^16.2.0", 35 | "react-dom": "^16.2.0", 36 | "rimraf": "^2.6.2", 37 | "webpack": "^4.41.2", 38 | "webpack-cli": "^3.1.1", 39 | "webpack-dev-server": "^3.1.0" 40 | }, 41 | "peerDependencies": { 42 | "d3": ">= 3.x < 4", 43 | "react": ">= 15.x", 44 | "react-dom": ">= 15.x" 45 | }, 46 | "babel": { 47 | "presets": [ 48 | "env", 49 | "stage-2" 50 | ] 51 | }, 52 | "dependencies": { 53 | "d3-save-svg": "0.0.2", 54 | "jspdf": "^1.5.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/src/App.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | height: 100%; 5 | width: 100%; 6 | font-size: 12px; 7 | font-family: 'SF UI Text', -apple-system, BlinkMacSystemFont, 'Segoe UI', 8 | Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 9 | 'Segoe UI Symbol'; 10 | -webkit-font-smoothing: antialiased; 11 | } 12 | #root, 13 | #react-org-chart { 14 | margin: 0; 15 | cursor: move; 16 | height: 100%; 17 | width: 100%; 18 | background-color: #f7f9fa; 19 | } 20 | .org-chart-person-name { 21 | font-weight: 500; 22 | } 23 | .org-chart-person-link:hover g { 24 | fill: #409cf9 !important; 25 | } 26 | .org-chart-node:hover .org-chart-person-reports { 27 | fill: #409cf9 !important; 28 | } 29 | .org-chart-person-dept.engineering { 30 | fill: #4caf50 !important; 31 | } 32 | .org-chart-person-dept.communications { 33 | fill: #3f51b5 !important; 34 | } 35 | .org-chart-person-dept.product { 36 | fill: #d500f9 !important; 37 | } 38 | .org-chart-person-dept.hr { 39 | fill: #2196f3 !important; 40 | } 41 | .org-chart-person-dept.marketing { 42 | fill: #f44336 !important; 43 | } 44 | .org-chart-person-dept.design { 45 | fill: #26c6da !important; 46 | } 47 | 48 | .zoom-buttons { 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | } 53 | 54 | .download-buttons { 55 | position: absolute; 56 | top: 0; 57 | right: 0; 58 | } 59 | 60 | .zoom-button { 61 | width: 40px; 62 | height: 40px; 63 | display: block !important; 64 | margin: 8px; 65 | } 66 | 67 | .btn { 68 | font-size: 0.875rem; 69 | text-transform: none; 70 | text-decoration-line: none; 71 | display: inline-block; 72 | font-weight: 600; 73 | text-align: center; 74 | vertical-align: middle; 75 | user-select: none; 76 | padding: 0.5rem 1rem; 77 | font-size: 0.875rem; 78 | line-height: 1.5rem; 79 | border-radius: 0.125rem; 80 | cursor: pointer; 81 | margin: 6px; 82 | } 83 | 84 | .btn-outline-primary { 85 | color: #374ea2; 86 | border-color: #374ea2; 87 | } 88 | 89 | .btn-outline-primary:not(:disabled):not(.disabled):active { 90 | color: #fff; 91 | background-color: #374ea2; 92 | border-color: #374ea2; 93 | } 94 | 95 | .github-link { 96 | font-size: 16px; 97 | margin-left: 8px; 98 | margin-right: 16px; 99 | } 100 | -------------------------------------------------------------------------------- /src/chart/exportOrgChartImage.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3') 2 | 3 | module.exports = exportOrgChartImage 4 | 5 | function exportOrgChartImage({ loadConfig }) { 6 | const config = loadConfig() 7 | const { id, downlowdedOrgChart, nodeLeftX, nodeRightX, nodeY } = config 8 | var w = nodeLeftX + nodeRightX 9 | var h = nodeY 10 | var ratio = w > 9000 ? 1 : 2 11 | 12 | // checking wether it has canvas in the convas-container div 13 | document.getElementById(`${id}-canvas-container`).querySelector('canvas') 14 | ? document 15 | .getElementById(`${id}-canvas-container`) 16 | .querySelector('canvas') 17 | .remove() 18 | : '' 19 | 20 | // creating a canvas element 21 | var canvas1 = document.createElement('canvas') 22 | canvas1.id = 'canvas1' 23 | canvas1.width = w * ratio 24 | canvas1.height = h * ratio 25 | document.getElementById(`${id}-canvas-container`).appendChild(canvas1) 26 | 27 | // creating duplicate org chart svg from original org chart svg 28 | var step = document.createElementNS('http://www.w3.org/2000/svg', 'svg') 29 | step.id = 'newsvg' 30 | step.setAttribute('width', w) 31 | step.setAttribute('height', h) 32 | step.setAttribute('viewBox', `${-nodeLeftX} 0 ${w} ${h + 200}`) 33 | step.innerHTML = document.getElementById('svg').innerHTML 34 | 35 | document.getElementById(`${id}-svg-container`).querySelector('svg') 36 | ? document 37 | .getElementById(`${id}-svg-container`) 38 | .querySelector('svg') 39 | .remove() 40 | : '' 41 | document.getElementById(`${id}-svg-container`).appendChild(step) 42 | 43 | // appending g element from svg 44 | const g = document.getElementById(`${id}-svg-container`).querySelector('g') 45 | g.setAttribute('transform', `translate(0,0)`) 46 | var html = new XMLSerializer().serializeToString( 47 | document.getElementById(`${id}-svg-container`).querySelector('svg') 48 | ) 49 | 50 | // generating image with base 64 51 | var imgSrc = 'data:image/svg+xml;base64,' + btoa(html) 52 | let canvas = document.getElementById('canvas1') 53 | let context = canvas.getContext('2d') 54 | let image = new Image() 55 | image.src = imgSrc 56 | 57 | // downloading the image 58 | image.onload = function() { 59 | context.drawImage(image, 0, 0, canvas.width, canvas.height) 60 | canvas.toBlob(function(blob) { 61 | let a = document.createElement('a') 62 | let url = URL.createObjectURL(blob) 63 | a.download = 'orgchart.jpg' 64 | a.href = url 65 | a.click() 66 | }) 67 | downlowdedOrgChart(true) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/chart/components/supervisorIcon.js: -------------------------------------------------------------------------------- 1 | const onParentClick = require('../onParentClick') 2 | // { 3 | // 4 | // 5 | // 6 | // 7 | // 8 | // 9 | 10 | // } 11 | module.exports = function supervisorIcon({ 12 | svg, 13 | config, 14 | treeData, 15 | x = 5, 16 | y = 5, 17 | }) { 18 | const container = svg 19 | .append('g') 20 | .attr('id', 'supervisorIcon') 21 | .on('click', d => { 22 | if (d3.event.defaultPrevented) return 23 | onParentClick(config, treeData) 24 | }) 25 | .attr('stroke', 'none') 26 | .attr('fill', 'none') 27 | .style('display', treeData.hasParent ? '' : 'none') 28 | .style('cursor', 'pointer') 29 | .append('g') 30 | 31 | const icon = container 32 | .append('g') 33 | .attr('id', 'icon') 34 | .attr('fill', 'none') 35 | .attr('fill-rule', 'evenodd') 36 | .attr('transform', `translate(51.5, -46)`) 37 | 38 | icon 39 | .append('circle') 40 | .attr('id', 'icon') 41 | .attr('stroke', '#C9C9C9') 42 | .attr('fill', '#FFF') 43 | .attr('cx', 15.5) 44 | .attr('cy', 15.5) 45 | .attr('r', 15.5) 46 | 47 | icon 48 | .append('path') 49 | .attr('stroke', '#C9C9C9') 50 | .attr('stroke-linecap', 'square') 51 | .attr('d', 'M15.5 45V31.5') 52 | 53 | icon 54 | .append('circle') 55 | .attr('id', 'icon') 56 | .attr('stroke', '#979797') 57 | .attr('fill', '#9C9C9C') 58 | .attr('cx', 9) 59 | .attr('cy', 16) 60 | .attr('r', 2) 61 | 62 | icon 63 | .append('circle') 64 | .attr('id', 'icon') 65 | .attr('stroke', '#979797') 66 | .attr('fill', '#9C9C9C') 67 | .attr('cx', 15.5) 68 | .attr('cy', 16) 69 | .attr('r', 2) 70 | 71 | icon 72 | .append('circle') 73 | .attr('id', 'icon') 74 | .attr('stroke', '#979797') 75 | .attr('fill', '#9C9C9C') 76 | .attr('cx', 22) 77 | .attr('cy', 16) 78 | .attr('r', 2) 79 | 80 | icon 81 | .append('rect') 82 | .attr('id', 'bounds') 83 | .attr('x', 0) 84 | .attr('y', 0) 85 | .attr('width', 33) 86 | .attr('height', 47) 87 | .attr('fill', 'transparent') 88 | } 89 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `yarn start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `yarn test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `yarn build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `yarn eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `yarn build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /src/chart/components/iconLink.js: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | */ 14 | 15 | module.exports = function iconLink({ svg, x = 5, y = 5 }) { 16 | const container = svg 17 | .append('g') 18 | .attr('stroke', 'none') 19 | .attr('fill', 'none') 20 | .style('cursor', 'pointer') 21 | .append('g') 22 | 23 | const icon = container 24 | .append('g') 25 | .attr('id', 'icon') 26 | .attr('fill', '#3344DD') 27 | .attr('transform', `translate(${x}, ${y})`) 28 | 29 | const arrow = icon 30 | .append('g') 31 | .attr('id', 'arrow') 32 | .attr( 33 | 'transform', 34 | 'translate(7.000000, 7.000000) scale(-1, 1) translate(-7.000000, -7.000000)' 35 | ) 36 | 37 | arrow 38 | .append('path') 39 | .attr( 40 | 'd', 41 | 'M3.41421356,2 L8.70710678,7.29289322 C9.09763107,7.68341751 9.09763107,8.31658249 8.70710678,8.70710678 C8.31658249,9.09763107 7.68341751,9.09763107 7.29289322,8.70710678 L2,3.41421356 L2,7 C2,7.55228475 1.55228475,8 1,8 C0.44771525,8 0,7.55228475 0,7 L0,1.49100518 C0,0.675320548 0.667758414,0 1.49100518,0 L7,0 C7.55228475,0 8,0.44771525 8,1 C8,1.55228475 7.55228475,2 7,2 L3.41421356,2 Z' 42 | ) 43 | 44 | arrow 45 | .append('path') 46 | // .attr('opacity', 0.7) 47 | .attr( 48 | 'd', 49 | 'M12,2 L12,12 L2,12 L2,11 C2,10.4477153 1.55228475,10 1,10 C0.44771525,10 0,10.4477153 0,11 L0,12.4953156 C0,13.3242086 0.674596865,14 1.50034732,14 L12.4996527,14 C13.3281027,14 14,13.3234765 14,12.4996527 L14,1.50034732 C14,0.669321781 13.3358906,0 12.4953156,0 L11,0 C10.4477153,0 10,0.44771525 10,1 C10,1.55228475 10.4477153,2 11,2 L12,2 Z' 50 | ) 51 | 52 | icon 53 | .append('rect') 54 | .attr('id', 'bounds') 55 | .attr('x', 0) 56 | .attr('y', 0) 57 | .attr('width', 24) 58 | .attr('height', 24) 59 | .attr('fill', 'transparent') 60 | } 61 | -------------------------------------------------------------------------------- /src/chart/onClick.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3') 2 | const { collapse } = require('../utils') 3 | 4 | module.exports = onClick 5 | 6 | function onClick(configOnClick) { 7 | const { loadConfig } = configOnClick 8 | 9 | return datum => { 10 | if (d3.event.defaultPrevented) return 11 | const config = loadConfig() 12 | const { loadChildren, render, onPersonClick } = config 13 | event.preventDefault() 14 | 15 | if (onPersonClick) { 16 | const result = onPersonClick(datum, d3.event) 17 | 18 | // If the `onPersonClick` handler returns `false` 19 | // Cancel the rest of this click handler 20 | if (typeof result === 'boolean' && !result) { 21 | return 22 | } 23 | } 24 | 25 | // If this person doesn't have children but `hasChild` is true, 26 | // attempt to load using the `loadChildren` config function 27 | if (!datum.children && !datum._children && datum.hasChild) { 28 | if (!loadChildren) { 29 | console.error( 30 | 'react-org-chart.onClick: loadChildren() not found in config' 31 | ) 32 | return 33 | } 34 | 35 | const result = loadChildren(datum) 36 | const handler = handleChildrenResult(config, datum) 37 | 38 | // Check if the result is a promise and render the children 39 | if (result.then) { 40 | return result.then(handler) 41 | } else { 42 | return handler(result) 43 | } 44 | } 45 | 46 | if (datum.children) { 47 | // Collapse the children 48 | config.callerNode = datum 49 | config.callerMode = 0 50 | datum._children = datum.children 51 | datum.children = null 52 | } else { 53 | // Expand the children 54 | config.callerNode = datum 55 | config.callerMode = 1 56 | datum.children = datum._children 57 | datum._children = null 58 | } 59 | 60 | // Pass in the clicked datum as the sourceNode which 61 | // tells the child nodes where to animate in from 62 | render({ 63 | ...config, 64 | sourceNode: datum, 65 | }) 66 | } 67 | } 68 | 69 | function handleChildrenResult(config, datum) { 70 | const { tree, render } = config 71 | 72 | return children => { 73 | const result = { 74 | ...datum, 75 | children, 76 | } 77 | 78 | // Collapse the nested children 79 | children.forEach(collapse) 80 | 81 | result.children.forEach(child => { 82 | if (!tree.nodes(datum)[0]._children) { 83 | tree.nodes(datum)[0]._children = [] 84 | } 85 | 86 | child.x = datum.x 87 | child.y = datum.y 88 | child.x0 = datum.x0 89 | child.y0 = datum.y0 90 | 91 | tree.nodes(datum)[0]._children.push(child) 92 | }) 93 | 94 | if (datum.children) { 95 | // Collapse the children 96 | config.callerNode = datum 97 | config.callerMode = 0 98 | datum._children = datum.children 99 | datum.children = null 100 | } else { 101 | // Expand the children 102 | config.callerNode = null 103 | config.callerMode = 1 104 | datum.children = datum._children 105 | datum._children = null 106 | } 107 | 108 | // Pass in the newly rendered datum as the sourceNode 109 | // which tells the child nodes where to animate in from 110 | render({ 111 | ...config, 112 | sourceNode: result, 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/chart/exportOrgChartPdf.js: -------------------------------------------------------------------------------- 1 | const jsPDF = require('jspdf') 2 | 3 | module.exports = exportOrgChartPdf 4 | 5 | function exportOrgChartPdf({ loadConfig }) { 6 | const config = loadConfig() 7 | const { 8 | id, 9 | downlowdedOrgChart, 10 | nodeLeftX, 11 | nodeRightX, 12 | nodeY, 13 | nodeHeight, 14 | margin, 15 | } = config 16 | 17 | // a4 width and heigth for landscape 18 | const a4Width = 3508 19 | const a4Height = 2480 20 | 21 | // svg width and height 22 | const svgWidth = nodeLeftX + nodeRightX 23 | const svgHeight = nodeY + nodeHeight + 48 24 | 25 | // calculating ratio for better quality if the svgWidth is less than a4Width 26 | const ratio = svgWidth > a4Width ? 1 : 2 27 | 28 | const widthWithRatio = svgWidth > a4Width ? svgWidth : svgWidth * ratio 29 | const heightWithRatio = svgWidth > a4Width ? svgHeight : svgHeight * ratio 30 | 31 | const defaultScale = svgWidth > 600 ? 0.87 : 0.6 32 | 33 | // scale 34 | const scaleX = a4Width / widthWithRatio 35 | const scaleY = a4Height / heightWithRatio 36 | const chooseScale = scaleX < scaleY ? scaleX : scaleY 37 | const scale = widthWithRatio > a4Width ? chooseScale - 0.04 : defaultScale 38 | const translateX = nodeLeftX * scale + margin.left / 2 39 | 40 | // Final width and height 41 | const width = widthWithRatio * 0.85 42 | const height = heightWithRatio * 0.85 43 | 44 | // checking wether it has canvas in the convas-container div 45 | document.getElementById(`${id}-canvas-container`).querySelector('canvas') 46 | ? document 47 | .getElementById(`${id}-canvas-container`) 48 | .querySelector('canvas') 49 | .remove() 50 | : '' 51 | 52 | // creating a canvas element 53 | var canvas1 = document.createElement('canvas') 54 | canvas1.id = 'canvas1' 55 | canvas1.width = svgWidth * ratio 56 | canvas1.height = svgHeight * ratio 57 | document.getElementById(`${id}-canvas-container`).appendChild(canvas1) 58 | 59 | // creating duplicate org chart svg from original org chart svg 60 | var step = document.createElementNS('http://www.w3.org/2000/svg', 'svg') 61 | step.id = 'newsvg' 62 | step.setAttribute('width', svgWidth) 63 | step.setAttribute('height', svgHeight) 64 | step.setAttribute('viewBox', `0 0 ${svgWidth} ${svgHeight}`) 65 | step.innerHTML = document.getElementById('svg').innerHTML 66 | 67 | document.getElementById(`${id}-svg-container`).querySelector('svg') 68 | ? document 69 | .getElementById(`${id}-svg-container`) 70 | .querySelector('svg') 71 | .remove() 72 | : '' 73 | document.getElementById(`${id}-svg-container`).appendChild(step) 74 | 75 | // appending g element from svg 76 | var g = document.getElementById(`${id}-svg-container`).querySelector('g') 77 | g.setAttribute('transform', `translate(${translateX}, 2) scale(${scale})`) 78 | var html = new XMLSerializer().serializeToString( 79 | document.getElementById(`${id}-svg-container`).querySelector('svg') 80 | ) 81 | 82 | // generating image with base 64 83 | const imgSrc = 'data:image/svg+xml;base64,' + btoa(html) 84 | const canvas = document.getElementById('canvas1') 85 | const context = canvas.getContext('2d') 86 | const image = new Image() 87 | image.src = imgSrc 88 | 89 | // downloading the image 90 | image.onload = function() { 91 | context.drawImage(image, 0, 0, width, height) 92 | const canvasData = canvas.toDataURL('image/jpeg,1.0') 93 | 94 | const pdf = new jsPDF('l', 'px', [a4Width, a4Height]) 95 | pdf.addImage(canvasData, 'JPEG', 15, 2, width, height) 96 | pdf.save('Orgchart.pdf') 97 | downlowdedOrgChart(true) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/utils/covertImageToBase64.js: -------------------------------------------------------------------------------- 1 | module.exports = covertImageToBase64 2 | 3 | function covertImageToBase64(src, callback, outputFormat) { 4 | var img = new Image() 5 | img.crossOrigin = 'Anonymous' 6 | img.onload = function () { 7 | var canvas = document.createElement('CANVAS') 8 | var ctx = canvas.getContext('2d') 9 | var dataURL 10 | canvas.height = this.naturalHeight 11 | canvas.width = this.naturalWidth 12 | ctx.drawImage(this, 0, 0) 13 | dataURL = canvas.toDataURL(outputFormat) 14 | if (dataURL === 'data:,') { 15 | dataURL = 'data:image/png;base64,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' 16 | } 17 | callback(dataURL) 18 | } 19 | img.src = src 20 | if (img.complete || img.complete === undefined) { 21 | img.src = 22 | 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==' 23 | img.src = src 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import './App.css' 3 | import OrgChart from '@unicef/react-org-chart' 4 | import { BrowserRouter, Route } from 'react-router-dom' 5 | import { tree, tree1, tree2, tree3, tree4 } from './Tree' 6 | import avatarPersonnel from './assets/avatar-personnel.svg' 7 | 8 | export default class App extends React.Component { 9 | constructor(props) { 10 | super(props) 11 | 12 | this.state = { 13 | tree: tree, 14 | downloadingChart: false, 15 | config: {}, 16 | highlightPostNumbers: [1], 17 | } 18 | } 19 | 20 | getChild = id => { 21 | switch (id) { 22 | case 100: 23 | return tree1 24 | case 36: 25 | return tree2 26 | case 56: 27 | return tree3 28 | case 25: 29 | return tree4 30 | default: 31 | return console.log('no children') 32 | } 33 | } 34 | 35 | getParent = d => { 36 | if (d.id === 100) { 37 | return { 38 | id: 500, 39 | person: { 40 | id: 500, 41 | avatar: avatarPersonnel, 42 | department: '', 43 | name: 'Pascal ruth', 44 | title: 'Member', 45 | totalReports: 1, 46 | }, 47 | hasChild: false, 48 | hasParent: true, 49 | children: [d], 50 | } 51 | } else if (d.id === 500) { 52 | return { 53 | id: 1, 54 | person: { 55 | id: 1, 56 | avatar: avatarPersonnel, 57 | department: '', 58 | name: 'Bryce joe', 59 | title: 'Director', 60 | totalReports: 1, 61 | }, 62 | hasChild: false, 63 | hasParent: false, 64 | children: [d], 65 | } 66 | } else { 67 | return d 68 | } 69 | } 70 | 71 | handleDownload = () => { 72 | this.setState({ downloadingChart: false }) 73 | } 74 | 75 | handleOnChangeConfig = config => { 76 | this.setState({ config: config }) 77 | } 78 | 79 | handleLoadConfig = () => { 80 | const { config } = this.state 81 | return config 82 | } 83 | 84 | render() { 85 | const { tree, downloadingChart } = this.state 86 | 87 | //For downloading org chart as image or pdf based on id 88 | const downloadImageId = 'download-image' 89 | const downloadPdfId = 'download-pdf' 90 | 91 | return ( 92 | 93 | 94 | 95 |
96 | 102 | 108 |
109 |
110 | 113 | 116 | 120 | Github 121 | 122 | {downloadingChart &&
Downloading chart
} 123 |
124 | { 129 | this.handleOnChangeConfig(config) 130 | }} 131 | loadConfig={d => { 132 | let configuration = this.handleLoadConfig(d) 133 | if (configuration) { 134 | return configuration 135 | } 136 | }} 137 | downlowdedOrgChart={d => { 138 | this.handleDownload() 139 | }} 140 | loadImage={d => { 141 | return Promise.resolve(avatarPersonnel) 142 | }} 143 | loadParent={d => { 144 | const parentData = this.getParent(d) 145 | return parentData 146 | }} 147 | loadChildren={d => { 148 | const childrenData = this.getChild(d.id) 149 | return childrenData 150 | }} 151 | /> 152 |
153 |
154 |
155 | ) 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /examples/src/Tree.js: -------------------------------------------------------------------------------- 1 | import avatarPersonnel from './assets/avatar-personnel.svg' 2 | 3 | export const tree = { 4 | id: 100, 5 | person: { 6 | id: 100, 7 | avatar: avatarPersonnel, 8 | department: '', 9 | name: 'Henry monger', 10 | title: 'Manager', 11 | totalReports: 3, 12 | }, 13 | hasChild: true, 14 | hasParent: true, 15 | children: [], 16 | } 17 | 18 | export const tree1 = [ 19 | { 20 | id: 36, 21 | person: { 22 | id: 36, 23 | avatar: avatarPersonnel, 24 | department: '', 25 | name: 'Tomasz polaski', 26 | title: 'IT Specialist', 27 | totalReports: 4, 28 | }, 29 | hasChild: true, 30 | hasParent: true, 31 | // children: [], 32 | }, 33 | { 34 | id: 32, 35 | person: { 36 | id: 32, 37 | avatar: avatarPersonnel, 38 | department: '', 39 | name: 'Emanuel walker', 40 | title: 'IT Specialist', 41 | totalReports: 0, 42 | }, 43 | hasChild: true, 44 | hasParent: true, 45 | children: [], 46 | }, 47 | { 48 | id: 25, 49 | person: { 50 | id: 25, 51 | avatar: avatarPersonnel, 52 | department: '', 53 | name: 'Kerry peter', 54 | title: 'IT Specialist', 55 | totalReports: 3, 56 | }, 57 | hasChild: true, 58 | hasParent: true, 59 | // children: [], 60 | }, 61 | ] 62 | 63 | export const tree2 = [ 64 | { 65 | id: 56, 66 | person: { 67 | id: 56, 68 | avatar: avatarPersonnel, 69 | department: '', 70 | name: 'Sam John', 71 | title: 'HR', 72 | totalReports: 2, 73 | link: 'https://github.com/unicef/react-org-chart', 74 | }, 75 | hasChild: true, 76 | hasParent: true, 77 | // children: [], 78 | }, 79 | { 80 | id: 66, 81 | person: { 82 | id: 66, 83 | avatar: avatarPersonnel, 84 | department: '', 85 | name: 'John doe', 86 | title: 'Developer', 87 | totalReports: 0, 88 | link: 'https://github.com/unicef/react-org-chart', 89 | }, 90 | hasChild: true, 91 | hasParent: true, 92 | children: [], 93 | }, 94 | { 95 | id: 76, 96 | person: { 97 | id: 76, 98 | avatar: avatarPersonnel, 99 | department: '', 100 | name: 'Emilia rogers', 101 | title: 'Developer', 102 | totalReports: 0, 103 | link: 'https://github.com/unicef/react-org-chart', 104 | }, 105 | hasChild: true, 106 | hasParent: true, 107 | children: [], 108 | }, 109 | { 110 | id: 60, 111 | person: { 112 | id: 60, 113 | avatar: avatarPersonnel, 114 | department: '', 115 | name: 'Ellen cott', 116 | title: 'IT Officer', 117 | totalReports: 0, 118 | }, 119 | hasChild: false, 120 | hasParent: true, 121 | children: [], 122 | }, 123 | ] 124 | 125 | export const tree3 = [ 126 | { 127 | id: 70, 128 | person: { 129 | id: 70, 130 | avatar: avatarPersonnel, 131 | department: '', 132 | name: 'Kenneth dom', 133 | title: 'IT Officer', 134 | totalReports: 0, 135 | }, 136 | hasChild: false, 137 | hasParent: true, 138 | children: [], 139 | }, 140 | { 141 | id: 45, 142 | person: { 143 | id: 45, 144 | avatar: avatarPersonnel, 145 | department: '', 146 | name: 'Kin baker', 147 | title: 'IT Officer', 148 | totalReports: 0, 149 | }, 150 | hasChild: false, 151 | hasParent: true, 152 | children: [], 153 | }, 154 | ] 155 | 156 | export const tree4 = [ 157 | { 158 | id: 102, 159 | person: { 160 | id: 102, 161 | avatar: avatarPersonnel, 162 | department: '', 163 | name: 'Hendy kinger', 164 | title: 'Manager', 165 | totalReports: 0, 166 | }, 167 | hasChild: true, 168 | hasParent: true, 169 | children: [], 170 | }, 171 | { 172 | id: 455, 173 | person: { 174 | id: 455, 175 | avatar: avatarPersonnel, 176 | department: '', 177 | name: 'Kate baker', 178 | title: 'IT Officer', 179 | totalReports: 0, 180 | }, 181 | hasChild: false, 182 | hasParent: true, 183 | children: [], 184 | }, 185 | { 186 | id: 444, 187 | person: { 188 | id: 444, 189 | avatar: avatarPersonnel, 190 | department: '', 191 | name: 'John medis', 192 | title: 'IT Officer', 193 | totalReports: 0, 194 | }, 195 | hasChild: false, 196 | hasParent: true, 197 | children: [], 198 | }, 199 | 200 | { 201 | id: 456, 202 | person: { 203 | id: 456, 204 | avatar: avatarPersonnel, 205 | department: '', 206 | name: 'Brett lee', 207 | title: 'IT Officer', 208 | totalReports: 0, 209 | }, 210 | hasChild: false, 211 | hasParent: true, 212 | children: [], 213 | }, 214 | ] 215 | -------------------------------------------------------------------------------- /src/chart/renderLines.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3') 2 | 3 | module.exports = renderLines 4 | 5 | function renderLines(config = {}) { 6 | const { 7 | svg, 8 | links, 9 | margin, 10 | nodeWidth, 11 | nodeHeight, 12 | borderColor, 13 | sourceNode, 14 | treeData, 15 | lineType, 16 | animationDuration, 17 | } = config 18 | 19 | const parentNode = sourceNode || treeData 20 | 21 | // Select all the links to render the lines 22 | const link = svg.selectAll('path.link').data( 23 | links.filter(link => link.source.id), 24 | d => d.target.id 25 | ) 26 | 27 | // Define the curved line function 28 | const curve = d3.svg 29 | .diagonal() 30 | .projection(d => [d.x + nodeWidth / 2, d.y + nodeHeight / 2]) 31 | 32 | // Define the angled line function 33 | const angle = d3.svg 34 | .line() 35 | .x(d => d.x) 36 | .y(d => d.y) 37 | .interpolate('linear') 38 | 39 | if (lineType === 'angle') { 40 | // Enter any new links at the parent's previous position. 41 | link 42 | .enter() 43 | .insert('path', 'g') 44 | .attr('class', 'link') 45 | .attr('fill', 'none') 46 | .attr('stroke', '#A9A9A9') 47 | .attr('stroke-opacity', 1) 48 | .attr('stroke-width', 1.25) 49 | .attr('d', d => { 50 | const linePoints = [ 51 | { 52 | x: d.source.x0 + parseInt(nodeWidth / 2), 53 | y: d.source.y0 + nodeHeight + 2, 54 | }, 55 | { 56 | x: d.source.x0 + parseInt(nodeWidth / 2), 57 | y: d.source.y0 + nodeHeight + 2, 58 | }, 59 | { 60 | x: d.source.x0 + parseInt(nodeWidth / 2), 61 | y: d.source.y0 + nodeHeight + 2, 62 | }, 63 | { 64 | x: d.source.x0 + parseInt(nodeWidth / 2), 65 | y: d.source.y0 + nodeHeight + 2, 66 | }, 67 | ] 68 | 69 | return angle(linePoints) 70 | }) 71 | 72 | // Transition links to their new position. 73 | link 74 | .transition() 75 | .duration(animationDuration) 76 | .attr('d', d => { 77 | const linePoints = [ 78 | { 79 | x: d.source.x + parseInt(nodeWidth / 2), 80 | y: d.source.y + nodeHeight, 81 | }, 82 | { 83 | x: d.source.x + parseInt(nodeWidth / 2), 84 | y: d.target.y - margin.top / 2, 85 | }, 86 | { 87 | x: d.target.x + parseInt(nodeWidth / 2), 88 | y: d.target.y - margin.top / 2, 89 | }, 90 | { 91 | x: d.target.x + parseInt(nodeWidth / 2), 92 | y: d.target.y, 93 | }, 94 | ] 95 | 96 | return angle(linePoints) 97 | }) 98 | 99 | // Animate the existing links to the parent's new position 100 | link 101 | .exit() 102 | .transition() 103 | .duration(animationDuration) 104 | .attr('d', d => { 105 | const lineNode = config.callerNode ? config.callerNode : parentNode 106 | const linePoints = [ 107 | { 108 | x: lineNode.x + parseInt(nodeWidth / 2), 109 | y: lineNode.y + nodeHeight + 2, 110 | }, 111 | { 112 | x: lineNode.x + parseInt(nodeWidth / 2), 113 | y: lineNode.y + nodeHeight + 2, 114 | }, 115 | { 116 | x: lineNode.x + parseInt(nodeWidth / 2), 117 | y: lineNode.y + nodeHeight + 2, 118 | }, 119 | { 120 | x: lineNode.x + parseInt(nodeWidth / 2), 121 | y: lineNode.y + nodeHeight + 2, 122 | }, 123 | ] 124 | 125 | return angle(linePoints) 126 | }) 127 | .each('end', () => { 128 | config.callerNode = null 129 | }) 130 | } else if (lineType === 'curve') { 131 | link 132 | .enter() 133 | .insert('path', 'g') 134 | .attr('class', 'link') 135 | .attr('stroke', borderColor) 136 | .attr('fill', 'none') 137 | .attr('x', nodeWidth / 2) 138 | .attr('y', nodeHeight / 2) 139 | .attr('d', d => { 140 | const source = { 141 | x: parentNode.x0, 142 | y: parentNode.y0, 143 | } 144 | 145 | return curve({ 146 | source, 147 | target: source, 148 | }) 149 | }) 150 | 151 | // Transition links to their new position. 152 | link 153 | .transition() 154 | .duration(animationDuration) 155 | .attr('d', curve) 156 | 157 | // Transition exiting nodes to the parent's new position. 158 | link 159 | .exit() 160 | .transition() 161 | .duration(animationDuration) 162 | .attr('d', function(d) { 163 | const source = { 164 | x: parentNode.x, 165 | y: parentNode.y, 166 | } 167 | return curve({ 168 | source, 169 | target: source, 170 | }) 171 | }) 172 | .remove() 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /examples/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl) 104 | .then(response => { 105 | // Ensure service worker exists, and that we really are getting a JS file. 106 | const contentType = response.headers.get('content-type'); 107 | if ( 108 | response.status === 404 || 109 | (contentType != null && contentType.indexOf('javascript') === -1) 110 | ) { 111 | // No service worker found. Probably a different app. Reload the page. 112 | navigator.serviceWorker.ready.then(registration => { 113 | registration.unregister().then(() => { 114 | window.location.reload(); 115 | }); 116 | }); 117 | } else { 118 | // Service worker found. Proceed as normal. 119 | registerValidSW(swUrl, config); 120 | } 121 | }) 122 | .catch(() => { 123 | console.log( 124 | 'No internet connection found. App is running in offline mode.' 125 | ); 126 | }); 127 | } 128 | 129 | export function unregister() { 130 | if ('serviceWorker' in navigator) { 131 | navigator.serviceWorker.ready.then(registration => { 132 | registration.unregister(); 133 | }); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/chart/render.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3') 2 | const { wrapText, helpers, covertImageToBase64 } = require('../utils') 3 | const renderLines = require('./renderLines') 4 | const exportOrgChartImage = require('./exportOrgChartImage') 5 | const exportOrgChartPdf = require('./exportOrgChartPdf') 6 | const onClick = require('./onClick') 7 | const iconLink = require('./components/iconLink') 8 | const supervisorIcon = require('./components/supervisorIcon') 9 | const CHART_NODE_CLASS = 'org-chart-node' 10 | const PERSON_LINK_CLASS = 'org-chart-person-link' 11 | const PERSON_NAME_CLASS = 'org-chart-person-name' 12 | const PERSON_TITLE_CLASS = 'org-chart-person-title' 13 | const PERSON_HIGHLIGHT = 'org-chart-person-highlight' 14 | const PERSON_REPORTS_CLASS = 'org-chart-person-reports' 15 | 16 | function render(config) { 17 | const { 18 | svgroot, 19 | svg, 20 | tree, 21 | animationDuration, 22 | nodeWidth, 23 | nodeHeight, 24 | nodePaddingX, 25 | nodePaddingY, 26 | nodeBorderRadius, 27 | backgroundColor, 28 | nameColor, 29 | titleColor, 30 | reportsColor, 31 | borderColor, 32 | avatarWidth, 33 | lineDepthY, 34 | treeData, 35 | sourceNode, 36 | onPersonLinkClick, 37 | loadImage, 38 | downloadImageId, 39 | downloadPdfId, 40 | elemWidth, 41 | margin, 42 | onConfigChange, 43 | } = config 44 | 45 | // Compute the new tree layout. 46 | const nodes = tree.nodes(treeData).reverse() 47 | const links = tree.links(nodes) 48 | 49 | config.links = links 50 | config.nodes = nodes 51 | 52 | // Normalize for fixed-depth. 53 | nodes.forEach(function(d) { 54 | d.y = d.depth * lineDepthY 55 | }) 56 | 57 | // Update the nodes 58 | const node = svg.selectAll('g.' + CHART_NODE_CLASS).data( 59 | nodes.filter(d => d.id), 60 | d => d.id 61 | ) 62 | 63 | const parentNode = sourceNode || treeData 64 | 65 | svg.selectAll('#supervisorIcon').remove() 66 | 67 | supervisorIcon({ 68 | svg: svg, 69 | config, 70 | treeData, 71 | x: 70, 72 | y: -24, 73 | }) 74 | 75 | // Enter any new nodes at the parent's previous position. 76 | const nodeEnter = node 77 | .enter() 78 | .insert('g') 79 | .attr('class', CHART_NODE_CLASS) 80 | .attr('transform', `translate(${parentNode.x0}, ${parentNode.y0})`) 81 | .on('click', onClick(config)) 82 | 83 | // Person Card Shadow 84 | nodeEnter 85 | .append('rect') 86 | .attr('width', nodeWidth) 87 | .attr('height', nodeHeight) 88 | .attr('fill', backgroundColor) 89 | .attr('stroke', borderColor) 90 | .attr('rx', nodeBorderRadius) 91 | .attr('ry', nodeBorderRadius) 92 | .attr('fill-opacity', 0.05) 93 | .attr('stroke-opacity', 0.025) 94 | .attr('filter', 'url(#boxShadow)') 95 | 96 | // Person Card Container 97 | nodeEnter 98 | .append('rect') 99 | .attr('class', d => (d.isHighlight ? `${PERSON_HIGHLIGHT} box` : 'box')) 100 | .attr('width', nodeWidth) 101 | .attr('height', nodeHeight) 102 | .attr('id', d => d.id) 103 | .attr('fill', backgroundColor) 104 | .attr('stroke', borderColor) 105 | .attr('rx', nodeBorderRadius) 106 | .attr('ry', nodeBorderRadius) 107 | .style('cursor', helpers.getCursorForNode) 108 | 109 | const namePos = { 110 | x: nodeWidth / 2, 111 | y: nodePaddingY * 1.8 + avatarWidth, 112 | } 113 | 114 | const avatarPos = { 115 | x: nodeWidth / 2 - avatarWidth / 2, 116 | y: nodePaddingY / 2, 117 | } 118 | 119 | // Person's Name 120 | nodeEnter 121 | .append('text') 122 | .attr('class', PERSON_NAME_CLASS + ' unedited') 123 | .attr('x', namePos.x) 124 | .attr('y', namePos.y) 125 | .attr('dy', '.3em') 126 | .style('cursor', 'pointer') 127 | .style('fill', nameColor) 128 | .style('font-size', 14) 129 | .text(d => d.person.name) 130 | // .on('click', onParentClick(config)) 131 | 132 | // Person's Title 133 | nodeEnter 134 | .append('text') 135 | .attr('class', PERSON_TITLE_CLASS + ' unedited') 136 | .attr('x', nodeWidth / 2) 137 | .attr('y', namePos.y + nodePaddingY * 2.4) 138 | .attr('dy', '0.1em') 139 | .style('font-size', 12) 140 | .style('cursor', 'pointer') 141 | .style('fill', titleColor) 142 | .text(d => d.person.title) 143 | 144 | const heightForTitle = 60 // getHeightForText(d.person.title) 145 | 146 | // Person's Reports 147 | nodeEnter 148 | .append('text') 149 | .attr('class', PERSON_REPORTS_CLASS) 150 | .attr('x', nodePaddingX + 8) 151 | .attr('y', namePos.y + nodePaddingY + heightForTitle) 152 | .attr('dy', '.9em') 153 | .style('font-size', 14) 154 | .style('font-weight', 400) 155 | .style('cursor', 'pointer') 156 | .style('fill', reportsColor) 157 | .text(helpers.getTextForTitle) 158 | 159 | // Person's Avatar 160 | nodeEnter 161 | .append('image') 162 | .attr('id', d => `image-${d.id}`) 163 | .attr('width', avatarWidth) 164 | .attr('height', avatarWidth) 165 | .attr('x', avatarPos.x) 166 | .attr('y', avatarPos.y) 167 | .attr('stroke', borderColor) 168 | .attr('s', d => { 169 | d.person.hasImage 170 | ? d.person.avatar 171 | : loadImage(d).then(res => { 172 | covertImageToBase64(res, function(dataUrl) { 173 | d3.select(`#image-${d.id}`).attr('href', dataUrl) 174 | d.person.avatar = dataUrl 175 | }) 176 | d.person.hasImage = true 177 | return d.person.avatar 178 | }) 179 | }) 180 | .attr('src', d => d.person.avatar) 181 | .attr('href', d => d.person.avatar) 182 | .attr('clip-path', 'url(#avatarClip)') 183 | 184 | // Person's Link 185 | const nodeLink = nodeEnter 186 | .append('a') 187 | .attr('class', PERSON_LINK_CLASS) 188 | .attr('display', d => (d.person.link ? '' : 'none')) 189 | .attr('xlink:href', d => d.person.link) 190 | .on('click', datum => { 191 | d3.event.stopPropagation() 192 | // TODO: fire link click handler 193 | if (onPersonLinkClick) { 194 | onPersonLinkClick(datum, d3.event) 195 | } 196 | }) 197 | 198 | iconLink({ 199 | svg: nodeLink, 200 | x: nodeWidth - 20, 201 | y: 8, 202 | }) 203 | 204 | // Transition nodes to their new position. 205 | const nodeUpdate = node 206 | .transition() 207 | .duration(animationDuration) 208 | .attr('transform', d => `translate(${d.x},${d.y})`) 209 | 210 | nodeUpdate 211 | .select('rect.box') 212 | .attr('fill', backgroundColor) 213 | .attr('stroke', borderColor) 214 | 215 | // Transition exiting nodes to the parent's new position. 216 | const nodeExit = node 217 | .exit() 218 | .transition() 219 | .duration(animationDuration) 220 | .attr('transform', d => `translate(${parentNode.x},${parentNode.y})`) 221 | .remove() 222 | 223 | // Update the links 224 | const link = svg.selectAll('path.link').data(links, d => d.target.id) 225 | 226 | // Wrap the title texts 227 | const wrapWidth = 124 228 | svg.selectAll('text.unedited.' + PERSON_NAME_CLASS).call(wrapText, wrapWidth) 229 | svg.selectAll('text.unedited.' + PERSON_TITLE_CLASS).call(wrapText, wrapWidth) 230 | 231 | // Render lines connecting nodes 232 | renderLines(config) 233 | 234 | // Stash the old positions for transition. 235 | nodes.forEach(function(d) { 236 | d.x0 = d.x 237 | d.y0 = d.y 238 | }) 239 | 240 | var nodeLeftX = -70 241 | var nodeRightX = 70 242 | var nodeY = 200 243 | nodes.map(d => { 244 | nodeLeftX = d.x < nodeLeftX ? d.x : nodeLeftX 245 | nodeRightX = d.x > nodeRightX ? d.x : nodeRightX 246 | nodeY = d.y > nodeY ? d.y : nodeY 247 | }) 248 | 249 | config.nodeRightX = nodeRightX 250 | config.nodeY = nodeY 251 | config.nodeLeftX = nodeLeftX * -1 252 | 253 | d3.select(downloadImageId).on('click', function() { 254 | exportOrgChartImage(config) 255 | }) 256 | 257 | d3.select(downloadPdfId).on('click', function() { 258 | exportOrgChartPdf(config) 259 | }) 260 | onConfigChange(config) 261 | } 262 | module.exports = render 263 | -------------------------------------------------------------------------------- /src/chart/index.js: -------------------------------------------------------------------------------- 1 | const d3 = require('d3') 2 | const { collapse, wrapText, helpers } = require('../utils') 3 | const defineBoxShadow = require('../defs/defineBoxShadow') 4 | const defineAvatarClip = require('../defs/defineAvatarClip') 5 | const render = require('./render') 6 | const defaultConfig = require('./config') 7 | 8 | module.exports = { 9 | init, 10 | } 11 | 12 | function init(options) { 13 | // Merge options with the default config 14 | const config = { 15 | ...defaultConfig, 16 | ...options, 17 | treeData: options.data, 18 | } 19 | 20 | if (!config.id) { 21 | console.error('react-org-chart: missing id for svg root') 22 | return 23 | } 24 | 25 | const { 26 | id, 27 | treeData, 28 | lineType, 29 | margin, 30 | nodeWidth, 31 | nodeHeight, 32 | nodeSpacing, 33 | shouldResize, 34 | zoomInId, 35 | zoomOutId, 36 | zoomExtentId, 37 | loadConfig, 38 | } = config 39 | 40 | // Calculate how many pixel nodes to be spaced based on the 41 | // type of line that needs to be rendered 42 | if (lineType == 'angle') { 43 | config.lineDepthY = nodeHeight + 40 44 | } else { 45 | config.lineDepthY = nodeHeight + 60 46 | } 47 | 48 | // Get the root element 49 | const elem = document.querySelector(id) 50 | 51 | if (!elem) { 52 | console.error(`react-org-chart: svg root DOM node not found (id: ${id})`) 53 | return 54 | } 55 | 56 | // Reset in case there's any existing DOM 57 | elem.innerHTML = '' 58 | 59 | const elemWidth = elem.offsetWidth 60 | const elemHeight = elem.offsetHeight 61 | 62 | // Setup the d3 tree layout 63 | config.tree = d3.layout 64 | .tree() 65 | .nodeSize([nodeWidth + nodeSpacing, nodeHeight + nodeSpacing]) 66 | 67 | // Calculate width of a node with expanded children 68 | const childrenWidth = parseInt((treeData.children.length * nodeWidth) / 2) 69 | 70 | // elemWidth || svgHeight > elemHeight ? chooseScale : 1 185 | let translateX = nodeLeftX * scale + margin.left / 2 186 | 187 | if (svgWidth > elemWidth || svgHeight > elemHeight) { 188 | //If width is more than height 189 | if (scaleX < scaleY) { 190 | interpolateZoom([translateX, 48], scale) 191 | //If height is more than width 192 | } else if (scaleX > scaleY) { 193 | translateX = elemWidth / 2 - margin.left / 2 194 | interpolateZoom([translateX, 48], scale) 195 | } 196 | } else { 197 | translateX = elemWidth / 2 - margin.left / 2 198 | interpolateZoom([translateX, 48], scale) 199 | } 200 | 201 | return 202 | } 203 | var clicked = d3.event.target, 204 | direction = 1, 205 | factor = 0.2, 206 | target_zoom = 1, 207 | center = [elemWidth / 2, elemHeight / 2], 208 | extent = zoom.scaleExtent(), 209 | translate = zoom.translate(), 210 | translate0 = [], 211 | l = [], 212 | view = { x: translate[0], y: translate[1], k: zoom.scale() } 213 | 214 | d3.event.preventDefault() 215 | direction = this.id === zoomInId ? 1 : -1 216 | target_zoom = zoom.scale() * (1 + factor * direction) 217 | 218 | if (target_zoom < extent[0] || target_zoom > extent[1]) { 219 | return false 220 | } 221 | 222 | translate0 = [(center[0] - view.x) / view.k, (center[1] - view.y) / view.k] 223 | view.k = target_zoom 224 | l = [translate0[0] * view.k + view.x, translate0[1] * view.k + view.y] 225 | 226 | view.x += center[0] - l[0] 227 | view.y += center[1] - l[1] 228 | 229 | interpolateZoom([view.x, view.y], view.k) 230 | } 231 | 232 | // d3 selects button on click 233 | d3.select(`#${zoomInId}`).on('click', zoomClick) 234 | d3.select(`#${zoomOutId}`).on('click', zoomClick) 235 | d3.select(`#${zoomExtentId}`).on('click', zoomClick) 236 | 237 | // Add listener for when the browser or parent node resizes 238 | const resize = () => { 239 | if (!elem) { 240 | global.removeEventListener('resize', resize) 241 | return 242 | } 243 | 244 | svgroot.attr('width', elem.offsetWidth).attr('height', elem.offsetHeight) 245 | } 246 | 247 | if (shouldResize) { 248 | global.addEventListener('resize', resize) 249 | } 250 | 251 | // Start initial render 252 | render(config) 253 | 254 | // Update DOM root height 255 | d3.select(id).style('height', elemHeight + margin.top + margin.bottom) 256 | 257 | //creating canvas and duplicate svg for image and PDF download 258 | const canvasContainer = document.createElement('div') 259 | canvasContainer.setAttribute('id', `${id}-canvas-container`) 260 | canvasContainer.setAttribute('style', 'display:none;') 261 | 262 | //duplicate svg container 263 | const svgContainer = document.createElement('div') 264 | svgContainer.setAttribute('id', `${id}-svg-container`) 265 | svgContainer.setAttribute('style', 'display:none;') 266 | 267 | //appending svg and canvas containers to root 268 | const orgChart = document.getElementById('root') 269 | orgChart.append(canvasContainer) 270 | orgChart.append(svgContainer) 271 | } 272 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # React Organizational Chart 2 | [![npm version](https://badge.fury.io/js/%40unicef%2Freact-org-chart.svg)](https://badge.fury.io/js/%40unicef%2Freact-org-chart) 3 | 4 | React component for displaying organizational charts. 5 | 6 | This component is based on [coreseekdev/react-org-chart](https://github.com/coreseekdev/react-org-chart). On top of it, we added a few customization to fulfill our requirements. 7 | 8 | ### [View demo](https://unicef.github.io/react-org-chart/) 9 | 10 | 11 | # Features 12 | 13 | From the original package: 14 | 15 | - High-performance D3-based SVG rendering 16 | - Lazy-load children with a custom function 17 | - Handle up to 1 million collapsed nodes and 5,000 expanded nodes 18 | - Pan (drag and drop) 19 | - Zoom in zoom out (with mouse wheel/scroll) 20 | 21 | What we added: 22 | 23 | - Lazy-load of parents (go up in the tree) 24 | - Zoom in, zoom out and zoom buttons. 25 | - Download orgchart as image or PDF 26 | 27 | ### React Props 28 | 29 | | **property** | **type** | **description** | **example** | 30 | | ----------------- | ---------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------ | 31 | | tree | `Object` | Nested data model with some of all the employees in the company (Required) | See sample below. | 32 | | nodeWidth | `Number` | Width of the component for each individual (Optional) | 180 | 33 | | nodeHeight | `Number` | Height of the component for each individual (Optional) | 100 | 34 | | nodeSpacing | `Number` | Spacing between each of the nodes in the chart (Optional) | 12 | 35 | | animationDuration | `Number` | Duration of the animations in milliseconds (Optional) | 350 | 36 | | lineType | `String` | Type of line that connects the nodes to each other (Optional) | “angle” “curve” | 37 | | downloadImageId | `String` | Id of the DOM element that, on click, will trigger the download of the org chart as PNG. OrgChart will bind the click event to the DOM element with this ID (Optional) | "download-image" (default) | 38 | | downloadPdfId | `String` | Id of the DOM element that, on click, will trigger the download of the org chart as PDF. OrgChart will bind the click event to the DOM element with this ID (Optional) (Optional) | "download-pdf" (default) | 39 | | zoomInId | `String` | Id of the DOM element that, on click, will trigger a zoom of the org chart. OrgChart will bind the click event to the DOM element with this ID (Optional) (Optional) | "zoom-in" (default) | 40 | | zoomOutId | `String` | Id of the DOM element that, on click, will trigger the zoom out of the org chart. OrgChart will bind the click event to the DOM element with this ID (Optional) | "zoom-out" (default) | 41 | | zoomExtentId | `String` | Id of the DOM element that, on click, will display whole org chart svg fit to screen. OrgChart will bind the click event to the DOM element with this ID(Optional) | "zoom-extent" (default) | 42 | | loadParent(personData) | `Function` | Load parent with one level of children (Optional) | See usage below | 43 | | loadChildren (personData) | `Function` | Load the children of particular node (Optional) | See usage below | 44 | | onConfigChange | `Function` | To set the latest config to state on change | See usage below | 45 | | loadConfig | `Function` | Pass latest config from state to OrgChart | See usage below | 46 | | loadImage(personData) | `Function` | To get image of person on API call (Optional) | See usage below | 47 | 48 | 49 | 50 | ### Sample tree data 51 | 52 | ```jsx 53 | 54 | { 55 | id: 1, 56 | person: { 57 | id: 1, 58 | avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/spbroma/128.jpg', 59 | department: '', 60 | name: 'Jane Doe', 61 | title: 'CEO', 62 | totalReports: 5 63 | }, 64 | hasChild: true, 65 | hasParent: false, 66 | isHighlight: true, 67 | children: [ 68 | { 69 | id: 2, 70 | person: { 71 | id: 2, 72 | avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/spbroma/128.jpg', 73 | department: '', 74 | name: 'John Foo', 75 | title: 'CTO', 76 | totalReports: 0 77 | }, 78 | hasChild: false, 79 | hasParent: true, 80 | isHighlight: false, 81 | children: [] 82 | }, 83 | ... 84 | ] 85 | } 86 | 87 | ``` 88 | 89 | ### Usage 90 | 91 | You have a complete working example in the **[examples/](https://github.com/unicef/react-org-chart/tree/master/examples)** folder 92 | 93 | ```jsx 94 | import React from 'react' 95 | import OrgChart from '@unicef/react-org-chart' 96 | 97 | handleLoadConfig = () => { 98 | const { config } = this.state 99 | return config 100 | } 101 | 102 | render(){ 103 | return ( 104 | { 109 | // Setting latest config to state 110 | this.setState({ config: config }) 111 | }} 112 | loadConfig={d => { 113 | // Called from d3 to get latest version of the config. 114 | const config = this.handleLoadConfig(d) 115 | return config 116 | }} 117 | loadParent={personData => { 118 | // getParentData(): To get the parent data from API 119 | const loadedParent = this.getParentData(personData) 120 | return Promise.resolve(loadedParent) 121 | }} 122 | loadChildren={personData => { 123 | // getChildrenData(): To get the children data from API 124 | const loadedChildren = this.getChildrenData(personData) 125 | return Promise.resolve(loadedChildren) 126 | }} 127 | loadImage={personData => { 128 | // getImage(): To get the image from API 129 | const image = getImage(personData.email) 130 | return Promise.resolve(image) 131 | }} 132 | /> 133 | ) 134 | } 135 | ``` 136 | 137 | 138 | # Development 139 | 140 | ```bash 141 | git clone https://github.com/unicef/react-org-chart.git 142 | cd react-org-chart 143 | npm install 144 | ``` 145 | 146 | To build in watch mode: 147 | 148 | ```bash 149 | npm start 150 | ``` 151 | 152 | To build for production 153 | 154 | ```bash 155 | npm run build 156 | ``` 157 | 158 | Running the example: 159 | 160 | ```bash 161 | cd example/ 162 | npm install # Only first time 163 | npm start 164 | ``` 165 | 166 | To deploy the example to gh-pages site 167 | 168 | ```bash 169 | npm run deploy 170 | ``` 171 | 172 | ## About UNICEF 173 | 174 | [UNICEF](https://www.unicef.org/) works in over 190 countries and territories to protect the rights of every child. UNICEF has spent more than 70 years working to improve the lives of children and their families. In UNICEF, we **believe all children have a right to survive, thrive and fulfill their potential – to the benefit of a better world**. 175 | 176 | [Donate](https://donate.unicef.org/donate/now) 177 | 178 | 179 | ## Collaborations and support 180 | 181 | Just fork the project and make a pull request. You may also [consider donating](https://donate.unicef.org/donate/now). 182 | 183 | 184 | # License 185 | 186 | Copyright 2019-2020 UNICEF http://www.unicef.org 187 | Developed by ICTD, Solutions Center and Support, Digital Tools and Platforms, Custom Applications Team, New York. 188 | 189 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 190 | 191 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 192 | 193 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 194 | --------------------------------------------------------------------------------