charts_rs/charts/
candlestick_chart.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License at
4//
5//     http://www.apache.org/licenses/LICENSE-2.0
6//
7// Unless required by applicable law or agreed to in writing, software
8// distributed under the License is distributed on an "AS IS" BASIS,
9// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
10// See the License for the specific language governing permissions and
11// limitations under the License.
12
13use super::canvas;
14use super::color::*;
15use super::common::*;
16use super::component::*;
17use super::params::*;
18use super::theme::{get_default_theme_name, get_theme, Theme, DEFAULT_Y_AXIS_WIDTH};
19use super::util::*;
20use super::Canvas;
21use crate::charts::measure_text_width_family;
22use charts_rs_derive::Chart;
23use serde::{Deserialize, Serialize};
24use std::sync::Arc;
25
26#[derive(Serialize, Deserialize, Clone, Debug, Default, Chart)]
27pub struct CandlestickChart {
28    pub width: f32,
29    pub height: f32,
30    pub x: f32,
31    pub y: f32,
32    pub margin: Box,
33    // [open price1, close price1, lowest price1, highest price1, open price2, close price2, ...]
34    pub series_list: Vec<Series>,
35    pub font_family: String,
36    pub background_color: Color,
37    pub is_light: bool,
38
39    // title
40    pub title_text: String,
41    pub title_font_size: f32,
42    pub title_font_color: Color,
43    pub title_font_weight: Option<String>,
44    pub title_margin: Option<Box>,
45    pub title_align: Align,
46    pub title_height: f32,
47
48    // sub title
49    pub sub_title_text: String,
50    pub sub_title_font_size: f32,
51    pub sub_title_font_color: Color,
52    pub sub_title_font_weight: Option<String>,
53    pub sub_title_margin: Option<Box>,
54    pub sub_title_align: Align,
55    pub sub_title_height: f32,
56
57    // legend
58    pub legend_font_size: f32,
59    pub legend_font_color: Color,
60    pub legend_font_weight: Option<String>,
61    pub legend_align: Align,
62    pub legend_margin: Option<Box>,
63    pub legend_category: LegendCategory,
64    pub legend_show: Option<bool>,
65
66    // x axis
67    pub x_axis_data: Vec<String>,
68    pub x_axis_height: f32,
69    pub x_axis_stroke_color: Color,
70    pub x_axis_font_size: f32,
71    pub x_axis_font_color: Color,
72    pub x_axis_font_weight: Option<String>,
73    pub x_axis_name_gap: f32,
74    pub x_axis_name_rotate: f32,
75    pub x_axis_margin: Option<Box>,
76    pub x_axis_hidden: bool,
77    pub x_boundary_gap: Option<bool>,
78
79    // y axis
80    pub y_axis_hidden: bool,
81    pub y_axis_configs: Vec<YAxisConfig>,
82
83    // grid
84    pub grid_stroke_color: Color,
85    pub grid_stroke_width: f32,
86
87    // series
88    pub series_stroke_width: f32,
89    pub series_label_font_color: Color,
90    pub series_label_font_size: f32,
91    pub series_label_font_weight: Option<String>,
92    pub series_label_formatter: String,
93    pub series_colors: Vec<Color>,
94    pub series_symbol: Option<Symbol>,
95    pub series_smooth: bool,
96    pub series_fill: bool,
97
98    pub candlestick_up_color: Color,
99    pub candlestick_up_border_color: Color,
100    pub candlestick_down_color: Color,
101    pub candlestick_down_border_color: Color,
102}
103
104impl CandlestickChart {
105    fn fill_default(&mut self) {
106        if self.candlestick_up_color.is_zero() {
107            self.candlestick_up_color = (236, 0, 0).into();
108        }
109        if self.candlestick_up_border_color.is_zero() {
110            self.candlestick_up_border_color = (138, 0, 0).into();
111        }
112        if self.candlestick_down_color.is_zero() {
113            self.candlestick_down_color = (0, 218, 60).into();
114        }
115        if self.candlestick_down_border_color.is_zero() {
116            self.candlestick_down_border_color = (0, 143, 40).into();
117        }
118    }
119    /// Creates a candlestick chart from json.
120    pub fn from_json(data: &str) -> canvas::Result<CandlestickChart> {
121        let mut c = CandlestickChart {
122            ..Default::default()
123        };
124        let value = c.fill_option(data)?;
125        if let Some(value) = get_color_from_value(&value, "candlestick_up_color") {
126            c.candlestick_up_color = value;
127        }
128        if let Some(value) = get_color_from_value(&value, "candlestick_up_border_color") {
129            c.candlestick_up_border_color = value;
130        }
131        if let Some(value) = get_color_from_value(&value, "candlestick_down_color") {
132            c.candlestick_down_color = value;
133        }
134        if let Some(value) = get_color_from_value(&value, "candlestick_down_border_color") {
135            c.candlestick_down_border_color = value;
136        }
137        if let Some(x_axis_hidden) = get_bool_from_value(&value, "x_axis_hidden") {
138            c.x_axis_hidden = x_axis_hidden;
139        }
140        if let Some(y_axis_hidden) = get_bool_from_value(&value, "y_axis_hidden") {
141            c.y_axis_hidden = y_axis_hidden;
142        }
143        c.fill_default();
144        Ok(c)
145    }
146    /// Creates a candlestick chart with custom theme.
147    pub fn new_with_theme(
148        mut series_list: Vec<Series>,
149        x_axis_data: Vec<String>,
150        theme: &str,
151    ) -> CandlestickChart {
152        // set the index of series
153        series_list
154            .iter_mut()
155            .enumerate()
156            .for_each(|(index, item)| {
157                item.index = Some(index);
158            });
159        let mut c = CandlestickChart {
160            series_list,
161            x_axis_data,
162            ..Default::default()
163        };
164        let theme = get_theme(theme);
165        c.fill_theme(theme);
166        c.fill_default();
167        c
168    }
169    /// Creates a candlestick chart with default theme.
170    pub fn new(series_list: Vec<Series>, x_axis_data: Vec<String>) -> CandlestickChart {
171        CandlestickChart::new_with_theme(series_list, x_axis_data, &get_default_theme_name())
172    }
173    /// Converts candlestick chart to svg.
174    pub fn svg(&self) -> canvas::Result<String> {
175        let mut c = Canvas::new_width_xy(self.width, self.height, self.x, self.y);
176
177        self.render_background(c.child(Box::default()));
178        let mut x_axis_height = self.x_axis_height;
179        if self.x_axis_hidden {
180            x_axis_height = 0.0;
181        }
182        c.margin = self.margin.clone();
183
184        let title_height = self.render_title(c.child(Box::default()));
185
186        let legend_height = self.render_legend(c.child(Box::default()));
187        // get the max height of title and legend
188        let axis_top = if legend_height > title_height {
189            legend_height
190        } else {
191            title_height
192        };
193
194        let (left_y_axis_values, mut left_y_axis_width) = self.get_y_axis_values(0);
195        if self.y_axis_hidden {
196            left_y_axis_width = 0.0;
197        }
198
199        let axis_height = c.height() - x_axis_height - axis_top;
200        let axis_width = c.width() - left_y_axis_width;
201        // minus the height of top text area
202        if axis_top > 0.0 {
203            c = c.child(Box {
204                top: axis_top,
205                ..Default::default()
206            });
207        }
208
209        self.render_grid(
210            c.child(Box {
211                left: left_y_axis_width,
212                ..Default::default()
213            }),
214            axis_width,
215            axis_height,
216        );
217
218        // y axis
219        if !self.y_axis_hidden {
220            self.render_y_axis(
221                c.child(Box::default()),
222                left_y_axis_values.data.clone(),
223                axis_height,
224                left_y_axis_width,
225                0,
226            );
227        }
228
229        // x axis
230        if !self.x_axis_hidden {
231            self.render_x_axis(
232                c.child(Box {
233                    top: c.height() - x_axis_height,
234                    left: left_y_axis_width,
235                    ..Default::default()
236                }),
237                self.x_axis_data.clone(),
238                axis_width,
239            );
240        }
241        let chunk_width = axis_width / self.x_axis_data.len() as f32;
242        let half_chunk_width = chunk_width / 2.0;
243        for series in self.series_list.iter() {
244            if series.category.is_some() {
245                continue;
246            }
247            // split the series point to chunk
248            // [open, close, lowest, highest]
249            let chunks = series.data.chunks(4);
250
251            for (index, chunk) in chunks.enumerate() {
252                if chunk.len() != 4 {
253                    continue;
254                }
255
256                let open = left_y_axis_values.get_offset_height(chunk[0], axis_height);
257                let close = left_y_axis_values.get_offset_height(chunk[1], axis_height);
258                let lowest = left_y_axis_values.get_offset_height(chunk[2], axis_height);
259                let highest = left_y_axis_values.get_offset_height(chunk[3], axis_height);
260                let mut fill = self.candlestick_up_color;
261                let mut border_color = self.candlestick_up_border_color;
262                // fall
263                if chunk[0] > chunk[1] {
264                    fill = self.candlestick_down_color;
265                    border_color = self.candlestick_down_border_color;
266                } else if chunk[0] == chunk[1] {
267                    border_color = Color::transparent();
268                }
269
270                let line_left = half_chunk_width + chunk_width * index as f32 - 1.0;
271                c.child(Box {
272                    left: left_y_axis_width,
273                    ..Default::default()
274                })
275                .line(Line {
276                    color: Some(fill),
277                    stroke_width: 1.0,
278                    left: line_left,
279                    top: lowest.min(highest),
280                    right: line_left,
281                    bottom: lowest.max(highest),
282                    ..Default::default()
283                });
284
285                c.child(Box {
286                    left: left_y_axis_width,
287                    ..Default::default()
288                })
289                .rect(Rect {
290                    color: Some(border_color),
291                    fill: Some(fill),
292                    left: half_chunk_width / 2.0 + chunk_width * index as f32 - 1.0,
293                    top: open.min(close),
294                    width: half_chunk_width,
295                    height: (open.max(close) - open.min(close)).max(1.0),
296                    ..Default::default()
297                });
298            }
299        }
300        let mut line_series_list = vec![];
301        self.series_list.iter().for_each(|item| {
302            if let Some(ref cat) = item.category {
303                if *cat == SeriesCategory::Line {
304                    line_series_list.push(item);
305                }
306            }
307        });
308
309        let y_axis_values_list = vec![&left_y_axis_values];
310        let max_height = c.height() - x_axis_height;
311        let line_series_labels_list = self.render_line(
312            c.child(Box {
313                left: left_y_axis_width,
314                ..Default::default()
315            }),
316            &line_series_list,
317            &y_axis_values_list,
318            max_height,
319            axis_height,
320            self.x_axis_data.len(),
321        );
322
323        self.render_series_label(
324            c.child(Box {
325                left: left_y_axis_width,
326                ..Default::default()
327            }),
328            line_series_labels_list,
329        );
330
331        c.svg()
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::CandlestickChart;
338    use crate::SeriesCategory;
339    use pretty_assertions::assert_eq;
340    #[test]
341    fn candlestick_chart_basic() {
342        let candlestick_chart = CandlestickChart::new(
343            vec![(
344                "",
345                vec![
346                    20.0, 34.0, 10.0, 38.0, 40.0, 35.0, 30.0, 50.0, 31.0, 38.0, 33.0, 44.0, 38.0,
347                    15.0, 5.0, 42.0,
348                ],
349            )
350                .into()],
351            vec![
352                "2017-10-24".to_string(),
353                "2017-10-25".to_string(),
354                "2017-10-26".to_string(),
355                "2017-10-27".to_string(),
356            ],
357        );
358        assert_eq!(
359            include_str!("../../asset/candlestick_chart/basic.svg"),
360            candlestick_chart.svg().unwrap()
361        );
362    }
363
364    #[test]
365    fn candlestick_chart_no_axis() {
366        let mut candlestick_chart = CandlestickChart::new(
367            vec![(
368                "",
369                vec![
370                    20.0, 34.0, 10.0, 38.0, 40.0, 35.0, 30.0, 50.0, 31.0, 38.0, 33.0, 44.0, 38.0,
371                    15.0, 5.0, 42.0,
372                ],
373            )
374                .into()],
375            vec![
376                "2017-10-24".to_string(),
377                "2017-10-25".to_string(),
378                "2017-10-26".to_string(),
379                "2017-10-27".to_string(),
380            ],
381        );
382        candlestick_chart.x_axis_hidden = true;
383        candlestick_chart.y_axis_hidden = true;
384        assert_eq!(
385            include_str!("../../asset/candlestick_chart/no_axis.svg"),
386            candlestick_chart.svg().unwrap()
387        );
388    }
389
390    #[test]
391    fn candlestick_chart_sh() {
392        let mut candlestick_chart = CandlestickChart::new(
393            vec![
394                // start at sixth point
395                (
396                    "MA5",
397                    vec![
398                        2352.93, 2378.48, 2394.81, 2409.64, 2420.04, 2426.66, 2429.33, 2428.01,
399                        2417.97, 2410.51, 2391.99, 2368.35, 2349.20, 2331.29, 2314.49, 2322.42,
400                        2331.49, 2321.01, 2327.60, 2334.39, 2326.13, 2317.95, 2325.39, 2317.45,
401                        2300.81, 2290.01, 2281.96, 2267.85, 2262.02, 2272.7, 2283.49, 2293.46,
402                        2310.80, 2318.85, 2315.63, 2298.04, 2279.71, 2261.25, 2247.26, 2232.06,
403                        2227.12, 2224.95, 2223.30, 2221.66, 2217.96, 2212.03, 2205.85, 2199.38,
404                        2194.99, 2202.56, 2214.61, 2212.55, 2217.45, 2217.79, 2204.45,
405                    ],
406                )
407                    .into(),
408                (
409                    "日K",
410                    vec![
411                        2320.26, 2320.26, 2287.3, 2362.94, 2300.0, 2291.3, 2288.26, 2308.38,
412                        2295.35, 2346.5, 2295.35, 2346.92, 2347.22, 2358.98, 2337.35, 2363.8,
413                        2360.75, 2382.48, 2347.89, 2383.76, 2383.43, 2385.42, 2371.23, 2391.82,
414                        2377.41, 2419.02, 2369.57, 2421.15, 2425.92, 2428.15, 2417.58, 2440.38,
415                        2411.0, 2433.13, 2403.3, 2437.42, 2432.68, 2434.48, 2427.7, 2441.73,
416                        2430.69, 2418.53, 2394.22, 2433.89, 2416.62, 2432.4, 2414.4, 2443.03,
417                        2441.91, 2421.56, 2415.43, 2444.8, 2420.26, 2382.91, 2373.53, 2427.07,
418                        2383.49, 2397.18, 2370.61, 2397.94, 2378.82, 2325.95, 2309.17, 2378.82,
419                        2322.94, 2314.16, 2308.76, 2330.88, 2320.62, 2325.82, 2315.01, 2338.78,
420                        2313.74, 2293.34, 2289.89, 2340.71, 2297.77, 2313.22, 2292.03, 2324.63,
421                        2322.32, 2365.59, 2308.92, 2366.16, 2364.54, 2359.51, 2330.86, 2369.65,
422                        2332.08, 2273.4, 2259.25, 2333.54, 2274.81, 2326.31, 2270.1, 2328.14,
423                        2333.61, 2347.18, 2321.6, 2351.44, 2340.44, 2324.29, 2304.27, 2352.02,
424                        2326.42, 2318.61, 2314.59, 2333.67, 2314.68, 2310.59, 2296.58, 2320.96,
425                        2309.16, 2286.6, 2264.83, 2333.29, 2282.17, 2263.97, 2253.25, 2286.33,
426                        2255.77, 2270.28, 2253.31, 2276.22, 2269.31, 2278.4, 2250.0, 2312.08,
427                        2267.29, 2240.02, 2239.21, 2276.05, 2244.26, 2257.43, 2232.02, 2261.31,
428                        2257.74, 2317.37, 2257.42, 2317.86, 2318.21, 2324.24, 2311.6, 2330.81,
429                        2321.4, 2328.28, 2314.97, 2332.0, 2334.74, 2326.72, 2319.91, 2344.89,
430                        2318.58, 2297.67, 2281.12, 2319.99, 2299.38, 2301.26, 2289.0, 2323.48,
431                        2273.55, 2236.3, 2232.91, 2273.55, 2238.49, 2236.62, 2228.81, 2246.87,
432                        2229.46, 2234.4, 2227.31, 2243.95, 2234.9, 2227.74, 2220.44, 2253.42,
433                        2232.69, 2225.29, 2217.25, 2241.34, 2196.24, 2211.59, 2180.67, 2212.59,
434                        2215.47, 2225.77, 2215.47, 2234.73, 2224.93, 2226.13, 2212.56, 2233.04,
435                        2236.98, 2219.55, 2217.26, 2242.48, 2218.09, 2206.78, 2204.44, 2226.26,
436                        2199.91, 2181.94, 2177.39, 2204.99, 2169.63, 2194.85, 2165.78, 2196.43,
437                        2195.03, 2193.8, 2178.47, 2197.51, 2181.82, 2197.6, 2175.44, 2206.03,
438                        2201.12, 2244.64, 2200.58, 2250.11, 2236.4, 2242.17, 2232.26, 2245.12,
439                        2242.62, 2184.54, 2182.81, 2242.62, 2187.35, 2218.32, 2184.11, 2226.12,
440                        2213.19, 2199.31, 2191.85, 2224.63, 2203.89, 2177.91, 2173.86, 2210.58,
441                    ],
442                )
443                    .into(),
444            ],
445            vec![
446                "2013/1/24".to_string(),
447                "2013/1/25".to_string(),
448                "2013/1/28".to_string(),
449                "2013/1/29".to_string(),
450                "2013/1/30".to_string(),
451                "2013/1/31".to_string(),
452                "2013/2/1".to_string(),
453                "2013/2/4".to_string(),
454                "2013/2/5".to_string(),
455                "2013/2/6".to_string(),
456                "2013/2/7".to_string(),
457                "2013/2/8".to_string(),
458                "2013/2/18".to_string(),
459                "2013/2/19".to_string(),
460                "2013/2/20".to_string(),
461                "2013/2/21".to_string(),
462                "2013/2/22".to_string(),
463                "2013/2/25".to_string(),
464                "2013/2/26".to_string(),
465                "2013/2/27".to_string(),
466                "2013/2/28".to_string(),
467                "2013/3/1".to_string(),
468                "2013/3/4".to_string(),
469                "2013/3/5".to_string(),
470                "2013/3/6".to_string(),
471                "2013/3/7".to_string(),
472                "2013/3/8".to_string(),
473                "2013/3/11".to_string(),
474                "2013/3/12".to_string(),
475                "2013/3/13".to_string(),
476                "2013/3/14".to_string(),
477                "2013/3/15".to_string(),
478                "2013/3/18".to_string(),
479                "2013/3/18".to_string(),
480                "2013/3/20".to_string(),
481                "2013/3/21".to_string(),
482                "2013/3/22".to_string(),
483                "2013/3/25".to_string(),
484                "2013/3/26".to_string(),
485                "2013/3/27".to_string(),
486                "2013/3/28".to_string(),
487                "2013/3/29".to_string(),
488                "2013/4/1".to_string(),
489                "2013/4/2".to_string(),
490                "2013/4/3".to_string(),
491                "2013/4/8".to_string(),
492                "2013/4/9".to_string(),
493                "2013/4/10".to_string(),
494                "2013/4/11".to_string(),
495                "2013/4/12".to_string(),
496                "2013/4/15".to_string(),
497                "2013/4/16".to_string(),
498                "2013/4/17".to_string(),
499                "2013/4/18".to_string(),
500                "2013/4/19".to_string(),
501                "2013/4/22".to_string(),
502                "2013/4/23".to_string(),
503                "2013/4/24".to_string(),
504                "2013/4/25".to_string(),
505                "2013/4/26".to_string(),
506            ],
507        );
508        candlestick_chart.series_list[0].category = Some(SeriesCategory::Line);
509        candlestick_chart.series_list[0].start_index = 5;
510        candlestick_chart.y_axis_configs[0].axis_min = Some(2100.0);
511        candlestick_chart.y_axis_configs[0].axis_max = Some(2460.0);
512        candlestick_chart.y_axis_configs[0].axis_formatter = Some("{t}".to_string());
513        assert_eq!(
514            include_str!("../../asset/candlestick_chart/sh.svg"),
515            candlestick_chart.svg().unwrap()
516        );
517    }
518}