Merge branch 'd3' into staging
This commit is contained in:
commit
89ad3f0553
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,4 +3,4 @@ db.sqlite
|
|||||||
.history/
|
.history/
|
||||||
Rocket.toml
|
Rocket.toml
|
||||||
frontend/node_modules/*
|
frontend/node_modules/*
|
||||||
static/
|
/static/
|
||||||
|
@ -1,33 +1,85 @@
|
|||||||
/*document.addEventListener('DOMContentLoaded', function() {
|
import * as d3 from 'd3';
|
||||||
setDistance('.set-distance-js');
|
|
||||||
|
const margin = { top: 20, right: 20, bottom: 50, left: 50 },
|
||||||
|
width = 960 - margin.left - margin.right,
|
||||||
|
height = 500 - margin.top - margin.bottom;
|
||||||
|
|
||||||
|
const parseTime = d3.timeParse('%Y-%m-%d');
|
||||||
|
|
||||||
|
data.forEach((d: any) => {
|
||||||
|
d.date = parseTime(d.date);
|
||||||
|
d.km = +d.km;
|
||||||
});
|
});
|
||||||
|
|
||||||
function setDistance(selector: string) {
|
const x = d3.scaleTime()
|
||||||
const fields = document.querySelectorAll(selector);
|
.domain(d3.extent(data, d => d.date))
|
||||||
if(fields) {
|
.range([0, width]);
|
||||||
Array.prototype.forEach.call(fields, (field: HTMLInputElement) => {
|
|
||||||
if(field.dataset.relation){
|
|
||||||
const relatedField = <HTMLInputElement>document.getElementById(field.dataset.relation);
|
|
||||||
|
|
||||||
if(relatedField) {
|
const y = d3.scaleLinear()
|
||||||
field.addEventListener('input', (e) => {
|
.domain([0, d3.max(data, d => d.km)])
|
||||||
e.preventDefault();
|
.range([height, 0]);
|
||||||
|
|
||||||
const dataList = <HTMLDataListElement>document.getElementById('destinations');
|
const line = d3.line()
|
||||||
if(dataList) {
|
.x(d => x(d.date))
|
||||||
var option = Array.prototype.find.call(dataList.options, function(option) {
|
.y(d => y(d.km));
|
||||||
return option.value === field.value;
|
|
||||||
});
|
const svg = d3.select('#container')
|
||||||
|
.append('svg')
|
||||||
// Get distance
|
.attr('width', width + margin.left + margin.right)
|
||||||
const distance = option.getAttribute('distance');
|
.attr('height', height + margin.top + margin.bottom)
|
||||||
if(distance) relatedField.value = distance;
|
.call(responsivefy)
|
||||||
}
|
.append('g')
|
||||||
});
|
.attr('transform', `translate(${margin.left},${margin.top})`);
|
||||||
}
|
|
||||||
}
|
svg.append('path')
|
||||||
});
|
.data([data])
|
||||||
|
.attr('class', 'line')
|
||||||
|
.attr('d', line);
|
||||||
|
|
||||||
|
svg.append('g')
|
||||||
|
.attr('transform', `translate(0,${height})`)
|
||||||
|
.call(d3.axisBottom(x));
|
||||||
|
|
||||||
|
svg.append('g')
|
||||||
|
.call(d3.axisLeft(y));
|
||||||
|
|
||||||
|
|
||||||
|
function responsivefy(svg: any) {
|
||||||
|
// container will be the DOM element
|
||||||
|
// that the svg is appended to
|
||||||
|
// we then measure the container
|
||||||
|
// and find its aspect ratio
|
||||||
|
const container = d3.select(svg.node().parentNode),
|
||||||
|
width = parseInt(svg.style('width'), 10),
|
||||||
|
height = parseInt(svg.style('height'), 10),
|
||||||
|
aspect = width / height;
|
||||||
|
|
||||||
|
// set viewBox attribute to the initial size
|
||||||
|
// control scaling with preserveAspectRatio
|
||||||
|
// resize svg on inital page load
|
||||||
|
svg.attr('viewBox', `0 0 ${width} ${height}`)
|
||||||
|
.attr('preserveAspectRatio', 'xMinYMid')
|
||||||
|
.call(resize);
|
||||||
|
|
||||||
|
// add a listener so the chart will be resized
|
||||||
|
// when the window resizes
|
||||||
|
// multiple listeners for the same event type
|
||||||
|
// requires a namespace, i.e., 'click.foo'
|
||||||
|
// api docs: https://goo.gl/F3ZCFr
|
||||||
|
d3.select(window).on(
|
||||||
|
'resize.' + container.attr('id'),
|
||||||
|
resize
|
||||||
|
);
|
||||||
|
|
||||||
|
// this is the code that resizes the chart
|
||||||
|
// it will be called on load
|
||||||
|
// and in response to window resizes
|
||||||
|
// gets the width of the container
|
||||||
|
// and resizes the svg to fill it
|
||||||
|
// while maintaining a consistent aspect ratio
|
||||||
|
function resize() {
|
||||||
|
const w = parseInt(container.style('width'));
|
||||||
|
svg.attr('width', w);
|
||||||
|
svg.attr('height', Math.round(w / aspect));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
export {}
|
|
||||||
|
@ -9,15 +9,17 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@types/d3": "^7.4.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.21",
|
"postcss": "^8.4.21",
|
||||||
"sass": "^1.60.0",
|
"sass": "^1.60.0",
|
||||||
"tailwindcss": "^3.3.1",
|
"tailwindcss": "^3.3.1",
|
||||||
"typescript": "^4.9.3",
|
"typescript": "^4.9.5",
|
||||||
"vite": "^4.2.0",
|
"vite": "^4.2.0",
|
||||||
"vite-plugin-static-copy": "^0.13.1"
|
"vite-plugin-static-copy": "^0.13.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"choices.js": "^10.2.0"
|
"choices.js": "^10.2.0",
|
||||||
|
"d3": "^7.8.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,3 +9,4 @@
|
|||||||
@import 'components/input';
|
@import 'components/input';
|
||||||
@import 'components/alert';
|
@import 'components/alert';
|
||||||
@import 'components/status';
|
@import 'components/status';
|
||||||
|
@import 'components/chart';
|
||||||
|
3
frontend/scss/components/_chart.scss
Normal file
3
frontend/scss/components/_chart.scss
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.line {
|
||||||
|
@apply fill-none stroke-2 stroke-primary-600;
|
||||||
|
}
|
15
frontend/static/csv/stats.csv
Normal file
15
frontend/static/csv/stats.csv
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
date,km
|
||||||
|
2023-01-01,5
|
||||||
|
2023-02-01,24
|
||||||
|
2023-03-01,56
|
||||||
|
2023-04-01,122
|
||||||
|
2023-04-13,232
|
||||||
|
2023-05-01,456
|
||||||
|
2023-06-01,567
|
||||||
|
2023-07-01,765
|
||||||
|
2023-08-01,801
|
||||||
|
2023-09-01,903
|
||||||
|
2023-10-01,1002
|
||||||
|
2023-11-01,1100
|
||||||
|
2023-12-01,1203
|
||||||
|
2023-12-30,1340
|
|
@ -1,3 +1,4 @@
|
|||||||
|
use crate::model::user::User;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use sqlx::{FromRow, Row, SqlitePool};
|
use sqlx::{FromRow, Row, SqlitePool};
|
||||||
|
|
||||||
@ -41,3 +42,26 @@ ORDER BY rowed_km DESC;
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct PersonalStat {
|
||||||
|
date: String,
|
||||||
|
km: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_personal(db: &SqlitePool, user: &User) -> Vec<PersonalStat> {
|
||||||
|
vec![
|
||||||
|
PersonalStat {
|
||||||
|
date: String::from("2023-01-01"),
|
||||||
|
km: 5,
|
||||||
|
},
|
||||||
|
PersonalStat {
|
||||||
|
date: String::from("2023-02-01"),
|
||||||
|
km: 24,
|
||||||
|
},
|
||||||
|
PersonalStat {
|
||||||
|
date: String::from("2023-08-30"),
|
||||||
|
km: 1340,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
@ -2,13 +2,17 @@ use rocket::{get, routes, Route, State};
|
|||||||
use rocket_dyn_templates::{context, Template};
|
use rocket_dyn_templates::{context, Template};
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
|
|
||||||
use crate::model::{stat::Stat, user::User};
|
use crate::model::{
|
||||||
|
stat::{self, Stat},
|
||||||
|
user::User,
|
||||||
|
};
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
async fn index(db: &State<SqlitePool>, user: User) -> Template {
|
async fn index(db: &State<SqlitePool>, user: User) -> Template {
|
||||||
let stat = Stat::get_rowed_km(db).await;
|
let stat = Stat::get_rowed_km(db).await;
|
||||||
|
let personal = stat::get_personal(db, &user).await;
|
||||||
|
|
||||||
Template::render("stat", context!(loggedin_user: &user, stat))
|
Template::render("stat", context!(loggedin_user: &user, stat, personal))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
|
@ -34,5 +34,16 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="container" class="w-full"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const data = [
|
||||||
|
{%- for p in personal %}
|
||||||
|
{ date: '{{p.date}}', km: {{p.km}} },
|
||||||
|
{%- endfor %}
|
||||||
|
]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script src="/public/logbook.js"></script>
|
||||||
{% endblock content%}
|
{% endblock content%}
|
||||||
|
Loading…
Reference in New Issue
Block a user