Skip to content

Commit 399e41e

Browse files
authored
Add a plot to the website (#12)
* add plot
1 parent ee7eaaa commit 399e41e

File tree

10 files changed

+493
-34
lines changed

10 files changed

+493
-34
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -167,3 +167,4 @@ cython_debug/
167167
#.idea/
168168

169169
.env
170+
.DS_Store

frontend/app/components/InfoBox.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const InfoBox: React.FC<InfoBoxProps> = ({ infoBoxVisible }) => {
2424
packages on PyPI, which includes all packages with at least ~100
2525
downloads per week. The results are then scored based on their
2626
similarity to the query and their number of weekly downloads, and the
27-
best results are displayed in the table below.
27+
best results are displayed in the plot and table above.
2828
</p>
2929
</div>
3030
);
+278
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
import React from "react";
2+
import { Scatter } from "react-chartjs-2";
3+
import {
4+
Chart,
5+
Tooltip,
6+
Legend,
7+
PointElement,
8+
LinearScale,
9+
Title,
10+
LogarithmicScale,
11+
CategoryScale,
12+
} from "chart.js";
13+
14+
Chart.register(
15+
Tooltip,
16+
Legend,
17+
PointElement,
18+
LinearScale,
19+
Title,
20+
LogarithmicScale,
21+
CategoryScale,
22+
);
23+
24+
interface Match {
25+
name: string;
26+
similarity: number;
27+
weekly_downloads: number;
28+
summary: string;
29+
}
30+
31+
interface ScatterPlotProps {
32+
results: Match[];
33+
}
34+
35+
const getColor = (
36+
similarity: number,
37+
downloads: number,
38+
minSim: number,
39+
maxSim: number,
40+
minLogDownloads: number,
41+
maxLogDownloads: number,
42+
) => {
43+
const baseColor = [54, 162, 235]; // Blue
44+
const highlightColor = [255, 99, 132]; // Red
45+
46+
const normalizedSimilarity = (similarity - minSim) / (maxSim - minSim);
47+
const normalizedDownloads =
48+
(Math.log10(downloads) - minLogDownloads) /
49+
(maxLogDownloads - minLogDownloads);
50+
51+
const weight = Math.min(
52+
((normalizedSimilarity + normalizedDownloads) / 2) * 1.5,
53+
1,
54+
);
55+
56+
const color = baseColor.map((base, index) =>
57+
Math.round(base + weight * (highlightColor[index] - base)),
58+
);
59+
60+
return `rgba(${color.join(",")}, 0.8)`;
61+
};
62+
63+
const getPointSize = (
64+
similarity: number,
65+
downloads: number,
66+
minSim: number,
67+
maxSim: number,
68+
minLogDownloads: number,
69+
maxLogDownloads: number,
70+
) => {
71+
const normalizedSimilarity = (similarity - minSim) / (maxSim - minSim);
72+
const normalizedDownloads =
73+
(Math.log10(downloads) - minLogDownloads) /
74+
(maxLogDownloads - minLogDownloads);
75+
76+
const minSize = 2;
77+
const size = Math.min(
78+
(normalizedSimilarity + normalizedDownloads) * 10 + minSize,
79+
25,
80+
);
81+
return size;
82+
};
83+
84+
const ScatterPlot: React.FC<ScatterPlotProps> = ({ results }) => {
85+
const similarities = results.map((result) => result.similarity);
86+
const downloads = results.map((result) => result.weekly_downloads);
87+
const logDownloads = downloads.map((download) => Math.log10(download));
88+
89+
const minSim = Math.min(...similarities);
90+
const maxSim = Math.max(...similarities);
91+
const minLogDownloads = Math.min(...logDownloads);
92+
const maxLogDownloads = Math.max(...logDownloads);
93+
94+
const data = {
95+
datasets: [
96+
{
97+
label: "Packages",
98+
data: results.map((result) => ({
99+
x: result.similarity,
100+
y: result.weekly_downloads,
101+
name: result.name,
102+
summary: result.summary,
103+
link: `https://pypi.org/project/${result.name}/`,
104+
})),
105+
backgroundColor: results.map((result) =>
106+
getColor(
107+
result.similarity,
108+
result.weekly_downloads,
109+
minSim,
110+
maxSim,
111+
minLogDownloads,
112+
maxLogDownloads,
113+
),
114+
),
115+
borderColor: results.map((result) =>
116+
getColor(
117+
result.similarity,
118+
result.weekly_downloads,
119+
minSim,
120+
maxSim,
121+
minLogDownloads,
122+
maxLogDownloads,
123+
),
124+
),
125+
pointRadius: results.map((result) =>
126+
getPointSize(
127+
result.similarity,
128+
result.weekly_downloads,
129+
minSim,
130+
maxSim,
131+
minLogDownloads,
132+
maxLogDownloads,
133+
),
134+
),
135+
hoverBackgroundColor: results.map((result) =>
136+
getColor(
137+
result.similarity,
138+
result.weekly_downloads,
139+
minSim,
140+
maxSim,
141+
minLogDownloads,
142+
maxLogDownloads,
143+
),
144+
),
145+
hoverBorderColor: results.map((result) =>
146+
getColor(
147+
result.similarity,
148+
result.weekly_downloads,
149+
minSim,
150+
maxSim,
151+
minLogDownloads,
152+
maxLogDownloads,
153+
),
154+
),
155+
pointHoverRadius: 15,
156+
},
157+
],
158+
};
159+
160+
const options = {
161+
responsive: true,
162+
maintainAspectRatio: false,
163+
plugins: {
164+
tooltip: {
165+
callbacks: {
166+
title: (context: any) => {
167+
const dataPoint = context[0].raw;
168+
return dataPoint.name;
169+
},
170+
beforeLabel: (context: any) => {
171+
const dataPoint = context.raw;
172+
return dataPoint.summary;
173+
},
174+
label: () => "",
175+
afterLabel: (context: any) => {
176+
const dataPoint = context.raw;
177+
return `\nWeekly downloads: ${dataPoint.y.toLocaleString()}`;
178+
},
179+
},
180+
titleFont: { size: 16, weight: "bold" },
181+
bodyFont: { size: 14 },
182+
footerFont: { size: 12 },
183+
displayColors: false,
184+
backgroundColor: "rgba(0, 0, 0, 0.8)",
185+
padding: 10,
186+
bodySpacing: 4,
187+
titleAlign: "left",
188+
bodyAlign: "left",
189+
footerAlign: "left",
190+
},
191+
legend: {
192+
display: false,
193+
},
194+
},
195+
scales: {
196+
x: {
197+
title: {
198+
display: true,
199+
text: "Similarity",
200+
color: "#FFFFFF",
201+
font: {
202+
size: 24,
203+
},
204+
},
205+
ticks: {
206+
color: "#FFFFFF",
207+
},
208+
},
209+
y: {
210+
title: {
211+
display: true,
212+
text: "Weekly Downloads",
213+
color: "#FFFFFF",
214+
font: {
215+
size: 24,
216+
},
217+
},
218+
ticks: {
219+
callback: function (value: any) {
220+
return value.toLocaleString();
221+
},
222+
color: "#FFFFFF",
223+
maxTicksLimit: 5,
224+
},
225+
type: "logarithmic",
226+
},
227+
},
228+
onClick: (event: any, elements: any) => {
229+
if (elements.length > 0) {
230+
const elementIndex = elements[0].index;
231+
const datasetIndex = elements[0].datasetIndex;
232+
const link = data.datasets[datasetIndex].data[elementIndex].link;
233+
window.open(link, "_blank");
234+
}
235+
},
236+
onHover: (event: any, elements: any) => {
237+
event.native.target.style.cursor = elements[0] ? "pointer" : "default";
238+
},
239+
elements: {
240+
point: {
241+
hoverRadius: 15,
242+
},
243+
},
244+
};
245+
246+
const plugins = [
247+
{
248+
id: "customLabels",
249+
afterDatasetsDraw: (chart: any) => {
250+
const ctx = chart.ctx;
251+
chart.data.datasets.forEach((dataset: any) => {
252+
dataset.data.forEach((dataPoint: any, index: number) => {
253+
const { x, y } = chart
254+
.getDatasetMeta(0)
255+
.data[index].tooltipPosition();
256+
ctx.fillStyle = "white";
257+
ctx.textAlign = "center";
258+
ctx.fillText(dataPoint.name, x, y - 10);
259+
});
260+
});
261+
},
262+
},
263+
];
264+
265+
return (
266+
<div className="overflow-auto w-full flex flex-col items-center">
267+
<h2 className="text-center text-white mb-4">
268+
Click a package to go to PyPI
269+
</h2>
270+
<hr className="border-gray-500 mb-4 w-full" />
271+
<div className="w-full h-[600px]">
272+
<Scatter data={data} options={options} plugins={plugins} />
273+
</div>
274+
</div>
275+
);
276+
};
277+
278+
export default ScatterPlot;
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from "react";
2+
3+
interface ToggleSwitchProps {
4+
option1: string;
5+
option2: string;
6+
selectedOption: string;
7+
onToggle: (option: string) => void;
8+
}
9+
10+
const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
11+
option1,
12+
option2,
13+
selectedOption,
14+
onToggle,
15+
}) => {
16+
return (
17+
<div className="flex space-x-4 bg-sky-800 p-2 rounded-lg shadow-md">
18+
<button
19+
className={`px-4 py-2 rounded ${
20+
selectedOption === option1
21+
? "bg-white text-sky-900"
22+
: " bg-sky-950 text-white"
23+
}`}
24+
onClick={() => onToggle(option1)}
25+
>
26+
{option1}
27+
</button>
28+
<button
29+
className={`px-4 py-2 rounded ${
30+
selectedOption === option2
31+
? "bg-white text-sky-900"
32+
: " bg-sky-950 text-white"
33+
}`}
34+
onClick={() => onToggle(option2)}
35+
>
36+
{option2}
37+
</button>
38+
</div>
39+
);
40+
};
41+
42+
export default ToggleSwitch;

0 commit comments

Comments
 (0)