forked from p34709852/monkey
VennDiagram component
This commit is contained in:
parent
6843606a4f
commit
83ed12249e
|
@ -0,0 +1,46 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types';
|
||||
import * as d3 from 'd3'
|
||||
|
||||
class ArcNode extends React.Component{
|
||||
|
||||
render() {
|
||||
|
||||
let {prefix, ref, index, data} = this.props;;
|
||||
|
||||
let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0);
|
||||
|
||||
return (
|
||||
|
||||
<g transform={'rotate(180)'} id={data.node.pillar}>
|
||||
<path
|
||||
|
||||
id={this._reactInternalFiber.key}
|
||||
className={'arcNode'}
|
||||
data-tooltip={data.tooltip}
|
||||
d={arc()}
|
||||
fill={data.hex}
|
||||
|
||||
/>
|
||||
<text x={0} dy={data.fontStyle.size * 1.2} fontSize={data.fontStyle.size} textAnchor='middle' pointerEvents={'none'}>
|
||||
<textPath href={'#' + this._reactInternalFiber.key} startOffset={'26.4%'}>
|
||||
{data.label}
|
||||
</textPath>
|
||||
</text>
|
||||
</g>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ArcNode.propTypes = {
|
||||
|
||||
prefix: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
data: PropTypes.object
|
||||
|
||||
}
|
||||
|
||||
export default ArcNode;
|
|
@ -0,0 +1,57 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class CircularNode extends React.Component{
|
||||
|
||||
render() {
|
||||
|
||||
let {prefix, ref, index, data} = this.props;
|
||||
|
||||
let tspans = data.label.split("|").map((d_, i_) =>{
|
||||
|
||||
let halfTextHeight = data.fontStyle.size * data.label.split("|").length / 2;
|
||||
let key = 'vennDiagramCircularNode' + index + '_Tspan' + i_;
|
||||
|
||||
return (
|
||||
|
||||
<tspan key={key} x={data.offset.x} y={data.offset.y - halfTextHeight + i_ * data.fontStyle.size}>{d_}</tspan>
|
||||
|
||||
)
|
||||
|
||||
})
|
||||
|
||||
let translate = 'translate(' + data.cx + ',' + data.cy + ')';
|
||||
|
||||
return (
|
||||
|
||||
<g transform={translate} id={data.node.pillar}>
|
||||
<circle
|
||||
|
||||
id={this._reactInternalFiber.key}
|
||||
className={'circularNode'}
|
||||
data-tooltip={data.tooltip}
|
||||
r={data.r}
|
||||
opacity={0.8}
|
||||
fill={data.hex}
|
||||
|
||||
/>
|
||||
<text textAnchor='middle' fill={data.fontStyle.color} dominantBaseline={'middle'} fontSize={data.fontStyle.size + 'px'} pointerEvents={'none'}>
|
||||
{tspans}
|
||||
</text>
|
||||
</g>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
CircularNode.propTypes = {
|
||||
|
||||
prefix: PropTypes.string,
|
||||
index: PropTypes.number,
|
||||
data: PropTypes.object
|
||||
|
||||
}
|
||||
|
||||
export default CircularNode;
|
|
@ -0,0 +1,49 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Tooltip extends React.Component{
|
||||
|
||||
render() {
|
||||
|
||||
const {prefix, bcolor, top, left, display, html } = this.props;
|
||||
|
||||
const style = {
|
||||
|
||||
backgroundColor: bcolor,
|
||||
border : '1px solid #FFFFFF',
|
||||
borderRadius: '2px',
|
||||
fontSize: 10,
|
||||
padding: 8,
|
||||
display,
|
||||
opacity: 0.9,
|
||||
position: 'fixed',
|
||||
top,
|
||||
left,
|
||||
pointerEvents: 'none'
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<div className='tooltip' style={style}>
|
||||
{html.split('\n').map((i_, key_) => { return <div key={prefix + 'Element' + key_}>{i_}</div>; })}
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Tooltip.propTypes = {
|
||||
|
||||
prefix: PropTypes.string,
|
||||
bcolor: PropTypes.string,
|
||||
top: PropTypes.number,
|
||||
left: PropTypes.number,
|
||||
display: PropTypes.string,
|
||||
html: PropTypes.string
|
||||
|
||||
}
|
||||
|
||||
export default Tooltip;
|
|
@ -0,0 +1,14 @@
|
|||
@import url('https://fonts.googleapis.com/css?family=Noto+Sans&display=swap');
|
||||
|
||||
body { margin: 0; font-family: "Noto Sans", sans-serif; }
|
||||
|
||||
svg {
|
||||
|
||||
-webkit-touch-callout: none; /* iOS Safari */
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-khtml-user-select: none; /* Konqueror HTML */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none; /* Non-prefixed version, currently supported by Chrome and Opera */
|
||||
|
||||
}
|
|
@ -0,0 +1,369 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types';
|
||||
import Dimensions from 'react-dimensions'
|
||||
import Tooltip from './Tooltip'
|
||||
import CircularNode from './CircularNode'
|
||||
import ArcNode from './ArcNode'
|
||||
import { TypographicUtilities } from './utility.js'
|
||||
import './index.css'
|
||||
|
||||
/*
|
||||
|
||||
TODO LIST
|
||||
|
||||
UPDATED [21.08.2019]
|
||||
|
||||
[-] SVG > ViewBox 0 0 512 512, so it would be responsive and scalable
|
||||
[-] Add resize listener to ResponsiveVennDiagram wrapper
|
||||
[-] I have noticed that you have PillarGrades at ZeroTrustReportPage, so how I can fetch the data out of it?
|
||||
|
||||
UPDATED [20.08.2019]
|
||||
|
||||
[x] I've seen that lots of D3 responsive examples are using 'wrapper' around the main component to
|
||||
get parent container dimensions. And lister for resize.
|
||||
|
||||
So, here it's Responsive(VennDiagram)
|
||||
|
||||
If it doesn't work, I have another alternative:
|
||||
|
||||
import React, { useRef, useEffect, useState, useLayoutEffect } from 'react'
|
||||
|
||||
const ResponsiveVennDiagram = props => {
|
||||
|
||||
const minWidth = 256;
|
||||
const targetRef = useRef();
|
||||
const [dimensions, setDimensions] = useState({ width: 0, heigth: 0 });
|
||||
let movement_timer = null;
|
||||
const RESET_TIMEOUT = 100;
|
||||
|
||||
const checkForDimensionsUpdate = () => {
|
||||
|
||||
if (targetRef.current) {
|
||||
|
||||
setDimensions({
|
||||
|
||||
width: Math.min(targetRef.current.offsetWidth, targetRef.current.offsetHeight),
|
||||
height: targetRef.current.offsetHeight
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
useLayoutEffect(() => { checkForDimensionsUpdate(); }, []);
|
||||
|
||||
window.addEventListener("resize", () => { clearInterval(movement_timer); movement_timer = setTimeout(checkForDimensionsUpdate, RESET_TIMEOUT); });
|
||||
|
||||
return (
|
||||
|
||||
<div ref={targetRef} }>
|
||||
<VennDiagram pillarsGrades={props.pillarsGrades} width={Math.max(minWidth, dimensions.width)} height={Math.max(minWidth, dimensions.width)} />
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
ResponsiveVennDiagram.propTypes = {
|
||||
|
||||
pillarsGrades: PropTypes.array
|
||||
|
||||
}
|
||||
|
||||
export default ResponsiveVennDiagram;
|
||||
|
||||
While your diagram laout is squared, the VennDiaagram gets the min(width, height)
|
||||
|
||||
[x] Colours have been updated.
|
||||
|
||||
[x] String prototypes have been moved to '.utilities.js'
|
||||
|
||||
[x] I have use the prefix 'vennDiagram' for all children elements to prevent naming conflicts with other components
|
||||
DOM objects.
|
||||
|
||||
[x] I have used PropTypes already a year ago on ThreeJS/ReactJS stack project.
|
||||
|
||||
[!] External callback is still on my list, can you make a mockup for external function which have to get pillar variable?
|
||||
|
||||
[!] Z-indices sorting on mouseover
|
||||
Would be n my 21.08.2019 TODO list. With D3.JS it's an easy task, however would try do
|
||||
make these z-soring without D3 framework.
|
||||
|
||||
UPDATED [20.08.2019]
|
||||
|
||||
[!] By now, there are three input props for VennDiagram component:
|
||||
data, width and height.
|
||||
|
||||
[-] Since layout has to be hardcoded, it's driven by this.layout object.
|
||||
There're many ways of setting circles/arc positions, now I'm using the
|
||||
stright-forward one.
|
||||
|
||||
Usually, I am put all hardcoded params to external JSON file, i.e config.json.
|
||||
Let me know if it's a good idea.
|
||||
|
||||
[-] Could rearange z-indecies for nodes on hover, so highlighted node would have highest z-index.
|
||||
[-] If you want callback on click even, please provide additional info what are you expecting to pass
|
||||
through this.
|
||||
|
||||
[!] I don't used to make lots of comments in my code, but treying to name everything in a way,
|
||||
so third person could instantly get its job and concept.
|
||||
|
||||
If it is not enoough just let me know.
|
||||
|
||||
[!] I have tried to avoid using D3 so much, especially its mouse events, cause it creates a bunch
|
||||
of listeners for every children DOM elements. I have tried to use raw SVG objects.
|
||||
The ArcNode is the only component where I've to call d3.arc constrictor.
|
||||
|
||||
[!] There are lots of discussion over different blogs an forums that ReactJS and D3.JS have a DOM
|
||||
issue conflict, [could find lots of articels at medium.org about that, for example,
|
||||
https://medium.com/@tibotiber/react-d3-js-balancing-performance-developer-experience-4da35f912484].
|
||||
|
||||
Since the current component has only a few DOM elements, I don't thing we would have any troubles,
|
||||
but please keep in mind that I could tweak current version with react-faux-dom.
|
||||
|
||||
Actually, by now, I'm using D3 only for math, for arc path calculations.
|
||||
|
||||
[!] Don't mind about code spacings, it's just for me, the final code would be clear out of them.
|
||||
|
||||
[-] On click, an EXTERNAL callback should be called with the pillar name as a parameter. That is to enable us to expand the click functionality in future without editing the internal implementation of the component.
|
||||
|
||||
[-] planned, [x] done, [!] see comments
|
||||
|
||||
@author Vladimir V KUCHINOV
|
||||
@email helloworld@vkuchinov.co.uk
|
||||
|
||||
*/
|
||||
|
||||
const VENN_MIN_WIDTH = 256;
|
||||
|
||||
class ResponsiveVennDiagram extends React.Component {
|
||||
|
||||
constructor(props) { super(props); }
|
||||
render() {
|
||||
|
||||
const {options, pillarsGrades} = this.props;
|
||||
let childrenWidth = this.props.containerWidth, childrenHeight = this.props.containerHeight;
|
||||
|
||||
if(childrenHeight === 0 || childrenHeight === NaN){ childrenHeight = childrenWidth; }
|
||||
else{ childrenWidth = Math.min(childrenWidth, childrenHeight) }
|
||||
|
||||
return (
|
||||
|
||||
<div ref={this.divElement}>
|
||||
<VennDiagram width={Math.max(childrenWidth, VENN_MIN_WIDTH)} height={Math.max(childrenHeight, VENN_MIN_WIDTH)} pillarsGrades={pillarsGrades} />
|
||||
</div> )
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ResponsiveVennDiagram.propTypes = {
|
||||
|
||||
pillarsGrades: PropTypes.array
|
||||
|
||||
}
|
||||
|
||||
export default Dimensions()(ResponsiveVennDiagram);
|
||||
|
||||
class VennDiagram extends React.Component{
|
||||
|
||||
constructor(props_){
|
||||
|
||||
super(props_);
|
||||
|
||||
this.state = { tooltip: { top: 0, left: 0, display: 'none', html: '' } }
|
||||
|
||||
this.colors = ['#777777', '#D9534F', '#F0AD4E', '#5CB85C'];
|
||||
this.prefix = 'vennDiagram';
|
||||
this.suffices = ['', '|tests are|conclusive', '|tests were|inconclusive', '|tests|performed'];
|
||||
this.fontStyles = [{size: Math.max(9, this.props.width / 32), color: 'white'}, {size: Math.max(6, this.props.width / 52), color: 'black'}];
|
||||
this.offset = this.props.width / 16;
|
||||
|
||||
this.thirdWidth = this.props.width / 3;
|
||||
this.sixthWidth = this.props.width / 6;
|
||||
this.width2By7 = 2 * this.props.width / 7
|
||||
this.width1By11 = this.props.width / 11;
|
||||
this.width1By28 = this.props.width / 28;
|
||||
|
||||
this.toggle = false;
|
||||
|
||||
this.layout = {
|
||||
|
||||
Data: { cx: 0, cy: 0, r: this.thirdWidth - this.offset * 2, offset: {x: 0, y: 0} },
|
||||
People: { cx: -this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: this.width1By11, y: 0} },
|
||||
Networks: { cx: this.width2By7, cy: 0, r: this.sixthWidth, offset: {x: -this.width1By11, y: 0} },
|
||||
Devices: { cx: 0, cy: this.width2By7, r: this.sixthWidth, offset: {x: 0, y: -this.width1By11} },
|
||||
Workloads : { cx: 0, cy: -this.width2By7, r: this.sixthWidth, offset: {x: 0, y: this.width1By11} },
|
||||
VisibilityAndAnalytics : { inner: this.thirdWidth - this.width1By28, outer: this.thirdWidth },
|
||||
AutomationAndOrchestration: { inner: this.thirdWidth - this.width1By28 * 2, outer: this.thirdWidth - this.width1By28 }
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
this.parseData();
|
||||
|
||||
}
|
||||
|
||||
_onMouseMove(e) {
|
||||
|
||||
if(!this.toggle){
|
||||
|
||||
let hidden = 'none';
|
||||
let html = '';
|
||||
let bcolor = '#DEDEDE';
|
||||
|
||||
e.stopPropagation();
|
||||
|
||||
document.querySelectorAll('circle, path').forEach((d_, i_) => { d_.setAttribute('opacity', 0.8)});
|
||||
|
||||
if(e.target.id.includes('Node')) {
|
||||
|
||||
html = e.target.dataset.tooltip;
|
||||
this.divElement.style.cursor = 'pointer';
|
||||
hidden = 'block'; e.target.setAttribute('opacity', 1.0);
|
||||
bcolor = e.target.getAttribute('fill');
|
||||
|
||||
}else{
|
||||
|
||||
this.divElement.style.cursor = 'default';
|
||||
|
||||
}
|
||||
|
||||
this.setState({target: e, tooltip: { target: e.target, bcolor: bcolor, top: e.clientY + 8, left: e.clientX + 8, display: hidden, html: html } });
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
_onClick(e) {
|
||||
|
||||
if(this.state.tooltip.target === e.target) { this.toggle = true; } else { this.toggle = false; }
|
||||
|
||||
//variable to external callback
|
||||
//e.target.parentNode.id)
|
||||
|
||||
}
|
||||
|
||||
relativeCoords (e) {
|
||||
|
||||
let bounds = e.target.getBoundingClientRect();
|
||||
var x = e.clientX - bounds.left;
|
||||
var y = e.clientY - bounds.top;
|
||||
return {x: x, y: y};
|
||||
|
||||
}
|
||||
|
||||
parseData(){
|
||||
|
||||
let self = this;
|
||||
let data = [];
|
||||
const omit = (prop, { [prop]: _, ...rest }) => rest;
|
||||
|
||||
this.props.pillarsGrades.forEach((d_, i_) => {
|
||||
|
||||
let params = omit('pillar', d_);
|
||||
let sum = Object.keys(params).reduce((sum_, key_) => sum_ + parseFloat(params[key_]||0), 0);
|
||||
let key = TypographicUtilities.removeAmpersand(d_.pillar);
|
||||
let html = self.buildTooltipHtmlContent(d_);
|
||||
let rule = 3;
|
||||
|
||||
if(sum === 0){ rule = 0 }
|
||||
else if(d_['Conclusive'] > 0){ rule = 1 }
|
||||
else if(d_['Conclusive'] === 0 && d_['Inconclusive'] > 0) { rule = 2 }
|
||||
|
||||
self.setLayoutElement(rule, key, html, d_);
|
||||
data.push(this.layout[key])
|
||||
|
||||
})
|
||||
|
||||
this.setState({ data: data });
|
||||
this.render();
|
||||
|
||||
}
|
||||
|
||||
buildTooltipHtmlContent(object_){ return Object.keys(object_).reduce((out_, key_) => out_ + TypographicUtilities.setTitle(key_) + ': ' + object_[key_] + '\n', ''); }
|
||||
|
||||
setLayoutElement(rule_, key_, html_, d_){
|
||||
|
||||
if(key_ === 'Data'){ this.layout[key_].fontStyle = this.fontStyles[0]; }
|
||||
else {this.layout[key_].fontStyle = this.fontStyles[1]; }
|
||||
|
||||
this.layout[key_].hex = this.colors[rule_];
|
||||
this.layout[key_].label = d_.pillar + this.suffices[rule_];
|
||||
this.layout[key_].node = d_;
|
||||
this.layout[key_].tooltip = html_;
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
if(this.state.data === undefined) { return null; }
|
||||
else {
|
||||
|
||||
let { width, height } = this.props;
|
||||
let translate = 'translate(' + width /2 + ',' + height/2 + ')';
|
||||
|
||||
let nodes = Object.values(this.layout).map((d_, i_) =>{
|
||||
|
||||
if(d_.hasOwnProperty('cx')){
|
||||
|
||||
return (
|
||||
|
||||
<CircularNode
|
||||
|
||||
prefix={this.prefix}
|
||||
key={this.prefix + 'Node_' + i_}
|
||||
index={i_}
|
||||
data={d_}
|
||||
|
||||
/>
|
||||
);
|
||||
|
||||
}else{
|
||||
|
||||
d_.label = TypographicUtilities.removeBrokenBar(d_.label);
|
||||
|
||||
return (
|
||||
|
||||
<ArcNode
|
||||
|
||||
prefix={this.prefix}
|
||||
key={this.prefix + 'Node_' + i_}
|
||||
index={i_}
|
||||
data={d_}
|
||||
|
||||
/>
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
<div id={this.prefix} ref={ (divElement) => this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)} onClick={this._onClick.bind(this)}>
|
||||
<svg width={width} height={height} xmlns='http://www.w3.org/2000/svg' xmlnsXlink='http://www.w3.org/1999/xlink'>
|
||||
<g id={this.prefix + 'TransformGroup'} ref={'vennDiagram'} transform={translate}>
|
||||
{nodes}
|
||||
</g>
|
||||
</svg>
|
||||
<Tooltip id={this.prefix + 'Tooltip'} prefix={this.prefix} {...this.state.tooltip} />
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
VennDiagram.propTypes = {
|
||||
|
||||
pillarsGrades: PropTypes.array,
|
||||
width: PropTypes.number,
|
||||
height: PropTypes.number
|
||||
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export class TypographicUtilities {
|
||||
|
||||
static removeAmpersand(string_) { return string_.replace(' & ', 'And'); }
|
||||
static removeBrokenBar(string_) { return string_.replace(/\|/g, ' '); }
|
||||
static setTitle(string_) { return string_.charAt(0).toUpperCase() + string_.substr(1).toLowerCase(); }
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue