From ee105ee9a0db4cef18d0b98d851d1306ff7ae348 Mon Sep 17 00:00:00 2001 From: sayali-2308 Date: Wed, 18 Mar 2026 18:49:56 -0400 Subject: [PATCH 1/3] fix: dark mode background for Reports People page --- .../sharedComponents/ReportPage/ReportPage.jsx | 4 ++-- .../ReportPage/ReportPage.module.css | 14 ++++++++------ .../components/ReportBlock/ReportBlock.jsx | 2 +- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/Reports/sharedComponents/ReportPage/ReportPage.jsx b/src/components/Reports/sharedComponents/ReportPage/ReportPage.jsx index 1e1b485949..46356e6ef2 100644 --- a/src/components/Reports/sharedComponents/ReportPage/ReportPage.jsx +++ b/src/components/Reports/sharedComponents/ReportPage/ReportPage.jsx @@ -1,11 +1,11 @@ -import { ReportHeader } from './components/ReportHeader'; import { ReportBlock } from './components/ReportBlock'; import { ReportCard } from './components/ReportCard'; +import { ReportHeader } from './components/ReportHeader'; import styles from './ReportPage.module.css'; export function ReportPage({ children, renderProfile, contentClassName, darkMode }) { return ( -
+
{renderProfile && (
{renderProfile()} diff --git a/src/components/Reports/sharedComponents/ReportPage/ReportPage.module.css b/src/components/Reports/sharedComponents/ReportPage/ReportPage.module.css index 991385c191..65e72092c4 100644 --- a/src/components/Reports/sharedComponents/ReportPage/ReportPage.module.css +++ b/src/components/Reports/sharedComponents/ReportPage/ReportPage.module.css @@ -9,15 +9,18 @@ padding-right: 24px; } +.report-page-wrapper-dark { + background-color: #0b1d3a !important; +} + .report-page-content { display: flex; flex-direction: column; row-gap: 32px; width: 100%; - min-width: 0; /* allow children to shrink if needed */ + min-width: 0; } - .report-page-profile { border-radius: 25px; width: 100%; @@ -28,10 +31,9 @@ width: 100%; } -@media (max-width: 435px) { - +@media (width <= 435px) { .report-page-content { - padding: 0px; + padding: 0; } .report-page-wrapper { @@ -41,7 +43,7 @@ .project-header { font-size: 32px; - padding: 5px 5px; + padding: 5px; text-align: center; background-color: white; border-radius: 20px; diff --git a/src/components/Reports/sharedComponents/ReportPage/components/ReportBlock/ReportBlock.jsx b/src/components/Reports/sharedComponents/ReportPage/components/ReportBlock/ReportBlock.jsx index bade69545b..24962bcba8 100644 --- a/src/components/Reports/sharedComponents/ReportPage/components/ReportBlock/ReportBlock.jsx +++ b/src/components/Reports/sharedComponents/ReportPage/components/ReportBlock/ReportBlock.jsx @@ -10,7 +10,7 @@ export function ReportBlock({ className, children, firstColor, secondColor, dark if (secondColor) { backgroundColor = color; } else if (darkMode) { - backgroundColor = '#3A506B'; + backgroundColor = '#1a2639'; // dark navy, matches the rest of dark mode } else { backgroundColor = firstColor || 'white'; } From d11c0b51a861183e1c8a92f1cdb9efcc51407f6f Mon Sep 17 00:00:00 2001 From: sayali-2308 Date: Mon, 13 Apr 2026 17:17:10 -0400 Subject: [PATCH 2/3] fix: fix dark mode styling for graphs, toggle, and total hours text --- src/components/Reports/InfringementsViz.jsx | 63 ++++++------- src/components/Reports/TimeEntriesViz.jsx | 90 +++++++------------ .../common/PieChart/ProjectPieChart.jsx | 19 ++-- .../PieChart/UserProjectPieChart.module.css | 81 +++++++++-------- 4 files changed, 122 insertions(+), 131 deletions(-) diff --git a/src/components/Reports/InfringementsViz.jsx b/src/components/Reports/InfringementsViz.jsx index 028645a591..d9588bdb76 100644 --- a/src/components/Reports/InfringementsViz.jsx +++ b/src/components/Reports/InfringementsViz.jsx @@ -1,10 +1,10 @@ /* 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'; function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { const [graphVisible, setGraphVisible] = React.useState(false); @@ -39,7 +39,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { return ( `${'
' + '
' + - '' + + `` + '
' + '
' + 'Exact date: '}${d3.timeFormat('%A, %B %e, %Y')(d.date)}
` + @@ -50,28 +50,26 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { ); }; + const textColor = darkMode ? 'color: #f9fafb;' : ''; const legendEl = function legendEl() { return ( - '
' + - '
' + - '' + - '
' + - '
' + - '' + - '
' + - '
' + - '' + - '
' + - '
' + `
` + + `
` + + `
` + + `
` + + `
` ); }; - const svg = d3 + const svgRoot = 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}`) + .style('background-color', darkMode ? '#1b2a41' : '#ffffff'); + + const svg = svgRoot .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); @@ -82,7 +80,8 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { svg .append('g') .attr('transform', `translate(0, ${height})`) - .call(d3.axisBottom(x)); + .call(d3.axisBottom(x)) + .selectAll('text').attr('fill', darkMode ? '#f9fafb' : 'black'); const y = d3 .scaleLinear() @@ -92,14 +91,15 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { d3 .axisLeft(y) .ticks(5) - .tickFormat(d3.format('d')), - ); + .tickFormat(d3.format('d'))) + .selectAll('text').attr('fill', darkMode ? '#f9fafb' : 'black'); + svg .append('path') .datum(bsCount) .attr('fill', 'none') - .attr('stroke', 'black') + .attr('stroke', darkMode ? '#f9fafb' : 'black') .attr('stroke-width', 1.5) .attr( 'd', @@ -130,7 +130,8 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { .append('div') .style('opacity', 0) .attr('class', `tooltip inf${d.id}`) - .style('background-color', 'white') + .style('background-color', darkMode ? '#1b2a41' : 'white') + .style('color', darkMode ? '#f9fafb' : 'black') .style('border', 'solid') .style('border-width', '2px') .style('border-radius', '5px') @@ -161,7 +162,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { .attr('class', 'infCountLabel') .attr('x', d => x(d.date) + 10) .attr('y', d => y(d.count) - 5) - .attr('fill', 'black') + .attr('fill', darkMode ? '#f9fafb' : 'black') .style('z-index', 999) .style('font-weight', 700) .style('display', 'none') @@ -175,7 +176,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { .attr('class', 'infDateLabel') .attr('x', d => x(d.date) + 10) .attr('y', d => y(d.count) - 5) - .attr('fill', 'black') + .attr('fill', darkMode ? '#f9fafb' : 'black') .style('z-index', 999) .style('font-weight', 700) .style('display', 'none') @@ -273,25 +274,25 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { -
+
- + {focusedInf.date ? focusedInf.date.toString() : 'Infringement'} - +
- +
- - + + {focusedInf.des ? focusedInf.des.map((desc) => ( - - + + )) : null} @@ -299,7 +300,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) {
Descriptions
Descriptions
{desc}
{desc}
- + diff --git a/src/components/Reports/TimeEntriesViz.jsx b/src/components/Reports/TimeEntriesViz.jsx index 2957868b59..0314f0a392 100644 --- a/src/components/Reports/TimeEntriesViz.jsx +++ b/src/components/Reports/TimeEntriesViz.jsx @@ -1,7 +1,7 @@ /* eslint-disable no-console */ /* eslint-disable testing-library/no-node-access */ -import React from 'react'; import * as d3 from 'd3'; +import React from 'react'; import { Button } from 'react-bootstrap'; import { boxStyle, boxStyleDark } from '../../styles'; @@ -34,25 +34,21 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { d3.selectAll('#tlplot > *').remove(); } catch (e) { console.error('Error clearing graph:', e); - // Alternative approach if d3 selector fails while (tlplotElement.firstChild) { tlplotElement.removeChild(tlplotElement.firstChild); } } - // Only proceed with graph rendering if we're showing the graph 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 generateTooltipElement(d) { return ( `${'
' + '
' + - '' + + `` + '
' + '
' + 'Exact date: '}${d3.timeFormat('%A, %B %e, %Y')(d.date)}
` + @@ -61,68 +57,58 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { ); }; + const textColor = darkMode ? 'color: #f9fafb;' : ''; const legendEl = function generateLegendElement(innerTotalHours) { return ( - `${'
' + - '
' + - 'Total Hours: '}${innerTotalHours.toFixed(2)}
` + - `
` + - `` + - `
` + - `
` + - `` + - `
` + - `
` + - `` + - `
` + + `
` + + `
Total Hours: ${innerTotalHours.toFixed(2)}
` + + `
` + + `
` + + `
` + `
` ); }; try { - // Wrap all D3 operations in try-catch for better error handling const d3Element = d3.select('#tlplot'); if (!d3Element) { console.error('Could not select #tlplot element'); return; } - const svg = d3Element + const svgRoot = d3Element .append('svg') .attr('width', '100%') .attr('height', height + margin.top + margin.bottom) .attr('viewBox', `0 0 ${containerWidth} ${height + margin.top + margin.bottom}`) + .style('background-color', darkMode ? '#1b2a41' : '#ffffff'); + + const svg = svgRoot .append('g') .attr('transform', `translate(${margin.left},${margin.top})`); - const x = d3 - .scaleTime() - .domain(d3.extent(logs, d => d.date)) - .range([0, width]); + const x = d3.scaleTime().domain(d3.extent(logs, d => d.date)).range([0, width]); svg .append('g') .attr('transform', `translate(0, ${height})`) - .call(d3.axisBottom(x)); + .call(d3.axisBottom(x)) + .selectAll('text') + .attr('fill', darkMode ? '#f9fafb' : 'black'); - const y = d3 - .scaleLinear() - .domain([0, maxHoursCount + 2]) - .range([height, 0]); - svg.append('g').call(d3.axisLeft(y)); + const y = d3.scaleLinear().domain([0, maxHoursCount + 2]).range([height, 0]); + svg + .append('g') + .call(d3.axisLeft(y)) + .selectAll('text') + .attr('fill', darkMode ? '#f9fafb' : 'black'); svg .append('path') .datum(logs) .attr('fill', 'none') - .attr('stroke', 'black') + .attr('stroke', darkMode ? '#f9fafb' : 'black') .attr('stroke-width', 1.5) - .attr( - 'd', - d3 - .line() - .x(d => x(d.date)) - .y(d => y(d.count)), - ); + .attr('d', d3.line().x(d => x(d.date)).y(d => y(d.count))); svg .append('g') @@ -138,14 +124,14 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { .attr('fill', 'white') .on('click', function handleEvent(event, d) { const prevTooltip = d3.select(`.ent${d.id}`); - if (prevTooltip.empty()) { const Tooltip = d3 .select('#tlplot') .append('div') .style('opacity', 0) .attr('class', `tooltip ent${d.id}`) - .style('background-color', 'white') + .style('background-color', darkMode ? '#1b2a41' : 'white') + .style('color', darkMode ? '#f9fafb' : 'black') .style('border', 'solid') .style('border-width', '2px') .style('border-radius', '5px') @@ -171,7 +157,7 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { .attr('class', 'entCountLabel') .attr('x', d => x(d.date) + 10) .attr('y', d => y(d.count) - 5) - .attr('fill', 'black') + .attr('fill', darkMode ? '#f9fafb' : 'black') .style('z-index', 999) .style('font-weight', 700) .style('display', 'none') @@ -185,16 +171,13 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { .attr('class', 'entDateLabel') .attr('x', d => x(d.date) + 10) .attr('y', d => y(d.count) - 5) - .attr('fill', 'black') + .attr('fill', darkMode ? '#f9fafb' : 'black') .style('z-index', 999) .style('font-weight', 700) .style('display', 'none') .text(d => d3.timeFormat('%m/%d/%Y')(d.date)); - const legend = d3 - .select('#tlplot') - .append('div') - .attr('class', 'legendContainer'); + const legend = d3.select('#tlplot').append('div').attr('class', 'legendContainer'); legend.html(legendEl(totalHours)); legend.select('.entLabelsOff').on('click', function handleEntLabelsOffClick() { @@ -224,14 +207,11 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { let maxHoursCount = 0; let totalHours = 0; - // Make sure timeEntries.period exists before trying to iterate if (timeEntries && timeEntries.period && Array.isArray(timeEntries.period)) { for (let i = 0; i < timeEntries.period.length; i += 1) { const entry = timeEntries.period[i]; - // Make sure we have valid hours and minutes const hours = parseInt(entry.hours, 10) || 0; const minutes = entry.minutes === '0' ? 0 : parseInt(entry.minutes, 10) || 0; - const convertedHours = hours + minutes / 60; totalHours += convertedHours; @@ -248,15 +228,12 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { } } - // Make sure d3.timeParse is available const parseDate = d3 && d3.timeParse ? d3.timeParse('%Y-%m-%d') : str => new Date(str); - // filter time entries by date if (!fromDate || !toDate || fromDate === '' || toDate === '') { - // if cond not needed Object.keys(timeEntriesDict).forEach((key, index) => { timeEntryvalues.push({ - id: index, // Add id for every entry + id: index, date: parseDate(key), count: timeEntriesDict[key].time, des: timeEntriesDict[key].des, @@ -270,7 +247,6 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { } else { let counter = 0; Object.keys(timeEntriesDict).forEach(currentKey => { - // Use safe date parsing const keyDate = new Date(currentKey); const fromDateObj = new Date(fromDate); const toDateObj = new Date(toDate); @@ -298,7 +274,6 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { }); } - // Safe sort if (timeEntryvalues.length > 0) { timeEntryvalues.sort(function sortDates(a, b) { return new Date(b.date) - new Date(a.date); @@ -317,12 +292,11 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { > {show ? 'Hide Time Entries Graph' : 'Show Time Entries Graph'} -
+
); } -// Set default props to avoid undefined errors TimeEntriesViz.defaultProps = { timeEntries: { period: [] }, fromDate: '', @@ -330,4 +304,4 @@ TimeEntriesViz.defaultProps = { darkMode: false, }; -export default TimeEntriesViz; +export default TimeEntriesViz; \ No newline at end of file diff --git a/src/components/common/PieChart/ProjectPieChart.jsx b/src/components/common/PieChart/ProjectPieChart.jsx index 4a3393f36a..93b4c2691e 100644 --- a/src/components/common/PieChart/ProjectPieChart.jsx +++ b/src/components/common/PieChart/ProjectPieChart.jsx @@ -1,12 +1,12 @@ /* eslint-disable import/prefer-default-export */ import { useMemo, useState } from 'react'; import { - ResponsiveContainer, - PieChart as RechartsPieChart, - Pie, Cell, - Tooltip, Label, + Pie, + PieChart as RechartsPieChart, + ResponsiveContainer, + Tooltip, } from 'recharts'; import { CHART_RADIUS, CHART_SIZE } from './constants'; // use same numbers as the D3 chart import styles from './UserProjectPieChart.module.css'; @@ -137,6 +137,12 @@ export default function UserProjectD3PieChart({ projectsData, darkMode }) { showPct ? [`${((Number(value) * 100) / total).toFixed(2)}%`, entry?.payload?.name] @@ -179,7 +185,10 @@ export default function UserProjectD3PieChart({ projectsData, darkMode }) { -
+
Total Hours: {' '} diff --git a/src/components/common/PieChart/UserProjectPieChart.module.css b/src/components/common/PieChart/UserProjectPieChart.module.css index 9ba4379825..f36bbe0e16 100644 --- a/src/components/common/PieChart/UserProjectPieChart.module.css +++ b/src/components/common/PieChart/UserProjectPieChart.module.css @@ -5,10 +5,12 @@ /* overflow: hidden; */ } + .donut-box { width: 240px; /* match the bottom donut look */ height: 240px; /* square so the ring isn’t cropped */ } + .pie-chart-wrapper svg, .pie-chart-wrapper .recharts-surface { /* border: none !important; */ @@ -16,6 +18,7 @@ box-shadow: none !important; } .pie-chart-wrapper svg { border: 0 !important; } + .pie-chart { margin-right: 32px; height: 100%; @@ -54,14 +57,14 @@ width: 240px; display: flex; justify-content: space-between; - color: rgba(0, 0, 0, 0.5); + color: rgb(0 0 0 / 50%); } .data-legend-info-part:not(:last-child) { margin-right: 24px; } - @media (max-width: 670px) { + @media (width <= 670px) { .pie-chart-wrapper { flex-direction: column; } @@ -71,12 +74,12 @@ position: absolute; text-align: center; padding: 0.5rem; - background: #ffffff; + background: #fff; color: #313639; border-radius: 8px; pointer-events: none; font-size: 1rem; - word-wrap: break-word; + overflow-wrap: break-word; white-space: normal; } @@ -101,42 +104,39 @@ .slider { position: absolute; cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; + inset: 0; background-color: #baaeae; transition: 0.4s; border-radius: 34px; } - .slider:before { - position: absolute; - content: ""; - height: 14px; - width: 14px; - left: 3px; - bottom: 3px; - background-color: rgb(15, 15, 15); - transition: 0.4s; - border-radius: 50%; - } - - input:checked + .slider { - background-color: #2196F3; - } - - input:checked + .slider:before { - transform: translateX(14px); - } + .slider::before { + position: absolute; + content: ""; + height: 14px; + width: 14px; + left: 3px; + bottom: 3px; + background-color: white; + transition: 0.4s; + border-radius: 50%; +} /* Optional: Dark mode adjustments */ .text-light .slider { - background-color: #ffffff; + background-color: #555; } - - .text-light input:checked + .slider { - background-color: #4CAF50; + + .text-light .slider::before { + background-color: #fff; + } + + input:checked + .slider { + background-color: #2196f3; + } + + input:checked + .slider::before { + transform: translateX(14px); } .pie-chart-legend-container { @@ -169,6 +169,7 @@ background: none; color: inherit; } + .pie-chart-legend-table tr{ background-color: white; } @@ -184,15 +185,17 @@ .pie-chart-legend-table-dark td { padding: 8px; text-align: left; - color:rgb(236, 233, 233) + color:rgb(236 233 233) } .pie-chart-legend-table-dark th { font-weight: bold; background: none; + /* color: inherit; */ } + .pie-chart-legend-table-dark tr{ background-color: #1f2a40; color: white; @@ -204,19 +207,23 @@ height: 16px; border-radius: 50%; } + .total-row { font-weight: bold; margin-top: 10px; } .data-total-value { - font-weight: bold; - margin-top: 10px; - } + font-weight: bold; + margin-top: 10px; + color: inherit; +} -.strong-text.text-light { color:#f5f5f5 !important; } +.strong-text.text-light, +.text-light strong, +.text-light .strong-text { color:#f5f5f5 !important; } - @media (max-width: 671px) { + @media (width <= 671px) { .pie-chart-legend-table-wrapper { max-height: 200px; } From d8c5e21c7e15358757a6e0351104d83d1fd49450 Mon Sep 17 00:00:00 2001 From: sayali-2308 Date: Mon, 13 Apr 2026 17:55:05 -0400 Subject: [PATCH 3/3] refactor: extract shared D3 graph utilities to reduce code duplication --- src/components/Reports/InfringementsViz.jsx | 232 +++++----------- src/components/Reports/TimeEntriesViz.jsx | 291 ++++++-------------- src/components/Reports/d3GraphUtils.js | 88 ++++++ 3 files changed, 244 insertions(+), 367 deletions(-) create mode 100644 src/components/Reports/d3GraphUtils.js diff --git a/src/components/Reports/InfringementsViz.jsx b/src/components/Reports/InfringementsViz.jsx index d9588bdb76..1f37f4f8f6 100644 --- a/src/components/Reports/InfringementsViz.jsx +++ b/src/components/Reports/InfringementsViz.jsx @@ -1,10 +1,18 @@ /* eslint-disable testing-library/no-node-access */ import * as d3 from 'd3'; import React from 'react'; - import { Button, Modal } from 'react-bootstrap'; 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); @@ -18,198 +26,97 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { 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 ( - `${'
' + - '
' + - `` + - '
' + - '
' + - 'Exact date: '}${d3.timeFormat('%A, %B %e, %Y')(d.date)}
` + - `Count: ${d.count === 1 ? d.count : `${d.count} See All` - }
` + - `Description: ${d.des[0]}
` + - `
` - ); - }; + const textColor = darkMode ? `color: #f9fafb;` : ''; + const legendHtml = + `
` + + `
` + + `
` + + `
` + + `
`; - const textColor = darkMode ? 'color: #f9fafb;' : ''; - const legendEl = function legendEl() { - return ( - `
` + - `
` + - `
` + - `
` + - `
` - ); - }; + const svgRoot = createSvgRoot('#infplot', containerWidth, height, margin, darkMode); + const svg = svgRoot.append('g').attr('transform', `translate(${margin.left},${margin.top})`); - const svgRoot = 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}`) - .style('background-color', darkMode ? '#1b2a41' : '#ffffff'); + 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]); - const svg = svgRoot - .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)) - .selectAll('text').attr('fill', darkMode ? '#f9fafb' : 'black'); - - const y = d3 - .scaleLinear() - .domain([0, maxSquareCount + 2]) - .range([height, 0]); - svg.append('g').call( - d3 - .axisLeft(y) - .ticks(5) - .tickFormat(d3.format('d'))) - .selectAll('text').attr('fill', darkMode ? '#f9fafb' : 'black'); - - - svg - .append('path') - .datum(bsCount) - .attr('fill', 'none') - .attr('stroke', darkMode ? '#f9fafb' : '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}`); - - if (prevTooltip.empty()) { - const Tooltip = d3 - .select('#infplot') - .append('div') - .style('opacity', 0) - .attr('class', `tooltip inf${d.id}`) - .style('background-color', darkMode ? '#1b2a41' : 'white') - .style('color', darkMode ? '#f9fafb' : 'black') - .style('border', 'solid') - .style('border-width', '2px') - .style('border-radius', '5px') - .style('padding', '5px') - .style('max-width', '500px') - .style('z-index', 1000); - - 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', darkMode ? '#f9fafb' : '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( + `
` + + `` + + `
Exact date: ${d3.timeFormat('%A, %B %e, %Y')(d.date)}
` + + `Count: ${d.count === 1 ? d.count : `${d.count} See All`}
` + + `Description: ${d.des[0]}
` + ) + .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', darkMode ? '#f9fafb' : '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)); + 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); @@ -224,11 +131,8 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { } } - // 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), @@ -237,9 +141,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { 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 { @@ -254,9 +156,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { 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; } }); @@ -290,7 +190,7 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) { {focusedInf.des - ? focusedInf.des.map((desc) => ( + ? focusedInf.des.map(desc => ( {desc} @@ -301,13 +201,11 @@ function InfringementsViz({ infringements, fromDate, toDate, darkMode }) {
- +
); } -export default InfringementsViz; +export default InfringementsViz; \ No newline at end of file diff --git a/src/components/Reports/TimeEntriesViz.jsx b/src/components/Reports/TimeEntriesViz.jsx index 0314f0a392..4d36dd506d 100644 --- a/src/components/Reports/TimeEntriesViz.jsx +++ b/src/components/Reports/TimeEntriesViz.jsx @@ -4,6 +4,15 @@ import * as d3 from 'd3'; import React from 'react'; import { Button } from 'react-bootstrap'; import { boxStyle, boxStyleDark } from '../../styles'; +import { + createAxes, + createDots, + createLabels, + createLegend, + createLine, + createSvgRoot, + createTooltip, +} from './d3GraphUtils'; function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { const [show, setShow] = React.useState(false); @@ -13,191 +22,90 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { }, [show, fromDate, toDate]); function displayGraph(logs, maxHoursCount, totalHours) { - if (!d3 || !d3.selectAll) { - return; - } + if (!d3 || !d3.selectAll) return; const tlplotElement = document.getElementById('tlplot'); + if (!tlplotElement) return; + + try { + d3.selectAll('#tlplot > *').remove(); + } catch (e) { + console.error('Error clearing graph:', e); + while (tlplotElement.firstChild) { + tlplotElement.removeChild(tlplotElement.firstChild); + } + } - if (tlplotElement) { - if (!show) { - try { - d3.selectAll('#tlplot > *').remove(); - } catch (e) { - console.error('Error clearing graph:', e); - while (tlplotElement.firstChild) { - tlplotElement.removeChild(tlplotElement.firstChild); - } - } - } else { - try { - d3.selectAll('#tlplot > *').remove(); - } catch (e) { - console.error('Error clearing graph:', e); - while (tlplotElement.firstChild) { - tlplotElement.removeChild(tlplotElement.firstChild); - } - } - - const margin = { top: 30, right: 20, bottom: 30, left: 20 }; - const containerWidth = '1000'; - const width = Math.min(containerWidth - margin.left - margin.right, 1000); - const height = 400 - margin.top - margin.bottom; - - const tooltipEl = function generateTooltipElement(d) { - return ( - `${'
' + - '
' + + if (!show) return; + + const margin = { top: 30, right: 20, bottom: 30, left: 20 }; + const containerWidth = '1000'; + const width = Math.min(containerWidth - margin.left - margin.right, 1000); + const height = 400 - margin.top - margin.bottom; + + const textColor = darkMode ? `color: #f9fafb;` : ''; + const legendHtml = + `
` + + `
Total Hours: ${totalHours.toFixed(2)}
` + + `
` + + `
` + + `
` + + `
`; + + try { + const d3Element = d3.select('#tlplot'); + if (!d3Element) { console.error('Could not select #tlplot element'); return; } + + const svgRoot = createSvgRoot('#tlplot', containerWidth, height, margin, darkMode); + const svg = svgRoot.append('g').attr('transform', `translate(${margin.left},${margin.top})`); + + const x = d3.scaleTime().domain(d3.extent(logs, d => d.date)).range([0, width]); + const y = d3.scaleLinear().domain([0, maxHoursCount + 2]).range([height, 0]); + + createAxes(svg, x, y, height, darkMode); + createLine(svg, logs, x, y, darkMode); + + const dots = createDots(svg, logs, x, y); + dots.on('click', function handleEvent(event, d) { + const prevTooltip = d3.select(`.ent${d.id}`); + if (prevTooltip.empty()) { + const Tooltip = createTooltip('#tlplot', { id: `ent${d.id}` }, darkMode); + Tooltip.attr('class', `tooltip ent${d.id}`) + .html( + `
` + `` + - '
' + - '
' + - 'Exact date: '}${d3.timeFormat('%A, %B %e, %Y')(d.date)}
` + - `Hours logged on this day: ${d.count.toFixed(2)}
` + - `
` - ); - }; - - const textColor = darkMode ? 'color: #f9fafb;' : ''; - const legendEl = function generateLegendElement(innerTotalHours) { - return ( - `
` + - `
Total Hours: ${innerTotalHours.toFixed(2)}
` + - `
` + - `
` + - `
` + - `
` - ); - }; - - try { - const d3Element = d3.select('#tlplot'); - if (!d3Element) { - console.error('Could not select #tlplot element'); - return; - } - - const svgRoot = d3Element - .append('svg') - .attr('width', '100%') - .attr('height', height + margin.top + margin.bottom) - .attr('viewBox', `0 0 ${containerWidth} ${height + margin.top + margin.bottom}`) - .style('background-color', darkMode ? '#1b2a41' : '#ffffff'); - - const svg = svgRoot - .append('g') - .attr('transform', `translate(${margin.left},${margin.top})`); - - const x = d3.scaleTime().domain(d3.extent(logs, d => d.date)).range([0, width]); - svg - .append('g') - .attr('transform', `translate(0, ${height})`) - .call(d3.axisBottom(x)) - .selectAll('text') - .attr('fill', darkMode ? '#f9fafb' : 'black'); - - const y = d3.scaleLinear().domain([0, maxHoursCount + 2]).range([height, 0]); - svg - .append('g') - .call(d3.axisLeft(y)) - .selectAll('text') - .attr('fill', darkMode ? '#f9fafb' : 'black'); - - svg - .append('path') - .datum(logs) - .attr('fill', 'none') - .attr('stroke', darkMode ? '#f9fafb' : '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(logs) - .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 handleEvent(event, d) { - const prevTooltip = d3.select(`.ent${d.id}`); - if (prevTooltip.empty()) { - const Tooltip = d3 - .select('#tlplot') - .append('div') - .style('opacity', 0) - .attr('class', `tooltip ent${d.id}`) - .style('background-color', darkMode ? '#1b2a41' : 'white') - .style('color', darkMode ? '#f9fafb' : 'black') - .style('border', 'solid') - .style('border-width', '2px') - .style('border-radius', '5px') - .style('padding', '5px') - .style('z-index', 1000); - - Tooltip.html(tooltipEl(d)) - .style('left', `${event.pageX + 10}px`) - .style('top', `${event.pageY}px`) - .style('opacity', 1); - - Tooltip.select('.close').on('click', function closeTooltip() { - Tooltip.remove(); - }); - } - }); - - svg - .append('g') - .selectAll('text') - .data(logs) - .join('text') - .attr('class', 'entCountLabel') - .attr('x', d => x(d.date) + 10) - .attr('y', d => y(d.count) - 5) - .attr('fill', darkMode ? '#f9fafb' : 'black') - .style('z-index', 999) - .style('font-weight', 700) - .style('display', 'none') - .text(d => d.count.toFixed(2)); - - svg - .append('g') - .selectAll('text') - .data(logs) - .join('text') - .attr('class', 'entDateLabel') - .attr('x', d => x(d.date) + 10) - .attr('y', d => y(d.count) - 5) - .attr('fill', darkMode ? '#f9fafb' : 'black') - .style('z-index', 999) - .style('font-weight', 700) - .style('display', 'none') - .text(d => d3.timeFormat('%m/%d/%Y')(d.date)); - - const legend = d3.select('#tlplot').append('div').attr('class', 'legendContainer'); - legend.html(legendEl(totalHours)); - - legend.select('.entLabelsOff').on('click', function handleEntLabelsOffClick() { - d3.selectAll('.entCountLabel').style('display', 'none'); - d3.selectAll('.entDateLabel').style('display', 'none'); + `
Exact date: ${d3.timeFormat('%A, %B %e, %Y')(d.date)}
` + + `Hours logged on this day: ${d.count.toFixed(2)}
` + ) + .style('left', `${event.pageX + 10}px`) + .style('top', `${event.pageY}px`) + .style('opacity', 1); + + Tooltip.select('.close').on('click', function closeTooltip() { + Tooltip.remove(); }); + } + }); - legend.select('.entCountLabelsOn').on('click', function handleEntCountLabelsOnClick() { - d3.selectAll('.entCountLabel').style('display', 'block'); - d3.selectAll('.entDateLabel').style('display', 'none'); - }); + createLabels(svg, logs, x, y, 'entCountLabel', darkMode, d => d.count.toFixed(2)); + createLabels(svg, logs, x, y, 'entDateLabel', darkMode, d => d3.timeFormat('%m/%d/%Y')(d.date)); - legend.select('.entDateLabelsOn').on('click', function handleEntDateLabelsOnClick() { - d3.selectAll('.entDateLabel').style('display', 'block'); - d3.selectAll('.entCountLabel').style('display', 'none'); - }); - } catch (error) { - console.error('Error rendering D3 graph:', error); - } - } + const legend = createLegend('#tlplot', legendHtml); + + legend.select('.entLabelsOff').on('click', function handleEntLabelsOffClick() { + d3.selectAll('.entCountLabel').style('display', 'none'); + d3.selectAll('.entDateLabel').style('display', 'none'); + }); + legend.select('.entCountLabelsOn').on('click', function handleEntCountLabelsOnClick() { + d3.selectAll('.entCountLabel').style('display', 'block'); + d3.selectAll('.entDateLabel').style('display', 'none'); + }); + legend.select('.entDateLabelsOn').on('click', function handleEntDateLabelsOnClick() { + d3.selectAll('.entDateLabel').style('display', 'block'); + d3.selectAll('.entCountLabel').style('display', 'none'); + }); + } catch (error) { + console.error('Error rendering D3 graph:', error); } } @@ -240,9 +148,7 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { isTangible: timeEntriesDict[key].isTangible, type: 'Entry', }); - if (timeEntriesDict[key].time > maxHoursCount) { - maxHoursCount = timeEntriesDict[key].time; - } + if (timeEntriesDict[key].time > maxHoursCount) maxHoursCount = timeEntriesDict[key].time; }); } else { let counter = 0; @@ -250,14 +156,7 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { const keyDate = new Date(currentKey); const fromDateObj = new Date(fromDate); const toDateObj = new Date(toDate); - - if ( - !isNaN(keyDate) && - !isNaN(fromDateObj) && - !isNaN(toDateObj) && - fromDateObj <= keyDate && - keyDate <= toDateObj - ) { + if (!isNaN(keyDate) && !isNaN(fromDateObj) && !isNaN(toDateObj) && fromDateObj <= keyDate && keyDate <= toDateObj) { timeEntryvalues.push({ id: counter, date: parseDate(currentKey), @@ -266,18 +165,14 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { isTangible: timeEntriesDict[currentKey].isTangible, type: 'Entry', }); - if (timeEntriesDict[currentKey].time > maxHoursCount) { - maxHoursCount = timeEntriesDict[currentKey].time; - } + if (timeEntriesDict[currentKey].time > maxHoursCount) maxHoursCount = timeEntriesDict[currentKey].time; counter += 1; } }); } if (timeEntryvalues.length > 0) { - timeEntryvalues.sort(function sortDates(a, b) { - return new Date(b.date) - new Date(a.date); - }); + timeEntryvalues.sort((a, b) => new Date(b.date) - new Date(a.date)); } displayGraph(timeEntryvalues, maxHoursCount, totalHours); @@ -285,11 +180,7 @@ function TimeEntriesViz({ timeEntries, fromDate, toDate, darkMode }) { return (
-
diff --git a/src/components/Reports/d3GraphUtils.js b/src/components/Reports/d3GraphUtils.js new file mode 100644 index 0000000000..b8218ec727 --- /dev/null +++ b/src/components/Reports/d3GraphUtils.js @@ -0,0 +1,88 @@ +import * as d3 from 'd3'; + +export function createSvgRoot(selector, containerWidth, height, margin, darkMode) { + return d3 + .select(selector) + .append('svg') + .attr('width', '100%') + .attr('height', height + margin.top + margin.bottom) + .attr('viewBox', `0 0 ${containerWidth} ${height + margin.top + margin.bottom}`) + .style('background-color', darkMode ? '#1b2a41' : '#ffffff'); +} + +export function createAxes(svg, x, y, height, darkMode) { + svg + .append('g') + .attr('transform', `translate(0, ${height})`) + .call(d3.axisBottom(x)) + .selectAll('text') + .attr('fill', darkMode ? '#f9fafb' : 'black'); + + svg + .append('g') + .call(d3.axisLeft(y)) + .selectAll('text') + .attr('fill', darkMode ? '#f9fafb' : 'black'); +} + +export function createLine(svg, data, x, y, darkMode) { + svg + .append('path') + .datum(data) + .attr('fill', 'none') + .attr('stroke', darkMode ? '#f9fafb' : 'black') + .attr('stroke-width', 1.5) + .attr('d', d3.line().x(d => x(d.date)).y(d => y(d.count))); +} + +export function createDots(svg, data, x, y) { + return svg + .append('g') + .selectAll('dot') + .data(data) + .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'); +} + +export function createLabels(svg, data, x, y, className, darkMode, textFn) { + svg + .append('g') + .selectAll('text') + .data(data) + .join('text') + .attr('class', className) + .attr('x', d => x(d.date) + 10) + .attr('y', d => y(d.count) - 5) + .attr('fill', darkMode ? '#f9fafb' : 'black') + .style('z-index', 999) + .style('font-weight', 700) + .style('display', 'none') + .text(textFn); +} + +export function createTooltip(selector, d, darkMode) { + return d3 + .select(selector) + .append('div') + .style('opacity', 0) + .attr('class', `tooltip ${d.id !== undefined ? d.id : ''}`) + .style('background-color', darkMode ? '#1b2a41' : 'white') + .style('color', darkMode ? '#f9fafb' : 'black') + .style('border', 'solid') + .style('border-width', '2px') + .style('border-radius', '5px') + .style('padding', '5px') + .style('z-index', 1000); +} + +export function createLegend(selector, html) { + const legend = d3.select(selector).append('div').attr('class', 'legendContainer'); + legend.html(html); + return legend; +} \ No newline at end of file