namespace CairoChart { public class Chart { public double x_min = 0.0; public double y_min = 0.0; public double width = 0.0; public double height = 0.0; public Cairo.Context context = null; public Color bg_color; public Text title = new Text ("Cairo Chart"); public Color border_color = Color(0, 0, 0, 0.3); public Legend legend = new Legend (); public Series[] series = {}; public Chart () { bg_color = Color (1, 1, 1); } public double cur_x_min = 0.0; public double cur_x_max = 1.0; public double cur_y_min = 0.0; public double cur_y_max = 1.0; protected virtual void check_cur_values () { if (cur_x_min > cur_x_max) cur_x_max = cur_x_min; if (cur_y_min > cur_y_max) cur_y_max = cur_y_min; } public virtual void clear () { draw_background (); } public virtual bool draw () { cur_x_min = x_min; cur_y_min = y_min; cur_x_max = x_min + width; cur_y_max = y_min + height; draw_chart_title (); check_cur_values (); legend.draw (this); check_cur_values (); set_vertical_axes_titles (); get_cursors_crossings(); calc_plot_area (); draw_horizontal_axis (); check_cur_values (); draw_vertical_axis (); check_cur_values (); draw_plot_area_border (); check_cur_values (); draw_series (); check_cur_values (); draw_cursors (); check_cur_values (); return true; } public virtual void set_source_rgba (Color color) { context.set_source_rgba (color.red, color.green, color.blue, color.alpha); } protected virtual void draw_background () { if (context != null) { set_source_rgba (bg_color); context.paint(); set_source_rgba (Color (0, 0, 0, 1)); } } // relative zoom limits protected double rz_x_min = 0.0; protected double rz_x_max = 1.0; protected double rz_y_min = 0.0; protected double rz_y_max = 1.0; int zoom_first_show = 0; public virtual void zoom_in (double x0, double y0, double x1, double y1) { for (var si = 0, max_i = series.length; si < max_i; ++si) { var s = series[si]; if (!s.zoom_show) continue; var real_x0 = get_real_x (s, x0); var real_x1 = get_real_x (s, x1); var real_y0 = get_real_y (s, y0); var real_y1 = get_real_y (s, y1); // if selected square does not intersect with the series's square if ( real_x1 <= s.axis_x.zoom_min || real_x0 >= s.axis_x.zoom_max || real_y0 <= s.axis_y.zoom_min || real_y1 >= s.axis_y.zoom_max) { s.zoom_show = false; continue; } if (real_x0 >= s.axis_x.zoom_min) { s.axis_x.zoom_min = real_x0; s.place.zoom_x_min = 0.0; } else { s.place.zoom_x_min = (s.axis_x.zoom_min - real_x0) / (real_x1 - real_x0); } if (real_x1 <= s.axis_x.zoom_max) { s.axis_x.zoom_max = real_x1; s.place.zoom_x_max = 1.0; } else { s.place.zoom_x_max = (s.axis_x.zoom_max - real_x0) / (real_x1 - real_x0); } if (real_y1 >= s.axis_y.zoom_min) { s.axis_y.zoom_min = real_y1; s.place.zoom_y_min = 0.0; } else { s.place.zoom_y_min = (s.axis_y.zoom_min - real_y1) / (real_y0 - real_y1); } if (real_y0 <= s.axis_y.zoom_max) { s.axis_y.zoom_max = real_y0; s.place.zoom_y_max = 1.0; } else { s.place.zoom_y_max = (s.axis_y.zoom_max - real_y1) / (real_y0 - real_y1); } } zoom_first_show = 0; for (var si = 0, max_i = series.length; si < max_i; ++si) if (series[si].zoom_show) { zoom_first_show = si; break; } var new_rz_x_min = rz_x_min + (x0 - plot_x_min) / (plot_x_max - plot_x_min) * (rz_x_max - rz_x_min); var new_rz_x_max = rz_x_min + (x1 - plot_x_min) / (plot_x_max - plot_x_min) * (rz_x_max - rz_x_min); var new_rz_y_min = rz_y_min + (y0 - plot_y_min) / (plot_y_max - plot_y_min) * (rz_y_max - rz_y_min); var new_rz_y_max = rz_y_min + (y1 - plot_y_min) / (plot_y_max - plot_y_min) * (rz_y_max - rz_y_min); rz_x_min = new_rz_x_min; rz_x_max = new_rz_x_max; rz_y_min = new_rz_y_min; rz_y_max = new_rz_y_max; } public virtual void zoom_out () { foreach (var s in series) { s.zoom_show = true; s.axis_x.zoom_min = s.axis_x.min; s.axis_x.zoom_max = s.axis_x.max; s.axis_y.zoom_min = s.axis_y.min; s.axis_y.zoom_max = s.axis_y.max; s.place.zoom_x_min = s.place.x_min; s.place.zoom_x_max = s.place.x_max; s.place.zoom_y_min = s.place.y_min; s.place.zoom_y_max = s.place.y_max; } rz_x_min = 0; rz_x_max = 1; rz_y_min = 0; rz_y_max = 1; zoom_first_show = 0; } public virtual void move (double delta_x, double delta_y) { delta_x /= plot_x_max - plot_x_min; delta_x *= - 1.0; delta_y /= plot_y_max - plot_y_min; delta_y *= - 1.0; var rzxmin = rz_x_min, rzxmax = rz_x_max, rzymin = rz_y_min, rzymax = rz_y_max; zoom_out(); delta_x *= plot_x_max - plot_x_min; delta_y *= plot_y_max - plot_y_min; var xmin = plot_x_min + (plot_x_max - plot_x_min) * rzxmin; var xmax = plot_x_min + (plot_x_max - plot_x_min) * rzxmax; var ymin = plot_y_min + (plot_y_max - plot_y_min) * rzymin; var ymax = plot_y_min + (plot_y_max - plot_y_min) * rzymax; delta_x *= rzxmax - rzxmin; delta_y *= rzymax - rzymin; if (xmin + delta_x < plot_x_min) delta_x = plot_x_min - xmin; if (xmax + delta_x > plot_x_max) delta_x = plot_x_max - xmax; if (ymin + delta_y < plot_y_min) delta_y = plot_y_min - ymin; if (ymax + delta_y > plot_y_max) delta_y = plot_y_max - ymax; zoom_in (xmin + delta_x, ymin + delta_y, xmax + delta_x, ymax + delta_y); } public double title_width { get; protected set; default = 0.0; } public double title_height { get; protected set; default = 0.0; } public double title_indent = 4; protected virtual void draw_chart_title () { var sz = title.get_size(context); title_height = sz.height + (legend.position == Legend.Position.TOP ? title_indent * 2 : title_indent); cur_y_min += title_height; set_source_rgba(title.color); context.move_to (width/2 - sz.width/2, sz.height + title_indent); title.show(context); } public Line.Style selection_style = Line.Style (); public virtual void draw_selection (double x0, double y0, double x1, double y1) { selection_style.set(this); context.rectangle (x0, y0, x1 - x0, y1 - y0); context.stroke(); } public double plot_x_min = 0; public double plot_x_max = 0; public double plot_y_min = 0; public double plot_y_max = 0; public bool joint_x { get; protected set; default = false; } public bool joint_y { get; protected set; default = false; } public Color joint_axis_color = Color (0, 0, 0, 1); protected virtual void set_vertical_axes_titles () { for (var si = 0; si < series.length; ++si) { var s = series[si]; s.axis_y.title.style.orientation = Font.Orientation.VERTICAL; } } protected virtual void calc_plot_area () { plot_x_min = cur_x_min + legend.indent; plot_x_max = cur_x_max - legend.indent; plot_y_min = cur_y_min + legend.indent; plot_y_max = cur_y_max - legend.indent; // Check for joint axes joint_x = joint_y = true; int nzoom_series_show = 0; for (var si = series.length - 1; si >=0; --si) { var s = series[si]; if (!s.zoom_show) continue; ++nzoom_series_show; if ( s.axis_x.position != series[0].axis_x.position || s.axis_x.zoom_min != series[0].axis_x.zoom_min || s.axis_x.zoom_max != series[0].axis_x.zoom_max || s.place.zoom_x_min != series[0].place.zoom_x_min || s.place.zoom_x_max != series[0].place.zoom_x_max || s.axis_x.type != series[0].axis_x.type) joint_x = false; if ( s.axis_y.position != series[0].axis_y.position || s.axis_y.zoom_min != series[0].axis_y.zoom_min || s.axis_y.zoom_max != series[0].axis_y.zoom_max || s.place.zoom_y_min != series[0].place.zoom_y_min || s.place.zoom_y_max != series[0].place.zoom_y_max) joint_y = false; } if (nzoom_series_show == 1) joint_x = joint_y = false; // Join and calc X-axes for (var si = series.length - 1, nskip = 0; si >=0; --si) { var s = series[si]; if (!s.zoom_show) continue; if (nskip != 0) {--nskip; continue;} double max_rec_width = 0; double max_rec_height = 0; s.axis_x.calc_rec_sizes (this, out max_rec_width, out max_rec_height, true); var max_font_indent = s.axis_x.font_indent; var max_axis_font_height = s.axis_x.title.text == "" ? 0 : s.axis_x.title.get_height(context) + s.axis_x.font_indent; // join relative x-axes with non-intersect places for (int sj = si - 1; sj >= 0; --sj) { var s2 = series[sj]; if (!s2.zoom_show) continue; bool has_intersection = false; for (int sk = si; sk > sj; --sk) { var s3 = series[sk]; if (!s3.zoom_show) continue; if (math.are_intersect(s2.place.zoom_x_min, s2.place.zoom_x_max, s3.place.zoom_x_min, s3.place.zoom_x_max) || s2.axis_x.position != s3.axis_x.position || s2.axis_x.type != s3.axis_x.type) { has_intersection = true; break; } } if (!has_intersection) { double tmp_max_rec_width = 0; double tmp_max_rec_height = 0; s2.axis_x.calc_rec_sizes (this, out tmp_max_rec_width, out tmp_max_rec_height, true); max_rec_width = double.max (max_rec_width, tmp_max_rec_width); max_rec_height = double.max (max_rec_height, tmp_max_rec_height); max_font_indent = double.max (max_font_indent, s2.axis_x.font_indent); max_axis_font_height = double.max (max_axis_font_height, s2.axis_x.title.text == "" ? 0 : s2.axis_x.title.get_height(context) + s.axis_x.font_indent); ++nskip; } else { break; } } // for 4.2. Cursor values for joint X axis if (joint_x && si == zoom_first_show && cursor_style.orientation == Cursor.Orientation.VERTICAL && cursors_crossings.length != 0) { switch (s.axis_x.position) { case Axis.Position.LOW: plot_y_max -= max_rec_height + s.axis_x.font_indent; break; case Axis.Position.HIGH: plot_y_min += max_rec_height + s.axis_x.font_indent; break; } } if (!joint_x || si == zoom_first_show) switch (s.axis_x.position) { case Axis.Position.LOW: plot_y_max -= max_rec_height + max_font_indent + max_axis_font_height; break; case Axis.Position.HIGH: plot_y_min += max_rec_height + max_font_indent + max_axis_font_height; break; } } // Join and calc Y-axes for (var si = series.length - 1, nskip = 0; si >=0; --si) { var s = series[si]; if (!s.zoom_show) continue; if (nskip != 0) {--nskip; continue;} double max_rec_width = 0; double max_rec_height = 0; s.axis_y.calc_rec_sizes (this, out max_rec_width, out max_rec_height, false); var max_font_indent = s.axis_y.font_indent; var max_axis_font_width = s.axis_y.title.text == "" ? 0 : s.axis_y.title.get_width(context) + s.axis_y.font_indent; // join relative x-axes with non-intersect places for (int sj = si - 1; sj >= 0; --sj) { var s2 = series[sj]; if (!s2.zoom_show) continue; bool has_intersection = false; for (int sk = si; sk > sj; --sk) { var s3 = series[sk]; if (!s3.zoom_show) continue; if (math.are_intersect(s2.place.zoom_y_min, s2.place.zoom_y_max, s3.place.zoom_y_min, s3.place.zoom_y_max) || s2.axis_y.position != s3.axis_y.position || s2.axis_x.type != s3.axis_x.type) { has_intersection = true; break; } } if (!has_intersection) { double tmp_max_rec_width = 0; double tmp_max_rec_height = 0; s2.axis_y.calc_rec_sizes (this, out tmp_max_rec_width, out tmp_max_rec_height, false); max_rec_width = double.max (max_rec_width, tmp_max_rec_width); max_rec_height = double.max (max_rec_height, tmp_max_rec_height); max_font_indent = double.max (max_font_indent, s2.axis_y.font_indent); max_axis_font_width = double.max (max_axis_font_width, s2.axis_y.title.text == "" ? 0 : s2.axis_y.title.get_width(context) + s.axis_y.font_indent); ++nskip; } else { break; } } // for 4.2. Cursor values for joint Y axis if (joint_y && si == zoom_first_show && cursor_style.orientation == Cursor.Orientation.HORIZONTAL && cursors_crossings.length != 0) { switch (s.axis_y.position) { case Axis.Position.LOW: plot_x_min += max_rec_width + s.axis_y.font_indent; break; case Axis.Position.HIGH: plot_x_max -= max_rec_width + s.axis_y.font_indent; break; } } if (!joint_y || si == zoom_first_show) switch (s.axis_y.position) { case Axis.Position.LOW: plot_x_min += max_rec_width + max_font_indent + max_axis_font_width; break; case Axis.Position.HIGH: plot_x_max -= max_rec_width + max_font_indent + max_axis_font_width; break; } } } protected virtual double compact_rec_x_pos (Series s, Float128 x, Text text) { var sz = text.get_size(context); return get_scr_x(s, x) - sz.width / 2.0 - sz.width * (x - (s.axis_x.zoom_min + s.axis_x.zoom_max) / 2.0) / (s.axis_x.zoom_max - s.axis_x.zoom_min); } protected virtual double compact_rec_y_pos (Series s, Float128 y, Text text) { var sz = text.get_size(context); return get_scr_y(s, y) + sz.height / 2.0 + sz.height * (y - (s.axis_y.zoom_min + s.axis_y.zoom_max) / 2.0) / (s.axis_y.zoom_max - s.axis_y.zoom_min); } protected CairoChart.Math math = new Math(); protected virtual void draw_horizontal_axis () { for (var si = series.length - 1, nskip = 0; si >=0; --si) { var s = series[si]; if (!s.zoom_show) continue; if (joint_x && si != zoom_first_show) continue; // 1. Detect max record width/height by axis.nrecords equally selected points using format. double max_rec_width, max_rec_height; s.axis_x.calc_rec_sizes (this, out max_rec_width, out max_rec_height, true); // 2. Calculate maximal available number of records, take into account the space width. long max_nrecs = (long) ((plot_x_max - plot_x_min) * (s.place.zoom_x_max - s.place.zoom_x_min) / max_rec_width); // 3. Calculate grid step. Float128 step = math.calc_round_step ((s.axis_x.zoom_max - s.axis_x.zoom_min) / max_nrecs, s.axis_x.type == Axis.Type.DATE_TIME); if (step > s.axis_x.zoom_max - s.axis_x.zoom_min) step = s.axis_x.zoom_max - s.axis_x.zoom_min; // 4. Calculate x_min (s.axis_x.zoom_min / step, round, multiply on step, add step if < s.axis_x.zoom_min). Float128 x_min = 0.0; if (step >= 1) { int64 x_min_nsteps = (int64) (s.axis_x.zoom_min / step); x_min = x_min_nsteps * step; } else { int64 round_axis_x_min = (int64)s.axis_x.zoom_min; int64 x_min_nsteps = (int64) ((s.axis_x.zoom_min - round_axis_x_min) / step); x_min = round_axis_x_min + x_min_nsteps * step; } if (x_min < s.axis_x.zoom_min) x_min += step; // 4.2. Cursor values for joint X axis if (joint_x && cursor_style.orientation == Cursor.Orientation.VERTICAL && cursors_crossings.length != 0) { switch (s.axis_x.position) { case Axis.Position.LOW: cur_y_max -= max_rec_height + s.axis_x.font_indent; break; case Axis.Position.HIGH: cur_y_min += max_rec_height + s.axis_x.font_indent; break; } } var sz = s.axis_x.title.get_size(context); // 4.5. Draw Axis title if (s.axis_x.title.text != "") { var scr_x = plot_x_min + (plot_x_max - plot_x_min) * (s.place.zoom_x_min + s.place.zoom_x_max) / 2.0; double scr_y = 0.0; switch (s.axis_x.position) { case Axis.Position.LOW: scr_y = cur_y_max - s.axis_x.font_indent; break; case Axis.Position.HIGH: scr_y = cur_y_min + s.axis_x.font_indent + sz.height; break; } context.move_to(scr_x - sz.width / 2.0, scr_y); set_source_rgba(s.axis_x.color); if (joint_x) set_source_rgba(joint_axis_color); s.axis_x.title.show(context); } // 5. Draw records, update cur_{x,y}_{min,max}. for (Float128 x = x_min, x_max = s.axis_x.zoom_max; math.point_belong (x, x_min, x_max); x += step) { if (joint_x) set_source_rgba(joint_axis_color); else set_source_rgba(s.axis_x.color); string text = "", time_text = ""; switch (s.axis_x.type) { case Axis.Type.NUMBERS: text = s.axis_x.format.printf((LongDouble)x); break; case Axis.Type.DATE_TIME: s.axis_x.format_date_time(x, out text, out time_text); break; } var scr_x = get_scr_x (s, x); var text_t = new Text(text, s.axis_x.font_style, s.axis_x.color); switch (s.axis_x.position) { case Axis.Position.LOW: var print_y = cur_y_max - s.axis_x.font_indent - (s.axis_x.title.text == "" ? 0 : sz.height + s.axis_x.font_indent); var print_x = compact_rec_x_pos (s, x, text_t); context.move_to (print_x, print_y); switch (s.axis_x.type) { case Axis.Type.NUMBERS: text_t.show(context); break; case Axis.Type.DATE_TIME: if (s.axis_x.date_format != "") text_t.show(context); var time_text_t = new Text(time_text, s.axis_x.font_style, s.axis_x.color); print_x = compact_rec_x_pos (s, x, time_text_t); context.move_to (print_x, print_y - (s.axis_x.date_format == "" ? 0 : text_t.get_height(context) + s.axis_x.font_indent)); if (s.axis_x.time_format != "") time_text_t.show(context); break; } // 6. Draw grid lines to the s.place.zoom_y_min. var line_style = s.grid.line_style; if (joint_x) line_style.color = Color(0, 0, 0, 0.5); line_style.set(this); double y = cur_y_max - max_rec_height - s.axis_x.font_indent - (s.axis_x.title.text == "" ? 0 : sz.height + s.axis_x.font_indent); context.move_to (scr_x, y); if (joint_x) context.line_to (scr_x, plot_y_min); else context.line_to (scr_x, double.min (y, plot_y_max - (plot_y_max - plot_y_min) * s.place.zoom_y_max)); break; case Axis.Position.HIGH: var print_y = cur_y_min + max_rec_height + s.axis_x.font_indent + (s.axis_x.title.text == "" ? 0 : sz.height + s.axis_x.font_indent); var print_x = compact_rec_x_pos (s, x, text_t); context.move_to (print_x, print_y); switch (s.axis_x.type) { case Axis.Type.NUMBERS: text_t.show(context); break; case Axis.Type.DATE_TIME: if (s.axis_x.date_format != "") text_t.show(context); var time_text_t = new Text(time_text, s.axis_x.font_style, s.axis_x.color); print_x = compact_rec_x_pos (s, x, time_text_t); context.move_to (print_x, print_y - (s.axis_x.date_format == "" ? 0 : text_t.get_height(context) + s.axis_x.font_indent)); if (s.axis_x.time_format != "") time_text_t.show(context); break; } // 6. Draw grid lines to the s.place.zoom_y_max. var line_style = s.grid.line_style; if (joint_x) line_style.color = Color(0, 0, 0, 0.5); line_style.set(this); double y = cur_y_min + max_rec_height + s.axis_x.font_indent + (s.axis_x.title.text == "" ? 0 : sz.height + s.axis_x.font_indent); context.move_to (scr_x, y); if (joint_x) context.line_to (scr_x, plot_y_max); else context.line_to (scr_x, double.max (y, plot_y_max - (plot_y_max - plot_y_min) * s.place.zoom_y_min)); break; } } context.stroke (); // join relative x-axes with non-intersect places for (int sj = si - 1; sj >= 0; --sj) { var s2 = series[sj]; if (!s2.zoom_show) continue; bool has_intersection = false; for (int sk = si; sk > sj; --sk) { var s3 = series[sk]; if (!s3.zoom_show) continue; if (math.are_intersect(s2.place.zoom_x_min, s2.place.zoom_x_max, s3.place.zoom_x_min, s3.place.zoom_x_max) || s2.axis_x.position != s3.axis_x.position || s2.axis_x.type != s3.axis_x.type) { has_intersection = true; break; } } if (!has_intersection) { ++nskip; } else { break; } } if (nskip != 0) {--nskip; continue;} switch (s.axis_x.position) { case Axis.Position.LOW: cur_y_max -= max_rec_height + s.axis_x.font_indent + (s.axis_x.title.text == "" ? 0 : sz.height + s.axis_x.font_indent); break; case Axis.Position.HIGH: cur_y_min += max_rec_height + s.axis_x.font_indent + (s.axis_x.title.text == "" ? 0 : sz.height + s.axis_x.font_indent); break; } } } protected virtual void draw_vertical_axis () { for (var si = series.length - 1, nskip = 0; si >=0; --si) { var s = series[si]; if (!s.zoom_show) continue; if (joint_y && si != zoom_first_show) continue; // 1. Detect max record width/height by axis.nrecords equally selected points using format. double max_rec_width, max_rec_height; s.axis_y.calc_rec_sizes (this, out max_rec_width, out max_rec_height, false); // 2. Calculate maximal available number of records, take into account the space width. long max_nrecs = (long) ((plot_y_max - plot_y_min) * (s.place.zoom_y_max - s.place.zoom_y_min) / max_rec_height); // 3. Calculate grid step. Float128 step = math.calc_round_step ((s.axis_y.zoom_max - s.axis_y.zoom_min) / max_nrecs); if (step > s.axis_y.zoom_max - s.axis_y.zoom_min) step = s.axis_y.zoom_max - s.axis_y.zoom_min; // 4. Calculate y_min (s.axis_y.zoom_min / step, round, multiply on step, add step if < s.axis_y.zoom_min). Float128 y_min = 0.0; if (step >= 1) { int64 y_min_nsteps = (int64) (s.axis_y.zoom_min / step); y_min = y_min_nsteps * step; } else { int64 round_axis_y_min = (int64)s.axis_y.zoom_min; int64 y_min_nsteps = (int64) ((s.axis_y.zoom_min - round_axis_y_min) / step); y_min = round_axis_y_min + y_min_nsteps * step; } if (y_min < s.axis_y.zoom_min) y_min += step; // 4.2. Cursor values for joint Y axis if (joint_y && cursor_style.orientation == Cursor.Orientation.HORIZONTAL && cursors_crossings.length != 0) { switch (s.axis_y.position) { case Axis.Position.LOW: cur_x_min += max_rec_width + s.axis_y.font_indent; break; case Axis.Position.HIGH: cur_x_max -= max_rec_width + s.axis_y.font_indent; break; } } var sz = s.axis_y.title.get_size(context); // 4.5. Draw Axis title if (s.axis_y.title.text != "") { var scr_y = plot_y_max - (plot_y_max - plot_y_min) * (s.place.zoom_y_min + s.place.zoom_y_max) / 2.0; switch (s.axis_y.position) { case Axis.Position.LOW: var scr_x = cur_x_min + s.axis_y.font_indent + sz.width; context.move_to(scr_x, scr_y + sz.height / 2.0); break; case Axis.Position.HIGH: var scr_x = cur_x_max - s.axis_y.font_indent; context.move_to(scr_x, scr_y + sz.height / 2.0); break; } set_source_rgba(s.axis_y.color); if (joint_y) set_source_rgba(joint_axis_color); s.axis_y.title.show(context); } // 5. Draw records, update cur_{x,y}_{min,max}. for (Float128 y = y_min, y_max = s.axis_y.zoom_max; math.point_belong (y, y_min, y_max); y += step) { if (joint_y) set_source_rgba(joint_axis_color); else set_source_rgba(s.axis_y.color); var text = s.axis_y.format.printf((LongDouble)y); var scr_y = get_scr_y (s, y); var text_t = new Text(text, s.axis_y.font_style, s.axis_y.color); var text_sz = text_t.get_size(context); switch (s.axis_y.position) { case Axis.Position.LOW: context.move_to (cur_x_min + max_rec_width - text_sz.width + s.axis_y.font_indent + (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent), compact_rec_y_pos (s, y, text_t)); text_t.show(context); // 6. Draw grid lines to the s.place.zoom_x_min. var line_style = s.grid.line_style; if (joint_y) line_style.color = Color(0, 0, 0, 0.5); line_style.set(this); double x = cur_x_min + max_rec_width + s.axis_y.font_indent + (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent); context.move_to (x, scr_y); if (joint_y) context.line_to (plot_x_max, scr_y); else context.line_to (double.max (x, plot_x_min + (plot_x_max - plot_x_min) * s.place.zoom_x_max), scr_y); break; case Axis.Position.HIGH: context.move_to (cur_x_max - text_sz.width - s.axis_y.font_indent - (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent), compact_rec_y_pos (s, y, text_t)); text_t.show(context); // 6. Draw grid lines to the s.place.zoom_x_max. var line_style = s.grid.line_style; if (joint_y) line_style.color = Color(0, 0, 0, 0.5); line_style.set(this); double x = cur_x_max - max_rec_width - s.axis_y.font_indent - (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent); context.move_to (x, scr_y); if (joint_y) context.line_to (plot_x_min, scr_y); else context.line_to (double.min (x, plot_x_min + (plot_x_max - plot_x_min) * s.place.zoom_x_min), scr_y); break; } } context.stroke (); // join relative x-axes with non-intersect places for (int sj = si - 1; sj >= 0; --sj) { var s2 = series[sj]; if (!s2.zoom_show) continue; bool has_intersection = false; for (int sk = si; sk > sj; --sk) { var s3 = series[sk]; if (!s3.zoom_show) continue; if (math.are_intersect(s2.place.zoom_y_min, s2.place.zoom_y_max, s3.place.zoom_y_min, s3.place.zoom_y_max) || s2.axis_y.position != s3.axis_y.position) { has_intersection = true; break; } } if (!has_intersection) { ++nskip; } else { break; } } if (nskip != 0) {--nskip; continue;} switch (s.axis_y.position) { case Axis.Position.LOW: cur_x_min += max_rec_width + s.axis_y.font_indent + (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent); break; case Axis.Position.HIGH: cur_x_max -= max_rec_width + s.axis_y.font_indent + (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent); break; } } } protected virtual void draw_plot_area_border () { set_source_rgba (border_color); context.set_dash(null, 0); context.move_to (plot_x_min, plot_y_min); context.line_to (plot_x_min, plot_y_max); context.line_to (plot_x_max, plot_y_max); context.line_to (plot_x_max, plot_y_min); context.line_to (plot_x_min, plot_y_min); context.stroke (); } protected virtual double get_scr_x (Series s, Float128 x) { return plot_x_min + (plot_x_max - plot_x_min) * (s.place.zoom_x_min + (x - s.axis_x.zoom_min) / (s.axis_x.zoom_max - s.axis_x.zoom_min) * (s.place.zoom_x_max - s.place.zoom_x_min)); } protected virtual double get_scr_y (Series s, Float128 y) { return plot_y_max - (plot_y_max - plot_y_min) * (s.place.zoom_y_min + (y - s.axis_y.zoom_min) / (s.axis_y.zoom_max - s.axis_y.zoom_min) * (s.place.zoom_y_max - s.place.zoom_y_min)); } protected virtual Point get_scr_point (Series s, Point p) { return Point (get_scr_x(s, p.x), get_scr_y(s, p.y)); } protected virtual Float128 get_real_x (Series s, double scr_x) { return s.axis_x.zoom_min + ((scr_x - plot_x_min) / (plot_x_max - plot_x_min) - s.place.zoom_x_min) * (s.axis_x.zoom_max - s.axis_x.zoom_min) / (s.place.zoom_x_max - s.place.zoom_x_min); } protected virtual Float128 get_real_y (Series s, double scr_y) { return s.axis_y.zoom_min + ((plot_y_max - scr_y) / (plot_y_max - plot_y_min) - s.place.zoom_y_min) * (s.axis_y.zoom_max - s.axis_y.zoom_min) / (s.place.zoom_y_max - s.place.zoom_y_min); } protected virtual Point get_real_point (Series s, Point p) { return Point (get_real_x(s, p.x), get_real_y(s, p.y)); } protected virtual bool x_in_plot_area (double x) { if (math.x_in_range(x, plot_x_min, plot_x_max)) return true; return false; } protected virtual bool y_in_plot_area (double y) { if (math.y_in_range(y, plot_y_min, plot_y_max)) return true; return false; } protected virtual bool point_in_plot_area (Point p) { if (math.point_in_rect (p, plot_x_min, plot_x_max, plot_y_min, plot_y_max)) return true; return false; } protected virtual void draw_series () { for (var si = 0; si < series.length; ++si) { var s = series[si]; if (!s.zoom_show) continue; if (s.points.length == 0) continue; var points = math.sort_points(s, s.sort); s.line_style.set(this); // draw series line for (int i = 1; i < points.length; ++i) { Point c, d; if (math.cut_line (Point(plot_x_min, plot_y_min), Point(plot_x_max, plot_y_max), Point(get_scr_x(s, points[i - 1].x), get_scr_y(s, points[i - 1].y)), Point(get_scr_x(s, points[i].x), get_scr_y(s, points[i].y)), out c, out d)) { context.move_to (c.x, c.y); context.line_to (d.x, d.y); } } context.stroke(); for (int i = 0; i < points.length; ++i) { var x = get_scr_x(s, points[i].x); var y = get_scr_y(s, points[i].y); if (point_in_plot_area (Point (x, y))) s.marker.draw_at_pos(this, x, y); } } } protected List cursors = new List (); protected Point active_cursor = Point (); protected bool is_cursor_active = false; public virtual void set_active_cursor (double x, double y, bool remove = false) { active_cursor = Point (scr2rel_x(x), scr2rel_y(y)); is_cursor_active = ! remove; } public virtual void add_active_cursor () { cursors.append (active_cursor); is_cursor_active = false; } public Cursor.Style cursor_style = Cursor.Style(); public virtual void remove_active_cursor () { if (cursors.length() == 0) return; var distance = width * width; uint rm_indx = 0; uint i = 0; foreach (var c in cursors) { double d = distance; switch (cursor_style.orientation) { case Cursor.Orientation.VERTICAL: d = (rel2scr_x(c.x) - rel2scr_x(active_cursor.x)).abs(); break; case Cursor.Orientation.HORIZONTAL: d = (rel2scr_y(c.y) - rel2scr_y(active_cursor.y)).abs(); break; } if (d < distance) { distance = d; rm_indx = i; } ++i; } if (distance < cursor_style.select_distance) cursors.delete_link(cursors.nth(rm_indx)); is_cursor_active = false; } protected virtual Float128 scr2rel_x (Float128 x) { return rz_x_min + (x - plot_x_min) / (plot_x_max - plot_x_min) * (rz_x_max - rz_x_min); } protected virtual Float128 scr2rel_y (Float128 y) { return rz_y_max - (plot_y_max - y) / (plot_y_max - plot_y_min) * (rz_y_max - rz_y_min); } protected virtual Point scr2rel_point (Point p) { return Point (scr2rel_x(p.x), scr2rel_y(p.y)); } protected virtual Float128 rel2scr_x(Float128 x) { return plot_x_min + (plot_x_max - plot_x_min) * (x - rz_x_min) / (rz_x_max - rz_x_min); } protected virtual Float128 rel2scr_y(Float128 y) { return plot_y_min + (plot_y_max - plot_y_min) * (y - rz_y_min) / (rz_y_max - rz_y_min); } protected virtual Point rel2scr_point (Point p) { return Point (rel2scr_x(p.x), rel2scr_y(p.y)); } protected struct CursorCross { uint series_index; Point point; Point size; bool show_x; bool show_date; bool show_time; bool show_y; Point scr_point; Point scr_value_point; } protected struct CursorCrossings { uint cursor_index; CursorCross[] crossings; } protected CursorCrossings[] cursors_crossings = {}; protected List get_all_cursors () { var all_cursors = cursors.copy_deep ((src) => { return src; }); if (is_cursor_active) all_cursors.append(active_cursor); return all_cursors; } protected void get_cursors_crossings () { var all_cursors = get_all_cursors(); CursorCrossings[] local_cursor_crossings = {}; for (var ci = 0, max_ci = all_cursors.length(); ci < max_ci; ++ci) { var c = all_cursors.nth_data(ci); switch (cursor_style.orientation) { case Cursor.Orientation.VERTICAL: if (c.x <= rz_x_min || c.x >= rz_x_max) continue; break; case Cursor.Orientation.HORIZONTAL: if (c.y <= rz_y_min || c.y >= rz_y_max) continue; break; } CursorCross[] crossings = {}; for (var si = 0, max_si = series.length; si < max_si; ++si) { var s = series[si]; if (!s.zoom_show) continue; Point[] points = {}; switch (cursor_style.orientation) { case Cursor.Orientation.VERTICAL: points = math.sort_points (s, s.sort); break; case Cursor.Orientation.HORIZONTAL: points = math.sort_points (s, s.sort); break; } for (var i = 0; i + 1 < points.length; ++i) { switch (cursor_style.orientation) { case Cursor.Orientation.VERTICAL: Float128 y = 0.0; if (math.vcross(get_scr_point(s, points[i]), get_scr_point(s, points[i+1]), rel2scr_x(c.x), plot_y_min, plot_y_max, out y)) { var point = Point(get_real_x(s, rel2scr_x(c.x)), get_real_y(s, y)); Point size; bool show_x, show_date, show_time, show_y; cross_what_to_show(s, out show_x, out show_time, out show_date, out show_y); calc_cross_sizes (s, point, out size, show_x, show_time, show_date, show_y); CursorCross cc = {si, point, size, show_x, show_date, show_time, show_y}; crossings += cc; } break; case Cursor.Orientation.HORIZONTAL: Float128 x = 0.0; if (math.hcross(get_scr_point(s, points[i]), get_scr_point(s, points[i+1]), plot_x_min, plot_x_max, rel2scr_y(c.y), out x)) { var point = Point(get_real_x(s, x), get_real_y(s, rel2scr_y(c.y))); Point size; bool show_x, show_date, show_time, show_y; cross_what_to_show(s, out show_x, out show_time, out show_date, out show_y); calc_cross_sizes (s, point, out size, show_x, show_time, show_date, show_y); CursorCross cc = {si, point, size, show_x, show_date, show_time, show_y}; crossings += cc; } break; } } } if (crossings.length != 0) { CursorCrossings ccs = {ci, crossings}; local_cursor_crossings += ccs; } } cursors_crossings = local_cursor_crossings; } protected virtual void calc_cursors_value_positions () { for (var ccsi = 0, max_ccsi = cursors_crossings.length; ccsi < max_ccsi; ++ccsi) { for (var cci = 0, max_cci = cursors_crossings[ccsi].crossings.length; cci < max_cci; ++cci) { // TODO: Ticket #142: find smart algorithm of cursors values placements unowned CursorCross[] cr = cursors_crossings[ccsi].crossings; cr[cci].scr_point = get_scr_point (series[cr[cci].series_index], cr[cci].point); var d_max = double.max (cr[cci].size.x / 1.5, cr[cci].size.y / 1.5); cr[cci].scr_value_point = Point (cr[cci].scr_point.x + d_max, cr[cci].scr_point.y - d_max); } } } protected virtual void cross_what_to_show (Series s, out bool show_x, out bool show_time, out bool show_date, out bool show_y) { show_x = show_time = show_date = show_y = false; switch (cursor_style.orientation) { case Cursor.Orientation.VERTICAL: show_y = true; if (!joint_x) switch (s.axis_x.type) { case Axis.Type.NUMBERS: show_x = true; break; case Axis.Type.DATE_TIME: if (s.axis_x.date_format != "") show_date = true; if (s.axis_x.time_format != "") show_time = true; break; } break; case Cursor.Orientation.HORIZONTAL: if (!joint_y) show_y = true; switch (s.axis_x.type) { case Axis.Type.NUMBERS: show_x = true; break; case Axis.Type.DATE_TIME: if (s.axis_x.date_format != "") show_date = true; if (s.axis_x.time_format != "") show_time = true; break; } break; } } protected virtual void calc_cross_sizes (Series s, Point p, out Point size, bool show_x = false, bool show_time = false, bool show_date = false, bool show_y = false) { if (show_x == show_time == show_date == show_y == false) cross_what_to_show(s, out show_x, out show_time, out show_date, out show_y); size = Point (); string date, time; s.axis_x.format_date_time(p.x, out date, out time); var date_t = new Text (date, s.axis_x.font_style, s.axis_x.color); var time_t = new Text (time, s.axis_x.font_style, s.axis_x.color); var x_t = new Text (s.axis_x.format.printf((LongDouble)p.x), s.axis_x.font_style, s.axis_x.color); var y_t = new Text (s.axis_y.format.printf((LongDouble)p.y), s.axis_y.font_style, s.axis_y.color); double h_x = 0.0, h_y = 0.0; if (show_x) { var sz = x_t.get_size(context); size.x = sz.width; h_x = sz.height; } if (show_date) { var sz = date_t.get_size(context); size.x = sz.width; h_x = sz.height; } if (show_time) { var sz = time_t.get_size(context); size.x = double.max(size.x, sz.width); h_x += sz.height; } if (show_y) { var sz = y_t.get_size(context); size.x += sz.width; h_y = sz.height; } if ((show_x || show_date || show_time) && show_y) size.x += double.max(s.axis_x.font_indent, s.axis_y.font_indent); if (show_date && show_time) h_x += s.axis_x.font_indent; size.y = double.max (h_x, h_y); } protected virtual void draw_cursors () { if (series.length == 0) return; var all_cursors = get_all_cursors(); calc_cursors_value_positions(); for (var cci = 0, max_cci = cursors_crossings.length; cci < max_cci; ++cci) { var low = Point(plot_x_max, plot_y_max); // low and high var high = Point(plot_x_min, plot_y_min); // points of the cursor unowned CursorCross[] ccs = cursors_crossings[cci].crossings; cursor_style.line_style.set(this); for (var ci = 0, max_ci = ccs.length; ci < max_ci; ++ci) { var si = ccs[ci].series_index; var s = series[si]; var p = ccs[ci].point; var scrx = get_scr_x(s, p.x); var scry = get_scr_y(s, p.y); if (scrx < low.x) low.x = scrx; if (scry < low.y) low.y = scry; if (scrx > high.x) high.x = scrx; if (scry > high.y) high.y = scry; if (joint_x) { switch (s.axis_x.position) { case Axis.Position.LOW: high.y = plot_y_max + s.axis_x.font_indent; break; case Axis.Position.HIGH: low.y = plot_y_min - s.axis_x.font_indent; break; case Axis.Position.BOTH: high.y = plot_y_max + s.axis_x.font_indent; low.y = plot_y_min - s.axis_x.font_indent; break; } } if (joint_y) { switch (s.axis_y.position) { case Axis.Position.LOW: low.x = plot_x_min - s.axis_y.font_indent; break; case Axis.Position.HIGH: high.x = plot_x_max + s.axis_y.font_indent; break; case Axis.Position.BOTH: low.x = plot_x_min - s.axis_y.font_indent; high.x = plot_x_max + s.axis_y.font_indent; break; } } context.move_to (ccs[ci].scr_point.x, ccs[ci].scr_point.y); context.line_to (ccs[ci].scr_value_point.x, ccs[ci].scr_value_point.y); } var c = all_cursors.nth_data(cursors_crossings[cci].cursor_index); switch (cursor_style.orientation) { case Cursor.Orientation.VERTICAL: if (low.y > high.y) continue; context.move_to (rel2scr_x(c.x), low.y); context.line_to (rel2scr_x(c.x), high.y); // show joint X value if (joint_x) { var s = series[zoom_first_show]; var x = get_real_x(s, rel2scr_x(c.x)); string text = "", time_text = ""; switch (s.axis_x.type) { case Axis.Type.NUMBERS: text = s.axis_x.format.printf((LongDouble)x); break; case Axis.Type.DATE_TIME: s.axis_x.format_date_time(x, out text, out time_text); break; default: break; } var text_t = new Text(text, s.axis_x.font_style, s.axis_x.color); var sz = text_t.get_size(context); var time_text_t = new Text(time_text, s.axis_x.font_style, s.axis_x.color); var print_y = 0.0; switch (s.axis_x.position) { case Axis.Position.LOW: print_y = y_min + height - s.axis_x.font_indent - (legend.position == Legend.Position.BOTTOM ? legend.height : 0); break; case Axis.Position.HIGH: print_y = y_min + title_height + s.axis_x.font_indent + (legend.position == Legend.Position.TOP ? legend.height : 0); switch (s.axis_x.type) { case Axis.Type.NUMBERS: print_y += sz.height; break; case Axis.Type.DATE_TIME: print_y += (s.axis_x.date_format == "" ? 0 : sz.height) + (s.axis_x.time_format == "" ? 0 : time_text_t.get_height(context)) + (s.axis_x.date_format == "" || s.axis_x.time_format == "" ? 0 : s.axis_x.font_indent); break; } break; } var print_x = compact_rec_x_pos (s, x, text_t); context.move_to (print_x, print_y); switch (s.axis_x.type) { case Axis.Type.NUMBERS: text_t.show(context); break; case Axis.Type.DATE_TIME: if (s.axis_x.date_format != "") text_t.show(context); print_x = compact_rec_x_pos (s, x, time_text_t); context.move_to (print_x, print_y - (s.axis_x.date_format == "" ? 0 : sz.height + s.axis_x.font_indent)); if (s.axis_x.time_format != "") time_text_t.show(context); break; } } break; case Cursor.Orientation.HORIZONTAL: if (low.x > high.x) continue; context.move_to (low.x, rel2scr_y(c.y)); context.line_to (high.x, rel2scr_y(c.y)); // show joint Y value if (joint_y) { var s = series[zoom_first_show]; var y = get_real_y(s, rel2scr_y(c.y)); var text_t = new Text(s.axis_y.format.printf((LongDouble)y, s.axis_y.font_style)); var print_y = compact_rec_y_pos (s, y, text_t); var print_x = 0.0; switch (s.axis_y.position) { case Axis.Position.LOW: print_x = x_min + s.axis_y.font_indent + (legend.position == Legend.Position.LEFT ? legend.width : 0); break; case Axis.Position.HIGH: print_x = x_min + width - text_t.get_width(context) - s.axis_y.font_indent - (legend.position == Legend.Position.RIGHT ? legend.width : 0); break; } context.move_to (print_x, print_y); text_t.show(context); } break; } context.stroke (); // show value (X, Y or [X;Y]) for (var ci = 0, max_ci = ccs.length; ci < max_ci; ++ci) { var si = ccs[ci].series_index; var s = series[si]; var point = ccs[ci].point; var size = ccs[ci].size; var svp = ccs[ci].scr_value_point; var show_x = ccs[ci].show_x; var show_date = ccs[ci].show_date; var show_time = ccs[ci].show_time; var show_y = ccs[ci].show_y; set_source_rgba(bg_color); context.rectangle (svp.x - size.x / 2, svp.y - size.y / 2, size.x, size.y); context.fill(); if (show_x) { set_source_rgba(s.axis_x.color); var text_t = new Text(s.axis_x.format.printf((LongDouble)point.x), s.axis_x.font_style); context.move_to (svp.x - size.x / 2, svp.y + text_t.get_height(context) / 2); if (joint_x) set_source_rgba (joint_axis_color); text_t.show(context); } if (show_time) { set_source_rgba(s.axis_x.color); string date = "", time = ""; s.axis_x.format_date_time(point.x, out date, out time); var text_t = new Text(time, s.axis_x.font_style); var sz = text_t.get_size(context); var y = svp.y + sz.height / 2; if (show_date) y -= sz.height / 2 + s.axis_x.font_indent / 2; context.move_to (svp.x - size.x / 2, y); if (joint_x) set_source_rgba (joint_axis_color); text_t.show(context); } if (show_date) { set_source_rgba(s.axis_x.color); string date = "", time = ""; s.axis_x.format_date_time(point.x, out date, out time); var text_t = new Text(date, s.axis_x.font_style); var sz = text_t.get_size(context); var y = svp.y + sz.height / 2; if (show_time) y += sz.height / 2 + s.axis_x.font_indent / 2; context.move_to (svp.x - size.x / 2, y); if (joint_x) set_source_rgba (joint_axis_color); text_t.show(context); } if (show_y) { set_source_rgba(s.axis_y.color); var text_t = new Text(s.axis_y.format.printf((LongDouble)point.y), s.axis_y.font_style); var sz = text_t.get_size(context); context.move_to (svp.x + size.x / 2 - sz.width, svp.y + sz.height / 2); if (joint_y) set_source_rgba (joint_axis_color); text_t.show(context); } } } } public bool get_cursors_delta (out Float128 delta) { delta = 0.0; if (series.length == 0) return false; if (cursors.length() + (is_cursor_active ? 1 : 0) != 2) return false; if (joint_x && cursor_style.orientation == Cursor.Orientation.VERTICAL) { Float128 val1 = get_real_x (series[zoom_first_show], rel2scr_x(cursors.nth_data(0).x)); Float128 val2 = 0; if (is_cursor_active) val2 = get_real_x (series[zoom_first_show], rel2scr_x(active_cursor.x)); else val2 = get_real_x (series[zoom_first_show], rel2scr_x(cursors.nth_data(1).x)); if (val2 > val1) delta = val2 - val1; else delta = val1 - val2; return true; } if (joint_y && cursor_style.orientation == Cursor.Orientation.HORIZONTAL) { Float128 val1 = get_real_y (series[zoom_first_show], rel2scr_y(cursors.nth_data(0).y)); Float128 val2 = 0; if (is_cursor_active) val2 = get_real_y (series[zoom_first_show], rel2scr_y(active_cursor.y)); else val2 = get_real_y (series[zoom_first_show], rel2scr_y(cursors.nth_data(1).y)); if (val2 > val1) delta = val2 - val1; else delta = val1 - val2; return true; } return false; } public string get_cursors_delta_str () { Float128 delta = 0.0; if (!get_cursors_delta(out delta)) return ""; var str = ""; var s = series[zoom_first_show]; if (joint_x) switch (s.axis_x.type) { case Axis.Type.NUMBERS: str = s.axis_x.format.printf((LongDouble)delta); break; case Axis.Type.DATE_TIME: var date = "", time = ""; int64 days = (int64)(delta / 24 / 3600); s.axis_x.format_date_time(delta, out date, out time); str = days.to_string() + " + " + time; break; } if (joint_y) { str = s.axis_y.format.printf((LongDouble)delta); } return str; } public Chart copy () { var chart = new Chart (); chart.active_cursor = this.active_cursor; chart.bg_color = this.bg_color; chart.border_color = this.border_color; chart.joint_x = this.joint_x; chart.joint_y = this.joint_y; chart.context = this.context; chart.cur_x_max = this.cur_x_max; chart.cur_x_min = this.cur_x_min; chart.cur_y_max = this.cur_y_max; chart.cur_y_min = this.cur_y_min; chart.cursor_style = this.cursor_style; chart.cursors = this.cursors.copy(); chart.cursors_crossings = this.cursors_crossings; chart.height = this.height; chart.is_cursor_active = this.is_cursor_active; chart.legend = this.legend.copy(); chart.plot_x_max = this.plot_x_max; chart.plot_x_min = this.plot_x_min; chart.plot_y_max = this.plot_y_max; chart.plot_y_min = this.plot_y_min; chart.rz_x_min = this.rz_x_min; chart.rz_x_max = this.rz_x_max; chart.rz_y_min = this.rz_y_min; chart.rz_y_max = this.rz_y_max; chart.selection_style = this.selection_style; chart.series = this.series; chart.title = this.title.copy(); chart.title_height = this.title_height; chart.title_indent = this.title_indent; chart.title_width = this.title_width; chart.width = this.width; chart.x_min = this.x_min; chart.y_min = this.y_min; chart.zoom_first_show = this.zoom_first_show; return chart; } } }