diff --git a/.gitignore b/.gitignore index 911a738..d17d371 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ db.sqlite .history/ Rocket.toml frontend/node_modules/* -static/ +/static/ diff --git a/frontend/logbook.ts b/frontend/logbook.ts index 26c5a18..7a100dd 100644 --- a/frontend/logbook.ts +++ b/frontend/logbook.ts @@ -1,33 +1,85 @@ -/*document.addEventListener('DOMContentLoaded', function() { - setDistance('.set-distance-js'); +import * as d3 from 'd3'; + +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 fields = document.querySelectorAll(selector); - if(fields) { - Array.prototype.forEach.call(fields, (field: HTMLInputElement) => { - if(field.dataset.relation){ - const relatedField = document.getElementById(field.dataset.relation); +const x = d3.scaleTime() +.domain(d3.extent(data, d => d.date)) +.range([0, width]); - if(relatedField) { - field.addEventListener('input', (e) => { - e.preventDefault(); - - const dataList = document.getElementById('destinations'); - if(dataList) { - var option = Array.prototype.find.call(dataList.options, function(option) { - return option.value === field.value; - }); - - // Get distance - const distance = option.getAttribute('distance'); - if(distance) relatedField.value = distance; - } - }); - } - } - }); +const y = d3.scaleLinear() +.domain([0, d3.max(data, d => d.km)]) +.range([height, 0]); + +const line = d3.line() +.x(d => x(d.date)) +.y(d => y(d.km)); + +const svg = d3.select('#container') +.append('svg') + .attr('width', width + margin.left + margin.right) + .attr('height', height + margin.top + margin.bottom) + .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 {} diff --git a/frontend/package.json b/frontend/package.json index 51e9ecb..9e1677c 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,15 +9,17 @@ "preview": "vite preview" }, "devDependencies": { + "@types/d3": "^7.4.1", "autoprefixer": "^10.4.14", "postcss": "^8.4.21", "sass": "^1.60.0", "tailwindcss": "^3.3.1", - "typescript": "^4.9.3", + "typescript": "^4.9.5", "vite": "^4.2.0", "vite-plugin-static-copy": "^0.13.1" }, "dependencies": { - "choices.js": "^10.2.0" + "choices.js": "^10.2.0", + "d3": "^7.8.5" } } diff --git a/frontend/scss/app.scss b/frontend/scss/app.scss index 12ad22b..46606c2 100644 --- a/frontend/scss/app.scss +++ b/frontend/scss/app.scss @@ -9,3 +9,4 @@ @import 'components/input'; @import 'components/alert'; @import 'components/status'; +@import 'components/chart'; diff --git a/frontend/scss/components/_chart.scss b/frontend/scss/components/_chart.scss new file mode 100644 index 0000000..fc2534d --- /dev/null +++ b/frontend/scss/components/_chart.scss @@ -0,0 +1,3 @@ +.line { + @apply fill-none stroke-2 stroke-primary-600; +} diff --git a/src/model/stat.rs b/src/model/stat.rs index 4f6ced9..77922f9 100644 --- a/src/model/stat.rs +++ b/src/model/stat.rs @@ -1,3 +1,4 @@ +use crate::model::user::User; use serde::Serialize; use sqlx::{FromRow, Row, SqlitePool}; @@ -41,3 +42,26 @@ ORDER BY rowed_km DESC; .collect() } } + +#[derive(Debug, Serialize)] +pub struct PersonalStat { + date: String, + km: i32, +} + +pub async fn get_personal(db: &SqlitePool, user: &User) -> Vec { + 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, + }, + ] +} diff --git a/src/tera/stat.rs b/src/tera/stat.rs index dc611ef..1e39aa9 100644 --- a/src/tera/stat.rs +++ b/src/tera/stat.rs @@ -2,13 +2,17 @@ use rocket::{get, routes, Route, State}; use rocket_dyn_templates::{context, Template}; use sqlx::SqlitePool; -use crate::model::{stat::Stat, user::User}; +use crate::model::{ + stat::{self, Stat}, + user::User, +}; #[get("/")] async fn index(db: &State, user: User) -> Template { 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 { diff --git a/templates/stat.html.tera b/templates/stat.html.tera index ab44157..780ba6b 100644 --- a/templates/stat.html.tera +++ b/templates/stat.html.tera @@ -34,5 +34,16 @@ {% endfor %} +
+ + + + {% endblock content%}