Skip to content

Commit e43d9f5

Browse files
committed
feat: add bot::genkai_point::plot::charming
1 parent 5815fe7 commit e43d9f5

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

src/bot/genkai_point/plot/charming.rs

+187
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
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+
std::{
9+
sync::{Arc, Condvar, Mutex},
10+
thread::{spawn, JoinHandle},
11+
time::Instant,
12+
},
13+
};
14+
15+
pub(crate) struct Charming {
16+
renderer_handle: RendererHandle,
17+
}
18+
19+
impl Charming {
20+
pub(crate) fn new() -> Self {
21+
let renderer_handle = spawn_renderer();
22+
23+
Self { renderer_handle }
24+
}
25+
}
26+
27+
impl Plotter for Charming {
28+
fn plot(&self, data: Vec<(String, Vec<f64>)>) -> Result<Vec<u8>> {
29+
let chart = data
30+
.iter()
31+
.fold(Chart::new(), |chart, (label, data)| {
32+
chart.series(Line::new().name(label).data(data.clone()))
33+
})
34+
.background_color("#FFFFFF")
35+
.x_axis(
36+
Axis::new()
37+
.name_location(NameLocation::Center)
38+
.name("時間経過(日)"),
39+
)
40+
.y_axis(
41+
Axis::new()
42+
.name_location(NameLocation::Center)
43+
.name("累計VC時間(時)"),
44+
);
45+
46+
self.renderer_handle.call(chart)
47+
}
48+
}
49+
50+
struct RendererHandle {
51+
port: Arc<Mutex<Job>>,
52+
cond: Arc<Condvar>,
53+
54+
_handle: JoinHandle<()>,
55+
}
56+
57+
impl RendererHandle {
58+
fn call(&self, chart: Chart) -> Result<Vec<u8>> {
59+
let mut guard = self
60+
.port
61+
.lock()
62+
.map_err(|_| anyhow!("failed to lock job queue"))?;
63+
64+
let chart = Unconsidered::wrap(chart);
65+
let session = Session::new();
66+
67+
let Job::Idle = std::mem::replace(&mut *guard, Job::Queued(chart, session)) else {
68+
unreachable!()
69+
};
70+
71+
self.cond.notify_all();
72+
73+
let mut guard = self
74+
.cond
75+
.wait_while(
76+
guard,
77+
|job| !matches!(job, Job::Finished(_, s) if session == *s),
78+
)
79+
.map_err(|_| anyhow!("failed to lock job queue"))?;
80+
81+
let Job::Finished(result, _) = std::mem::replace(&mut *guard, Job::Idle) else {
82+
dbg!(&*guard);
83+
unreachable!()
84+
};
85+
86+
result.unwrap()
87+
}
88+
}
89+
90+
fn spawn_renderer() -> RendererHandle {
91+
let port = Arc::new(Mutex::new(Job::Idle));
92+
let cond = Arc::new(Condvar::new());
93+
94+
let _handle = {
95+
let port = port.clone();
96+
let cond = cond.clone();
97+
98+
spawn(|| renderer_main(port, cond))
99+
};
100+
101+
RendererHandle {
102+
port,
103+
cond,
104+
_handle,
105+
}
106+
}
107+
108+
fn renderer_main(port: Arc<Mutex<Job>>, cond: Arc<Condvar>) {
109+
let mut renderer = ImageRenderer::new(1280, 720);
110+
111+
while let Ok(Ok(mut job)) = port
112+
.lock()
113+
.map(|guard| cond.wait_while(guard, |job| !matches!(job, Job::Queued(..))))
114+
{
115+
let Job::Queued(arg0, session) = std::mem::replace(&mut *job, Job::Running) else {
116+
unreachable!()
117+
};
118+
119+
let arg0 = arg0.unwrap();
120+
let ret = renderer
121+
.render_format(ImageFormat::Png, &arg0)
122+
.map_err(|_| anyhow!("no detail provided"));
123+
124+
let ret = Unconsidered::wrap(ret);
125+
let Job::Running = std::mem::replace(&mut *job, Job::Finished(ret, session)) else {
126+
unreachable!()
127+
};
128+
129+
cond.notify_all();
130+
}
131+
}
132+
133+
#[derive(Debug, PartialEq, Eq)]
134+
enum Job {
135+
Idle,
136+
Queued(Unconsidered<Chart>, Session),
137+
Running,
138+
Finished(Unconsidered<Result<Vec<u8>>>, Session),
139+
}
140+
141+
struct Unconsidered<T>(T);
142+
143+
impl<T> Unconsidered<T> {
144+
fn wrap(val: T) -> Self {
145+
Self(val)
146+
}
147+
148+
fn unwrap(self) -> T {
149+
self.0
150+
}
151+
}
152+
153+
impl<T> std::fmt::Debug for Unconsidered<T> {
154+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155+
write!(f, "{{unconsidered}}")
156+
}
157+
}
158+
159+
impl<T> PartialEq for Unconsidered<T> {
160+
fn eq(&self, _: &Self) -> bool {
161+
true
162+
}
163+
}
164+
165+
impl<T> Eq for Unconsidered<T> {}
166+
167+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168+
struct Session(Instant);
169+
170+
impl Session {
171+
fn new() -> Self {
172+
Self(Instant::now())
173+
}
174+
}
175+
176+
#[test]
177+
fn test() {
178+
let result = Charming::new()
179+
.plot(vec![
180+
("kawaemon".into(), vec![1.0, 4.0, 6.0, 7.0]),
181+
("kawak".into(), vec![2.0, 5.0, 11.0, 14.0]),
182+
])
183+
.unwrap();
184+
185+
// should we assert_eq with actual png?
186+
assert_ne!(result.len(), 0);
187+
}

src/bot/genkai_point/plot/mod.rs

+3
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,

0 commit comments

Comments
 (0)