Skip to content

Commit a454028

Browse files
authored
Merge pull request #96 from approvers/feat/add-plotter-charming
feat: 限界ポイントのグラフプロットに `charming` を使えるように
2 parents 9cb95f0 + 2360f7c commit a454028

File tree

8 files changed

+858
-14
lines changed

8 files changed

+858
-14
lines changed

Cargo.lock

+648
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+11
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ memory_db = []
1313

1414
plot_matplotlib = ["inline-python"]
1515
plot_plotters = ["plotters", "png"]
16+
plot_charming = ["charming", "crossbeam"]
1617

1718
plot_plotters_static = ["plot_plotters", "plotters/ab_glyph"]
1819
plot_plotters_dynamic = ["plot_plotters", "plotters/ttf"]
@@ -59,6 +60,16 @@ inline-python = { version = "0.12", optional = true }
5960
# plot_plotters
6061
png = { version = "0.17", optional = true }
6162

63+
# plot_charming
64+
crossbeam = { version = "0.8", optional = true }
65+
66+
[dependencies.charming]
67+
version = "0.3"
68+
optional = true
69+
default-features = false
70+
features = ["ssr"]
71+
72+
6273
[dependencies.serenity]
6374
version = "0.12"
6475
optional = true

src/bot/genkai_point/mod.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ use {
1818
chrono::{DateTime, Duration, Utc},
1919
clap::ValueEnum,
2020
once_cell::sync::Lazy,
21-
std::{cmp::Ordering, collections::HashMap, fmt::Write},
21+
std::{cmp::Ordering, collections::HashMap, fmt::Write, future::Future},
2222
tokio::sync::Mutex,
2323
};
2424

@@ -160,7 +160,7 @@ pub(crate) trait GenkaiPointDatabase: Send + Sync {
160160
}
161161

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

166166
#[derive(Debug)]

src/bot/genkai_point/plot/charming.rs

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
use {
2+
crate::bot::genkai_point::plot::Plotter,
3+
anyhow::{anyhow, Result},
4+
charming::{
5+
component::Axis, element::name_location::NameLocation, series::Line, Chart, ImageFormat,
6+
ImageRenderer,
7+
},
8+
crossbeam::channel::{Receiver, Sender},
9+
std::thread,
10+
tokio::sync::oneshot,
11+
};
12+
13+
pub(crate) struct Charming {
14+
renderer: Renderer,
15+
}
16+
17+
impl Charming {
18+
pub(crate) fn new() -> Self {
19+
let renderer = Renderer::spawn();
20+
21+
Self { renderer }
22+
}
23+
}
24+
25+
impl Plotter for Charming {
26+
async fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
27+
let chart = data
28+
.iter()
29+
.fold(Chart::new(), |chart, (label, data)| {
30+
chart.series(Line::new().name(label).data(data.clone()))
31+
})
32+
.background_color("#FFFFFF")
33+
.x_axis(
34+
Axis::new()
35+
.name_location(NameLocation::Center)
36+
.name("時間経過(日)"),
37+
)
38+
.y_axis(
39+
Axis::new()
40+
.name_location(NameLocation::Center)
41+
.name("累計VC時間(時)"),
42+
);
43+
44+
self.renderer.render(chart).await
45+
}
46+
}
47+
48+
struct Request {
49+
data: Chart,
50+
bell: oneshot::Sender<Response>,
51+
}
52+
struct Response {
53+
image: Result<Vec<u8>>,
54+
}
55+
56+
struct Renderer {
57+
tx: Sender<Request>,
58+
_thread_handle: thread::JoinHandle<()>,
59+
}
60+
61+
impl Renderer {
62+
fn render_thread(rx: Receiver<Request>) {
63+
let mut renderer = ImageRenderer::new(1280, 720);
64+
65+
for req in rx {
66+
let image = renderer
67+
.render_format(ImageFormat::Png, &req.data)
68+
.map_err(|e| anyhow!("charming error: {e:#?}"));
69+
70+
req.bell.send(Response { image }).ok();
71+
}
72+
}
73+
74+
fn spawn() -> Self {
75+
let (tx, rx) = crossbeam::channel::unbounded::<Request>();
76+
77+
let handle = std::thread::spawn(|| Self::render_thread(rx));
78+
79+
Self {
80+
tx,
81+
_thread_handle: handle,
82+
}
83+
}
84+
85+
async fn render(&self, data: Chart) -> Result<Vec<u8>> {
86+
let (tx, rx) = oneshot::channel();
87+
88+
self.tx.send(Request { data, bell: tx }).unwrap();
89+
90+
rx.await.unwrap().image
91+
}
92+
}
93+
94+
#[tokio::test]
95+
async fn test() {
96+
let charming = std::sync::Arc::new(Charming::new());
97+
98+
let mut handles = vec![];
99+
100+
#[allow(unused_variables)]
101+
for i in 0..10 {
102+
let charming = charming.clone();
103+
104+
handles.push(tokio::spawn(async move {
105+
let result = charming
106+
.plot(vec![
107+
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
108+
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
109+
])
110+
.await
111+
.unwrap();
112+
113+
// should we assert_eq with actual png?
114+
assert_ne!(result.len(), 0);
115+
116+
// uncomment this to see image artifacts
117+
// tokio::fs::write(format!("./out{i}.png"), result)
118+
// .await
119+
// .unwrap();
120+
}));
121+
}
122+
123+
for h in handles {
124+
h.await.unwrap();
125+
}
126+
}

src/bot/genkai_point/plot/matplotlib.rs

+9-7
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ impl Matplotlib {
2020
}
2121

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

50-
#[test]
51-
fn test_plot_to_image() {
52-
let result = Matplotlib.plot(vec![
53-
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
54-
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
55-
]);
50+
#[tokio::test]
51+
async fn test_plot_to_image() {
52+
let result = Matplotlib {}
53+
.plot(vec![
54+
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
55+
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
56+
])
57+
.await;
5658

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

src/bot/genkai_point/plot/mod.rs

+4
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ pub(crate) mod matplotlib;
1515
#[cfg(feature = "plot_plotters")]
1616
pub(crate) mod plotters;
1717

18+
#[cfg(feature = "plot_charming")]
19+
pub(crate) mod charming;
20+
1821
pub(super) async fn plot<P: Plotter + Send>(
1922
db: &impl GenkaiPointDatabase,
2023
ctx: &dyn Context,
@@ -80,6 +83,7 @@ pub(super) async fn plot<P: Plotter + Send>(
8083
// use tokio::task::spawn_blocking to solve this problem.
8184
let image = plotter
8285
.plot(prottable_data)
86+
.await
8387
.context("failed to plot graph")?;
8488

8589
Ok(Some(image))

src/bot/genkai_point/plot/plotters.rs

+4-3
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl Plotters {
3232
}
3333

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

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

111-
#[test]
112-
fn test() {
111+
#[tokio::test]
112+
async fn test() {
113113
let result = Plotters::new()
114114
.plot(vec![
115115
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
116116
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
117117
])
118+
.await
118119
.unwrap();
119120

120121
// should we assert_eq with actual png?

src/main.rs

+54-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use {
1717

1818
assert_one_feature!("discord_client", "console_client");
1919
assert_one_feature!("mongo_db", "memory_db");
20-
assert_one_feature!("plot_plotters", "plot_matplotlib");
20+
assert_one_feature!("plot_plotters", "plot_matplotlib", "plot_charming");
2121

2222
fn setup_sentry() -> Option<ClientInitGuard> {
2323
let Ok(sentry_dsn) = env_var("SENTRY_DSN") else {
@@ -74,7 +74,9 @@ async fn async_main() -> Result<()> {
7474
#[cfg(feature = "plot_plotters")]
7575
let plotter = plot::plotters::Plotters::new();
7676
#[cfg(feature = "plot_matplotlib")]
77-
let plotter = plot::plotters::Matplotlib::new();
77+
let plotter = plot::matplotlib::Matplotlib::new();
78+
#[cfg(feature = "plot_charming")]
79+
let plotter = plot::charming::Charming::new();
7880

7981
let pgp_whitelist = env_var("PGP_SOURCE_DOMAIN_WHITELIST")?
8082
.split(',')
@@ -121,6 +123,56 @@ macro_rules! assert_one_feature {
121123
" feature."
122124
));
123125
};
126+
($a:literal, $b:literal, $c:literal) => {
127+
#[cfg(all(feature = $a, feature = $b, feature = $c))]
128+
compile_error!(concat!(
129+
"You can't enable both of ",
130+
$a,
131+
" and ",
132+
$b,
133+
" and ",
134+
$c,
135+
" feature at the same time."
136+
));
137+
138+
#[cfg(all(feature = $a, feature = $b))]
139+
compile_error!(concat!(
140+
"You can't enable both of ",
141+
$a,
142+
" and ",
143+
$b,
144+
" feature at the same time."
145+
));
146+
147+
#[cfg(all(feature = $b, feature = $c))]
148+
compile_error!(concat!(
149+
"You can't enable both of ",
150+
$b,
151+
" and ",
152+
$c,
153+
" feature at the same time."
154+
));
155+
156+
#[cfg(all(feature = $c, feature = $a))]
157+
compile_error!(concat!(
158+
"You can't enable both of ",
159+
$c,
160+
" and ",
161+
$a,
162+
" feature at the same time."
163+
));
164+
165+
#[cfg(not(any(feature = $a, feature = $b, feature = $c)))]
166+
compile_error!(concat!(
167+
"You must enable either ",
168+
$a,
169+
" or ",
170+
$b,
171+
" or ",
172+
$c,
173+
" feature."
174+
));
175+
};
124176
}
125177

126178
use assert_one_feature;

0 commit comments

Comments
 (0)