diff --git a/pypistats/templates/package.html b/pypistats/templates/package.html index 670cb14..f32e851 100644 --- a/pypistats/templates/package.html +++ b/pypistats/templates/package.html @@ -79,6 +79,8 @@

{{ package }}


Downloads last month: {{ "{:,.0f}".format(recent['month']) }} +
+ 7-day smoothing

{% endblock %} diff --git a/pypistats/views/general.py b/pypistats/views/general.py index 34ccff7..728d3df 100644 --- a/pypistats/views/general.py +++ b/pypistats/views/general.py @@ -159,7 +159,7 @@ def package_page(package): recent[r.category] = r.downloads # PyPI metadata - metadata = None + metadata = dict() if package != "__all__": try: metadata = requests.get(f"https://pypi.python.org/pypi/{package}/json", timeout=5).json() @@ -171,6 +171,7 @@ def package_page(package): # Get data from db model_data = [] + use_smoothing = request.args.get("smooth") is not None for model in MODELS: records = ( model.query.filter_by(package=package) @@ -191,7 +192,11 @@ def package_page(package): for metric in metrics: model_data.append( - {"metric": metric, "name": model.__tablename__, "data": data_function[metric](records, category_key)} + { + "metric": metric, + "name": model.__tablename__, + "data": data_function[metric](records, category_key=category_key, use_smoothing=use_smoothing), + } ) # Build the plots @@ -240,7 +245,29 @@ def package_page(package): plots.append(plot) - return render_template("package.html", package=package, plots=plots, metadata=metadata, recent=recent, user=g.user) + return render_template( + "package.html", + package=package, + plots=plots, + metadata=metadata, + recent=recent, + user=g.user, + use_smoothing=use_smoothing, + ) + + +def smooth_data(data, window=7): + # Ensure data is sorted by date + data["x"], data["y"] = zip(*[(x, y) for x, y in sorted(zip(data["x"], data["y"]), key=lambda pair: pair[0])]) + # Smooth data on a rolling window + smoothed_data = deepcopy(data) + smoothed_data["y"] = list(smoothed_data["y"]) + for i in range(len(data["y"])): + window_start = max(0, i - window // 2) + window_end = min(len(data["y"]), i + window // 2 + 1) + window_data = data["y"][window_start:window_end] + smoothed_data["y"][i] = sum(window_data) / len(window_data) + return smoothed_data def python_minor_key(version): @@ -252,7 +279,7 @@ def python_minor_key(version): return key -def get_download_data(records, category_key=None): +def get_download_data(records, category_key=None, use_smoothing=False): """Organize the data for the absolute plots.""" data = defaultdict(lambda: {"x": [], "y": []}) @@ -301,10 +328,16 @@ def get_download_data(records, category_key=None): if category not in date_categories: data[category]["x"].append(str(records[-1].date)) data[category]["y"].append(0) + + if use_smoothing: + # Smooth data using a 7-day window + for category in all_categories: + data[category] = smooth_data(data[category]) + return data -def get_proportion_data(records, category_key=None): +def get_proportion_data(records, category_key=None, use_smoothing=False): """Organize the data for the fill plots.""" data = defaultdict(lambda: {"x": [], "y": [], "text": []}) @@ -349,6 +382,11 @@ def get_proportion_data(records, category_key=None): data[category]["y"].append(value) data[category]["text"].append("{0:.2f}%".format(value) + " = {:,}".format(date_categories[category])) + if use_smoothing: + # Smooth data using a 7-day window + for category in all_categories: + data[category] = smooth_data(data[category]) + return data diff --git a/tests/test_functional.py b/tests/test_functional.py index 38d5d67..4c3a75a 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -139,6 +139,9 @@ def test_package_metadata_and_charts(client, date): def test_pypi_unavailable(client, date, pypi_request): pypi_request.side_effect = requests.Timeout("PyPI unavailable") assert "No metadata found." in assert_package_page(client, date, 10, 30, 30) + response = client.get("/packages/sample-package?smooth=true") + assert response.status_code == 200 + assert "No metadata found." in response.get_data(as_text=True) def test_all_packages_page(client, pypi_request):