Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
253 changes: 76 additions & 177 deletions src/components/Reports/InfringementsViz.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
/* eslint-disable testing-library/no-node-access */
import React from 'react';
import * as d3 from 'd3';

import React from 'react';
import { Button, Modal } from 'react-bootstrap';
import styles from './PeopleReport/PeopleReport.module.css';
import { boxStyle, boxStyleDark } from '../../styles';
import styles from './PeopleReport/PeopleReport.module.css';
import {
createAxes,
createDots,
createLabels,
createLegend,
createLine,
createSvgRoot,
createTooltip,
} from './d3GraphUtils';

function InfringementsViz({ infringements, fromDate, toDate, darkMode }) {
const [graphVisible, setGraphVisible] = React.useState(false);
Expand All @@ -18,197 +26,97 @@

const handleModalShow = d => {
setFocusedInf(d);
if (graphVisible === false) {
setModalVisible(!modalVisible);
}
setGraphVisible(!graphVisible); // Open the graph when opening the modal
if (graphVisible === false) setModalVisible(!modalVisible);
setGraphVisible(!graphVisible);
};

function displayGraph(bsCount, maxSquareCount) {
if (!graphVisible) {
d3.selectAll('#infplot > *').remove();
} else {
d3.selectAll('#infplot > *').remove();

const margin = { top: 30, right: 20, bottom: 30, left: 20 };
const containerWidth = '1000';
// Adjusted width based on the available space
const width = Math.min(containerWidth - margin.left - margin.right, 1000);

const height = 400 - margin.top - margin.bottom;

const tooltipEl = function tooltipEl(d) {
return (
`${'<div class="tip__container">' +
'<div class="close">' +
'<button>&times</button>' +
'</div>' +
'<div>' +
'Exact date: '}${d3.timeFormat('%A, %B %e, %Y')(d.date)}<br>` +
`Count: ${d.count === 1 ? d.count : `${d.count} <span class="detailsModal"><a>See All</a></span>`
}<br>` +
`Description: ${d.des[0]}</div>` +
`</div>`
);
};

const legendEl = function legendEl() {
return (
'<div class="lengendSubContainer">' +
'<div class="infLabelsOff">' +
'<button>Labels Off</button>' +
'</div>' +
'<div class="infCountLabelsOn">' +
'<button>Show Squares</button>' +
'</div>' +
'<div class="infDateLabelsOn">' +
'<button>Show Dates</button>' +
'</div>' +
'</div>'
);
};

const svg = d3
.select('#infplot')
.append('svg')
.attr('width', '100%')
.attr('height', height + margin.top + margin.bottom)
.attr('viewBox', `0 0 ${containerWidth} ${height + margin.top + margin.bottom}`)
.append('g')
.attr('transform', `translate(${margin.left},${margin.top})`);

const x = d3
.scaleTime()
.domain(d3.extent(bsCount, d => d.date))
.range([0, width]);
svg
.append('g')
.attr('transform', `translate(0, ${height})`)
.call(d3.axisBottom(x));

const y = d3
.scaleLinear()
.domain([0, maxSquareCount + 2])
.range([height, 0]);
svg.append('g').call(
d3
.axisLeft(y)
.ticks(5)
.tickFormat(d3.format('d')),
);
const textColor = darkMode ? `color: #f9fafb;` : '';
const legendHtml =
`<div class="lengendSubContainer" style="${textColor}">` +
`<div class="infLabelsOff"><button style="${textColor}">Labels Off</button></div>` +
`<div class="infCountLabelsOn"><button style="${textColor}">Show Squares</button></div>` +
`<div class="infDateLabelsOn"><button style="${textColor}">Show Dates</button></div>` +
`</div>`;

svg
.append('path')
.datum(bsCount)
.attr('fill', 'none')
.attr('stroke', 'black')
.attr('stroke-width', 1.5)
.attr(
'd',
d3
.line()
.x(d => x(d.date))
.y(d => y(d.count)),
);

svg
.append('g')
.selectAll('dot')
.data(bsCount)
.join('circle')
.attr('class', 'myCircle')
.attr('cx', d => x(d.date))
.attr('cy', d => y(d.count))
.attr('r', 3)
.attr('stroke', '#69b3a2')
.attr('stroke-width', 3)
.attr('fill', 'white')
.on('click', function handleCircleClick(event, d) {
const prevTooltip = d3.select(`.inf${d.id}`);
const svgRoot = createSvgRoot('#infplot', containerWidth, height, margin, darkMode);
const svg = svgRoot.append('g').attr('transform', `translate(${margin.left},${margin.top})`);

if (prevTooltip.empty()) {
const Tooltip = d3
.select('#infplot')
.append('div')
.style('opacity', 0)
.attr('class', `tooltip inf${d.id}`)
.style('background-color', 'white')
.style('border', 'solid')
.style('border-width', '2px')
.style('border-radius', '5px')
.style('padding', '5px')
.style('max-width', '500px')
.style('z-index', 1000);
const x = d3.scaleTime().domain(d3.extent(bsCount, d => d.date)).range([0, width]);
const y = d3.scaleLinear().domain([0, maxSquareCount + 2]).range([height, 0]);

Tooltip.html(tooltipEl(d))
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY}px`)
.style('opacity', 1);

Tooltip.select('.close').on('click', function handleCloseClick() {
Tooltip.remove();
});

Tooltip.select('.detailsModal').on('click', function handleDetailsModalClick() {
handleModalShow(d);
});
}
});
createAxes(svg, x, y, height, darkMode);

svg
.append('g')
.call(d3.axisLeft(y).ticks(5).tickFormat(d3.format('d')))
.selectAll('text')
.data(bsCount)
.join('text')
.attr('class', 'infCountLabel')
.attr('x', d => x(d.date) + 10)
.attr('y', d => y(d.count) - 5)
.attr('fill', 'black')
.style('z-index', 999)
.style('font-weight', 700)
.style('display', 'none')
.text(d => parseInt(d.count, 10));
.attr('fill', darkMode ? '#f9fafb' : 'black');

createLine(svg, bsCount, x, y, darkMode);

const dots = createDots(svg, bsCount, x, y);
dots.on('click', function handleCircleClick(event, d) {
const prevTooltip = d3.select(`.inf${d.id}`);
if (prevTooltip.empty()) {
const Tooltip = createTooltip('#infplot', d, darkMode);
Tooltip.attr('class', `tooltip inf${d.id}`)
.style('max-width', '500px')
.html(
`<div class="tip__container"><div class="close">` +
`<button style="color: ${darkMode ? '#f9fafb' : 'black'}; background: transparent; border: none;">&times</button>` +
`</div><div>Exact date: ${d3.timeFormat('%A, %B %e, %Y')(d.date)}<br>` +
`Count: ${d.count === 1 ? d.count : `${d.count} <span class="detailsModal"><a>See All</a></span>`}<br>` +

Check warning on line 79 in src/components/Reports/InfringementsViz.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this code to not use nested template literals.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2I3vnHkffvx81wKxV2&open=AZ2I3vnHkffvx81wKxV2&pullRequest=5015
`Description: ${d.des[0]}</div></div>`
)
.style('left', `${event.pageX + 10}px`)
.style('top', `${event.pageY}px`)
.style('opacity', 1);

Tooltip.select('.close').on('click', function handleCloseClick() {
Tooltip.remove();
});
Tooltip.select('.detailsModal').on('click', function handleDetailsModalClick() {
handleModalShow(d);
});
}
});

svg
.append('g')
.selectAll('text')
.data(bsCount)
.join('text')
.attr('class', 'infDateLabel')
.attr('x', d => x(d.date) + 10)
.attr('y', d => y(d.count) - 5)
.attr('fill', 'black')
.style('z-index', 999)
.style('font-weight', 700)
.style('display', 'none')
.text(d => d3.timeFormat('%m/%d/%Y')(d.date));
createLabels(svg, bsCount, x, y, 'infCountLabel', darkMode, d => parseInt(d.count, 10));

Check warning on line 95 in src/components/Reports/InfringementsViz.jsx

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Prefer `Number.parseInt` over `parseInt`.

See more on https://sonarcloud.io/project/issues?id=OneCommunityGlobal_HighestGoodNetworkApp&issues=AZ2I3vnHkffvx81wKxV3&open=AZ2I3vnHkffvx81wKxV3&pullRequest=5015
createLabels(svg, bsCount, x, y, 'infDateLabel', darkMode, d => d3.timeFormat('%m/%d/%Y')(d.date));

const legend = d3
.select('#infplot')
.append('div')
.attr('class', 'legendContainer');
legend.html(legendEl());
const legend = createLegend('#infplot', legendHtml);

legend.select('.infLabelsOff').on('click', function handleLabelsOffClick() {
d3.selectAll('.infCountLabel').style('display', 'none');
d3.selectAll('.infDateLabel').style('display', 'none');
});

legend.select('.infCountLabelsOn').on('click', function handleCountLabelsOnClick() {
d3.selectAll('.infCountLabel').style('display', 'block');
d3.selectAll('.infDateLabel').style('display', 'none');
});

legend.select('.infDateLabelsOn').on('click', function handleDateLabelsOnClick() {
d3.selectAll('.infDateLabel').style('display', 'block');
d3.selectAll('.infCountLabel').style('display', 'none');
});
}
}

const generateGraph = () => {
const dict = {};
const value = [];
let maxSquareCount = 0;

// aggregate infringements
for (let i = 0; i < infringements.length; i += 1) {
if (infringements[i].date in dict) {
dict[infringements[i].date].ids.push(infringements[i]._id);
Expand All @@ -223,11 +131,8 @@
}
}

// filter infringements by date
if (fromDate === '' || toDate === '') {
// condition no longer needed
Object.keys(dict).forEach(key => {
// Use if statement to filter unwanted properties from the prototype chain
if (Object.prototype.hasOwnProperty.call(dict, key)) {
value.push({
date: d3.timeParse('%Y-%m-%d')(key),
Expand All @@ -236,9 +141,7 @@
type: 'Infringement',
ids: dict[key].ids,
});
if (dict[key].count > maxSquareCount) {
maxSquareCount = dict[key].count;
}
if (dict[key].count > maxSquareCount) maxSquareCount = dict[key].count;
}
});
} else {
Expand All @@ -253,9 +156,7 @@
type: 'Infringement',
ids: dict[key].ids,
});
if (dict[key].count > maxSquareCount) {
maxSquareCount = dict[key].count;
}
if (dict[key].count > maxSquareCount) maxSquareCount = dict[key].count;
counter += 1;
}
});
Expand All @@ -273,40 +174,38 @@
<Button onClick={handleModalShow} aria-expanded={graphVisible} style={darkMode ? boxStyleDark : boxStyle}>
{graphVisible ? 'Hide Infringements Graph' : 'Show Infringements Graph'}
</Button>
<div className={`${styles.kaitest} ${darkMode ? 'bg-light mt-2' : ''}`} id="infplot" data-testid="infplot" />
<div className={`${styles.kaitest} ${darkMode ? 'mt-2' : ''}`} id="infplot" data-testid="infplot" />

<Modal size="lg" show={modalVisible} onHide={handleModalClose}>
<Modal.Header closeButton>
<Modal.Header closeButton style={darkMode ? { backgroundColor: '#1b2a41', color: '#f9fafb', borderColor: '#374151' } : {}}>
<Modal.Title>{focusedInf.date ? focusedInf.date.toString() : 'Infringement'}</Modal.Title>
</Modal.Header>
<Modal.Body>
<Modal.Body style={darkMode ? { backgroundColor: '#1b2a41', color: '#f9fafb' } : {}}>
<div id="inf">
<table>
<table style={darkMode ? { backgroundColor: '#1b2a41', color: '#f9fafb', width: '100%' } : { width: '100%' }}>
<thead>
<tr>
<th>Descriptions</th>
<tr style={darkMode ? { backgroundColor: '#1b2a41' } : {}}>
<th style={darkMode ? { backgroundColor: '#1b2a41', color: '#f9fafb' } : {}}>Descriptions</th>
</tr>
</thead>
<tbody>
{focusedInf.des
? focusedInf.des.map((desc) => (
<tr key={desc}>
<td>{desc}</td>
? focusedInf.des.map(desc => (
<tr key={desc} style={darkMode ? { backgroundColor: '#1b2a41' } : {}}>
<td style={darkMode ? { backgroundColor: '#1b2a41', color: '#f9fafb' } : {}}>{desc}</td>
</tr>
))
: null}
</tbody>
</table>
</div>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={handleModalClose}>
Close
</Button>
<Modal.Footer style={darkMode ? { backgroundColor: '#1b2a41', borderColor: '#374151' } : {}}>
<Button variant="secondary" onClick={handleModalClose}>Close</Button>
</Modal.Footer>
</Modal>
</div>
);
}

export default InfringementsViz;
export default InfringementsViz;
Loading
Loading