Skip to content

site stats page alpha#660

Open
king-of-the-hive wants to merge 1 commit intohiveboardgame:mainfrom
king-of-the-hive:site-stats-alpha
Open

site stats page alpha#660
king-of-the-hive wants to merge 1 commit intohiveboardgame:mainfrom
king-of-the-hive:site-stats-alpha

Conversation

@king-of-the-hive
Copy link
Contributor

@king-of-the-hive king-of-the-hive commented Nov 6, 2025

Just a very simple rough first try for the site stats page.

If you guys can try on live data and post the screenshot would be great. Not in the pages directory yet akin to puzzles and whatnot.

I have some ideas on what else can be added but I need think how to get some more fake data in my local db...

image

@IongIer
Copy link
Collaborator

IongIer commented Nov 7, 2025

Screenshot_15 Screenshot_14

A few quick thoughts:

  1. Grey text doesn't work well in darkmode.
  2. The wins by color table should probably filter out casual games if it doesn't already.
  3. The White > 0 and Black > 0 are confusing to me and the data looks off, what do they represent and how come they don't have the same number of games?
    4.Also the numbers don't seem to sum up properly. 35 total white wins but adding em up in the table leads to less, same for the black ones. Is it because the table actually only sums up rated games whereas the totals include casual?....yeah this seems to be the case, the totals should probably mention % of which are rated or something like that.

As for what to add. Not sure would need to think about it.

""
};

let query = format!(r#"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We never use raw sql, but diesel functions. That said this might be hard to achieve without raw sql but give it a try

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried! It cannot do multiple group bys one of which is a case statement...

@king-of-the-hive
Copy link
Contributor Author

  1. The wins by color table should probably filter out casual games if it doesn't already.

Yes, those are rated. I'll add a note.

  1. The White > 0 and Black > 0 are confusing to me and the data looks off, what do they represent and how come they don't have the same number of games?

Those are basically Black or White has a small advantage between 0 and 100. I'll think about labels. But the number is not the same, either white has higher rating in a game or black.

  1. Also the numbers don't seem to sum up properly. 35 total white wins but adding em up in the table leads to less, same for the black ones. Is it because the table actually only sums up rated games whereas the totals include casual?....yeah this seems to be the case, the totals should probably mention % of which are rated or something like that.

Yup yup will add a clarification!

@king-of-the-hive king-of-the-hive force-pushed the site-stats-alpha branch 2 times, most recently from 32c74c2 to 248951b Compare November 11, 2025 05:57
@IongIer
Copy link
Collaborator

IongIer commented Nov 11, 2025

I really dislike the huge type and function names like SiteStatisticsWinrateByRatingDifferenceResponse, totally a taste thing tho dunno if felix agrees.
Also this seems like a lot more code than I would expect is needed for this feature but I could be wrong.

@king-of-the-hive
Copy link
Contributor Author

I really dislike the huge type and function names like SiteStatisticsWinrateByRatingDifferenceResponse, totally a taste thing tho dunno if felix agrees. Also this seems like a lot more code than I would expect is needed for this feature but I could be wrong.

I'm happy to optimize names and code, but need hints for the direction 🙂

@king-of-the-hive
Copy link
Contributor Author

king-of-the-hive commented Nov 13, 2025

I guess I could move all the aggregate functions out of Game or User objects directly into the response file. Not that it actually reduces the amount of code but at least keeps the objects free of complex functions. (Might be not possible though, all the diesel stuff should be in db?)

I will reduce the names too.

Other than that I don't see a lot of potential for the code to be more concise. It does a lot of aggregations to display a ton of data...

@IongIer
Copy link
Collaborator

IongIer commented Nov 13, 2025

I guess I could move all the aggregate functions out of Game or User objects directly into the response file. Not that it actually reduces the amount of code but at least keeps the objects free of complex functions. (Might be not possible though, all the diesel stuff should be in db?)

I will reduce the names too.

Other than that I don't see a lot of potential for the code to be more concise. It does a lot of aggregations to display a ton of data...

I will get back to you on this when I've had a more thorough think. But yeah all the diesel stuff needs to stay in the db so don't move that out.


#[component]
pub fn Statistics() -> impl IntoView {
let (selected_speed, set_selected_speed) = signal("All speeds".to_string());
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1.We have types for speed and gametype don't use strings like "All speeds" if the type can't easily do what you want it to do you can add methods to the type for that. This probably happens elsewhere as well, wherever you use a string you need to use a type, the type get to_stringed when going into the db and get converted back into types in the responses and handed back as types to the frontend.

2.Unless you have a good reason to have separate read and write signals (like you are using a library that needs separate signals) use RwSignal instead with RwSignal::new(...) and that way you have both get and set on the same signal. The codebase uses RwSignals everywhere and separate read and write rarely.

let label_for_fallback = label.clone();
let label_for_error = label.clone();

let user_resource = LocalResource::new(move || {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of looping over n users here and doing another db call for each, the response for this type should contain the complete user data (get a vector of users from the user model based on what the game model returns and that way it's two queries not n+1)

<button
type="button"
class=move || {
if selected_period.get() == period {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case this is fine because SiteStatisticsTimePeriod is just a plain enum but if were anything more complex you want to use .with and that way you do the comparison without cloning the value.

selected_period.with(|p| p == period)

{speeds
.iter()
.map(|speed| {
let speed_clone = speed.clone();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

switching to types instead of strings here will avoid all the needing to clone multiple times.
In general if you find yourself needing to do a bunch of let x,x1,x2 = y.clone()... consider using let x = StoredValue::new(y) and wherever you need that value you call x.get_value()

.map(|add_val| {
view! {
<div class="text-m text-gray-500 dark:text-gray-300 ml-1">
{format!("{:.1}", add_val)} "%"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clippy will complain about this, use the add_value inside the format string where it's possible. Once you are done with the pr check yourself by running cargo clippy and follow it's suggestions.

@king-of-the-hive king-of-the-hive marked this pull request as draft November 18, 2025 19:11
@king-of-the-hive
Copy link
Contributor Author

I'm yet to tackle issues raised by Ion, so don't look at it yet :D

@king-of-the-hive king-of-the-hive force-pushed the site-stats-alpha branch 6 times, most recently from 2880d42 to 9085c08 Compare November 21, 2025 08:28
@king-of-the-hive king-of-the-hive marked this pull request as ready for review November 21, 2025 08:29
@king-of-the-hive
Copy link
Contributor Author

I think I fixed all issues and rebased to the current code so marking ready for review.

@king-of-the-hive
Copy link
Contributor Author

Some of the files touched are formatters' work. I can revert those if necessary.

@IongIer
Copy link
Collaborator

IongIer commented Nov 27, 2025

I think I fixed all issues and rebased to the current code so marking ready for review.

Please check that it still compiles after a rebase. In this case it doesn't as there are duplicate definitions

error[E0428]: the name `GameRatings` is defined multiple times
--> db/src/models/game.rs:127:1
|
53  | pub struct GameRatings {
| ---------------------- previous definition of the type `GameRatings` here
...
127 | pub struct GameRatings {
| ^^^^^^^^^^^^^^^^^^^^^^ `GameRatings` redefined here
|
= note: `GameRatings` must be defined only once in the type namespace of this module

error[E0592]: duplicate definitions with name `get_rating_history_for_player`
--> db/src/models/game.rs:1706:5
|
1303 | /     pub async fn get_rating_history_for_player(
1304 | |         player: Uuid,
1305 | |         game_speed: &GameSpeed,
1306 | |         conn: &mut DbConn<'_>,
1307 | |     ) -> Result<Vec<GameRatings>, DbError> {
| |__________________________________________- other definition for `get_rating_history_for_player`
...
1706 | /     pub async fn get_rating_history_for_player(
1707 | |         player: Uuid,
1708 | |         game_speed: &GameSpeed,
1709 | |         conn: &mut DbConn<'_>,
1710 | |     ) -> Result<Vec<GameRatings>, DbError> {
| |__________________________________________^ duplicate definitions for `get_rating_history_for_player`

@king-of-the-hive
Copy link
Contributor Author

I think I fixed all issues and rebased to the current code so marking ready for review.

Please check that it still compiles after a rebase. In this case it doesn't as there are duplicate definitions

Oops, deleted the duplicates.

Copy link
Collaborator

@klautcomputing klautcomputing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only skimmed this, there is some stuff I would like to change. Most importantly there should not be changes to the engine, the raw SQL queries (this is why we use an ORM) and the VeryLongJavaLikeNames.
I will give this a better look tomorrow with Ion and leave you some more detailed comments.

MLP,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy, Default)]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think you should touch the engine in this PR :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did what Ion asked me to do... it's just some extra methods on the enum, it doesn't touch any substantial logic. I could move this new logic into a new shared type or something, but that mean breaking DRY. Anyhow, any specific suggestions appreciated.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might not have expressed myself clearly enough, but when I said add methods to the types, I didn't also mean extend an enum with an extra variant for example, like GameSpeed:AllSpeeds. I was trying to say use what's already there from that type instead of using special strings to achieve your goal.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The engine has no need for GameTypeFilter or the SQL stuff, why would we keep it in the engine then? Also it doesn't make the code DRYer, the enum could live somewhere else. If you compile the the engine's code now, it is actually dead code...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might not have expressed myself clearly enough, but when I said add methods to the types, I didn't also mean extend an enum with an extra variant for example, like GameSpeed:AllSpeeds. I was trying to say use what's already there from that type instead of using special strings to achieve your goal.

If the speeds should be passed as a type between the db through responses to the component as you suggested then I'm not sure how to do it without adding to the type itself. I need AllSpeeds for almost all aggregates where you see UNION ALL. I'd really appreciate specific suggestions instead of just "you can't do this" comments.

(This also extends to the raw SQL comments: I checked the documentation and different GPTs, there's no way to have multiple group bys by calculated columns. They themselves admit that in their readme: https://github.com/diesel-rs/diesel?tab=readme-ov-file#raw-sql . If you have a specific way in mind on how to do it, please let me know, I'm happy to rewrite it. )

@king-of-the-hive
Copy link
Contributor Author

I understand the raw SQL concern but it would be significantly more complex code and instead of pulling aggregates it would load thousand of games into memory without any substantial benefit.

The long names I shortened some already, if you have any specific you don't like I can F2 no problem :)

@IongIer
Copy link
Collaborator

IongIer commented Dec 2, 2025

I understand the raw SQL concern but it would be significantly more complex code and instead of pulling aggregates it would load thousand of games into memory without any substantial benefit.

The long names I shortened some already, if you have any specific you don't like I can F2 no problem :)

More complex maybe but diesel can express the queries you wrote and writing them in diesel wouldn't mean loading the games in memory, it can load exactly what the raw sql is loading.

@IongIer
Copy link
Collaborator

IongIer commented Dec 2, 2025

One thing that is missing is this /statistics page is never exposed as a link anywhere internally. There should be buttons in the dropdowns leading to the page.

Copy link
Collaborator

@klautcomputing klautcomputing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IongIer and I checked out the feature and we have some feedback for you on how it work.
I am going to look at the code a bit more when it's working nicely :)

#[derive(
Debug, Serialize, PartialEq, Eq, Deserialize, Clone, Copy, Hash, PartialOrd, Ord, Default,
)]
pub enum SiteStatisticsTimePeriod {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pub enum SiteStatisticsTimePeriod {
pub enum TimePeriod {


view! {
<h1 class="text-xl font-bold text-center">
"Player ratings distribution"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why it's always 1.0, this doesn't make sense to me, doesn't it need to add up to 1 in total? And 1.0 is 100%?
Image

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This just means the users in the db don't have enough games probably and there's exactly one user for each bucket (buckets are step 10). Try to create 200-300 games per user?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All users have 50+ games, also then the scale doesn't make sense because there can't be fractional users :)
I am gonna retry with more users and games and see whether I can get different results.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All users have 50+ games, also then the scale doesn't make sense because there can't be fractional users :)
I am gonna retry with more users and games and see whether I can get different results.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It should look something like this with enough rankable users. The graph is fractional only because of super small test db.

image

}

#[component]
pub fn SpeedSelector(speeds: Vec<GameSpeed>, selected_speed: RwSignal<GameSpeed>) -> impl IntoView {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it doesn't make sense to repeat all the buttons if they are all linked.
Image
Also clicking one of them makes the whole layout change and jump. I think the right thing to do here is to have the speedselectors and the other selectors (time period, include bot, ...) all be in one floating header so that you always have access to them and don't need to scroll around.


#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Copy, Default)]
pub enum GameTypeFilter {
Base,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure where to leave this comment so here it goes: Base games should not display elo statistics as we have no elo for base games :)

fallback=|| {
view! {
<div class="text-center py-8 text-gray-500 dark:text-gray-400">
"No data available"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's weird that the layout changes and that there's not even a heading anymore when you don't have data available.

Image

Copy link
Contributor Author

@king-of-the-hive king-of-the-hive Dec 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah I guess I can try to make a common floating selector and that would actually allow to get rid of the "Games by type and speed" table, the speed selector will just update the single stat cards. And will fix the headers when data is missing.

@king-of-the-hive
Copy link
Contributor Author

king-of-the-hive commented Dec 7, 2025

I made filters collapsible and floating and moved stuff out of the engine, plus cleaned up some minor things. I don't see a clean way to get rid of AllSpeeds and raw SQL, but, again, if you do, please let me know what it looks like.

@king-of-the-hive
Copy link
Contributor Author

/statistics page is never exposed as a link anywhere internally.

I think it's ok if it's not exposed at first tbh, I can make a further diff for that. I'd like to see what the page looks like on live data.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants