Merge pull request #412 from guardicore/400-zero-trust-mvp-venn-diagram

VennDiagram component first version is complete.
This commit is contained in:
Shay Nehmad 2019-08-26 15:26:40 +03:00 committed by GitHub
commit 59581d3cc1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 1044 additions and 64 deletions

View File

@ -0,0 +1,18 @@
def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet):
"""
Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet.
:param ip_addresses: List[str]: List of IP addresses to test.
:param source_subnet: NetworkRange: Subnet to want an IP to not be in.
:param target_subnet: NetworkRange: Subnet we want an IP to be in.
:return: The cross segment IP if in source but not in target, else None. Union[str, None]
"""
if get_ip_if_in_subnet(ip_addresses, target_subnet) is not None:
return None
return get_ip_if_in_subnet(ip_addresses, source_subnet)
def get_ip_if_in_subnet(ip_addresses, source_subnet):
for ip_address in ip_addresses:
if source_subnet.is_in_range(ip_address):
return ip_address
return None

View File

@ -0,0 +1,19 @@
from common.network.network_range import *
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
class TestSegmentationUtils(IslandTestCase):
def test_get_ip_in_src_and_not_in_dst(self):
self.fail_if_not_testing_env()
source = CidrRange("1.1.1.0/24")
target = CidrRange("2.2.2.0/24")
self.assertIsNone(get_ip_in_src_and_not_in_dst(
[text_type("2.2.2.2")], source, target
))
self.assertIsNone(get_ip_in_src_and_not_in_dst(
[text_type("3.3.3.3"), text_type("4.4.4.4")], source, target
))
self.assertIsNotNone(get_ip_in_src_and_not_in_dst(
[text_type("8.8.8.8"), text_type("1.1.1.1")], source, target
))

View File

@ -113,7 +113,7 @@ class InfectionMonkey(object):
if monkey_tunnel:
monkey_tunnel.start()
StateTelem(False).send()
StateTelem(is_done=False).send()
TunnelTelem().send()
if WormConfiguration.collect_system_info:
@ -225,7 +225,7 @@ class InfectionMonkey(object):
InfectionMonkey.close_tunnel()
firewall.close()
else:
StateTelem(False).send() # Signal the server (before closing the tunnel)
StateTelem(is_done=True).send() # Signal the server (before closing the tunnel)
InfectionMonkey.close_tunnel()
firewall.close()
if WormConfiguration.send_log_to_server:

View File

@ -1,7 +1,11 @@
import abc
import json
import logging
from infection_monkey.control import ControlClient
logger = logging.getLogger(__name__)
__author__ = 'itay.mizeretz'
@ -19,7 +23,9 @@ class BaseTelem(object):
"""
Sends telemetry to island
"""
ControlClient.send_telemetry(self.telem_category, self.get_data())
data = self.get_data()
logger.debug("Sending {} telemetry. Data: {}".format(self.telem_category, json.dumps(data)))
ControlClient.send_telemetry(self.telem_category, data)
@abc.abstractproperty
def telem_category(self):

View File

@ -32,6 +32,8 @@ class Finding(Document):
test = StringField(required=True, choices=TESTS)
status = StringField(required=True, choices=ORDERED_TEST_STATUSES)
events = EmbeddedDocumentListField(document_type=Event)
# http://docs.mongoengine.org/guide/defining-documents.html#document-inheritance
meta = {'allow_inheritance': True}
# LOGIC
def get_test_explanation(self):

View File

@ -0,0 +1,52 @@
from mongoengine import StringField
from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_CONCLUSIVE, STATUS_POSITIVE
from monkey_island.cc.models.zero_trust.finding import Finding
def need_to_overwrite_status(saved_status, new_status):
return (saved_status == STATUS_POSITIVE) and (new_status == STATUS_CONCLUSIVE)
class SegmentationFinding(Finding):
"""
trying to add conclusive:
If the finding doesn't exist at all: create conclusive
else:
if positive, turn to conclusive
add event
trying to add positive:
If the finding doesn't exist at all: create positive
else: add event
"""
first_subnet = StringField()
second_subnet = StringField()
@staticmethod
def create_or_add_to_existing_finding(subnets, status, segmentation_event):
assert len(subnets) == 2
# Sort them so A -> B and B -> A segmentation findings will be the same one.
subnets.sort()
existing_findings = SegmentationFinding.objects(first_subnet=subnets[0], second_subnet=subnets[1])
if len(existing_findings) == 0:
# No finding exists - create.
new_finding = SegmentationFinding(
first_subnet=subnets[0],
second_subnet=subnets[1],
test=TEST_SEGMENTATION,
status=status,
events=[segmentation_event]
)
new_finding.save()
else:
# A finding exists (should be one). Add the event to it.
assert len(existing_findings) == 1
existing_finding = existing_findings[0]
existing_finding.events.append(segmentation_event)
if need_to_overwrite_status(existing_finding.status, status):
existing_finding.status = status
existing_finding.save()

View File

@ -0,0 +1,52 @@
from common.data.zero_trust_consts import STATUS_CONCLUSIVE, EVENT_TYPE_MONKEY_NETWORK
from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
class TestSegmentationFinding(IslandTestCase):
def test_create_or_add_to_existing_finding(self):
self.fail_if_not_testing_env()
self.clean_finding_db()
first_segment = "1.1.1.0/24"
second_segment = "2.2.2.0-2.2.2.254"
third_segment = "3.3.3.3"
event = Event.create_event("bla", "bla", EVENT_TYPE_MONKEY_NETWORK)
SegmentationFinding.create_or_add_to_existing_finding(
subnets=[first_segment, second_segment],
status=STATUS_CONCLUSIVE,
segmentation_event=event
)
self.assertEquals(len(SegmentationFinding.objects()), 1)
self.assertEquals(len(SegmentationFinding.objects()[0].events), 1)
SegmentationFinding.create_or_add_to_existing_finding(
# !!! REVERSE ORDER
subnets=[second_segment, first_segment],
status=STATUS_CONCLUSIVE,
segmentation_event=event
)
self.assertEquals(len(SegmentationFinding.objects()), 1)
self.assertEquals(len(SegmentationFinding.objects()[0].events), 2)
SegmentationFinding.create_or_add_to_existing_finding(
# !!! REVERSE ORDER
subnets=[first_segment, third_segment],
status=STATUS_CONCLUSIVE,
segmentation_event=event
)
self.assertEquals(len(SegmentationFinding.objects()), 2)
SegmentationFinding.create_or_add_to_existing_finding(
# !!! REVERSE ORDER
subnets=[second_segment, third_segment],
status=STATUS_CONCLUSIVE,
segmentation_event=event
)
self.assertEquals(len(SegmentationFinding.objects()), 3)

View File

@ -0,0 +1,5 @@
from monkey_island.cc.services.config import ConfigService
def get_config_network_segments_as_subnet_groups():
return [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])]

View File

@ -9,10 +9,12 @@ from enum import Enum
from six import text_type
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
from monkey_island.cc.database import mongo
from monkey_island.cc.models import Monkey
from monkey_island.cc.report_exporter_manager import ReportExporterManager
from monkey_island.cc.services.config import ConfigService
from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
from monkey_island.cc.services.edge import EdgeService
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.utils import local_ip_addresses, get_subnets
@ -423,23 +425,6 @@ class ReportService:
return issues
@staticmethod
def get_ip_in_src_and_not_in_dst(ip_addresses, source_subnet, target_subnet):
"""
Finds an IP address in ip_addresses which is in source_subnet but not in target_subnet.
:param ip_addresses: List of IP addresses to test.
:param source_subnet: Subnet to want an IP to not be in.
:param target_subnet: Subnet we want an IP to be in.
:return:
"""
for ip_address in ip_addresses:
if target_subnet.is_in_range(ip_address):
return None
for ip_address in ip_addresses:
if source_subnet.is_in_range(ip_address):
return ip_address
return None
@staticmethod
def get_cross_segment_issues_of_single_machine(source_subnet_range, target_subnet_range):
"""
@ -502,9 +487,9 @@ class ReportService:
target_ip = scan['data']['machine']['ip_addr']
if target_subnet_range.is_in_range(text_type(target_ip)):
monkey = NodeService.get_monkey_by_guid(scan['monkey_guid'])
cross_segment_ip = ReportService.get_ip_in_src_and_not_in_dst(monkey['ip_addresses'],
source_subnet_range,
target_subnet_range)
cross_segment_ip = get_ip_in_src_and_not_in_dst(monkey['ip_addresses'],
source_subnet_range,
target_subnet_range)
if cross_segment_ip is not None:
cross_segment_issues.append(
@ -552,7 +537,7 @@ class ReportService:
cross_segment_issues = []
# For now the feature is limited to 1 group.
subnet_groups = [ConfigService.get_config_value(['basic_network', 'network_analysis', 'inaccessible_subnets'])]
subnet_groups = get_config_network_segments_as_subnet_groups()
for subnet_group in subnet_groups:
cross_segment_issues += ReportService.get_cross_segment_issues_per_subnet_group(scans, subnet_group)

View File

@ -3,11 +3,13 @@ import copy
from monkey_island.cc.database import mongo
from monkey_island.cc.services.telemetry.processing.utils import get_edge_by_scan_or_exploit_telemetry
from monkey_island.cc.services.telemetry.zero_trust_tests.data_endpoints import test_open_data_endpoints
from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import test_segmentation_violation
def process_scan_telemetry(telemetry_json):
update_edges_and_nodes_based_on_scan_telemetry(telemetry_json)
test_open_data_endpoints(telemetry_json)
test_segmentation_violation(telemetry_json)
def update_edges_and_nodes_based_on_scan_telemetry(telemetry_json):

View File

@ -1,4 +1,6 @@
from monkey_island.cc.services.node import NodeService
from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import \
test_positive_findings_for_unreached_segments
def process_state_telemetry(telemetry_json):
@ -8,3 +10,6 @@ def process_state_telemetry(telemetry_json):
NodeService.set_monkey_dead(monkey, True)
else:
NodeService.set_monkey_dead(monkey, False)
if telemetry_json['data']['done']:
test_positive_findings_for_unreached_segments(telemetry_json)

View File

@ -0,0 +1,100 @@
import itertools
from six import text_type
from common.data.zero_trust_consts import STATUS_CONCLUSIVE, EVENT_TYPE_MONKEY_NETWORK, STATUS_POSITIVE, \
EVENT_TYPE_ISLAND
from common.network.network_range import NetworkRange
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst, get_ip_if_in_subnet
from monkey_island.cc.models import Monkey
from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
from monkey_island.cc.services.configuration.utils import get_config_network_segments_as_subnet_groups
SEGMENTATION_VIOLATION_EVENT_TEXT = \
"Segmentation violation! Monkey on '{hostname}', with the {source_ip} IP address (in segment {source_seg}) " \
"managed to communicate cross segment to {target_ip} (in segment {target_seg})."
def is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
if source_subnet == target_subnet:
return False
source_subnet_range = NetworkRange.get_range_obj(source_subnet)
target_subnet_range = NetworkRange.get_range_obj(target_subnet)
if target_subnet_range.is_in_range(text_type(target_ip)):
cross_segment_ip = get_ip_in_src_and_not_in_dst(
current_monkey.ip_addresses,
source_subnet_range,
target_subnet_range)
return cross_segment_ip is not None
def test_segmentation_violation(scan_telemetry_json):
# TODO - lower code duplication between this and report.py.
# TODO - single machine
current_monkey = Monkey.get_single_monkey_by_guid(scan_telemetry_json['monkey_guid'])
target_ip = scan_telemetry_json['data']['machine']['ip_addr']
subnet_groups = get_config_network_segments_as_subnet_groups()
for subnet_group in subnet_groups:
subnet_pairs = itertools.product(subnet_group, subnet_group)
for subnet_pair in subnet_pairs:
source_subnet = subnet_pair[0]
target_subnet = subnet_pair[1]
if is_segmentation_violation(current_monkey, target_ip, source_subnet, target_subnet):
event = get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet)
SegmentationFinding.create_or_add_to_existing_finding(
subnets=[source_subnet, target_subnet],
status=STATUS_CONCLUSIVE,
segmentation_event=event
)
def get_segmentation_violation_event(current_monkey, source_subnet, target_ip, target_subnet):
return Event.create_event(
title="Segmentation event",
message=SEGMENTATION_VIOLATION_EVENT_TEXT.format(
hostname=current_monkey.hostname,
source_ip=get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(source_subnet)),
source_seg=source_subnet,
target_ip=target_ip,
target_seg=target_subnet
),
event_type=EVENT_TYPE_MONKEY_NETWORK
)
def test_positive_findings_for_unreached_segments(state_telemetry_json):
flat_all_subnets = [item for sublist in get_config_network_segments_as_subnet_groups() for item in sublist]
current_monkey = Monkey.get_single_monkey_by_guid(state_telemetry_json['monkey_guid'])
create_or_add_findings_for_all_pairs(flat_all_subnets, current_monkey)
def create_or_add_findings_for_all_pairs(all_subnets, current_monkey):
# Filter the subnets that this monkey is part of.
this_monkey_subnets = []
for subnet in all_subnets:
if get_ip_if_in_subnet(current_monkey.ip_addresses, NetworkRange.get_range_obj(subnet)) is not None:
this_monkey_subnets.append(subnet)
# Get all the other subnets.
other_subnets = list(set(all_subnets) - set(this_monkey_subnets))
# Calculate the cartesian product - (this monkey subnets X other subnets). These pairs are the pairs that the monkey
# should have tested.
all_subnets_pairs_for_this_monkey = itertools.product(this_monkey_subnets, other_subnets)
for subnet_pair in all_subnets_pairs_for_this_monkey:
SegmentationFinding.create_or_add_to_existing_finding(
subnets=list(subnet_pair),
status=STATUS_POSITIVE,
segmentation_event=Event.create_event(
"Segmentation test done",
message="Monkey on {hostname} is done attempting cross-segment communications from `{src_seg}` "
"segments to `{dst_seg}` segments.".format(
hostname=current_monkey.hostname,
src_seg=subnet_pair[0],
dst_seg=subnet_pair[1]),
event_type=EVENT_TYPE_ISLAND
)
)

View File

@ -0,0 +1,51 @@
import uuid
from common.data.zero_trust_consts import TEST_SEGMENTATION, STATUS_POSITIVE, STATUS_CONCLUSIVE, \
EVENT_TYPE_MONKEY_NETWORK
from monkey_island.cc.models import Monkey
from monkey_island.cc.models.zero_trust.event import Event
from monkey_island.cc.models.zero_trust.finding import Finding
from monkey_island.cc.models.zero_trust.segmentation_finding import SegmentationFinding
from monkey_island.cc.services.telemetry.zero_trust_tests.segmentation import create_or_add_findings_for_all_pairs
from monkey_island.cc.testing.IslandTestCase import IslandTestCase
FIRST_SUBNET = "1.1.1.1"
SECOND_SUBNET = "2.2.2.0/24"
THIRD_SUBNET = "3.3.3.3-3.3.3.200"
class TestSegmentationTests(IslandTestCase):
def test_create_findings_for_all_done_pairs(self):
self.fail_if_not_testing_env()
self.clean_finding_db()
all_subnets = [FIRST_SUBNET, SECOND_SUBNET, THIRD_SUBNET]
monkey = Monkey(
guid=str(uuid.uuid4()),
ip_addresses=[FIRST_SUBNET])
# no findings
self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 0)
# This is like the monkey is done and sent done telem
create_or_add_findings_for_all_pairs(all_subnets, monkey)
# There are 2 subnets in which the monkey is NOT
self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_POSITIVE)), 2)
# This is a monkey from 2nd subnet communicated with 1st subnet.
SegmentationFinding.create_or_add_to_existing_finding(
[FIRST_SUBNET, SECOND_SUBNET],
STATUS_CONCLUSIVE,
Event.create_event(title="sdf", message="asd", event_type=EVENT_TYPE_MONKEY_NETWORK)
)
print("Printing all segmentation findings")
all_findings = Finding.objects(test=TEST_SEGMENTATION)
for f in all_findings:
print(f.to_json())
self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_POSITIVE)), 1)
self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION, status=STATUS_CONCLUSIVE)), 1)
self.assertEquals(len(Finding.objects(test=TEST_SEGMENTATION)), 2)

View File

@ -3087,8 +3087,7 @@
"commander": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.15.1.tgz",
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==",
"dev": true
"integrity": "sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag=="
},
"commondir": {
"version": "1.0.1",
@ -3657,6 +3656,270 @@
"es5-ext": "^0.10.9"
}
},
"d3": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/d3/-/d3-5.11.0.tgz",
"integrity": "sha512-LXgMVUAEAzQh6WfEEOa8tJX4RA64ZJ6twC3CJ+Xzid+fXWLTZkkglagXav/eOoQgzQi5rzV0xC4Sfspd6hFDHA==",
"requires": {
"d3-array": "1",
"d3-axis": "1",
"d3-brush": "1",
"d3-chord": "1",
"d3-collection": "1",
"d3-color": "1",
"d3-contour": "1",
"d3-dispatch": "1",
"d3-drag": "1",
"d3-dsv": "1",
"d3-ease": "1",
"d3-fetch": "1",
"d3-force": "1",
"d3-format": "1",
"d3-geo": "1",
"d3-hierarchy": "1",
"d3-interpolate": "1",
"d3-path": "1",
"d3-polygon": "1",
"d3-quadtree": "1",
"d3-random": "1",
"d3-scale": "2",
"d3-scale-chromatic": "1",
"d3-selection": "1",
"d3-shape": "1",
"d3-time": "1",
"d3-time-format": "2",
"d3-timer": "1",
"d3-transition": "1",
"d3-voronoi": "1",
"d3-zoom": "1"
}
},
"d3-array": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-1.2.4.tgz",
"integrity": "sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw=="
},
"d3-axis": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-1.0.12.tgz",
"integrity": "sha512-ejINPfPSNdGFKEOAtnBtdkpr24c4d4jsei6Lg98mxf424ivoDP2956/5HDpIAtmHo85lqT4pruy+zEgvRUBqaQ=="
},
"d3-brush": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-1.1.3.tgz",
"integrity": "sha512-v8bbYyCFKjyCzFk/tdWqXwDykY8YWqhXYjcYxfILIit085VZOpj4XJKOMccTsvWxgzSLMJQg5SiqHjslsipEDg==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"d3-chord": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-1.0.6.tgz",
"integrity": "sha512-JXA2Dro1Fxw9rJe33Uv+Ckr5IrAa74TlfDEhE/jfLOaXegMQFQTAgAw9WnZL8+HxVBRXaRGCkrNU7pJeylRIuA==",
"requires": {
"d3-array": "1",
"d3-path": "1"
}
},
"d3-collection": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/d3-collection/-/d3-collection-1.0.7.tgz",
"integrity": "sha512-ii0/r5f4sjKNTfh84Di+DpztYwqKhEyUlKoPrzUFfeSkWxjW49xU2QzO9qrPrNkpdI0XJkfzvmTu8V2Zylln6A=="
},
"d3-color": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.3.0.tgz",
"integrity": "sha512-NHODMBlj59xPAwl2BDiO2Mog6V+PrGRtBfWKqKRrs9MCqlSkIEb0Z/SfY7jW29ReHTDC/j+vwXhnZcXI3+3fbg=="
},
"d3-contour": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-1.3.2.tgz",
"integrity": "sha512-hoPp4K/rJCu0ladiH6zmJUEz6+u3lgR+GSm/QdM2BBvDraU39Vr7YdDCicJcxP1z8i9B/2dJLgDC1NcvlF8WCg==",
"requires": {
"d3-array": "^1.1.1"
}
},
"d3-dispatch": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.5.tgz",
"integrity": "sha512-vwKx+lAqB1UuCeklr6Jh1bvC4SZgbSqbkGBLClItFBIYH4vqDJCA7qfoy14lXmJdnBOdxndAMxjCbImJYW7e6g=="
},
"d3-drag": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-1.2.4.tgz",
"integrity": "sha512-ICPurDETFAelF1CTHdIyiUM4PsyZLaM+7oIBhmyP+cuVjze5vDZ8V//LdOFjg0jGnFIZD/Sfmk0r95PSiu78rw==",
"requires": {
"d3-dispatch": "1",
"d3-selection": "1"
}
},
"d3-dsv": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-1.1.1.tgz",
"integrity": "sha512-1EH1oRGSkeDUlDRbhsFytAXU6cAmXFzc52YUe6MRlPClmWb85MP1J5x+YJRzya4ynZWnbELdSAvATFW/MbxaXw==",
"requires": {
"commander": "2",
"iconv-lite": "0.4",
"rw": "1"
}
},
"d3-ease": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.5.tgz",
"integrity": "sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ=="
},
"d3-fetch": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-1.1.2.tgz",
"integrity": "sha512-S2loaQCV/ZeyTyIF2oP8D1K9Z4QizUzW7cWeAOAS4U88qOt3Ucf6GsmgthuYSdyB2HyEm4CeGvkQxWsmInsIVA==",
"requires": {
"d3-dsv": "1"
}
},
"d3-force": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-1.2.1.tgz",
"integrity": "sha512-HHvehyaiUlVo5CxBJ0yF/xny4xoaxFxDnBXNvNcfW9adORGZfyNF1dj6DGLKyk4Yh3brP/1h3rnDzdIAwL08zg==",
"requires": {
"d3-collection": "1",
"d3-dispatch": "1",
"d3-quadtree": "1",
"d3-timer": "1"
}
},
"d3-format": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-1.3.2.tgz",
"integrity": "sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ=="
},
"d3-geo": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-1.11.6.tgz",
"integrity": "sha512-z0J8InXR9e9wcgNtmVnPTj0TU8nhYT6lD/ak9may2PdKqXIeHUr8UbFLoCtrPYNsjv6YaLvSDQVl578k6nm7GA==",
"requires": {
"d3-array": "1"
}
},
"d3-hierarchy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-1.1.8.tgz",
"integrity": "sha512-L+GHMSZNwTpiq4rt9GEsNcpLa4M96lXMR8M/nMG9p5hBE0jy6C+3hWtyZMenPQdwla249iJy7Nx0uKt3n+u9+w=="
},
"d3-interpolate": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.3.2.tgz",
"integrity": "sha512-NlNKGopqaz9qM1PXh9gBF1KSCVh+jSFErrSlD/4hybwoNX/gt1d8CDbDW+3i+5UOHhjC6s6nMvRxcuoMVNgL2w==",
"requires": {
"d3-color": "1"
}
},
"d3-path": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.8.tgz",
"integrity": "sha512-J6EfUNwcMQ+aM5YPOB8ZbgAZu6wc82f/0WFxrxwV6Ll8wBwLaHLKCqQ5Imub02JriCVVdPjgI+6P3a4EWJCxAg=="
},
"d3-polygon": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-1.0.5.tgz",
"integrity": "sha512-RHhh1ZUJZfhgoqzWWuRhzQJvO7LavchhitSTHGu9oj6uuLFzYZVeBzaWTQ2qSO6bz2w55RMoOCf0MsLCDB6e0w=="
},
"d3-quadtree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-1.0.6.tgz",
"integrity": "sha512-NUgeo9G+ENQCQ1LsRr2qJg3MQ4DJvxcDNCiohdJGHt5gRhBW6orIB5m5FJ9kK3HNL8g9F4ERVoBzcEwQBfXWVA=="
},
"d3-random": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-1.1.2.tgz",
"integrity": "sha512-6AK5BNpIFqP+cx/sreKzNjWbwZQCSUatxq+pPRmFIQaWuoD+NrbVWw7YWpHiXpCQ/NanKdtGDuB+VQcZDaEmYQ=="
},
"d3-scale": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-2.2.2.tgz",
"integrity": "sha512-LbeEvGgIb8UMcAa0EATLNX0lelKWGYDQiPdHj+gLblGVhGLyNbaCn3EvrJf0A3Y/uOOU5aD6MTh5ZFCdEwGiCw==",
"requires": {
"d3-array": "^1.2.0",
"d3-collection": "1",
"d3-format": "1",
"d3-interpolate": "1",
"d3-time": "1",
"d3-time-format": "2"
}
},
"d3-scale-chromatic": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-1.5.0.tgz",
"integrity": "sha512-ACcL46DYImpRFMBcpk9HhtIyC7bTBR4fNOPxwVSl0LfulDAwyiHyPOTqcDG1+t5d4P9W7t/2NAuWu59aKko/cg==",
"requires": {
"d3-color": "1",
"d3-interpolate": "1"
}
},
"d3-selection": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.0.tgz",
"integrity": "sha512-EYVwBxQGEjLCKF2pJ4+yrErskDnz5v403qvAid96cNdCMr8rmCYfY5RGzWz24mdIbxmDf6/4EAH+K9xperD5jg=="
},
"d3-shape": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.5.tgz",
"integrity": "sha512-VKazVR3phgD+MUCldapHD7P9kcrvPcexeX/PkMJmkUov4JM8IxsSg1DvbYoYich9AtdTsa5nNk2++ImPiDiSxg==",
"requires": {
"d3-path": "1"
}
},
"d3-time": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-1.0.11.tgz",
"integrity": "sha512-Z3wpvhPLW4vEScGeIMUckDW7+3hWKOQfAWg/U7PlWBnQmeKQ00gCUsTtWSYulrKNA7ta8hJ+xXc6MHrMuITwEw=="
},
"d3-time-format": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-2.1.3.tgz",
"integrity": "sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==",
"requires": {
"d3-time": "1"
}
},
"d3-timer": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.9.tgz",
"integrity": "sha512-rT34J5HnQUHhcLvhSB9GjCkN0Ddd5Y8nCwDBG2u6wQEeYxT/Lf51fTFFkldeib/sE/J0clIe0pnCfs6g/lRbyg=="
},
"d3-transition": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.2.0.tgz",
"integrity": "sha512-VJ7cmX/FPIPJYuaL2r1o1EMHLttvoIuZhhuAlRoOxDzogV8iQS6jYulDm3xEU3TqL80IZIhI551/ebmCMrkvhw==",
"requires": {
"d3-color": "1",
"d3-dispatch": "1",
"d3-ease": "1",
"d3-interpolate": "1",
"d3-selection": "^1.1.0",
"d3-timer": "1"
}
},
"d3-voronoi": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/d3-voronoi/-/d3-voronoi-1.1.4.tgz",
"integrity": "sha512-dArJ32hchFsrQ8uMiTBLq256MpnZjeuBtdHpaDlYuQyjU0CVzCJl/BVW+SkszaAeH95D/8gxqAhgx0ouAWAfRg=="
},
"d3-zoom": {
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-1.8.3.tgz",
"integrity": "sha512-VoLXTK4wvy1a0JpH2Il+F2CiOhVu7VRXWF5M/LroMIh3/zBAC3WAt7QoIvPibOavVo20hN6/37vwAsdBejLyKQ==",
"requires": {
"d3-dispatch": "1",
"d3-drag": "1",
"d3-interpolate": "1",
"d3-selection": "1",
"d3-transition": "1"
}
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
@ -8419,8 +8682,7 @@
"ansi-regex": {
"version": "2.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"aproba": {
"version": "1.2.0",
@ -8441,14 +8703,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@ -8463,20 +8723,17 @@
"code-point-at": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"core-util-is": {
"version": "1.0.2",
@ -8593,8 +8850,7 @@
"inherits": {
"version": "2.0.3",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"ini": {
"version": "1.3.5",
@ -8606,7 +8862,6 @@
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@ -8621,7 +8876,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@ -8629,14 +8883,12 @@
"minimist": {
"version": "0.0.8",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"minipass": {
"version": "2.2.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@ -8655,7 +8907,6 @@
"version": "0.5.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@ -8736,8 +8987,7 @@
"number-is-nan": {
"version": "1.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"object-assign": {
"version": "4.1.1",
@ -8749,7 +8999,6 @@
"version": "1.4.0",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"wrappy": "1"
}
@ -8835,8 +9084,7 @@
"safe-buffer": {
"version": "5.1.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"safer-buffer": {
"version": "2.1.2",
@ -8872,7 +9120,6 @@
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@ -8892,7 +9139,6 @@
"version": "3.0.1",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@ -8936,14 +9182,12 @@
"wrappy": {
"version": "1.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"yallist": {
"version": "3.0.2",
"bundled": true,
"dev": true,
"optional": true
"dev": true
}
}
},
@ -15091,6 +15335,11 @@
"aproba": "^1.1.1"
}
},
"rw": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
"integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q="
},
"rxjs": {
"version": "6.3.3",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.3.3.tgz",
@ -19041,6 +19290,7 @@
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "1.0.0",
"concat-map": "0.0.1"
@ -19209,6 +19459,7 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}

View File

@ -70,6 +70,7 @@
"bootstrap": "3.4.1",
"classnames": "^2.2.6",
"core-js": "^2.5.7",
"d3": "^5.11.0",
"downloadjs": "^1.4.7",
"fetch": "^1.1.0",
"file-saver": "^2.0.2",

View File

View File

@ -1,7 +1,7 @@
import React, {Component} from "react";
import PillarLabel from "./PillarLabel";
import PaginatedTable from "../common/PaginatedTable";
import * as PropTypes from "prop-types";
import ResponsiveVennDiagram from "./venn-components/ResponsiveVennDiagram";
const columns = [
{
@ -20,13 +20,8 @@ const columns = [
class PillarOverview extends Component {
render() {
const data = this.props.grades.map((grade) => {
const newGrade = JSON.parse(JSON.stringify(grade));
newGrade.pillar = {name: grade.pillar, status: this.props.pillarsToStatuses[grade.pillar]};
return newGrade;
});
return (<div id={this.constructor.name}>
<PaginatedTable data={data} columns={columns} pageSize={10}/>
<ResponsiveVennDiagram pillarsGrades={this.props.grades} />
</div>);
}
}
@ -35,5 +30,4 @@ export default PillarOverview;
PillarOverview.propTypes = {
grades: PropTypes.array,
status: PropTypes.string,
};

View File

@ -0,0 +1,38 @@
import React from 'react'
import PropTypes from 'prop-types';
import * as d3 from 'd3'
class ArcNode extends React.Component {
render() {
let {prefix, index, data} = this.props;
let arc = d3.arc().innerRadius(data.inner).outerRadius(data.outer).startAngle(0).endAngle(Math.PI * 2.0);
let id = prefix + 'Node_' + index;
return (
<g transform={'rotate(180)'} id={data.node.pillar}>
<path
id={prefix + 'Node_' + index}
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={'#' + id} startOffset={'26.4%'}>
{data.label}
</textPath>
</text>
</g>
);
}
}
ArcNode.propTypes = {
data: PropTypes.object
};
export default ArcNode;

View File

@ -0,0 +1,43 @@
import React from 'react'
import PropTypes from 'prop-types';
class CircularNode extends React.Component {
render() {
let {prefix, 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={prefix + 'Node_' + index}
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 = {
index: PropTypes.number,
data: PropTypes.object
};
export default CircularNode;

View File

@ -0,0 +1,28 @@
import React from 'react'
import PropTypes from 'prop-types'
import Dimensions from 'react-dimensions'
import VennDiagram from './VennDiagram'
const VENN_MIN_WIDTH = '300px';
class ResponsiveVennDiagram extends React.Component {
constructor(props) {
super(props);
}
render() {
const {pillarsGrades} = this.props;
return (
<div ref={this.divElement} style={{textAlign: 'center'}}>
<VennDiagram style={{minWidth: VENN_MIN_WIDTH, minHeight: VENN_MIN_WIDTH}} pillarsGrades={pillarsGrades}/>
</div>
);
}
}
ResponsiveVennDiagram.propTypes = {
pillarsGrades: PropTypes.array
};
export default Dimensions()(ResponsiveVennDiagram);

View File

@ -0,0 +1,43 @@
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;

View File

@ -0,0 +1,15 @@
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();
}
}

View File

@ -0,0 +1,17 @@
@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 */
}

View File

@ -0,0 +1,253 @@
import React from 'react'
import PropTypes from 'prop-types'
import Tooltip from './Tooltip'
import CircularNode from './CircularNode'
import ArcNode from './ArcNode'
import {TypographicUtilities} from './Utility.js'
import './VennDiagram.css'
class VennDiagram extends React.Component {
constructor(props_) {
super(props_);
this.state = {tooltip: {top: 0, left: 0, display: 'none', html: ''}};
this.width = this.height = 512;
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.width / 32), color: 'white'}, {
size: Math.max(6, this.width / 52),
color: 'black'
}];
this.offset = this.width / 16;
this.thirdWidth = this.width / 3;
this.sixthWidth = this.width / 6;
this.width2By7 = 2 * this.width / 7;
this.width1By11 = this.width / 11;
this.width1By28 = this.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
}
};
/*
RULE #1: All scores have to be equal 0, except Unexecuted [U] which could be also a negative integer
sum(C, I, P) has to be <=0
RULE #2: Conclusive [C] has to be > 0,
sum(C) > 0
RULE #3: Inconclusive [I] has to be > 0 while Conclusive has to be 0,
sum(C, I) > 0 and C * I = 0, while C has to be 0
RULE #4: Positive [P] and Unexecuted have to be positive
sum(P, U) >= 2 and P * U = positive integer, while
if the P is bigger by 2 then negative U, first conditional
would be true.
*/
this.rules = [
{
id: 'Rule #1', f: function (d_) {
return d_['Conclusive'] + d_['Inconclusive'] + d_['Positive'] === 0;
}
},
{
id: 'Rule #2', f: function (d_) {
return d_['Conclusive'] > 0;
}
},
{
id: 'Rule #3', f: function (d_) {
return d_['Conclusive'] === 0 && d_['Inconclusive'] > 0;
}
},
{
id: 'Rule #4', f: function (d_) {
return d_['Positive'] + d_['Unexecuted'] >= 2 && d_['Positive'] * d_['Unexecuted'] > 0;
}
}
];
this._onScroll = this._onScroll.bind(this);
}
componentDidMount() {
this.parseData();
window.addEventListener('scroll', this._onScroll);
}
_onMouseMove(e) {
let self = this;
if (!this.toggle) {
let hidden = 'none';
let html = '';
let bcolor = '#DEDEDE';
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', 0.95);
bcolor = e.target.getAttribute('fill');
// Set highest z-index
e.target.parentNode.parentNode.appendChild(e.target.parentNode);
} else {
this.divElement.style.cursor = 'default';
// Return z indices to default
Object.keys(this.layout).forEach(function (d_, i_) {
document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode);
})
}
this.setState({
target: e,
tooltip: {
target: e.target,
bcolor: bcolor,
top: e.clientY + 8,
left: e.clientX + 8,
display: hidden,
html: html
}
});
}
}
_onScroll(e) {
this.divElement.style.cursor = 'default';
this.setState({target: null, tooltip: {target: null, bcolor: 'none', top: 0, left: 0, display: 'none', html: ''}});
}
_onClick(e) {
this.toggle = this.state.tooltip.target === e.target;
//variable to external callback
//e.target.parentNode.id)
}
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 = null;
for (let j = 0; j < self.rules.length; j++) {
if (self.rules[j].f(d_)) {
rule = j;
break;
}
}
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 (rule_ == null) {
throw Error('The node scores are invalid');
}
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 {
// equivalent to center translate (width/2, height/2)
let viewPortParameters = (-this.width / 2) + ' ' + (-this.height / 2) + ' ' + this.width + ' ' + this.height;
let nodes = Object.values(this.layout).map((d_, i_) => {
if (d_.hasOwnProperty('cx')) {
return (
<CircularNode
prefix={this.prefix}
key={this.prefix + 'CircularNode' + i_}
index={i_}
data={d_}
/>
);
} else {
d_.label = TypographicUtilities.removeBrokenBar(d_.label);
return (
<ArcNode
prefix={this.prefix}
key={this.prefix + 'ArcNode' + i_}
index={i_}
data={d_}
/>
);
}
});
return (
<div ref={(divElement) => this.divElement = divElement} onMouseMove={this._onMouseMove.bind(this)}
onClick={this._onClick.bind(this)}>
<svg id={this.prefix} viewBox={viewPortParameters} width={'100%'} height={'100%'}
xmlns='http://www.w3.org/2000/svg' xmlnsXlink='http://www.w3.org/1999/xlink'>
{nodes}
</svg>
<Tooltip id={this.prefix + 'Tooltip'} prefix={this.prefix} {...this.state.tooltip} />
</div>
)
}
}
}
VennDiagram.propTypes = {
pillarsGrades: PropTypes.array
};
export default VennDiagram;