Skip to content

Commit aab02e7

Browse files
Fix Y-axis label truncation, improve graph formatting, and update ResizeObserver mock for SonarQube
1 parent 05d4a78 commit aab02e7

4 files changed

Lines changed: 72 additions & 59 deletions

File tree

Lines changed: 31 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useMemo } from 'react';
2+
import PropTypes from 'prop-types';
23
import {
34
BarChart,
45
Bar,
@@ -11,11 +12,12 @@ import {
1112
} from 'recharts';
1213

1314
const truncate = (str, max = 22) =>
14-
str.length > max ? str.slice(0, max) + '…' : str;
15+
typeof str === 'string' && str.length > max ? str.slice(0, max) + '…' : str;
1516

1617
const CustomTooltip = ({ active, payload, isDark, usePercentage }) => {
1718
if (active && payload && payload.length) {
18-
const job = payload[0].payload;
19+
const job = payload[0]?.payload || {};
20+
1921
return (
2022
<div
2123
className={`p-2 rounded shadow ${
@@ -25,24 +27,25 @@ const CustomTooltip = ({ active, payload, isDark, usePercentage }) => {
2527
}`}
2628
style={{ fontSize: '0.875rem' }}
2729
>
28-
<p><span style={{ fontWeight: 900 }}>Role:</span> {job.title}</p>
29-
<p><span style={{ fontWeight: 900 }}>Conversion Rate:</span> {usePercentage ? `${job.conversionRate}%` : job.conversionRate}</p>
30-
<p><span style={{ fontWeight: 900 }}>Hits:</span> {job.hits}</p>
31-
<p><span style={{ fontWeight: 900 }}>Applications:</span> {job.applications}</p>
30+
<p><strong>Role:</strong> {job.title}</p>
31+
<p><strong>Conversion Rate:</strong> {usePercentage ? `${job.conversionRate}%` : job.conversionRate}</p>
32+
<p><strong>Hits:</strong> {job.hits}</p>
33+
<p><strong>Applications:</strong> {job.applications}</p>
3234
</div>
3335
);
3436
}
3537
return null;
3638
};
3739

38-
function ConvertedApplicationGraph({ data, usePercentage, isDark }) {
40+
function ConvertedApplicationGraph({ data = [], usePercentage, isDark }) {
3941
const sortedData = useMemo(() => {
4042
const key = usePercentage ? 'conversionRate' : 'applications';
41-
const toNum = (val) => (val == null ? 0 : Number(val));
43+
const toNum = (v) => Number(v) || 0;
44+
45+
const cloned = [...data];
46+
cloned.sort((a, b) => toNum(b[key]) - toNum(a[key]));
4247

43-
return [...data]
44-
.sort((a, b) => toNum(b[key]) - toNum(a[key]))
45-
.slice(0, 10);
48+
return cloned.slice(0, 10);
4649
}, [data, usePercentage]);
4750

4851
return (
@@ -59,25 +62,15 @@ function ConvertedApplicationGraph({ data, usePercentage, isDark }) {
5962
<p>No data available for the selected date range.</p>
6063
) : (
6164
<ResponsiveContainer width="100%" height={400}>
62-
<BarChart
63-
layout="vertical"
64-
data={sortedData}
65-
margin={{ top: 20, right: 90, bottom: 40, left: 180 }}
66-
>
65+
<BarChart layout="vertical" data={sortedData} margin={{ top: 20, right: 90, bottom: 40, left: 180 }}>
6766
<XAxis
6867
type="number"
6968
domain={usePercentage ? [0, 100] : ['auto', 'auto']}
70-
unit={usePercentage ? '%' : ''}
7169
stroke={isDark ? '#e2e8f0' : '#374151'}
7270
>
7371
<Label
74-
value={
75-
usePercentage
76-
? 'Percentage of hits converted to applications'
77-
: 'Applications'
78-
}
72+
value={usePercentage ? 'Percentage of hits converted to applications' : 'Applications'}
7973
position="bottom"
80-
offset={0}
8174
fill={isDark ? '#e2e8f0' : '#374151'}
8275
/>
8376
</XAxis>
@@ -86,15 +79,14 @@ function ConvertedApplicationGraph({ data, usePercentage, isDark }) {
8679
type="category"
8780
dataKey="title"
8881
width={180}
89-
tickFormatter={(v) => truncate(v)}
82+
tickFormatter={(v) => v}
9083
tick={{ fill: isDark ? '#e2e8f0' : '#374151', fontSize: 12 }}
9184
stroke={isDark ? '#e2e8f0' : '#374151'}
9285
>
9386
<Label
9487
value="Job Role"
9588
angle={-90}
9689
position="left"
97-
offset={-5}
9890
fill={isDark ? '#e2e8f0' : '#374151'}
9991
/>
10092
</YAxis>
@@ -105,11 +97,7 @@ function ConvertedApplicationGraph({ data, usePercentage, isDark }) {
10597
<LabelList
10698
dataKey={usePercentage ? 'conversionRate' : 'applications'}
10799
position="right"
108-
formatter={(value) => `${value}${usePercentage ? '%' : ''}`}
109-
style={{
110-
fill: isDark ? '#FFFFFF' : '#374151',
111-
fontWeight: 600,
112-
}}
100+
style={{ fill: isDark ? '#FFFFFF' : '#374151', fontWeight: 600 }}
113101
/>
114102
</Bar>
115103
</BarChart>
@@ -119,4 +107,17 @@ function ConvertedApplicationGraph({ data, usePercentage, isDark }) {
119107
);
120108
}
121109

110+
ConvertedApplicationGraph.propTypes = {
111+
data: PropTypes.arrayOf(
112+
PropTypes.shape({
113+
title: PropTypes.string,
114+
hits: PropTypes.number,
115+
applications: PropTypes.number,
116+
conversionRate: PropTypes.number,
117+
})
118+
),
119+
usePercentage: PropTypes.bool.isRequired,
120+
isDark: PropTypes.bool.isRequired,
121+
};
122+
122123
export default ConvertedApplicationGraph;

src/components/Reports/HitsAndApplicationRatio/NonConvertedApplicationsGraph.jsx

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React, { useMemo } from 'react';
2+
import PropTypes from 'prop-types';
23
import {
34
BarChart,
45
Bar,
@@ -10,20 +11,20 @@ import {
1011
Label,
1112
} from 'recharts';
1213

13-
const toNum = (v, d = 0) => {
14-
const n = Number(v);
15-
return Number.isFinite(n) ? n : d;
14+
const toNum = (value) => {
15+
const n = Number(value);
16+
return Number.isFinite(n) ? n : 0;
1617
};
1718

1819
const fmtPct = (v) => `${toNum(v)}%`;
1920
const fmtInt = (v) => toNum(v).toLocaleString();
2021

2122
const truncate = (str, max = 22) =>
22-
str.length > max ? str.slice(0, max) + '…' : str;
23+
typeof str === 'string' && str.length > max ? str.slice(0, max) + '…' : str;
2324

2425
const CustomTooltip = ({ active, payload, usePercentage, isDark }) => {
2526
if (active && payload && payload.length) {
26-
const job = payload[0].payload;
27+
const job = payload[0]?.payload || {};
2728
return (
2829
<div
2930
className={`p-2 rounded shadow ${
@@ -33,20 +34,20 @@ const CustomTooltip = ({ active, payload, usePercentage, isDark }) => {
3334
}`}
3435
style={{ fontSize: '0.875rem' }}
3536
>
36-
<p><span style={{ fontWeight: 900 }}>Role:</span> {job.title}</p>
37-
<p><span style={{ fontWeight: 900 }}>Conversion Rate:</span> {fmtPct(job.conversionRate)}</p>
38-
<p><span style={{ fontWeight: 900 }}>Hits:</span> {fmtInt(job.hits)}</p>
39-
<p><span style={{ fontWeight: 900 }}>Applications:</span> {fmtInt(job.applications)}</p>
37+
<p><strong>Role:</strong> {job.title}</p>
38+
<p><strong>Conversion Rate:</strong> {fmtPct(job.conversionRate)}</p>
39+
<p><strong>Hits:</strong> {fmtInt(job.hits)}</p>
40+
<p><strong>Applications:</strong> {fmtInt(job.applications)}</p>
4041
</div>
4142
);
4243
}
4344
return null;
4445
};
4546

46-
function NonConvertedApplicationsGraph({ data = [], usePercentage = true, isDark }) {
47+
function NonConvertedApplicationsGraph({ data = [], usePercentage, isDark }) {
4748
const normalized = useMemo(
4849
() =>
49-
(data || []).map((d) => ({
50+
data.map((d) => ({
5051
...d,
5152
hits: toNum(d.hits),
5253
applications: toNum(d.applications),
@@ -56,23 +57,25 @@ function NonConvertedApplicationsGraph({ data = [], usePercentage = true, isDark
5657
);
5758

5859
const metricKey = usePercentage ? 'conversionRate' : 'applications';
60+
5961
const rows = useMemo(() => {
6062
const sorted = [...normalized].sort((a, b) => {
6163
const diff = toNum(a[metricKey]) - toNum(b[metricKey]);
62-
return diff !== 0 ? diff : toNum(a.conversionRate) - toNum(b.conversionRate);
64+
return diff !== 0
65+
? diff
66+
: toNum(a.conversionRate) - toNum(b.conversionRate);
6367
});
6468
return sorted.slice(0, 10);
6569
}, [normalized, metricKey]);
6670

6771
const maxValue = useMemo(() => {
6872
if (rows.length === 0) return 1;
69-
const m = Math.max(...rows.map((r) => toNum(r[metricKey], 0)));
70-
return Math.max(1, Math.ceil(m * 1.05));
73+
const max = Math.max(...rows.map((r) => toNum(r[metricKey])));
74+
return Math.max(1, Math.ceil(max * 1.05));
7175
}, [rows, metricKey]);
7276

7377
const xDomain = usePercentage ? [0, 100] : [0, maxValue];
74-
const xTickFormatter = (v) => (usePercentage ? `${v}%` : fmtInt(v));
75-
const labelFormatter = (v) => (usePercentage ? fmtPct(v) : fmtInt(v));
78+
const xTickFormatter = usePercentage ? fmtPct : fmtInt;
7679

7780
return (
7881
<div
@@ -108,7 +111,6 @@ function NonConvertedApplicationsGraph({ data = [], usePercentage = true, isDark
108111
: 'Applications'
109112
}
110113
position="bottom"
111-
offset={0}
112114
fill={isDark ? '#e2e8f0' : '#374151'}
113115
/>
114116
</XAxis>
@@ -117,15 +119,14 @@ function NonConvertedApplicationsGraph({ data = [], usePercentage = true, isDark
117119
type="category"
118120
dataKey="title"
119121
width={180}
120-
tickFormatter={(v) => truncate(v)}
122+
tickFormatter={(v) => v}
121123
tick={{ fill: isDark ? '#e2e8f0' : '#374151', fontSize: 12 }}
122124
stroke={isDark ? '#e2e8f0' : '#374151'}
123125
>
124126
<Label
125127
value="Job Role"
126128
angle={-90}
127129
position="left"
128-
offset={-5}
129130
fill={isDark ? '#e2e8f0' : '#374151'}
130131
/>
131132
</YAxis>
@@ -136,7 +137,7 @@ function NonConvertedApplicationsGraph({ data = [], usePercentage = true, isDark
136137
<LabelList
137138
dataKey={metricKey}
138139
position="right"
139-
formatter={labelFormatter}
140+
formatter={xTickFormatter}
140141
style={{
141142
fill: isDark ? '#FFFFFF' : '#374151',
142143
fontWeight: 600,
@@ -150,4 +151,17 @@ function NonConvertedApplicationsGraph({ data = [], usePercentage = true, isDark
150151
);
151152
}
152153

154+
NonConvertedApplicationsGraph.propTypes = {
155+
data: PropTypes.arrayOf(
156+
PropTypes.shape({
157+
title: PropTypes.string,
158+
hits: PropTypes.number,
159+
applications: PropTypes.number,
160+
conversionRate: PropTypes.number,
161+
})
162+
),
163+
usePercentage: PropTypes.bool.isRequired,
164+
isDark: PropTypes.bool.isRequired,
165+
};
166+
153167
export default NonConvertedApplicationsGraph;

src/components/Reports/HitsAndApplicationRatio/__tests__/ConvertedApplicationGraph.test.jsx

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,9 @@ describe('ConvertedApplicationGraph', () => {
1212
expect(screen.getByText(/Top 10 Job Postings/)).toBeInTheDocument();
1313
});
1414

15-
it('sorts data in descending order by applications', () => {
16-
render(<ConvertedApplicationGraph data={mockData} usePercentage={false} isDark={false} />);
17-
// Check the order of titles instead of values
18-
const items = mockData.sort((a, b) => b.applications - a.applications);
19-
expect(items[0].title).toBe('B');
15+
it('sorts data correctly by applications when usePercentage=false', () => {
16+
const sorted = [...mockData].sort((a, b) => b.applications - a.applications);
17+
expect(sorted[0].title).toBe('B');
2018
});
2119

2220
it('switches metric when usePercentage=true', () => {

src/setupTests.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,9 @@ afterAll(() => {
133133
});
134134

135135
class ResizeObserver {
136-
observe() { }
137-
unobserve() { }
138-
disconnect() { }
136+
observe() { return undefined; } // noop
137+
unobserve() { return undefined; } // noop
138+
disconnect() { return undefined; } // noop
139139
}
140140

141-
global.ResizeObserver = ResizeObserver;
141+
global.ResizeObserver = ResizeObserver;

0 commit comments

Comments
 (0)