Skip to content

Commit 13b54bb

Browse files
committed
interactive demo via @wq/analyst
1 parent d00992c commit 13b54bb

18 files changed

+279
-14
lines changed

.github/workflows/pages.yml

+8
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,16 @@ jobs:
3333
run: |
3434
curl -L -s https://unpkg.com/wq > docs/js/wq.js
3535
curl -L -s https://unpkg.com/@wq/markdown@latest > docs/js/markdown.js
36+
curl -L -s https://unpkg.com/@wq/analyst@next > docs/js/analyst.js
37+
curl -L -s https://unpkg.com/@wq/chart@next > docs/js/chart.js
3638
sed -i "s/^import\(.*\)https:\/\/unpkg.com\/wq/import\1.\/wq.js/" docs/js/*.js
3739
sed -i "s/^import\(.*\)https:\/\/unpkg.com\/@wq\/markdown@next/import\1.\/markdown.js/" docs/js/*.js
40+
sed -i "s/^import\(.*\)https:\/\/unpkg.com\/@wq\/analyst/import\1.\/analyst.js/" docs/js/*.js
41+
sed -i "s/^import\(.*\)https:\/\/unpkg.com\/@wq\/chart/import\1.\/chart.js/" docs/js/*.js
42+
- name: Export Django site
43+
run: |
44+
python -m pip install django djangorestframework pandas openpyxl matplotlib
45+
python -m unittest tests.generate_docs
3846
- name: Build with Jekyll
3947
uses: actions/jekyll-build-pages@v1
4048
with:

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@
55
build
66
dist
77
node_modules
8+
docs/static
9+
docs/timeseries.*
10+
docs/weather.*

docs/_layouts/default.html

+9
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@
1313
margin-right: auto;
1414
max-width: 100%;
1515
}
16+
.MuiAppBar-colorPrimary img {
17+
border-radius: 4px;
18+
padding-left: 4px;
19+
padding-right: 4px;
20+
margin-left: -18px !important;
21+
margin-top: 4px;
22+
margin-bottom: 4px;
23+
background-color: rgba(0, 0, 0, 0.6);
24+
}
1625
</style>
1726
<script async src="https://www.googletagmanager.com/gtag/js?id=G-LTN8HFGJT2"></script>
1827
<script>

docs/index.md

+20-2
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,32 @@ wq_config:
1212

1313
#### [Django REST Framework] + [pandas] = A Model-driven Visualization API
1414

15-
**Django REST Pandas** (DRP) provides a simple way to generate and serve [pandas] DataFrames via the [Django REST Framework]. The resulting API can serve up CSV (and a number of [other formats][formats] for consumption by a client-side visualization tool like [d3.js].
15+
**Django REST Pandas** (DRP) provides a simple way to generate and serve [pandas] DataFrames via the [Django REST Framework]. The resulting API can serve up CSV (and a number of [other formats][formats] for consumption by a client-side visualization tool like [@wq/analyst]:
16+
17+
```js
18+
// @wq/analyst
19+
{
20+
"title": "Live Demo",
21+
"url": "/weather.csv",
22+
"initial_rows": 10,
23+
"initial_order": {
24+
"date": "desc",
25+
},
26+
"formats": {
27+
"csv": "CSV",
28+
"xlsx": "Excel",
29+
"json": "JSON",
30+
"html": "HTML"
31+
}
32+
}
33+
```
1634

1735
[**Django REST Pandas on GitHub**](https://github.com/wq/django-rest-pandas)
1836

1937
[pandas]: https://pandas.pydata.org/
2038
[Django REST Framework]: https://www.django-rest-framework.org/
2139
[formats]: ./renderers/index.md
22-
[d3.js]: ./@wq/chart.md
40+
[@wq/analyst]: ./@wq/analyst.md
2341

2442
## News
2543

docs/js/$index.js

+9-2
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@ layout: null
33
---
44

55
import wq, { modules } from 'https://unpkg.com/wq';
6-
import markdown, { renderers } from 'https://unpkg.com/@wq/markdown@next';
6+
import markdown, { components } from 'https://unpkg.com/@wq/markdown@next';
7+
import analyst from 'https://unpkg.com/@wq/analyst';
8+
9+
import Demo from './demo.js';
710

811
const React = modules['react'];
912
const { Typography, Link } = modules['@wq/material'];
1013

11-
wq.use(markdown);
14+
components.code = Demo;
15+
16+
wq.use([markdown, analyst]);
1217

1318
const config = {
1419
site_title: 'Django REST Pandas',
20+
logo: '/images/icons/django-rest-pandas.svg',
1521
store: {
1622
service: '',
1723
defaults: {
@@ -52,6 +58,7 @@ function pageConf(page) {
5258
icon: page.wq_config.icon_data ? page.wq_config.name : null,
5359
markdown: page.content,
5460
list: true,
61+
form: [],
5562
cache: 'all',
5663
can_change: false,
5764
can_add: false,

docs/js/demo.js

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { modules } from "https://unpkg.com/wq";
2+
import { components } from "https://unpkg.com/@wq/markdown";
3+
import { Analyst } from "https://unpkg.com/@wq/analyst";
4+
5+
const React = modules.react;
6+
const Code = components.code;
7+
8+
export default function CodeDetect(props) {
9+
const { children: value } = props;
10+
if (value.includes("// @wq/analyst")) {
11+
const config = parseConfig(value);
12+
if (config) {
13+
return React.createElement(Analyst, config);
14+
} else {
15+
return React.createElement(Code, {
16+
children: "// Error parsing @wq/analyst config\n\n" + value,
17+
});
18+
}
19+
} else {
20+
return React.createElement(Code, props);
21+
}
22+
}
23+
24+
function parseConfig(value) {
25+
value = value.replace("// @wq/analyst", "").trim();
26+
try {
27+
return JSON.parse(value);
28+
} catch {
29+
return null;
30+
}
31+
}

tests/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@
77
setup_test_environment()
88
django.setup()
99
call_command("makemigrations", "testapp", interactive=False)
10+
call_command("makemigrations", "weather", interactive=False)
1011
call_command("migrate", interactive=False)

tests/files/multitimeseries.html

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
<html lang="en-us" dir="ltr">
44
<head>
55
<title>Multi Time Series</title>
6-
<link rel="stylesheet" href="admin/css/base.css">
6+
<link rel="stylesheet" href="/static/admin/css/base.css">
77

8-
<link rel="stylesheet" href="admin/css/dark_mode.css">
9-
<script src="admin/js/theme.js" defer></script>
8+
<link rel="stylesheet" href="/static/admin/css/dark_mode.css">
9+
<script src="/static/admin/js/theme.js" defer></script>
1010

1111

1212

13-
<link rel="stylesheet" type="text/css" href="admin/css/forms.css">
13+
<link rel="stylesheet" type="text/css" href="/static/admin/css/forms.css">
1414

1515

1616

1717

1818
<meta name="viewport" content="width=device-width, initial-scale=1.0">
19-
<link rel="stylesheet" href="admin/css/responsive.css">
19+
<link rel="stylesheet" href="/static/admin/css/responsive.css">
2020

2121

2222
<meta name="robots" content="NONE,NOARCHIVE">

tests/files/timeseries.html

+5-5
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,20 @@
33
<html lang="en-us" dir="ltr">
44
<head>
55
<title>Time Series Custom</title>
6-
<link rel="stylesheet" href="admin/css/base.css">
6+
<link rel="stylesheet" href="/static/admin/css/base.css">
77

8-
<link rel="stylesheet" href="admin/css/dark_mode.css">
9-
<script src="admin/js/theme.js" defer></script>
8+
<link rel="stylesheet" href="/static/admin/css/dark_mode.css">
9+
<script src="/static/admin/js/theme.js" defer></script>
1010

1111

1212

13-
<link rel="stylesheet" type="text/css" href="admin/css/forms.css">
13+
<link rel="stylesheet" type="text/css" href="/static/admin/css/forms.css">
1414

1515

1616

1717

1818
<meta name="viewport" content="width=device-width, initial-scale=1.0">
19-
<link rel="stylesheet" href="admin/css/responsive.css">
19+
<link rel="stylesheet" href="/static/admin/css/responsive.css">
2020

2121

2222
<meta name="robots" content="NONE,NOARCHIVE">

tests/generate_docs.py

+52
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import unittest
2+
from rest_framework.test import APITestCase
3+
from tests.testapp.models import TimeSeries
4+
from tests.weather.models import Station
5+
from django.core.management import call_command
6+
import pathlib
7+
8+
9+
DOCS = pathlib.Path("docs")
10+
11+
STATIONS = {
12+
"MSP": "USW00014922",
13+
"ATL": "USW00013874",
14+
"LAX": "USW00023174",
15+
}
16+
17+
class DocsTestCase(APITestCase):
18+
def setUp(self):
19+
data = (
20+
("2014-01-01", 0.5),
21+
("2014-01-02", 0.4),
22+
("2014-01-03", 0.6),
23+
("2014-01-04", 0.2),
24+
("2014-01-05", 0.1),
25+
)
26+
for date, value in data:
27+
TimeSeries.objects.create(date=date, value=value)
28+
29+
for name, code in STATIONS.items():
30+
station = Station.objects.create(name=name, code=code)
31+
station.load_weather()
32+
33+
def test_docs(self):
34+
call_command('collectstatic', interactive=False)
35+
for url in (
36+
"timeseries.html",
37+
"timeseries.csv",
38+
"timeseries.json",
39+
"timeseries.xlsx",
40+
"timeseries.png",
41+
"timeseries.svg",
42+
"weather.html",
43+
"weather.csv",
44+
"weather.json",
45+
"weather.xlsx",
46+
"weather.png",
47+
"weather.svg",
48+
):
49+
response = self.client.get(f"/{url}")
50+
path = DOCS / url
51+
path.parent.mkdir(parents=True, exist_ok=True)
52+
path.write_bytes(response.content)

tests/settings.py

+4
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"django.contrib.contenttypes",
66
"django.contrib.messages",
77
"django.contrib.sessions",
8+
"django.contrib.staticfiles",
89
"tests.testapp",
10+
"tests.weather",
911
"rest_pandas",
1012
"rest_framework",
1113
)
@@ -16,6 +18,8 @@
1618
}
1719
}
1820
ROOT_URLCONF = "tests.urls"
21+
STATIC_URL = "/static"
22+
STATIC_ROOT = "docs/static"
1923
TEMPLATES = [
2024
{
2125
"BACKEND": "django.template.backends.django.DjangoTemplates",

tests/urls.py

+1
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33

44
urlpatterns = [
55
path("", include("tests.testapp.urls")),
6+
path("", include("tests.weather.urls")),
67
path("admin", admin.site.urls),
78
]
+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Generated by Django 5.0.3 on 2024-04-02 02:59
2+
3+
import django.db.models.deletion
4+
from django.db import migrations, models
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
initial = True
10+
11+
dependencies = []
12+
13+
operations = [
14+
migrations.CreateModel(
15+
name="Station",
16+
fields=[
17+
(
18+
"id",
19+
models.AutoField(
20+
auto_created=True,
21+
primary_key=True,
22+
serialize=False,
23+
verbose_name="ID",
24+
),
25+
),
26+
("name", models.CharField(max_length=50, unique=True)),
27+
("code", models.CharField(max_length=20, unique=True)),
28+
],
29+
),
30+
migrations.CreateModel(
31+
name="Weather",
32+
fields=[
33+
(
34+
"id",
35+
models.AutoField(
36+
auto_created=True,
37+
primary_key=True,
38+
serialize=False,
39+
verbose_name="ID",
40+
),
41+
),
42+
("date", models.DateField(verbose_name="Date")),
43+
(
44+
"tavg",
45+
models.IntegerField(null=True, verbose_name="Average Temp (°F)"),
46+
),
47+
("tmax", models.IntegerField(verbose_name="Max Temp (°F)")),
48+
("tmin", models.IntegerField(verbose_name="Min Temp (°F)")),
49+
("prcp", models.FloatField(verbose_name="Precipitation (in)")),
50+
("snow", models.FloatField(null=True, verbose_name="Snow (in)")),
51+
("snwd", models.FloatField(null=True, verbose_name="Snow Depth (in)")),
52+
(
53+
"station",
54+
models.ForeignKey(
55+
on_delete=django.db.models.deletion.PROTECT,
56+
to="weather.station",
57+
),
58+
),
59+
],
60+
),
61+
]

tests/weather/migrations/__init__.py

Whitespace-only changes.

tests/weather/models.py

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
from django.db import models
2+
import requests
3+
4+
5+
DATA_URL = "https://www.ncei.noaa.gov/access/past-weather/{code}/data.csv"
6+
7+
8+
class Station(models.Model):
9+
name = models.CharField(max_length=50, unique=True)
10+
code = models.CharField(max_length=20, unique=True)
11+
12+
def load_weather(self):
13+
response = requests.get(DATA_URL.format(code=self.code))
14+
15+
for i, row in enumerate(response.iter_lines(decode_unicode=True)):
16+
if i < 2:
17+
continue
18+
assert row.count(",") == 6
19+
date, tavg, tmax, tmin, prcp, snow, snwd = row.split(",")
20+
if date < '2020-01-01':
21+
continue
22+
self.weather_set.create(
23+
date=date,
24+
tavg=tavg or None,
25+
tmax=tmax or tavg,
26+
tmin=tmin,
27+
prcp=prcp or None,
28+
snow=snow or None,
29+
snwd=snwd or None,
30+
)
31+
32+
33+
class Weather(models.Model):
34+
station = models.ForeignKey(Station, on_delete=models.PROTECT)
35+
date = models.DateField(verbose_name="Date")
36+
tavg = models.IntegerField(verbose_name="Average Temp (°F)", null=True)
37+
tmax = models.IntegerField(verbose_name="Max Temp (°F)")
38+
tmin = models.IntegerField(verbose_name="Min Temp (°F)")
39+
prcp = models.FloatField(verbose_name="Precipitation (in)")
40+
snow = models.FloatField(verbose_name="Snow (in)", null=True)
41+
snwd = models.FloatField(verbose_name="Snow Depth (in)", null=True)

tests/weather/serializers.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
from rest_framework import serializers
2+
from .models import Weather
3+
4+
5+
class WeatherSerializer(serializers.ModelSerializer):
6+
station = serializers.ReadOnlyField(source="station.name", label="Station")
7+
8+
class Meta:
9+
model = Weather
10+
exclude = ["id"]
11+
pandas_index = ["date"] # Date
12+
pandas_unstacked_header = ["Station"]

0 commit comments

Comments
 (0)