Skip to content

Commit

Permalink
cleaned up a bit
Browse files Browse the repository at this point in the history
various cleanup

cleaned up a bit (#10)

move test

add plot

wip

add colorscale

made plot reactive

fix
  • Loading branch information
fpgmaas committed Jul 2, 2024
1 parent d1f5818 commit 086ce61
Show file tree
Hide file tree
Showing 18 changed files with 539 additions and 1,849 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,4 @@ cython_debug/
#.idea/

.env
.DS_Store
14 changes: 5 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@ build: ## Build wheel file using poetry
@echo "🚀 Creating wheel file"
@poetry build

.PHONY: docs-test
docs-test: ## Test if documentation can be built without warnings or errors
@poetry run mkdocs build -s

.PHONY: docs
docs: ## Build and serve the documentation
@poetry run mkdocs serve

.PHONY: serve
serve: ## Serve API with uvicorn
serve: ## Serve API with uvicorn in development mode
@poetry run uvicorn pypi_scout.api.main:app --reload

.PHONY: frontend
frontend: ## Serve frontend in development mode
@cd frontend; npm run dev

.PHONY: help
help:
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/components/InfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const InfoBox: React.FC<InfoBoxProps> = ({ infoBoxVisible }) => {
packages on PyPI, which includes all packages with at least ~100
downloads per week. The results are then scored based on their
similarity to the query and their number of weekly downloads, and the
best results are displayed in the table below.
best results are displayed in the plot and table above.
</p>
</div>
);
Expand Down
278 changes: 278 additions & 0 deletions frontend/app/components/ScatterPlot.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import React from "react";
import { Scatter } from "react-chartjs-2";
import {
Chart,
Tooltip,
Legend,
PointElement,
LinearScale,
Title,
LogarithmicScale,
CategoryScale,
} from "chart.js";

Chart.register(
Tooltip,
Legend,
PointElement,
LinearScale,
Title,
LogarithmicScale,
CategoryScale,
);

interface Match {
name: string;
similarity: number;
weekly_downloads: number;
summary: string;
}

interface ScatterPlotProps {
results: Match[];
}

const getColor = (
similarity: number,
downloads: number,
minSim: number,
maxSim: number,
minLogDownloads: number,
maxLogDownloads: number,
) => {
const baseColor = [54, 162, 235]; // Blue
const highlightColor = [255, 99, 132]; // Red

const normalizedSimilarity = (similarity - minSim) / (maxSim - minSim);
const normalizedDownloads =
(Math.log10(downloads) - minLogDownloads) /
(maxLogDownloads - minLogDownloads);

const weight = Math.min(
((normalizedSimilarity + normalizedDownloads) / 2) * 1.5,
1,
);

const color = baseColor.map((base, index) =>
Math.round(base + weight * (highlightColor[index] - base)),
);

return `rgba(${color.join(",")}, 0.8)`;
};

const getPointSize = (
similarity: number,
downloads: number,
minSim: number,
maxSim: number,
minLogDownloads: number,
maxLogDownloads: number,
) => {
const normalizedSimilarity = (similarity - minSim) / (maxSim - minSim);
const normalizedDownloads =
(Math.log10(downloads) - minLogDownloads) /
(maxLogDownloads - minLogDownloads);

const minSize = 2;
const size = Math.min(
(normalizedSimilarity + normalizedDownloads) * 10 + minSize,
25,
);
return size;
};

const ScatterPlot: React.FC<ScatterPlotProps> = ({ results }) => {
const similarities = results.map((result) => result.similarity);
const downloads = results.map((result) => result.weekly_downloads);
const logDownloads = downloads.map((download) => Math.log10(download));

const minSim = Math.min(...similarities);
const maxSim = Math.max(...similarities);
const minLogDownloads = Math.min(...logDownloads);
const maxLogDownloads = Math.max(...logDownloads);

const data = {
datasets: [
{
label: "Packages",
data: results.map((result) => ({
x: result.similarity,
y: result.weekly_downloads,
name: result.name,
summary: result.summary,
link: `https://pypi.org/project/${result.name}/`,
})),
backgroundColor: results.map((result) =>
getColor(
result.similarity,
result.weekly_downloads,
minSim,
maxSim,
minLogDownloads,
maxLogDownloads,
),
),
borderColor: results.map((result) =>
getColor(
result.similarity,
result.weekly_downloads,
minSim,
maxSim,
minLogDownloads,
maxLogDownloads,
),
),
pointRadius: results.map((result) =>
getPointSize(
result.similarity,
result.weekly_downloads,
minSim,
maxSim,
minLogDownloads,
maxLogDownloads,
),
),
hoverBackgroundColor: results.map((result) =>
getColor(
result.similarity,
result.weekly_downloads,
minSim,
maxSim,
minLogDownloads,
maxLogDownloads,
),
),
hoverBorderColor: results.map((result) =>
getColor(
result.similarity,
result.weekly_downloads,
minSim,
maxSim,
minLogDownloads,
maxLogDownloads,
),
),
pointHoverRadius: 15,
},
],
};

const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
tooltip: {
callbacks: {
title: (context: any) => {
const dataPoint = context[0].raw;
return dataPoint.name;
},
beforeLabel: (context: any) => {
const dataPoint = context.raw;
return dataPoint.summary;
},
label: () => "",
afterLabel: (context: any) => {
const dataPoint = context.raw;
return `\nWeekly downloads: ${dataPoint.y.toLocaleString()}`;
},
},
titleFont: { size: 16, weight: "bold" },
bodyFont: { size: 14 },
footerFont: { size: 12 },
displayColors: false,
backgroundColor: "rgba(0, 0, 0, 0.8)",
padding: 10,
bodySpacing: 4,
titleAlign: "left",
bodyAlign: "left",
footerAlign: "left",
},
legend: {
display: false,
},
},
scales: {
x: {
title: {
display: true,
text: "Similarity",
color: "#FFFFFF",
font: {
size: 24,
},
},
ticks: {
color: "#FFFFFF",
},
},
y: {
title: {
display: true,
text: "Weekly Downloads",
color: "#FFFFFF",
font: {
size: 24,
},
},
ticks: {
callback: function (value: any) {
return value.toLocaleString();
},
color: "#FFFFFF",
maxTicksLimit: 5,
},
type: "logarithmic",
},
},
onClick: (event: any, elements: any) => {
if (elements.length > 0) {
const elementIndex = elements[0].index;
const datasetIndex = elements[0].datasetIndex;
const link = data.datasets[datasetIndex].data[elementIndex].link;
window.open(link, "_blank");
}
},
onHover: (event: any, elements: any) => {
event.native.target.style.cursor = elements[0] ? "pointer" : "default";
},
elements: {
point: {
hoverRadius: 15,
},
},
};

const plugins = [
{
id: "customLabels",
afterDatasetsDraw: (chart: any) => {
const ctx = chart.ctx;
chart.data.datasets.forEach((dataset: any) => {
dataset.data.forEach((dataPoint: any, index: number) => {
const { x, y } = chart
.getDatasetMeta(0)
.data[index].tooltipPosition();
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.fillText(dataPoint.name, x, y - 10);
});
});
},
},
];

return (
<div className="overflow-auto w-full flex flex-col items-center">
<h2 className="text-center text-white mb-4">
Click a package to go to PyPI
</h2>
<hr className="border-gray-500 mb-4 w-full" />
<div className="w-full h-[600px]">
<Scatter data={data} options={options} plugins={plugins} />
</div>
</div>
);
};

export default ScatterPlot;
42 changes: 42 additions & 0 deletions frontend/app/components/ToggleSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";

interface ToggleSwitchProps {
option1: string;
option2: string;
selectedOption: string;
onToggle: (option: string) => void;
}

const ToggleSwitch: React.FC<ToggleSwitchProps> = ({
option1,
option2,
selectedOption,
onToggle,
}) => {
return (
<div className="flex space-x-4 bg-sky-800 p-2 rounded-lg shadow-md">
<button
className={`px-4 py-2 rounded ${
selectedOption === option1
? "bg-white text-sky-900"
: " bg-sky-950 text-white"
}`}
onClick={() => onToggle(option1)}
>
{option1}
</button>
<button
className={`px-4 py-2 rounded ${
selectedOption === option2
? "bg-white text-sky-900"
: " bg-sky-950 text-white"
}`}
onClick={() => onToggle(option2)}
>
{option2}
</button>
</div>
);
};

export default ToggleSwitch;
Loading

0 comments on commit 086ce61

Please sign in to comment.