Skip to content

Commit

Permalink
Merge pull request #4 from coreyja/ca/main/svg-graph
Browse files Browse the repository at this point in the history
Add Basic SVG Graph of Checkin Duration
  • Loading branch information
coreyja committed Mar 14, 2024
2 parents b57ab62 + fdf67c1 commit b33362d
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 5 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

157 changes: 154 additions & 3 deletions src/routes/current_user/pages.rs
Expand Up @@ -5,8 +5,10 @@ use axum::{
response::{IntoResponse, Redirect},
Form,
};
use chrono::{DateTime, Utc};
use cja::{app_state::AppState as _, server::session::DBSession};
use maud::html;
use maud::{html, Render};
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use crate::{app_state::AppState, templates::IntoTemplate};
Expand Down Expand Up @@ -103,27 +105,37 @@ pub async fn show(
.await
.unwrap();

let checkins = sqlx::query!(
let checkins = sqlx::query_as!(
Checkin,
r#"
SELECT *
FROM Checkins
WHERE page_id = $1
AND created_at >= now() - INTERVAL '6 hours'
AND duration_nanos is not null
ORDER BY created_at DESC
LIMIT 10
"#,
page_id
)
.fetch_all(state.db())
.await
.unwrap();

let mut checkins_for_graph = checkins.clone();
checkins_for_graph.reverse();
let graph = CheckinGraph {
checkins: checkins_for_graph,
};

html! {
h1 { (page.name) }

p { (page.path) }

h2 { "Checkins" }

(graph)

ul {
@for checkin in checkins {
li {
Expand All @@ -143,3 +155,142 @@ pub async fn show(
.await
.unwrap()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct Checkin {
checkin_id: Uuid,
page_id: Uuid,
outcome: String,
status_code: Option<i32>,
duration_nanos: Option<i64>,
created_at: DateTime<Utc>,
}

struct CheckinGraph {
checkins: Vec<Checkin>,
}

struct GraphPoint {
x: f64,
y: f64,
label: String,
}

struct YAxisLine {
width: usize,
y_pos: usize,
label: String,
}

impl Render for YAxisLine {
fn render(&self) -> maud::Markup {
let Self {
width,
y_pos,
label,
} = self;

html! {
path d=(format!("M0 {0} L{width} {0}", y_pos)) fill="none" stroke="blue" stroke-dasharray="2" stroke-width="0.25" {}
text x=(0) y=(y_pos) font-size="5" fill="blue" { (label) }
}
}
}

impl Render for CheckinGraph {
fn render(&self) -> maud::Markup {
let full_height = 100;
let height_padding = 10;

let width = 200;
let height = full_height - height_padding * 2;

let min_time = self.checkins.iter().map(|p| p.created_at).min().unwrap();
let max_time = self.checkins.iter().map(|p| p.created_at).max().unwrap();

let total_time = max_time - min_time;
let total_time = total_time.to_std().unwrap();

let min_duration_nanos = self
.checkins
.iter()
.map(|p| p.duration_nanos.unwrap())
.min()
.unwrap()
/ 1_000_000
* 1_000_000;
let min_label =
humantime::format_duration(Duration::from_nanos(min_duration_nanos as u64)).to_string();

let max_duration_nanos = self
.checkins
.iter()
.map(|p| p.duration_nanos.unwrap())
.max()
.unwrap()
/ 1_000_000
* 1_000_000;
let max_label =
humantime::format_duration(Duration::from_nanos(max_duration_nanos as u64)).to_string();

let height_range = max_duration_nanos - min_duration_nanos;

let points = self
.checkins
.iter()
.map(|p| GraphPoint {
x: (((p.created_at - min_time).to_std().unwrap().as_nanos() as f64
/ total_time.as_nanos() as f64)
* width as f64),
y: ((full_height - height_padding) as f64)
- (((p.duration_nanos.unwrap() as f64 - min_duration_nanos as f64)
/ height_range as f64)
* height as f64),
label: humantime::format_duration(Duration::from_nanos(
p.duration_nanos.unwrap() as u64
))
.to_string(),
})
.collect::<Vec<_>>();

let svg_path = points
.iter()
.enumerate()
.map(|(i, GraphPoint { x, y, .. })| {
if i == 0 {
format!("M{x} {y}")
} else {
format!("L{x} {y}")
}
})
.collect::<Vec<_>>()
.join(" ");

html! {
svg class="w-full" viewBox="0 0 200 100" {

// y max line
(YAxisLine { width, y_pos: height_padding, label: format!("Max: {max_label}")})

// y max line
(YAxisLine { width, y_pos: full_height - height_padding, label: format!("Min: {min_label}")})

// This is the actual point line
path d=(svg_path) fill="none" stroke="black" {}

@for GraphPoint { x, y, label } in points.iter() {
// Group for Hover State
g class="group" {
// Invisible Circle to make hover state bigger
circle cx=(x) cy=(y) r=(4) class="fill-transparent stroke-transparent" {}
// Point on line
circle cx=(x) cy=(y) r=(2) class="group-hover:fill-red-500" {}

// Label, hidden till group hover
text x=(x) y=(y + 5.0) font-size=4 class="hidden group-hover:block fill-red-500" { (label) }
}
}
}
}
}
}

0 comments on commit b33362d

Please sign in to comment.