Skip to content

Commit 2b760de

Browse files
committed
add topic refinement
1 parent 94dfa50 commit 2b760de

11 files changed

Lines changed: 1970 additions & 262 deletions

package-lock.json

Lines changed: 955 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
"@react-sigma/core": "^4.0.3",
1515
"@sigma/export-image": "^3.0.0-beta.1",
1616
"@sigma/node-border": "^3.0.0-beta.7",
17+
"@types/d3": "^7.4.3",
1718
"bootstrap": "^5.3.3",
1819
"chroma-js": "^3.1.2",
1920
"classnames": "^2.5.1",
21+
"d3": "^7.9.0",
2022
"file-saver": "^2.0.5",
2123
"graphology": "^0.25.4",
2224
"graphology-gexf": "^0.13.2",
@@ -28,6 +30,7 @@
2830
"iwanthue": "^2.0.0",
2931
"jotai": "^2.10.3",
3032
"lodash": "^4.17.21",
33+
"lucide-react": "^0.510.0",
3134
"rc-slider": "^11.1.7",
3235
"react": "^18.3.1",
3336
"react-animate-height": "^3.2.3",
@@ -58,15 +61,19 @@
5861
"@types/react-transition-group": "^4.4.11",
5962
"@vitejs/plugin-react-swc": "^3.8.0",
6063
"@vitest/browser": "^2.1.6",
64+
"autoprefixer": "^10.4.21",
6165
"eslint": "^9.15.0",
6266
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
6367
"eslint-plugin-react-refresh": "^0.4.14",
6468
"globals": "^15.12.0",
6569
"nodemon": "^3.0.0",
6670
"playwright": "^1.49.0",
71+
"postcss": "^8.5.3",
6772
"prettier": "^3.4.1",
6873
"rollup": "^4.34.8",
6974
"sass": "^1.69.5",
75+
"tailwind-scrollbar": "^4.0.2",
76+
"tailwindcss": "^4.1.6",
7077
"typescript": "^5.7.2",
7178
"typescript-eslint": "^8.16.0",
7279
"vite": "^6.1.0",
@@ -90,4 +97,4 @@
9097
"last 1 safari version"
9198
]
9299
}
93-
}
100+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import React, { FC } from "react";
2+
import { FaArrowLeft } from "react-icons/fa";
3+
import { MultiRangeSlider } from "./MultiRangeSlider";
4+
import { HistogramBars } from "./HistogramBars"; // We'll also need to separate this
5+
6+
interface FrequencySelectorProps {
7+
isLoading: boolean;
8+
originalTopic: string;
9+
maxCount: number;
10+
frequencyRange: { min: number; max: number };
11+
setFrequencyRange: (range: { min: number; max: number }) => void;
12+
extractedTopics: Array<{ name: string; count: number }>;
13+
selectedTopics: string[];
14+
highlightedTopic?: string;
15+
setHighlightedTopic: (topic: string | undefined) => void;
16+
goBackToHome: () => void;
17+
nextStep: () => void;
18+
}
19+
20+
export const FrequencySelector: FC<FrequencySelectorProps> = ({
21+
isLoading,
22+
originalTopic,
23+
maxCount,
24+
frequencyRange,
25+
setFrequencyRange,
26+
extractedTopics,
27+
selectedTopics,
28+
highlightedTopic,
29+
setHighlightedTopic,
30+
goBackToHome,
31+
nextStep
32+
}) => {
33+
return (
34+
<div className="card shadow-sm" style={{ minHeight: '800px', display: 'flex', flexDirection: 'column' }}>
35+
<div className="card-body d-flex flex-column">
36+
<h2 className="card-title mb-3">Select Topics by Frequency</h2>
37+
<p className="text-muted mb-4">
38+
Adjust the frequency range to select topics related to{" "}
39+
<span className="badge bg-primary" style={{ fontSize: '1rem' }}>
40+
{originalTopic}
41+
</span>
42+
&nbsp;Topics with higher frequency are more common in related repositories.
43+
</p>
44+
45+
{isLoading ? (
46+
<div className="d-flex justify-content-center align-items-center" style={{ height: "300px" }}>
47+
<div className="spinner-border text-primary" role="status">
48+
<span className="visually-hidden">Loading...</span>
49+
</div>
50+
</div>
51+
) : (
52+
<div className="flex-grow-1">
53+
<div className="mb-4">
54+
<label className="form-label">Frequency Range</label>
55+
<div style={{ width: '80%' }}>
56+
<MultiRangeSlider
57+
min={0}
58+
max={maxCount}
59+
onChange={({ min, max }) => setFrequencyRange({ min, max })}
60+
value={frequencyRange}
61+
/>
62+
</div>
63+
</div>
64+
65+
<div className="mb-3">
66+
<HistogramBars
67+
data={extractedTopics}
68+
range={frequencyRange}
69+
highlightedTopic={highlightedTopic}
70+
/>
71+
</div>
72+
73+
<div>
74+
<h3 className="h5 mb-2">Selected Topics ({selectedTopics.length})</h3>
75+
<div className="d-flex flex-wrap gap-2">
76+
{selectedTopics.map(topic => (
77+
<span
78+
key={topic}
79+
className="badge rounded-pill py-2 px-3"
80+
style={{
81+
fontSize: '1rem',
82+
backgroundColor: highlightedTopic === topic ? '#ffc107' : '#198754',
83+
cursor: 'pointer',
84+
transition: 'all 0.2s ease'
85+
}}
86+
onMouseEnter={() => setHighlightedTopic(topic)}
87+
onMouseLeave={() => setHighlightedTopic(undefined)}
88+
>
89+
{topic}
90+
</span>
91+
))}
92+
{selectedTopics.length === 0 && (
93+
<p className="text-muted fst-italic">
94+
No topics selected. Adjust the frequency range to select topics.
95+
</p>
96+
)}
97+
</div>
98+
</div>
99+
</div>
100+
)}
101+
102+
<div className="d-flex justify-content-between mt-auto pt-4">
103+
<button
104+
className="btn btn-outline-secondary"
105+
onClick={goBackToHome}
106+
>
107+
Cancel
108+
</button>
109+
<button
110+
className="btn btn-primary"
111+
onClick={nextStep}
112+
disabled={selectedTopics.length === 0}
113+
>
114+
Next
115+
</button>
116+
</div>
117+
</div>
118+
</div>
119+
);
120+
};

src/components/HistogramBars.tsx

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import React, { FC, useEffect, useRef } from "react";
2+
import * as d3 from "d3";
3+
4+
interface HistogramBarsProps {
5+
data: Array<{ name: string; count: number }>;
6+
range: { min: number; max: number };
7+
highlightedTopic?: string;
8+
}
9+
10+
export const HistogramBars: FC<HistogramBarsProps> = ({ data, range, highlightedTopic }) => {
11+
const svgRef = useRef<SVGSVGElement>(null);
12+
13+
useEffect(() => {
14+
if (!svgRef.current || !data.length) return;
15+
16+
// Clear previous content
17+
d3.select(svgRef.current).selectAll("*").remove();
18+
19+
// Set dimensions with more bottom margin for labels
20+
const margin = { top: 30, right: 20, bottom: 100, left: 40 };
21+
const width = svgRef.current.clientWidth - margin.left - margin.right;
22+
const height = 300 - margin.top - margin.bottom;
23+
24+
// Create SVG
25+
const svg = d3.select(svgRef.current)
26+
.attr("width", width + margin.left + margin.right)
27+
.attr("height", height + margin.top + margin.bottom)
28+
.append("g")
29+
.attr("transform", `translate(${margin.left},${margin.top})`);
30+
31+
// Create scales
32+
const x = d3.scaleBand()
33+
.range([0, width])
34+
.padding(0.1)
35+
.domain(data.map(d => d.name));
36+
37+
const y = d3.scaleLinear()
38+
.range([height, 0])
39+
.domain([0, d3.max(data, d => d.count) || 0]);
40+
41+
// Create and add bars
42+
svg.selectAll(".bar")
43+
.data(data)
44+
.enter()
45+
.append("rect")
46+
.attr("class", "bar")
47+
.attr("x", d => x(d.name) || 0)
48+
.attr("width", x.bandwidth())
49+
.attr("y", d => y(d.count))
50+
.attr("height", d => height - y(d.count))
51+
.attr("fill", d => {
52+
if (highlightedTopic === d.name) return '#ffc107'; // Highlight color
53+
return (d.count >= range.min && d.count <= range.max) ? '#0d6efd' : '#e9ecef';
54+
})
55+
.attr("opacity", d => highlightedTopic && highlightedTopic !== d.name ? 0.5 : 1)
56+
.on("mouseover", (event, d) => {
57+
// Show tooltip
58+
const tooltip = svg.append("g")
59+
.attr("class", "tooltip")
60+
.attr("transform", `translate(${x(d.name) || 0},${y(d.count) - 20})`);
61+
62+
tooltip.append("text")
63+
.attr("x", x.bandwidth() / 2)
64+
.attr("y", 0)
65+
.attr("text-anchor", "middle")
66+
.style("font-size", "12px")
67+
.text(`${d.name}: ${d.count}`);
68+
})
69+
.on("mouseout", () => {
70+
// Remove tooltip
71+
svg.selectAll(".tooltip").remove();
72+
});
73+
74+
// Add count labels on top of bars
75+
svg.selectAll(".count-label")
76+
.data(data)
77+
.enter()
78+
.append("text")
79+
.attr("class", "count-label")
80+
.attr("x", d => (x(d.name) || 0) + x.bandwidth() / 2)
81+
.attr("y", d => y(d.count) - 5)
82+
.attr("text-anchor", "middle")
83+
.style("font-size", "12px")
84+
.style("fill", "#6c757d")
85+
.text(d => d.count);
86+
87+
// Add y-axis
88+
svg.append("g")
89+
.call(d3.axisLeft(y).ticks(5));
90+
91+
// Add x-axis with rotated labels
92+
svg.append("g")
93+
.attr("transform", `translate(0,${height})`)
94+
.call(d3.axisBottom(x))
95+
.selectAll("text")
96+
.style("text-anchor", "end")
97+
.style("font-size", "14px")
98+
.style("font-weight", "500")
99+
.attr("dx", "-1em")
100+
.attr("dy", ".15em")
101+
.attr("transform", "rotate(-45)");
102+
103+
}, [data, range, highlightedTopic]);
104+
105+
return (
106+
<div style={{ width: '100%', height: '400px', padding: '10px' }}>
107+
<svg ref={svgRef} style={{ width: '100%', height: '100%' }}></svg>
108+
</div>
109+
);
110+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
.slider-container {
2+
position: relative;
3+
width: 100%;
4+
height: 50px;
5+
}
6+
7+
.slider {
8+
position: relative;
9+
width: 100%;
10+
height: 5px;
11+
}
12+
13+
.slider__track,
14+
.slider__range {
15+
position: absolute;
16+
height: 5px;
17+
border-radius: 3px;
18+
}
19+
20+
.slider__track {
21+
background-color: #ced4da;
22+
width: 100%;
23+
z-index: 1;
24+
}
25+
26+
.slider__range {
27+
background-color: #0d6efd;
28+
z-index: 2;
29+
}
30+
31+
.slider__left-value,
32+
.slider__right-value {
33+
position: absolute;
34+
top: 20px;
35+
color: #495057;
36+
}
37+
38+
.slider__left-value {
39+
left: 0;
40+
}
41+
42+
.slider__right-value {
43+
right: 0;
44+
}
45+
46+
/* Removing the default appearance */
47+
.thumb,
48+
.thumb::-webkit-slider-thumb {
49+
-webkit-appearance: none;
50+
-webkit-tap-highlight-color: transparent;
51+
}
52+
53+
.thumb {
54+
pointer-events: none;
55+
position: absolute;
56+
height: 0;
57+
width: 100%;
58+
outline: none;
59+
}
60+
61+
.thumb--left {
62+
z-index: 3;
63+
}
64+
65+
.thumb--right {
66+
z-index: 4;
67+
}
68+
69+
/* For Chrome browsers */
70+
.thumb::-webkit-slider-thumb {
71+
background-color: #ffffff;
72+
border: 2px solid #0d6efd;
73+
border-radius: 50%;
74+
cursor: pointer;
75+
height: 18px;
76+
width: 18px;
77+
margin-top: 4px;
78+
pointer-events: all;
79+
position: relative;
80+
}
81+
82+
/* For Firefox browsers */
83+
.thumb::-moz-range-thumb {
84+
background-color: #ffffff;
85+
border: 2px solid #0d6efd;
86+
border-radius: 50%;
87+
cursor: pointer;
88+
height: 18px;
89+
width: 18px;
90+
margin-top: 4px;
91+
pointer-events: all;
92+
position: relative;
93+
}

0 commit comments

Comments
 (0)