Skip to content

Commit

Permalink
Merge pull request #96 from approvers/feat/add-plotter-charming
Browse files Browse the repository at this point in the history
feat: 限界ポイントのグラフプロットに `charming` を使えるように
  • Loading branch information
nanai10a authored Jun 19, 2024
2 parents 9cb95f0 + 2360f7c commit a454028
Show file tree
Hide file tree
Showing 8 changed files with 858 additions and 14 deletions.
648 changes: 648 additions & 0 deletions Cargo.lock

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ memory_db = []

plot_matplotlib = ["inline-python"]
plot_plotters = ["plotters", "png"]
plot_charming = ["charming", "crossbeam"]

plot_plotters_static = ["plot_plotters", "plotters/ab_glyph"]
plot_plotters_dynamic = ["plot_plotters", "plotters/ttf"]
Expand Down Expand Up @@ -59,6 +60,16 @@ inline-python = { version = "0.12", optional = true }
# plot_plotters
png = { version = "0.17", optional = true }

# plot_charming
crossbeam = { version = "0.8", optional = true }

[dependencies.charming]
version = "0.3"
optional = true
default-features = false
features = ["ssr"]


[dependencies.serenity]
version = "0.12"
optional = true
Expand Down
4 changes: 2 additions & 2 deletions src/bot/genkai_point/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use {
chrono::{DateTime, Duration, Utc},
clap::ValueEnum,
once_cell::sync::Lazy,
std::{cmp::Ordering, collections::HashMap, fmt::Write},
std::{cmp::Ordering, collections::HashMap, fmt::Write, future::Future},
tokio::sync::Mutex,
};

Expand Down Expand Up @@ -160,7 +160,7 @@ pub(crate) trait GenkaiPointDatabase: Send + Sync {
}

pub(crate) trait Plotter: Send + Sync + 'static {
fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>>;
fn plot(&self, data: Vec<(String, Vec<f64>)>) -> impl Future<Output = Result<Vec<u8>>> + Send;
}

#[derive(Debug)]
Expand Down
126 changes: 126 additions & 0 deletions src/bot/genkai_point/plot/charming.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use {
crate::bot::genkai_point::plot::Plotter,
anyhow::{anyhow, Result},
charming::{
component::Axis, element::name_location::NameLocation, series::Line, Chart, ImageFormat,
ImageRenderer,
},
crossbeam::channel::{Receiver, Sender},
std::thread,
tokio::sync::oneshot,
};

pub(crate) struct Charming {
renderer: Renderer,
}

impl Charming {
pub(crate) fn new() -> Self {
let renderer = Renderer::spawn();

Self { renderer }
}
}

impl Plotter for Charming {
async fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
let chart = data
.iter()
.fold(Chart::new(), |chart, (label, data)| {
chart.series(Line::new().name(label).data(data.clone()))
})
.background_color("#FFFFFF")
.x_axis(
Axis::new()
.name_location(NameLocation::Center)
.name("時間経過(日)"),
)
.y_axis(
Axis::new()
.name_location(NameLocation::Center)
.name("累計VC時間(時)"),
);

self.renderer.render(chart).await
}
}

struct Request {
data: Chart,
bell: oneshot::Sender<Response>,
}
struct Response {
image: Result<Vec<u8>>,
}

struct Renderer {
tx: Sender<Request>,
_thread_handle: thread::JoinHandle<()>,
}

impl Renderer {
fn render_thread(rx: Receiver<Request>) {
let mut renderer = ImageRenderer::new(1280, 720);

for req in rx {
let image = renderer
.render_format(ImageFormat::Png, &req.data)
.map_err(|e| anyhow!("charming error: {e:#?}"));

req.bell.send(Response { image }).ok();
}
}

fn spawn() -> Self {
let (tx, rx) = crossbeam::channel::unbounded::<Request>();

let handle = std::thread::spawn(|| Self::render_thread(rx));

Self {
tx,
_thread_handle: handle,
}
}

async fn render(&self, data: Chart) -> Result<Vec<u8>> {
let (tx, rx) = oneshot::channel();

self.tx.send(Request { data, bell: tx }).unwrap();

rx.await.unwrap().image
}
}

#[tokio::test]
async fn test() {
let charming = std::sync::Arc::new(Charming::new());

let mut handles = vec![];

#[allow(unused_variables)]
for i in 0..10 {
let charming = charming.clone();

handles.push(tokio::spawn(async move {
let result = charming
.plot(vec![
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
])
.await
.unwrap();

// should we assert_eq with actual png?
assert_ne!(result.len(), 0);

// uncomment this to see image artifacts
// tokio::fs::write(format!("./out{i}.png"), result)
// .await
// .unwrap();
}));
}

for h in handles {
h.await.unwrap();
}
}
16 changes: 9 additions & 7 deletions src/bot/genkai_point/plot/matplotlib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ impl Matplotlib {
}

impl Plotter for Matplotlib {
fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
async fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
let result: Result<PythonContext, _> = std::panic::catch_unwind(|| {
python! {
import io
Expand All @@ -47,12 +47,14 @@ impl Plotter for Matplotlib {
}
}

#[test]
fn test_plot_to_image() {
let result = Matplotlib.plot(vec![
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
]);
#[tokio::test]
async fn test_plot_to_image() {
let result = Matplotlib {}
.plot(vec![
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
])
.await;

// should we assert_eq with actual png?
assert_ne!(result.unwrap().len(), 0);
Expand Down
4 changes: 4 additions & 0 deletions src/bot/genkai_point/plot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub(crate) mod matplotlib;
#[cfg(feature = "plot_plotters")]
pub(crate) mod plotters;

#[cfg(feature = "plot_charming")]
pub(crate) mod charming;

pub(super) async fn plot<P: Plotter + Send>(
db: &impl GenkaiPointDatabase,
ctx: &dyn Context,
Expand Down Expand Up @@ -80,6 +83,7 @@ pub(super) async fn plot<P: Plotter + Send>(
// use tokio::task::spawn_blocking to solve this problem.
let image = plotter
.plot(prottable_data)
.await
.context("failed to plot graph")?;

Ok(Some(image))
Expand Down
7 changes: 4 additions & 3 deletions src/bot/genkai_point/plot/plotters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ impl Plotters {
}

impl Plotter for Plotters {
fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
async fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
const SIZE: (usize, usize) = (1280, 720);

let mut buffer = vec![0; SIZE.0 * SIZE.1 * 3];
Expand Down Expand Up @@ -108,13 +108,14 @@ impl Plotter for Plotters {
}
}

#[test]
fn test() {
#[tokio::test]
async fn test() {
let result = Plotters::new()
.plot(vec![
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
])
.await
.unwrap();

// should we assert_eq with actual png?
Expand Down
56 changes: 54 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use {

assert_one_feature!("discord_client", "console_client");
assert_one_feature!("mongo_db", "memory_db");
assert_one_feature!("plot_plotters", "plot_matplotlib");
assert_one_feature!("plot_plotters", "plot_matplotlib", "plot_charming");

fn setup_sentry() -> Option<ClientInitGuard> {
let Ok(sentry_dsn) = env_var("SENTRY_DSN") else {
Expand Down Expand Up @@ -74,7 +74,9 @@ async fn async_main() -> Result<()> {
#[cfg(feature = "plot_plotters")]
let plotter = plot::plotters::Plotters::new();
#[cfg(feature = "plot_matplotlib")]
let plotter = plot::plotters::Matplotlib::new();
let plotter = plot::matplotlib::Matplotlib::new();
#[cfg(feature = "plot_charming")]
let plotter = plot::charming::Charming::new();

let pgp_whitelist = env_var("PGP_SOURCE_DOMAIN_WHITELIST")?
.split(',')
Expand Down Expand Up @@ -121,6 +123,56 @@ macro_rules! assert_one_feature {
" feature."
));
};
($a:literal, $b:literal, $c:literal) => {
#[cfg(all(feature = $a, feature = $b, feature = $c))]
compile_error!(concat!(
"You can't enable both of ",
$a,
" and ",
$b,
" and ",
$c,
" feature at the same time."
));

#[cfg(all(feature = $a, feature = $b))]
compile_error!(concat!(
"You can't enable both of ",
$a,
" and ",
$b,
" feature at the same time."
));

#[cfg(all(feature = $b, feature = $c))]
compile_error!(concat!(
"You can't enable both of ",
$b,
" and ",
$c,
" feature at the same time."
));

#[cfg(all(feature = $c, feature = $a))]
compile_error!(concat!(
"You can't enable both of ",
$c,
" and ",
$a,
" feature at the same time."
));

#[cfg(not(any(feature = $a, feature = $b, feature = $c)))]
compile_error!(concat!(
"You must enable either ",
$a,
" or ",
$b,
" or ",
$c,
" feature."
));
};
}

use assert_one_feature;

0 comments on commit a454028

Please sign in to comment.