diff --git a/website/package.json b/website/package.json
index d9b665e3..b0a151cb 100644
--- a/website/package.json
+++ b/website/package.json
@@ -63,7 +63,7 @@
"acorn-to-esprima": "^2.0.8",
"astexplorer-go": "^1.0.0",
"astexplorer-refmt": "^1.0.1",
- "astexplorer-syn": "^1.0.48",
+ "astexplorer-syn": "1.0.48",
"babel-core": "npm:babel-core@^6",
"babel-eslint": "^7.2.3",
"babel-eslint8": "npm:babel-eslint@^8",
@@ -112,6 +112,7 @@
"jscodeshift": "^0.11.0",
"json-stringify-safe": "^5.0.1",
"json-to-ast": "^2.1.0",
+ "lodash": "^4.17.21",
"lodash.isequal": "^4.5.0",
"luaparse": "^0.3.0",
"lucene": "^2.1.1",
@@ -132,6 +133,7 @@
"pug-lexer": "^4.1.0",
"pug-parser": "^5.0.1",
"react": "16",
+ "react-d3-tree": "^3.3.5",
"react-dom": "^16.0.1",
"react-redux": "^7.2.0",
"recast": "^0.21.1",
diff --git a/website/src/components/visualization/TreeVisualization.js b/website/src/components/visualization/TreeVisualization.js
new file mode 100644
index 00000000..5423026f
--- /dev/null
+++ b/website/src/components/visualization/TreeVisualization.js
@@ -0,0 +1,233 @@
+import PropTypes from 'prop-types';
+import React, { cloneElement, Fragment } from 'react';
+import {publish, subscribe} from '../../utils/pubsub.js';
+import {logEvent} from '../../utils/logger';
+import {treeAdapterFromParseResult} from '../../core/TreeAdapter.js';
+import TreeD3 from 'react-d3-tree';
+import _ from 'lodash'
+
+import './css/treeVisualization.css'
+
+const {useReducer, useMemo, useEffect,createRef} = React;
+
+const STORAGE_KEY = 'tree_settings';
+
+function initSettings() {
+ const storedSettings = global.localStorage.getItem(STORAGE_KEY);
+ return storedSettings ?
+ JSON.parse(storedSettings) :
+ {
+ autofocus: true,
+ hideFunctions: true,
+ hideEmptyKeys: false,
+ hideLocationData: false,
+ hideTypeKeys: false,
+ };
+}
+
+function reducer(state, element) {
+ const newState = {...state, [element.name]: element.checked};
+
+ global.localStorage.setItem(STORAGE_KEY, JSON.stringify(newState));
+ logEvent(
+ 'tree_view_settings',
+ element.checked ? 'enabled' : 'disabled',
+ element.name,
+ );
+
+ return newState;
+}
+
+function makeCheckbox(name, settings, updateSettings) {
+ return (
+ updateSettings(event.target)}
+ />
+ );
+}
+
+export default function TreeVisualization({parseResult, position}) {
+ const [settings, updateSettings] = useReducer(reducer, null, initSettings);
+ const treeAdapter = useMemo(
+ () => treeAdapterFromParseResult(parseResult, settings),
+ [parseResult.treeAdapter, settings],
+ );
+
+ let TreeRef = createRef();
+
+ // is the node in the range
+ function isInRange(node){
+ if(node === null || position === null) return false
+ return treeAdapter.isInRange(node, node.name, position) || treeAdapter.hasChildrenInRange(node, node.name, position)
+ }
+
+ function walk(node,parentNode,preName){
+ let rsl = {}
+ rsl.name = preName === undefined ? treeAdapter.getNodeName(node) : preName
+ rsl.name = rsl.name === undefined ? '' : rsl.name + ''
+ let keysObj = Array.from(treeAdapter.walkNode(node))
+ .filter(({key}) => key !== 'length') || []
+ rsl.node = parentNode || node
+ rsl.children = []
+ if(typeof node === 'object') {
+ keysObj.forEach(item=>{
+ if(Array.isArray(item.value)){
+ let tempArr = []
+ item.value.forEach(secItem=>{
+ secItem !== null && secItem !== undefined && tempArr.push(walk(secItem))
+ })
+ rsl.children.push({name:item.key,children:tempArr,node:node})
+ }else if(typeof item.value === 'object') {
+ rsl.children.push(walk(item.value,undefined,item.key))
+ } else {
+ rsl.children.push({name:item.key + ':' + item.value,node:node})
+ }
+ })
+ } else {
+ rsl.name = node === undefined ? '' : node + ''
+ rsl.node = parentNode || null
+ delete rsl.children
+ }
+ return rsl
+ }
+ let cloneParseResult = _.cloneDeep(parseResult)
+ const orgChart = [walk(cloneParseResult.ast,cloneParseResult.ast)]
+
+ function nodeMouseOver(node,event){
+ event.stopPropagation();
+ let data = node.nodeDatum
+ let range = treeAdapter.getRange(data.node);
+ if(range && data.__rd3t.depth !== 0) publish('HIGHLIGHT', {node: data.node, range});
+ }
+
+ function nodeMouseOut(node,event){
+ event.stopPropagation();
+ let data = node.nodeDatum
+ let range = treeAdapter.getRange(data.node);
+ if(range && data.__rd3t.depth !== 0) publish('CLEAR_HIGHLIGHT', {node: data.node, range});
+ }
+
+ function nodeClick(node,event){
+ event.stopPropagation();
+ node.toggleNode()
+ }
+
+ let top = null
+ let inRangeNodes = []
+ let width = document.documentElement.clientWidth/2
+ let height = document.documentElement.clientHeight/2
+ let treeVisualizationContainer = document.getElementById('treeVisualizationContainer')
+ if(treeVisualizationContainer) {
+ width = document.getElementById('treeVisualizationContainer').clientWidth
+ height = document.getElementById('treeVisualizationContainer').clientHeight
+ }
+
+ useEffect(()=>{
+ let targetNode = null
+ inRangeNodes.forEach(item=>{
+ if(targetNode === null) {
+ targetNode = item
+ }else if(targetNode.hierarchyPointNode.depth < item.hierarchyPointNode.depth ) {
+ targetNode = item
+ }
+ item.nodeDatum.__rd3t.collapsed = false
+ })
+ setTimeout(() => {
+ if(targetNode) {
+ let [targetG,transform,position] = [null,'','']
+ targetG = document.getElementById(targetNode.nodeDatum.__rd3t.id)
+ targetG && (transform = targetG.getAttribute('transform') || '')
+ transform && (position = transform.replace(/^translate\(/,'').replace(/\)$/,''))
+ position = position.split(',').map(item=>Number(item))
+ targetNode.hierarchyPointNode.x = position[0]
+ targetNode.hierarchyPointNode.y = position[1]
+ TreeRef && targetNode && TreeRef.current.centerNode(targetNode.hierarchyPointNode)
+ inRangeNodes.length === 0 && top !== null && TreeRef.current.centerNode(top.hierarchyPointNode)
+ }
+ }, 100);
+ subscribe('PANEL_RESIZE', () => {
+ let treeVisualizationContainer = document.getElementById('treeVisualizationContainer')
+ if(treeVisualizationContainer) {
+ width = document.getElementById('treeVisualizationContainer').clientWidth
+ height = document.getElementById('treeVisualizationContainer').clientHeight
+ }
+ })
+ })
+
+ function renderNode(node){
+ if(top === null) top = node
+ let nodeData = node.nodeDatum.node
+ if(node.nodeDatum.node && isInRange(nodeData)){
+ inRangeNodes.push(node)
+ }
+ return (
+
+ {node.nodeDatum && node.nodeDatum.name}
+