diff --git a/CMakeLists.txt b/CMakeLists.txt index e0693e2..8f4288e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -6,8 +6,8 @@ SET (PROJECT_LOWERCASE_NAME "cairo-chart") SET (PROJECT_DESCRIPTION "GtkChart for Gtk.DrawingArea (Cairo).") SET (MAJOR 0) -SET (MINOR 1) -SET (PATCH 1) +SET (MINOR 2) +SET (PATCH 0) LIST (APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/backbone) diff --git a/cmake/backbone b/cmake/backbone index 5aec8e8..9ba1995 160000 --- a/cmake/backbone +++ b/cmake/backbone @@ -1 +1 @@ -Subproject commit 5aec8e830a52e88f61c8844ac15dca72b1ef7f06 +Subproject commit 9ba1995060ebb1c65ac9855e3671b3992c859059 diff --git a/src/Area.vala b/src/Area.vala new file mode 100644 index 0000000..e1e641d --- /dev/null +++ b/src/Area.vala @@ -0,0 +1,128 @@ +namespace CairoChart { + + /** + * Area rectangle. + */ + public class Area { + + /** + * Left bound. + */ + public double x0 = 0; + + /** + * Top bound. + */ + public double x1 = 1; + + /** + * Right bound. + */ + public double y0 = 0; + + /** + * Bottom bound. + */ + public double y1 = 1; + + /** + * ``Area`` width. + */ + public virtual double width { + get { + return x1 - x0; + } + set { + x1 = x0 + value; + } + } + + /** + * ``Area`` height. + */ + public virtual double height { + get { + return y1 - y0; + } + set { + y1 = y0 + value; + } + } + + /** + * Constructs a new ``Area``. + */ + public Area () { } + + /** + * Constructs a new ``Area`` by other ``Area``. + * @param area ``Area`` instance. + */ + public Area.with_area (Area area) { + this.x0 = area.x0; + this.y0 = area.y0; + this.x1 = area.x1; + this.y1 = area.y1; + } + + /** + * Constructs a new ``Area`` with absolute coordinates. + * @param x0 left bound. + * @param y0 top bound. + * @param x1 right bound. + * @param y1 bottom bound. + */ + public Area.with_abs (double x0, double y0, + double x1, double y1) { + this.x0 = x0; + this.y0 = y0; + this.x1 = x1; + this.y1 = y1; + } + + /** + * Constructs a new ``Area`` by 2 points. + * @param p0 top left position. + * @param p1 bottom right position. + */ + public Area.with_points (Point p0, Point p1) { + this.x0 = p0.x; + this.y0 = p0.y; + this.x1 = p1.x; + this.y1 = p1.y; + } + + /** + * Constructs a new ``Area`` with relative coordinates. + * @param x0 left bound. + * @param y0 top bound. + * @param width ``Area`` width. + * @param height ``Area`` height. + */ + public Area.with_rel (double x0, double y0, + double width, double height) { + this.x0 = x0; + this.y0 = y0; + this.width = width; + this.height = height; + } + + /** + * Constructs a new ``Area`` by ``Cairo.Rectangle``. + * @param rectangle ``Cairo.Rectangle`` instance. + */ + public Area.with_rectangle (Cairo.Rectangle rectangle) { + this.x0 = rectangle.x; + this.y0 = rectangle.y; + this.width = rectangle.width; + this.height = rectangle.height; + } + + /** + * Gets a copy of the ``Chart``. + */ + public virtual Area copy () { + return new Area.with_area(this); + } + } +} diff --git a/src/Axis.vala b/src/Axis.vala index 6c14f7f..67644b3 100644 --- a/src/Axis.vala +++ b/src/Axis.vala @@ -1,46 +1,102 @@ -namespace Gtk.CairoChart { - // If one of axis:title or axis:min/max are different - // then draw separate axis for each/all series - // or specify series name near the axis +namespace CairoChart { + + /** + * ``Chart`` axis. + */ public class Axis { - Float128 _min = 0; - Float128 _max = 0; - public Float128 min { - get { return _min; } - set { _min = zoom_min = value; } - default = 0; - } - public Float128 max { - get { return _max; } - set { _max = zoom_max = value; } - default = 1; - } - public Float128 zoom_min = 0; - public Float128 zoom_max = 1; - public Text title = new Text (""); - public enum Type { - NUMBERS = 0, - DATE_TIME - } - public enum ScaleType { - LINEAR = 0, // default - // LOGARITHMIC, // TODO - // etc - } - public Type type; - public ScaleType scale_type; + + protected unowned Chart chart; + protected unowned Series ser; + protected string _format = "%.2Lf"; + protected string _date_format = "%Y.%m.%d"; + protected string _time_format = "%H:%M:%S"; + protected int _dsec_signs = 2; // 2 signs = centiseconds + protected bool is_x; + + /** + * ``Axis`` title. + */ + public Text title; + + /** + * ``Axis`` range/limits. + */ + public Range range = new Range(); + + /** + * ``Axis`` relative range/limits. + */ + public Range place = new Range(); + + /** + * ``Axis`` position. + */ public enum Position { + /** + * Bottom/Left ``Axis``. + */ LOW = 0, + + /** + * Top/Right ``Axis``. + */ HIGH = 1, + + /** + * 2 ``Axes``. + */ BOTH = 2 } + + /** + * Position. + */ public Position position = Position.LOW; - string _format = "%.2Lf"; - string _date_format = "%Y.%m.%d"; - string _time_format = "%H:%M:%S"; - int _dsec_signs = 2; // 2 signs = centiseconds - public string format { + /** + * Data type. + */ + public enum DType { + /** + * Float128 numbers. + */ + NUMBERS = 0, + + /** + * Date/Time. + */ + DATE_TIME + } + + /** + * Data type. + */ + public DType dtype; + + /** + * ``Axis`` scale type. + */ + public enum Scale { + /** + * Linear scale. + */ + LINEAR = 0, + + /** + * Logarithmic scale. + */ + // LOGARITHMIC, + } + + /** + * Scale type. + */ + public Scale scale; + + /** + * Float128 numbers print string format. + */ + public virtual string format { get { return _format; } set { // TODO: check format @@ -48,7 +104,11 @@ namespace Gtk.CairoChart { } default = "%.2Lf"; } - public string date_format { + + /** + * Date print string format. + */ + public virtual string date_format { get { return _date_format; } set { // TODO: check format @@ -56,7 +116,11 @@ namespace Gtk.CairoChart { } default = "%Y.%m.%d"; } - public string time_format { + + /** + * Time print string format. + */ + public virtual string time_format { get { return _time_format; } set { // TODO: check format @@ -64,7 +128,13 @@ namespace Gtk.CairoChart { } default = "%H:%M:%S"; } - public int dsec_signs { + + /** + * Number of second's signs after point. + * + * 2 signs means centiseconds, 3 signs means milliseconds, etc... + */ + public virtual int dsec_signs { get { return _dsec_signs; } set { // TODO: check format @@ -72,30 +142,441 @@ namespace Gtk.CairoChart { } default = 2; } - public FontStyle font_style = FontStyle (); - public Color color = Color (); - public LineStyle line_style = LineStyle (); - public double font_indent = 5; - public Axis copy () { - var axis = new Axis (); + /** + * ``Axis`` Font style. + */ + public Font font = new Font (); + + /** + * ``Axis`` color. + */ + public Color color = Color (); + + /** + * ``Axis`` line style. + */ + public LineStyle grid_style = LineStyle(Color(0, 0, 0, 0.25)); // Color(), 1, {2, 3}); + + /** + * Number of equally placed points to evaluate records sizes. + */ + public int nrecords = 8; + + /** + * Constructs a new ``Axis``. + * @param chart ``Chart`` instance. + * @param ser ``Series`` instance. + * @param is_x is X-axis or not (Y-axis otherwise). + */ + public Axis (Chart chart, Series ser, bool is_x) { + this.chart = chart; + this.ser = ser; + this.is_x = is_x; + title = new Text (chart, ""); + } + + /** + * Gets a copy of the ``Axis``. + */ + public virtual Axis copy () { + var axis = new Axis (chart, ser, is_x); axis._date_format = this._date_format; axis._dsec_signs = this._dsec_signs; axis._format = this._format; axis._time_format = this._time_format; axis.color = this.color; - axis.font_indent = this.font_indent; - axis.font_style = this.font_style; - axis.line_style = this.line_style; - axis.max = this.max; - axis.min = this.min; + axis.font = this.font.copy(); + axis.grid_style = this.grid_style; + axis.range = this.range.copy(); + axis.place = this.place.copy(); axis.position = this.position; - axis.scale_type = this.scale_type; + axis.scale = this.scale; axis.title = this.title.copy(); - axis.type = this.type; + axis.dtype = this.dtype; + axis.nrecords = this.nrecords; return axis; } - public Axis () {} + /** + * Gets screen position by real ``Axis`` value. + * @param axis_value real ``Axis`` value. + */ + public virtual double scr_pos (Float128 axis_value) { + return is_x ? chart.plarea.x0 + chart.plarea.width * (place.zmin + (axis_value - range.zmin) / range.zrange * place.zrange) + : chart.plarea.y0 + chart.plarea.height * (1 - (place.zmin + (axis_value - range.zmin) / range.zrange * place.zrange)); + } + + /** + * Gets real ``Axis`` value by screen position. + * @param scr_pos screen position. + */ + public virtual Float128 axis_val (double scr_pos) { + return is_x ? range.zmin + ((scr_pos - chart.plarea.x0) / chart.plarea.width - place.zmin) * range.zrange / place.zrange + : range.zmin + ((chart.plarea.y1 - scr_pos) / chart.plarea.height - place.zmin) * range.zrange / place.zrange; + } + + /** + * Prints date/time to strings with a current formats. + * @param date returns formatted date string. + * @param time returns formatted time string. + */ + public virtual void print_dt (Float128 x, out string date, out string time) { + date = time = ""; + var dt = new DateTime.from_unix_utc((int64)x); + date = dt.format(date_format); + var dsec_str = + ("%."+(dsec_signs.to_string())+"Lf").printf((LongDouble)(x - (int64)x)).offset(1); + if (time_format != "") + time = dt.format(time_format) + dsec_str; + } + + /** + * Gets compact placement position on the screen. + * @param axis_value real ``Axis`` value. + * @param text to place on the screen. + */ + public virtual double compact_rec_pos (Float128 axis_value, Text text) { + return is_x ? scr_pos(axis_value) - text.width / 2 - text.width * (axis_value - (range.zmin + range.zmax) / 2) / range.zrange + : scr_pos(axis_value) + text.height / 2 + text.height * (axis_value - (range.zmin + range.zmax) / 2) / range.zrange; + } + + /** + * Zooms out ``Axis``. + */ + public virtual void zoom_out () { + range.zoom_out(); + place.zoom_out(); + } + + /** + * Draws axis. + * @param nskip number of series to skip printing. + */ + public virtual void draw (ref int nskip) { + if (!ser.zoom_show) return; + + var si = Math.find_arr(chart.series, ser); + if (si == -1) return; + + if ((is_x && chart.joint_x || !is_x && chart.joint_y) && si != chart.zoom_1st_idx) return; + + // 1. Detect max record width/height by axis.nrecords equally selected points using format. + double max_rec_width, max_rec_height; + calc_rec_sizes (this, out max_rec_width, out max_rec_height, is_x); + + // 2. Calculate maximal available number of records, take into account the space width. + long max_nrecs = (long) (is_x ? chart.plarea.width * place.zrange / max_rec_width + : chart.plarea.height * place.zrange / max_rec_height); + + // 3. Calculate grid step. + Float128 step = Math.calc_round_step (range.zrange / max_nrecs, dtype == DType.DATE_TIME); + if (step > range.zrange) + step = range.zrange; + + // 4. Calculate min (range.zmin / step, round, multiply on step, add step if < range.zmin). + Float128 min = 0; + if (step >= 1) { + int64 min_nsteps = (int64) (range.zmin / step); + min = min_nsteps * step; + } else { + int64 round_axis_min = (int64)range.zmin; + int64 min_nsteps = (int64) ((range.zmin - round_axis_min) / step); + min = round_axis_min + min_nsteps * step; + } + if (min < range.zmin) min += step; + + // 4.2. Cursor values for joint axes + if (chart.cursors.has_crossings) + if ( is_x && chart.joint_x && chart.cursors.style.orientation == Cursors.Orientation.VERTICAL + || !is_x && chart.joint_y && chart.cursors.style.orientation == Cursors.Orientation.HORIZONTAL) { + var tmp = 0.0; + if (is_x) tmp = max_rec_height + font.vspacing; + else tmp = max_rec_width + font.hspacing; + switch (position) { + case Position.LOW: + if (is_x) chart.evarea.y1 -= tmp; + else chart.evarea.x0 += tmp; + break; + case Position.HIGH: + if (is_x) chart.evarea.y0 += tmp; + else chart.evarea.x1 -= tmp; + break; + } + } + + var max_rec_spacing = 2 * (is_x ? font.vspacing : font.hspacing); + var max_title_width = title.text == "" ? 0 : title.width + font.hspacing; + var max_title_height = title.text == "" ? 0 : title.height + font.vspacing; + + var dx = max_rec_width + max_rec_spacing + max_title_width; + var dy = max_rec_height + max_rec_spacing + max_title_height; + chart.evarea.x0 -= dx; chart.evarea.x1 += dx; chart.evarea.y0 -= dy; chart.evarea.y1 += dy; + + if (nskip != 0) + --nskip; + else { + var max_rec_height_all = max_rec_height; + var max_rec_width_all = max_rec_width; + if (is_x) + join_rel_axes (si, true, ref max_rec_width_all, ref max_rec_height_all, ref max_rec_spacing, ref max_title_height, ref nskip); + else + join_rel_axes (si, true, ref max_rec_width_all, ref max_rec_height_all, ref max_rec_spacing, ref max_title_width, ref nskip); + + if (is_x && (!chart.joint_x || si == chart.zoom_1st_idx)) { + var tmp = max_rec_height_all + max_rec_spacing + max_title_height; + switch (position) { + case Position.LOW: chart.evarea.y1 -= tmp; break; + case Position.HIGH: chart.evarea.y0 += tmp; break; + } + } + if (!is_x && (!chart.joint_y || si == chart.zoom_1st_idx)) { + var tmp = max_rec_width_all + max_rec_spacing + max_title_width; + switch (position) { + case Position.LOW: chart.evarea.x0 += tmp; break; + case Position.HIGH: chart.evarea.x1 -= tmp; break; + } + } + } + + // 4.5. Draw Axis title + if (title.text != "") { + if (is_x) { + var scr_x = chart.plarea.x0 + chart.plarea.width * (place.zmin + place.zmax) / 2; + var scr_y = 0.0; + switch (position) { + case Position.LOW: scr_y = chart.evarea.y1 - font.vspacing; break; + case Position.HIGH: scr_y = chart.evarea.y0 + font.vspacing + title.height; break; + } + chart.ctx.move_to(scr_x - title.width / 2, scr_y); + chart.color = color; + } else { + var scr_y = chart.plarea.y0 + chart.plarea.height * (1 - (place.zmin + place.zmax) / 2); + switch (position) { + case Position.LOW: + var scr_x = chart.evarea.x0 + font.hspacing + title.width; + chart.ctx.move_to(scr_x, scr_y + title.height / 2); + break; + case Position.HIGH: + var scr_x = chart.evarea.x1 - font.hspacing; + chart.ctx.move_to(scr_x, scr_y + title.height / 2); + break; + } + } + if (is_x && chart.joint_x || !is_x && chart.joint_y) + chart.color = chart.joint_color; + else + chart.color = color; + title.show(); + } + + if (is_x) draw_recs (min, step, max_rec_height); + else draw_recs (min, step, max_rec_width); + + chart.evarea.x0 += dx; chart.evarea.x1 -= dx; chart.evarea.y0 += dy; chart.evarea.y1 -= dy; + + chart.ctx.stroke (); + } + + /** + * Joins equal axes. + * @param nskip returns number of series to skip printing. + */ + public virtual void join_axes (ref int nskip) { + if (!ser.zoom_show) return; + if (nskip != 0) {--nskip; return;} + var max_rec_width = 0.0, max_rec_height = 0.0; + calc_rec_sizes (this, out max_rec_width, out max_rec_height, is_x); + var max_rec_spacing = 2 * (is_x ? font.vspacing : font.hspacing); + var max_title_width = title.text == "" ? 0 : title.width + font.hspacing; + var max_title_height = title.text == "" ? 0 : title.height + font.vspacing; + + var si = Math.find_arr(chart.series, ser); + if (si == -1) return; + + if (is_x) + join_rel_axes (si, true, ref max_rec_width, ref max_rec_height, ref max_rec_spacing, ref max_title_height, ref nskip); + else + join_rel_axes (si, true, ref max_rec_width, ref max_rec_height, ref max_rec_spacing, ref max_title_width, ref nskip); + + // for 4.2. Cursor values for joint X axis + if (si == chart.zoom_1st_idx && chart.cursors.has_crossings) { + switch (chart.cursors.style.orientation) { + case Cursors.Orientation.VERTICAL: + if (is_x && chart.joint_x) { + var tmp = max_rec_height + font.vspacing; + switch (position) { + case Position.LOW: chart.plarea.y1 -= tmp; break; + case Position.HIGH: chart.plarea.y0 += tmp; break; + } + } + break; + case Cursors.Orientation.HORIZONTAL: + if (!is_x && chart.joint_y) { + var tmp = max_rec_width + font.hspacing; + switch (position) { + case Position.LOW: chart.plarea.x0 += tmp; break; + case Position.HIGH: chart.plarea.x1 -= tmp; break; + } + } + break; + } + } + if (is_x && (!chart.joint_x || si == chart.zoom_1st_idx)) { + var tmp = max_rec_height + max_rec_spacing + max_title_height; + switch (position) { + case Position.LOW: chart.plarea.y1 -= tmp; break; + case Position.HIGH: chart.plarea.y0 += tmp; break; + } + } + if (!is_x && (!chart.joint_y || si == chart.zoom_1st_idx)) { + var tmp = max_rec_width + max_rec_spacing + max_title_width; + switch (position) { + case Position.LOW: chart.plarea.x0 += tmp; break; + case Position.HIGH: chart.plarea.x1 -= tmp; break; + } + } + } + + protected virtual void calc_rec_sizes (Axis axis, out double max_rec_width, out double max_rec_height, bool horizontal = true) { + max_rec_width = max_rec_height = 0; + for (var i = 0; i < axis.nrecords; ++i) { + Float128 x = (int64)(axis.range.zmin + axis.range.zrange / axis.nrecords * i) + 1/3.0; + switch (axis.dtype) { + case DType.NUMBERS: + var text = new Text (chart, axis.format.printf((LongDouble)x) + (horizontal ? "_" : ""), axis.font); + max_rec_width = double.max (max_rec_width, text.width + 2 * text.font.hspacing); + max_rec_height = double.max (max_rec_height, text.height + 2 * text.font.vspacing); + break; + case DType.DATE_TIME: + string date, time; + axis.print_dt(x, out date, out time); + + var h = 0.0; + if (axis.date_format != "") { + var text = new Text (chart, date + (horizontal ? "_" : ""), axis.font); + max_rec_width = double.max (max_rec_width, text.width + 2 * text.font.hspacing); + h = text.height; + } + if (axis.time_format != "") { + var text = new Text (chart, time + (horizontal ? "_" : ""), axis.font); + max_rec_width = double.max (max_rec_width, text.width + 2 * text.font.hspacing); + h += text.height; + } + max_rec_height = double.max (max_rec_height, h); + break; + } + } + } + + protected virtual void join_rel_axes (int si, + bool calc_max_values, + ref double max_rec_width, + ref double max_rec_height, + ref double max_rec_spacing, + ref double max_title_size, + ref int nskip) { + for (int sj = si - 1; sj >= 0; --sj) { + var s2 = chart.series[sj]; + if (!s2.zoom_show) continue; + bool has_intersection = false; + Axis a2 = s2.axis_x; if (!is_x) a2 = s2.axis_y; + for (int sk = si; sk > sj; --sk) { + var s3 = chart.series[sk]; + if (!s3.zoom_show) continue; + Axis a3 = s3.axis_x; if (!is_x) a3 = s3.axis_y; + if (Math.coord_cross(a2.place.zmin, a2.place.zmax, a3.place.zmin, a3.place.zmax) + || a2.position != a3.position || a2.dtype != a3.dtype) { + has_intersection = true; + break; + } + } + if (!has_intersection) { + if (calc_max_values) { + var tmp_max_rec_width = 0.0, tmp_max_rec_height = 0.0; + calc_rec_sizes (a2, out tmp_max_rec_width, out tmp_max_rec_height, is_x); + 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_rec_spacing = double.max (max_rec_spacing, 2 * (is_x ? a2.font.vspacing : a2.font.hspacing)); + max_title_size = double.max ( + max_title_size, + a2.title.text == "" ? 0 + : is_x ? a2.title.height + font.vspacing + : a2.title.width + font.hspacing + ); + } + ++nskip; + } else { + break; + } + } + } + + protected virtual void draw_recs (Float128 min, Float128 step, double max_rec_size) { + for (Float128 v = min; Math.point_belong (v, min, range.zmax); v += step) { + if (is_x && chart.joint_x || !is_x && chart.joint_y) { + chart.color = chart.joint_color; + grid_style.color.red = chart.joint_color.red; + grid_style.color.green = chart.joint_color.green; + grid_style.color.blue = chart.joint_color.blue; + } else + chart.color = color; + string text = "", time_text = ""; var time_text_t = new Text(chart); var crpt = 0.0; + switch (dtype) { + case DType.NUMBERS: text = format.printf((LongDouble)v); break; + case DType.DATE_TIME: + print_dt(v, out text, out time_text); + time_text_t = new Text(chart, time_text, font, color); + crpt = compact_rec_pos (v, time_text_t); + break; + } + var scr_v = scr_pos (v); + var text_t = new Text(chart, text, font, color); + var tthv = title.text == "" ? font.vspacing : title.height + 2 * font.vspacing; + var ttwh = title.text == "" ? font.hspacing : title.width + 2 * font.hspacing; + var dtf = (date_format == "" ? 0 : text_t.height + font.vspacing); + var py0 = chart.plarea.y0, ey0 = chart.evarea.y0 + max_rec_size + tthv, ey1 = chart.evarea.y1 - tthv; + var px0 = chart.plarea.x0, ex0 = chart.evarea.x0 + max_rec_size + ttwh, ex1 = chart.evarea.x1 - ttwh; + var ph = chart.plarea.height, pw = chart.plarea.width; + if (is_x) { + var crp = compact_rec_pos (v, text_t); + switch (position) { + case Position.LOW: + chart.ctx.move_to (crp, ey1); text_t.show(); + chart.ctx.move_to (crpt, ey1 - dtf); time_text_t.show(); + ser.axis_x.grid_style.apply(chart); + double y = ey1 - max_rec_size; chart.ctx.move_to (scr_v, y); + if (chart.joint_x) chart.ctx.line_to (scr_v, py0); + else chart.ctx.line_to (scr_v, double.min (y, py0 + ph * (1 - ser.axis_y.place.zmax))); + break; + case Position.HIGH: + chart.ctx.move_to (crp, ey0); text_t.show(); + chart.ctx.move_to (crpt, ey0 - dtf); time_text_t.show(); + ser.axis_x.grid_style.apply(chart); chart.ctx.move_to (scr_v, ey0); + if (chart.joint_x) chart.ctx.line_to (scr_v, chart.plarea.y1); + else chart.ctx.line_to (scr_v, double.max (ey0, py0 + ph * (1 - ser.axis_y.place.zmin))); + break; + } + } else { + var crp = compact_rec_pos (v, text_t); + switch (position) { + case Position.LOW: + chart.ctx.move_to (ex0 - text_t.width, crp); + text_t.show(); ser.axis_y.grid_style.apply(chart); + chart.ctx.move_to (ex0, scr_v); + if (chart.joint_y) chart.ctx.line_to (chart.plarea.x1, scr_v); + else chart.ctx.line_to (double.max (ex0, px0 + pw * ser.axis_x.place.zmax), scr_v); + break; + case Position.HIGH: + chart.ctx.move_to (ex1 - text_t.width, crp); + text_t.show(); ser.axis_y.grid_style.apply(chart); + double x = ex1 - max_rec_size; chart.ctx.move_to (x, scr_v); + if (chart.joint_y) chart.ctx.line_to (px0, scr_v); + else chart.ctx.line_to (double.min (x, px0 + pw * ser.axis_x.place.zmin), scr_v); + break; + } + } + } + } } } diff --git a/src/Chart.vala b/src/Chart.vala index 4798263..266b3e6 100644 --- a/src/Chart.vala +++ b/src/Chart.vala @@ -1,1736 +1,427 @@ -namespace Gtk.CairoChart { +namespace CairoChart { + + /** + * Cairo/GTK+ ``Chart``. + */ 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; + /** + * ``Chart`` Position. + */ + public Area area = new Area(); - public Cairo.Context context = null; + /** + * ``Chart`` Title. + */ + public Text title; - public Color bg_color; - public bool show_legend = true; - public Text title = new Text ("Cairo Chart"); + /** + * Background ``Color``. + */ + public Color bg_color = Color(1, 1, 1); + + /** + * Border ``Color``. + */ public Color border_color = Color(0, 0, 0, 0.3); - - public Legend legend = new Legend (); - + /** + * ``Chart`` Series array. + */ public Series[] series = {}; + /** + * Legend. + */ + public Legend legend; + + /** + * Joint/common {@link Axis} ``Color``. + */ + public Color joint_color = Color (0, 0, 0, 1); + + /** + * Selection line style. + */ + public LineStyle selection_style = LineStyle (); + + /** + * Zoom Scroll speed. + */ + public double zoom_scroll_speed = 64.0; + + /** + * Plot area bounds. + */ + public Area plarea = new Area(); + + /** + * Zoom area limits (relative coordinates: 0-1). + */ + public Area zoom = new Area(); + + /** + * Cairo ``Context`` of the Drawing Area. + */ + public Cairo.Context ctx = null; + + /** + * Current evaluated plot area. + */ + public Area evarea = new Area(); + + /** + * ``Chart`` cursors. + */ + public virtual Cursors cursors { get; protected set; default = null; } + + /** + * Joint/common X axes or not. + */ + public virtual bool joint_x { get; protected set; default = false; } + + /** + * Joint/common Y axes or not. + */ + public virtual bool joint_y { get; protected set; default = false; } + + /** + * Index of the 1'st shown series in a zoomed area. + */ + public virtual int zoom_1st_idx { get; protected set; default = 0; } + + /** + * Set paint color for further drawing. + */ + public virtual Color color { + protected get { return Color(); } + set { ctx.set_source_rgba (value.red, value.green, value.blue, value.alpha); } + default = Color(); + } + + /** + * Constructs a new ``Chart``. + */ public Chart () { - bg_color = Color (1, 1, 1); + cursors = new Cursors (this); + title = new Text(this, "Cairo Chart"); + legend = new Legend(this); } - protected double cur_x_min = 0.0; - protected double cur_x_max = 1.0; - protected double cur_y_min = 0.0; - protected double cur_y_max = 1.0; - - public 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; + /** + * Gets a copy of the ``Chart``. + */ + public virtual Chart copy () { + var chart = new Chart (); + chart.area = this.area.copy(); + chart.bg_color = this.bg_color; + chart.border_color = this.border_color; + chart.ctx = this.ctx; + chart.cursors = this.cursors.copy(); + chart.evarea = this.evarea.copy(); + chart.joint_color = this.joint_color; + chart.joint_x = this.joint_x; + chart.joint_y = this.joint_y; + chart.legend = this.legend.copy(); + chart.plarea = this.plarea.copy(); + chart.selection_style = this.selection_style; + chart.series = this.series; + chart.title = this.title.copy(); + chart.zoom = this.zoom.copy(); + chart.zoom_1st_idx = this.zoom_1st_idx; + return chart; } + /** + * Clears the ``Chart`` with a {@link bg_color} background color. + */ public virtual void clear () { - draw_background (); + if (ctx != null) { + color = bg_color; + ctx.paint(); + } } + /** + * Draws the ``Chart``. + */ 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; + evarea = area.copy(); - draw_chart_title (); - check_cur_values (); + draw_title (); + fix_evarea (); - draw_legend (); - check_cur_values (); + legend.draw (); + fix_evarea (); - set_vertical_axes_titles (); + rot_axes_titles (); - get_cursors_crossings(); + cursors.eval_crossings(); - calc_plot_area (); // Calculate plot area + eval_plarea (); - draw_horizontal_axis (); - check_cur_values (); + draw_axes (); + fix_evarea (); - draw_vertical_axis (); - check_cur_values (); - - draw_plot_area_border (); - check_cur_values (); + draw_plarea_border (); + fix_evarea (); draw_series (); - check_cur_values (); + fix_evarea (); - draw_cursors (); - check_cur_values (); + cursors.draw (); + fix_evarea (); return true; } - public virtual void set_source_rgba (Color color) { - context.set_source_rgba (color.red, color.green, color.blue, color.alpha); + /** + * Draws selection with a {@link selection_style} line style. + * @param area selection area. + */ + public virtual void draw_selection (Area area) { + selection_style.apply(this); + ctx.rectangle (area.x0, area.y0, area.width, area.height); + ctx.stroke(); } - protected virtual void draw_background () { - if (context != null) { - set_source_rgba (bg_color); - context.paint(); - set_source_rgba (Color (0, 0, 0, 1)); - } - } - - protected double rel_zoom_x_min = 0.0; - protected double rel_zoom_x_max = 1.0; - protected double rel_zoom_y_min = 0.0; - protected double rel_zoom_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]; + /** + * Zooms in the ``Chart``. + * @param area selected zoom area. + */ + public virtual void zoom_in (Area area) { + foreach (var s in series) { 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); + var real_x0 = s.axis_x.axis_val (area.x0); + var real_x1 = s.axis_x.axis_val (area.x1); + var real_width = real_x1 - real_x0; + var real_y0 = s.axis_y.axis_val (area.y0); + var real_y1 = s.axis_y.axis_val (area.y1); + var real_height = real_y0 - real_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) { + if ( real_x1 <= s.axis_x.range.zmin || real_x0 >= s.axis_x.range.zmax + || real_y0 <= s.axis_y.range.zmin || real_y1 >= s.axis_y.range.zmax) { s.zoom_show = false; continue; } - if (real_x0 >= s.axis_x.zoom_min) { - s.axis_x.zoom_min = real_x0; - s.place.zoom_x_low = 0.0; + if (real_x0 >= s.axis_x.range.zmin) { + s.axis_x.range.zmin = real_x0; + s.axis_x.place.zmin = 0; } else { - s.place.zoom_x_low = (s.axis_x.zoom_min - real_x0) / (real_x1 - real_x0); + s.axis_x.place.zmin = (s.axis_x.range.zmin - real_x0) / real_width; } - if (real_x1 <= s.axis_x.zoom_max) { - s.axis_x.zoom_max = real_x1; - s.place.zoom_x_high = 1.0; + if (real_x1 <= s.axis_x.range.zmax) { + s.axis_x.range.zmax = real_x1; + s.axis_x.place.zmax = 1; } else { - s.place.zoom_x_high = (s.axis_x.zoom_max - real_x0) / (real_x1 - real_x0); + s.axis_x.place.zmax = (s.axis_x.range.zmax - real_x0) / real_width; } - if (real_y1 >= s.axis_y.zoom_min) { - s.axis_y.zoom_min = real_y1; - s.place.zoom_y_low = 0.0; + if (real_y1 >= s.axis_y.range.zmin) { + s.axis_y.range.zmin = real_y1; + s.axis_y.place.zmin = 0; } else { - s.place.zoom_y_low = (s.axis_y.zoom_min - real_y1) / (real_y0 - real_y1); + s.axis_y.place.zmin = (s.axis_y.range.zmin - real_y1) / real_height; } - if (real_y0 <= s.axis_y.zoom_max) { - s.axis_y.zoom_max = real_y0; - s.place.zoom_y_high = 1.0; + if (real_y0 <= s.axis_y.range.zmax) { + s.axis_y.range.zmax = real_y0; + s.axis_y.place.zmax = 1; } else { - s.place.zoom_y_high = (s.axis_y.zoom_max - real_y1) / (real_y0 - real_y1); + s.axis_y.place.zmax = (s.axis_y.range.zmax - real_y1) / real_height; } } - zoom_first_show = 0; - for (var si = 0, max_i = series.length; si < max_i; ++si) + zoom_1st_idx = 0; + for (var si = 0; si < series.length; ++si) if (series[si].zoom_show) { - zoom_first_show = si; + zoom_1st_idx = si; break; } - - var new_rel_zoom_x_min = rel_zoom_x_min + (x0 - plot_area_x_min) / (plot_area_x_max - plot_area_x_min) * (rel_zoom_x_max - rel_zoom_x_min); - var new_rel_zoom_x_max = rel_zoom_x_min + (x1 - plot_area_x_min) / (plot_area_x_max - plot_area_x_min) * (rel_zoom_x_max - rel_zoom_x_min); - var new_rel_zoom_y_min = rel_zoom_y_min + (y0 - plot_area_y_min) / (plot_area_y_max - plot_area_y_min) * (rel_zoom_y_max - rel_zoom_y_min); - var new_rel_zoom_y_max = rel_zoom_y_min + (y1 - plot_area_y_min) / (plot_area_y_max - plot_area_y_min) * (rel_zoom_y_max - rel_zoom_y_min); - rel_zoom_x_min = new_rel_zoom_x_min; - rel_zoom_x_max = new_rel_zoom_x_max; - rel_zoom_y_min = new_rel_zoom_y_min; - rel_zoom_y_max = new_rel_zoom_y_max; + var new_zoom = zoom.copy(); + var rmpx = area.x0 - plarea.x0; + var zdpw = zoom.width / plarea.width; + new_zoom.x0 += rmpx * zdpw; + var x_max = zoom.x0 + (rmpx + area.width) * zdpw; + new_zoom.width = x_max - new_zoom.x0; + var rmpy = area.y0 - plarea.y0; + var zdph = zoom.height / plarea.height; + new_zoom.y0 += rmpy * zdph; + var y_max = zoom.y0 + (rmpy + area.height) * zdph; + new_zoom.height = y_max - new_zoom.y0; + zoom = new_zoom.copy(); } + /** + * Zooms out the ``Chart``. + */ 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_low = s.place.x_low; - s.place.zoom_x_high = s.place.x_high; - s.place.zoom_y_low = s.place.y_low; - s.place.zoom_y_high = s.place.y_high; - } - rel_zoom_x_min = 0; - rel_zoom_x_max = 1; - rel_zoom_y_min = 0; - rel_zoom_y_max = 1; - - zoom_first_show = 0; + foreach (var s in series) s.zoom_out(); + zoom = new Area.with_abs (0, 0, 1, 1); + zoom_1st_idx = 0; } - public virtual void move (double delta_x, double delta_y) { - delta_x /= plot_area_x_max - plot_area_x_min; delta_x *= - 1.0; - delta_y /= plot_area_y_max - plot_area_y_min; delta_y *= - 1.0; - var rzxmin = rel_zoom_x_min, rzxmax = rel_zoom_x_max, rzymin = rel_zoom_y_min, rzymax = rel_zoom_y_max; + /** + * Moves the ``Chart``. + * @param delta delta Δ(x;y) value to move the ``Chart``. + */ + public virtual void move (Point delta) { + var d = delta; + + if (plarea.width.abs() < 1 || plarea.height.abs() < 1) return; + + d.x /= -plarea.width; d.y /= -plarea.height; + + var z = zoom.copy(); + zoom_out(); - //draw(); // TODO: optimize here - delta_x *= plot_area_x_max - plot_area_x_min; - delta_y *= plot_area_y_max - plot_area_y_min; - var xmin = plot_area_x_min + (plot_area_x_max - plot_area_x_min) * rzxmin; - var xmax = plot_area_x_min + (plot_area_x_max - plot_area_x_min) * rzxmax; - var ymin = plot_area_y_min + (plot_area_y_max - plot_area_y_min) * rzymin; - var ymax = plot_area_y_min + (plot_area_y_max - plot_area_y_min) * rzymax; - delta_x *= rzxmax - rzxmin; delta_y *= rzymax - rzymin; + d.x *= plarea.width; d.y *= plarea.height; - if (xmin + delta_x < plot_area_x_min) delta_x = plot_area_x_min - xmin; - if (xmax + delta_x > plot_area_x_max) delta_x = plot_area_x_max - xmax; - if (ymin + delta_y < plot_area_y_min) delta_y = plot_area_y_min - ymin; - if (ymax + delta_y > plot_area_y_max) delta_y = plot_area_y_max - ymax; + var x0 = plarea.x0 + plarea.width * z.x0; + var x1 = plarea.x0 + plarea.width * z.x1; + var y0 = plarea.y0 + plarea.height * z.y0; + var y1 = plarea.y0 + plarea.height * z.y1; - zoom_in (xmin + delta_x, ymin + delta_y, xmax + delta_x, ymax + delta_y); - //draw(); // TODO: optimize here + d.x *= z.width; d.y *= z.height; + + if (x0 + d.x < plarea.x0) d.x = plarea.x0 - x0; + if (x1 + d.x > plarea.x1) d.x = plarea.x1 - x1; + if (y0 + d.y < plarea.y0) d.y = plarea.y0 - y0; + if (y1 + d.y > plarea.y1) d.y = plarea.y1 - y1; + + zoom_in( + new Area.with_rel( + x0 + d.x, + y0 + d.y, + plarea.width * z.width, + plarea.height * z.height + ) + ); } - protected double title_width = 0.0; - protected double title_height = 0.0; - - public double title_vindent = 4; - - protected virtual void show_text(Text text) { - context.select_font_face(text.style.family, - text.style.slant, - text.style.weight); - context.set_font_size(text.style.size); - if (text.style.orientation == FontOrient.VERTICAL) { - context.rotate(- Math.PI / 2.0); - context.show_text(text.text); - context.rotate(Math.PI / 2.0); - } else { - context.show_text(text.text); - } + /** + * Zooms in the ``Chart`` by event point (scrolling). + * @param p event position. + */ + public virtual void zoom_scroll_in (Point p) { + var w = plarea.width, h = plarea.height; + if (w < 8 || h < 8) return; + zoom_in ( + new Area.with_abs( + plarea.x0 + (p.x - plarea.x0) / w * zoom_scroll_speed, + plarea.y0 + (p.y - plarea.y0) / h * zoom_scroll_speed, + plarea.x1 - (plarea.x1 - p.x) / w * zoom_scroll_speed, + plarea.y1 - (plarea.y1 - p.y) / h * zoom_scroll_speed + ) + ); } - protected virtual void draw_chart_title () { - var sz = title.size(context); - title_height = sz.height + (legend.position == Legend.Position.TOP ? title_vindent * 2 : title_vindent); - cur_y_min += title_height; - set_source_rgba(title.color); - context.move_to (width/2 - sz.width/2 - sz.x_bearing, sz.height + title_vindent); - show_text(title); + /** + * Zooms out the ``Chart`` by event point (scrolling). + * @param p event position. + */ + public virtual void zoom_scroll_out (Point p) { + var z = zoom.copy(), pa = plarea.copy(); + var w = plarea.width, h = plarea.height; + if (w < 8 || h < 8) return; + + zoom_out(); + + var x0 = plarea.x0 + plarea.width * z.x0; + var x1 = plarea.x0 + plarea.width * z.x1; + var y0 = plarea.y0 + plarea.height * z.y0; + var y1 = plarea.y0 + plarea.height * z.y1; + + var dx0 = (p.x - pa.x0) / w * zoom_scroll_speed; + var dx1 = (pa.x1 - p.x) / w * zoom_scroll_speed; + var dy0 = (p.y - pa.y0) / h * zoom_scroll_speed; + var dy1 = (pa.y1 - p.y) / h * zoom_scroll_speed; + + if (x0 - dx0 < plarea.x0) x0 = plarea.x0; else x0 -= dx0; + if (x1 + dx1 > plarea.x1) x1 = plarea.x1; else x1 += dx1; + if (y0 - dy0 < plarea.y0) y0 = plarea.y0; else y0 -= dy0; + if (y1 + dy1 > plarea.y1) y1 = plarea.y1; else y1 += dy1; + + zoom_in (new Area.with_abs(x0, y0, x1, y1)); } - protected double legend_width = 0; - protected double legend_height = 0; - - protected enum LegendProcessType { - CALC = 0, // default - DRAW + protected virtual void fix_evarea () { + if (evarea.width < 0) evarea.width = 0; + if (evarea.height < 0) evarea.height = 0; + } + protected virtual void rot_axes_titles () { + foreach (var s in series) + s.axis_y.title.font.orient = Gtk.Orientation.VERTICAL; } - protected virtual void set_line_style (LineStyle style) { - set_source_rgba(style.color); - context.set_line_join(style.line_join); - context.set_line_cap(style.line_cap); - context.set_line_width(style.width); - context.set_dash(style.dashes, style.dash_offset); - } - - protected virtual void draw_legend_rect (out double x0, out double y0) { - x0 = y0 = 0.0; - if (context != null) { - switch (legend.position) { - case Legend.Position.TOP: - x0 = (width - legend_width) / 2; - y0 = title_height; - break; - - case Legend.Position.BOTTOM: - x0 = (width - legend_width) / 2; - y0 = height - legend_height; - break; - - case Legend.Position.LEFT: - x0 = 0; - y0 = (height - legend_height) / 2; - break; - - case Legend.Position.RIGHT: - x0 = width - legend_width; - y0 = (height - legend_height) / 2; - break; - } - set_source_rgba(legend.bg_color); - context.rectangle (x0, y0, legend_width, legend_height); - context.fill(); - set_line_style(legend.border_style); - context.move_to (x0, y0); - context.rel_line_to (legend_width, 0); - context.rel_line_to (0, legend_height); - context.rel_line_to (-legend_width, 0); - context.rel_line_to (0, -legend_height); - context.stroke (); - } - } - - public double legend_line_length = 30.0; - public double legend_text_hspace = 10.0; - public double legend_text_vspace = 2.0; - public double marker_size = 8.0; - - protected virtual void draw_marker_at_pos (Series.MarkerType marker_type, - double x, double y) { - context.move_to (x, y); - switch (marker_type) { - case Series.MarkerType.SQUARE: - context.rectangle (x - marker_size / 2, y - marker_size / 2, - marker_size, marker_size); - context.fill(); - break; - - case Series.MarkerType.CIRCLE: - context.arc (x, y, marker_size / 2, 0, 2*Math.PI); - context.fill(); - break; - - case Series.MarkerType.TRIANGLE: - context.move_to (x - marker_size / 2, y - marker_size / 2); - context.line_to (x + marker_size / 2, y - marker_size / 2); - context.line_to (x, y + marker_size / 2); - context.line_to (x - marker_size / 2, y - marker_size / 2); - context.fill(); - break; - - case Series.MarkerType.PRICLE_SQUARE: - context.rectangle (x - marker_size / 2, y - marker_size / 2, - marker_size, marker_size); - context.stroke(); - break; - - case Series.MarkerType.PRICLE_CIRCLE: - context.arc (x, y, marker_size / 2, 0, 2*Math.PI); - context.stroke(); - break; - - case Series.MarkerType.PRICLE_TRIANGLE: - context.move_to (x - marker_size / 2, y - marker_size / 2); - context.line_to (x + marker_size / 2, y - marker_size / 2); - context.line_to (x, y + marker_size / 2); - context.line_to (x - marker_size / 2, y - marker_size / 2); - context.stroke(); - break; - } - } - - double [] max_font_heights; - protected virtual void process_legend (LegendProcessType process_type) { - var legend_x0 = 0.0, legend_y0 = 0.0; - var heights_idx = 0; - var leg_width_sum = 0.0; - var leg_height_sum = 0.0; - double max_font_h = 0.0; - - // prepare - switch (process_type) { - case LegendProcessType.CALC: - legend_width = 0.0; - legend_height = 0.0; - max_font_heights = {}; - heights_idx = 0; - break; - case LegendProcessType.DRAW: - draw_legend_rect(out legend_x0, out legend_y0); - break; - } - - foreach (var s in series) { - - if (!s.zoom_show) continue; - - var title_sz = s.title.size(context); - - // carry - switch (legend.position) { - case Legend.Position.TOP: - case Legend.Position.BOTTOM: - var ser_title_width = title_sz.width + legend_line_length; - if (leg_width_sum + (leg_width_sum == 0 ? 0 : legend_text_hspace) + ser_title_width > width) { // carry - leg_height_sum += max_font_h; - switch (process_type) { - case LegendProcessType.CALC: - max_font_heights += max_font_h; - legend_width = double.max(legend_width, leg_width_sum); - break; - case LegendProcessType.DRAW: - heights_idx++; - break; - } - leg_width_sum = 0.0; - max_font_h = 0; - } - break; - } - - switch (process_type) { - case LegendProcessType.DRAW: - var x = legend_x0 + leg_width_sum + (leg_width_sum == 0.0 ? 0.0 : legend_text_hspace); - var y = legend_y0 + leg_height_sum + max_font_heights[heights_idx]; - - // series title - context.move_to (x + legend_line_length - title_sz.x_bearing, y); - set_source_rgba (s.title.color); - show_text(s.title); - - // series line style - context.move_to (x, y - title_sz.height / 2); - set_line_style(s.line_style); - context.rel_line_to (legend_line_length, 0); - context.stroke(); - draw_marker_at_pos (s.marker_type, x + legend_line_length / 2, y - title_sz.height / 2); - break; - } - - switch (legend.position) { - case Legend.Position.TOP: - case Legend.Position.BOTTOM: - var ser_title_width = title_sz.width + legend_line_length; - leg_width_sum += (leg_width_sum == 0 ? 0 : legend_text_hspace) + ser_title_width; - max_font_h = double.max (max_font_h, title_sz.height) + (leg_height_sum != 0 ? legend_text_vspace : 0); - break; - - case Legend.Position.LEFT: - case Legend.Position.RIGHT: - switch (process_type) { - case LegendProcessType.CALC: - max_font_heights += title_sz.height + (leg_height_sum != 0 ? legend_text_vspace : 0); - legend_width = double.max (legend_width, title_sz.width + legend_line_length); - break; - case LegendProcessType.DRAW: - heights_idx++; - break; - } - leg_height_sum += title_sz.height + (leg_height_sum != 0 ? legend_text_vspace : 0); - break; - } - } - - // TOP, BOTTOM - switch (legend.position) { - case Legend.Position.TOP: - case Legend.Position.BOTTOM: - if (leg_width_sum != 0) { - leg_height_sum += max_font_h; - switch (process_type) { - case LegendProcessType.CALC: - max_font_heights += max_font_h; - legend_width = double.max(legend_width, leg_width_sum); - break; - } - } - break; - } - - switch (process_type) { - case LegendProcessType.CALC: - legend_height = leg_height_sum; - switch (legend.position) { - case Legend.Position.TOP: - cur_y_min += legend_height; - break; - case Legend.Position.BOTTOM: - cur_y_max -= legend_height; - break; - case Legend.Position.LEFT: - cur_x_min += legend_width; - break; - case Legend.Position.RIGHT: - cur_x_max -= legend_width; - break; - } - break; - } - } - - protected virtual void draw_legend () { - process_legend (LegendProcessType.CALC); - process_legend (LegendProcessType.DRAW); - } - - public LineStyle selection_style = LineStyle (); - - public virtual void draw_selection (double x0, double y0, double x1, double y1) { - set_line_style (selection_style); - context.rectangle (x0, y0, x1 - x0, y1 - y0); - context.stroke(); - } - - protected int axis_rec_npoints = 128; - - protected virtual void calc_axis_rec_sizes (Axis axis, out double max_rec_width, out double max_rec_height, bool is_horizontal = true) { - max_rec_width = max_rec_height = 0; - for (var i = 0; i < axis_rec_npoints; ++i) { - Float128 x = (int64)(axis.zoom_min + (axis.zoom_max - axis.zoom_min) / axis_rec_npoints * i) + 1.0/3.0; - switch (axis.type) { - case Axis.Type.NUMBERS: - var text = new Text (axis.format.printf((LongDouble)x) + (is_horizontal ? "_" : ""), axis.font_style); - var sz = text.size(context); - max_rec_width = double.max (max_rec_width, sz.width); - max_rec_height = double.max (max_rec_height, sz.height); - break; - case Axis.Type.DATE_TIME: - string date, time; - format_date_time(axis, x, out date, out time); - - var h = 0.0; - if (axis.date_format != "") { - var text = new Text (date + (is_horizontal ? "_" : ""), axis.font_style); - var sz = text.size(context); - max_rec_width = double.max (max_rec_width, sz.width); - h = sz.height; - } - if (axis.time_format != "") { - var text = new Text (time + (is_horizontal ? "_" : ""), axis.font_style); - var sz = text.size(context); - max_rec_width = double.max (max_rec_width, sz.width); - h += sz.height; - } - max_rec_height = double.max (max_rec_height, h); - break; - } - } - } - - protected virtual Float128 calc_round_step (Float128 aver_step, bool date_time = false) { - Float128 step = 1.0; - - if (aver_step > 1.0) { - if (date_time) while (step < aver_step) step *= 60; - if (date_time) while (step < aver_step) step *= 60; - if (date_time) while (step < aver_step) step *= 24; - while (step < aver_step) step *= 10; - if (step / 5 > aver_step) step /= 5; - while (step / 2 > aver_step) step /= 2; - } else if (aver_step > 0) { - while (step / 10 > aver_step) step /= 10; - if (step / 5 > aver_step) step /= 5; - while (step / 2 > aver_step) step /= 2; - } - - return step; - } - - public double plot_area_x_min = 0; - public double plot_area_x_max = 0; - public double plot_area_y_min = 0; - public double plot_area_y_max = 0; - - public bool common_x_axes { get; protected set; default = false; } - public bool common_y_axes { get; protected set; default = false; } - public Color common_axis_color = Color (0, 0, 0, 1); - - bool are_intersect (double a_min, double a_max, double b_min, double b_max) { - if ( a_min < a_max <= b_min < b_max - || b_min < b_max <= a_min < a_max) + protected virtual bool equal_x_axis (Series s1, Series s2) { + if ( s1.axis_x.position != s2.axis_x.position + || s1.axis_x.range.zmin != s2.axis_x.range.zmin + || s1.axis_x.range.zmax != s2.axis_x.range.zmax + || s1.axis_x.place.zmin != s2.axis_x.place.zmin + || s1.axis_x.place.zmax != s2.axis_x.place.zmax + || s1.axis_x.dtype != s2.axis_x.dtype + ) return false; return true; } - 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 = FontOrient.VERTICAL; - } - } - - protected virtual void calc_plot_area () { - plot_area_x_min = cur_x_min + legend.indent; - plot_area_x_max = cur_x_max - legend.indent; - plot_area_y_min = cur_y_min + legend.indent; - plot_area_y_max = cur_y_max - legend.indent; - - // Check for common axes - common_x_axes = common_y_axes = 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_low != series[0].place.zoom_x_low - || s.place.zoom_x_high != series[0].place.zoom_x_high - || s.axis_x.type != series[0].axis_x.type) - common_x_axes = 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_low != series[0].place.zoom_y_low - || s.place.zoom_y_high != series[0].place.zoom_y_high) - common_y_axes = false; - } - if (nzoom_series_show == 1) common_x_axes = common_y_axes = 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; - calc_axis_rec_sizes (s.axis_x, 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 (are_intersect(s2.place.zoom_x_low, s2.place.zoom_x_high, s3.place.zoom_x_low, s3.place.zoom_x_high) - || 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; - calc_axis_rec_sizes (s2.axis_x, 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 common X axis - if (common_x_axes && si == zoom_first_show && cursors_orientation == CursorOrientation.VERTICAL && cursors_crossings.length != 0) { - switch (s.axis_x.position) { - case Axis.Position.LOW: plot_area_y_max -= max_rec_height + s.axis_x.font_indent; break; - case Axis.Position.HIGH: plot_area_y_min += max_rec_height + s.axis_x.font_indent; break; - } - } - - if (!common_x_axes || si == zoom_first_show) - switch (s.axis_x.position) { - case Axis.Position.LOW: plot_area_y_max -= max_rec_height + max_font_indent + max_axis_font_height; break; - case Axis.Position.HIGH: plot_area_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; - calc_axis_rec_sizes (s.axis_y, 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 (are_intersect(s2.place.zoom_y_low, s2.place.zoom_y_high, s3.place.zoom_y_low, s3.place.zoom_y_high) - || 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; - calc_axis_rec_sizes (s2.axis_y, 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 common Y axis - if (common_y_axes && si == zoom_first_show && cursors_orientation == CursorOrientation.HORIZONTAL && cursors_crossings.length != 0) { - switch (s.axis_y.position) { - case Axis.Position.LOW: plot_area_x_min += max_rec_width + s.axis_y.font_indent; break; - case Axis.Position.HIGH: plot_area_x_max -= max_rec_width + s.axis_y.font_indent; break; - } - } - - if (!common_y_axes || si == zoom_first_show) - switch (s.axis_y.position) { - case Axis.Position.LOW: plot_area_x_min += max_rec_width + max_font_indent + max_axis_font_width; break; - case Axis.Position.HIGH: plot_area_x_max -= max_rec_width + max_font_indent + max_axis_font_width; break; - } - } - } - - bool point_belong (Float128 p, Float128 a, Float128 b) { - if (a > b) { Float128 tmp = a; a = b; b = tmp; } - if (a <= p <= b) return true; - return false; - } - - protected virtual double compact_rec_x_pos (Series s, Float128 x, Text text) { - var sz = text.size(context); - return get_scr_x(s, x) - sz.width / 2.0 - sz.x_bearing - - 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.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 virtual void format_date_time (Axis axis, Float128 x, out string date, out string time) { - date = time = ""; - var dt = new DateTime.from_unix_utc((int64)x); - date = dt.format(axis.date_format); - var dsec_str = - ("%."+(axis.dsec_signs.to_string())+"Lf").printf((LongDouble)(x - (int64)x)).offset(1); - time = dt.format(axis.time_format) + dsec_str; - } - - 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 (common_x_axes && si != zoom_first_show) continue; - - // 1. Detect max record width/height by axis_rec_npoints equally selected points using format. - double max_rec_width, max_rec_height; - calc_axis_rec_sizes (s.axis_x, 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_area_x_max - plot_area_x_min) * (s.place.zoom_x_high - s.place.zoom_x_low) / max_rec_width); - - // 3. Calculate grid step. - Float128 step = 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 common X axis - if (common_x_axes && cursors_orientation == CursorOrientation.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.size(context); - - // 4.5. Draw Axis title - if (s.axis_x.title.text != "") { - var scr_x = plot_area_x_min + (plot_area_x_max - plot_area_x_min) * (s.place.zoom_x_low + s.place.zoom_x_high) / 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 (common_x_axes) set_source_rgba(common_axis_color); - show_text(s.axis_x.title); - } - - // 5. Draw records, update cur_{x,y}_{min,max}. - for (Float128 x = x_min, x_max = s.axis_x.zoom_max; point_belong (x, x_min, x_max); x += step) { - if (common_x_axes) set_source_rgba(common_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: - format_date_time(s.axis_x, 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: - show_text(text_t); - break; - case Axis.Type.DATE_TIME: - if (s.axis_x.date_format != "") show_text(text_t); - 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 != "") show_text(time_text_t); - break; - } - // 6. Draw grid lines to the s.place.zoom_y_low. - var line_style = s.grid.line_style; - if (common_x_axes) line_style.color = Color(0, 0, 0, 0.5); - set_line_style(line_style); - 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 (common_x_axes) - context.line_to (scr_x, plot_area_y_min); - else - context.line_to (scr_x, double.min (y, plot_area_y_max - (plot_area_y_max - plot_area_y_min) * s.place.zoom_y_high)); - 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: - show_text(text_t); - break; - case Axis.Type.DATE_TIME: - if (s.axis_x.date_format != "") show_text(text_t); - 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 != "") show_text(time_text_t); - break; - } - // 6. Draw grid lines to the s.place.zoom_y_high. - var line_style = s.grid.line_style; - if (common_x_axes) line_style.color = Color(0, 0, 0, 0.5); - set_line_style(line_style); - 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 (common_x_axes) - context.line_to (scr_x, plot_area_y_max); - else - context.line_to (scr_x, double.max (y, plot_area_y_max - (plot_area_y_max - plot_area_y_min) * s.place.zoom_y_low)); - 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 (are_intersect(s2.place.zoom_x_low, s2.place.zoom_x_high, s3.place.zoom_x_low, s3.place.zoom_x_high) - || 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 (common_y_axes && si != zoom_first_show) continue; - // 1. Detect max record width/height by axis_rec_npoints equally selected points using format. - double max_rec_width, max_rec_height; - calc_axis_rec_sizes (s.axis_y, 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_area_y_max - plot_area_y_min) * (s.place.zoom_y_high - s.place.zoom_y_low) / max_rec_height); - - // 3. Calculate grid step. - Float128 step = 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 common Y axis - if (common_y_axes && cursors_orientation == CursorOrientation.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.size(context); - - // 4.5. Draw Axis title - if (s.axis_y.title.text != "") { - var scr_y = plot_area_y_max - (plot_area_y_max - plot_area_y_min) * (s.place.zoom_y_low + s.place.zoom_y_high) / 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 (common_y_axes) set_source_rgba(common_axis_color); - show_text(s.axis_y.title); - } - - // 5. Draw records, update cur_{x,y}_{min,max}. - for (Float128 y = y_min, y_max = s.axis_y.zoom_max; point_belong (y, y_min, y_max); y += step) { - if (common_y_axes) set_source_rgba(common_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.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 - text_sz.x_bearing - + (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent), - compact_rec_y_pos (s, y, text_t)); - show_text(text_t); - // 6. Draw grid lines to the s.place.zoom_x_low. - var line_style = s.grid.line_style; - if (common_y_axes) line_style.color = Color(0, 0, 0, 0.5); - set_line_style(line_style); - 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 (common_y_axes) - context.line_to (plot_area_x_max, scr_y); - else - context.line_to (double.max (x, plot_area_x_min + (plot_area_x_max - plot_area_x_min) * s.place.zoom_x_high), scr_y); - break; - case Axis.Position.HIGH: - context.move_to (cur_x_max - text_sz.width - s.axis_y.font_indent - text_sz.x_bearing - - (s.axis_y.title.text == "" ? 0 : sz.width + s.axis_y.font_indent), - compact_rec_y_pos (s, y, text_t)); - show_text(text_t); - // 6. Draw grid lines to the s.place.zoom_x_high. - var line_style = s.grid.line_style; - if (common_y_axes) line_style.color = Color(0, 0, 0, 0.5); - set_line_style(line_style); - 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 (common_y_axes) - context.line_to (plot_area_x_min, scr_y); - else - context.line_to (double.min (x, plot_area_x_min + (plot_area_x_max - plot_area_x_min) * s.place.zoom_x_low), 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 (are_intersect(s2.place.zoom_y_low, s2.place.zoom_y_high, s3.place.zoom_y_low, s3.place.zoom_y_high) - || 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_area_x_min, plot_area_y_min); - context.line_to (plot_area_x_min, plot_area_y_max); - context.line_to (plot_area_x_max, plot_area_y_max); - context.line_to (plot_area_x_max, plot_area_y_min); - context.line_to (plot_area_x_min, plot_area_y_min); - context.stroke (); - } - - protected virtual double get_scr_x (Series s, Float128 x) { - return plot_area_x_min + (plot_area_x_max - plot_area_x_min) * (s.place.zoom_x_low + (x - s.axis_x.zoom_min) - / (s.axis_x.zoom_max - s.axis_x.zoom_min) * (s.place.zoom_x_high - s.place.zoom_x_low)); - } - - protected virtual double get_scr_y (Series s, Float128 y) { - return plot_area_y_max - (plot_area_y_max - plot_area_y_min) * (s.place.zoom_y_low + (y - s.axis_y.zoom_min) - / (s.axis_y.zoom_max - s.axis_y.zoom_min) * (s.place.zoom_y_high - s.place.zoom_y_low)); - } - 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_area_x_min) / (plot_area_x_max - plot_area_x_min) - s.place.zoom_x_low) - * (s.axis_x.zoom_max - s.axis_x.zoom_min) / (s.place.zoom_x_high - s.place.zoom_x_low); - } - protected virtual Float128 get_real_y (Series s, double scr_y) { - return s.axis_y.zoom_min + ((plot_area_y_max - scr_y) / (plot_area_y_max - plot_area_y_min) - s.place.zoom_y_low) - * (s.axis_y.zoom_max - s.axis_y.zoom_min) / (s.place.zoom_y_high - s.place.zoom_y_low); - } - 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_range (double x, double x0, double x1) { - if (x0 <= x <= x1 || x1 <= x <= x0) - return true; - return false; - } - - protected virtual bool y_in_range (double y, double y0, double y1) { - if (y0 <= y <= y1 || y1 <= y <= y0) - return true; - return false; - } - - protected virtual bool x_in_plot_area (double x) { - if (x_in_range(x, plot_area_x_min, plot_area_x_max)) - return true; - return false; - } - - protected virtual bool y_in_plot_area (double y) { - if (y_in_range(y, plot_area_y_min, plot_area_y_max)) - return true; - return false; - } - - protected virtual bool point_in_rect (Point p, double x0, double x1, double y0, double y1) { - if (x_in_range(p.x, x0, x1) && y_in_range(p.y, y0, y1)) - return true; - return false; - } - - protected virtual bool point_in_plot_area (Point p) { - if (point_in_rect (p, plot_area_x_min, plot_area_x_max, plot_area_y_min, plot_area_y_max)) - return true; - return false; - } - - protected virtual bool hcross (Point a1, Point a2, Float128 h_x1, Float128 h_x2, Float128 h_y, out Float128 x) { - x = 0; - if (a1.y == a2.y) return false; - if (a1.y >= h_y && a2.y >= h_y || a1.y <= h_y && a2.y <= h_y) return false; - x = a1.x + (a2.x - a1.x) * (h_y - a1.y) / (a2.y - a1.y); - if (h_x1 <= x <= h_x2 || h_x2 <= x <= h_x1) - return true; - return false; - } - - protected virtual bool vcross (Point a1, Point a2, Float128 v_x, Float128 v_y1, Float128 v_y2, out Float128 y) { - y = 0; - if (a1.x == a2.x) return false; - if (a1.x >= v_x && a2.x >= v_x || a1.x <= v_x && a2.x <= v_x) return false; - y = a1.y + (a2.y - a1.y) * (v_x - a1.x) / (a2.x - a1.x); - if (v_y1 <= y <= v_y2 || v_y2 <= y <= v_y1) - return true; - return false; - } - - delegate int PointComparator(Point a, Point b); - void sort_points_delegate(Point[] points, PointComparator compare) { - for(var i = 0; i < points.length; ++i) { - for(var j = i + 1; j < points.length; ++j) { - if(compare(points[i], points[j]) > 0) { - var tmp = points[i]; - points[i] = points[j]; - points[j] = tmp; - } - } - } - } - - protected virtual bool cut_line (Point a, Point b, out Point c, out Point d) { - int ncross = 0; - Float128 x = 0, y = 0; - Point pc[4]; - if (hcross(a, b, plot_area_x_min, plot_area_x_max, plot_area_y_min, out x)) - pc[ncross++] = Point(x, plot_area_y_min); - if (hcross(a, b, plot_area_x_min, plot_area_x_max, plot_area_y_max, out x)) - pc[ncross++] = Point(x, plot_area_y_max); - if (vcross(a, b, plot_area_x_min, plot_area_y_min, plot_area_y_max, out y)) - pc[ncross++] = Point(plot_area_x_min, y); - if (vcross(a, b, plot_area_x_max, plot_area_y_min, plot_area_y_max, out y)) - pc[ncross++] = Point(plot_area_x_max, y); - c = a; - d = b; - if (ncross == 0) { - if (point_in_plot_area (a) && point_in_plot_area (b)) - return true; + protected virtual bool equal_y_axis (Series s1, Series s2) { + if ( s1.axis_y.position != s2.axis_y.position + || s1.axis_y.range.zmin != s2.axis_y.range.zmin + || s1.axis_y.range.zmax != s2.axis_y.range.zmax + || s1.axis_y.place.zmin != s2.axis_y.place.zmin + || s1.axis_y.place.zmax != s2.axis_y.place.zmax + || s1.axis_y.dtype != s2.axis_y.dtype + ) return false; - } - if (ncross >= 2) { - c = pc[0]; d = pc[1]; - return true; - } - if (ncross == 1) { - if (point_in_plot_area (a)) { - c = a; - d = pc[0]; - return true; - } else if (point_in_plot_area (b)) { - c = b; - d = pc[0]; - return true; - } - } - return false; + return true; } - protected virtual Point[] sort_points (Series s, Series.Sort sort) { - var points = s.points; - switch(sort) { - case Series.Sort.BY_X: - sort_points_delegate(points, (a, b) => { - if (a.x < b.x) return -1; - if (a.x > b.x) return 1; - return 0; - }); - break; - case Series.Sort.BY_Y: - sort_points_delegate(points, (a, b) => { - if (a.y < b.y) return -1; - if (a.y > b.y) return 1; - return 0; - }); - break; - } - return points; - } + protected virtual void eval_plarea () { + plarea = evarea.copy(); - protected virtual void draw_series () { - for (var si = 0; si < series.length; ++si) { - var s = series[si]; + // Check for joint axes + joint_x = joint_y = true; + int nshow = 0; + foreach (var s in series) { if (!s.zoom_show) continue; - if (s.points.length == 0) continue; - var points = sort_points(s, s.sort); - set_line_style(s.line_style); - // draw series line - for (int i = 1; i < points.length; ++i) { - Point c, d; - if (cut_line (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))) - draw_marker_at_pos(s.marker_type, x, y); - } + if (!equal_x_axis(s, series[0])) joint_x = false; + if (!equal_y_axis(s, series[0])) joint_y = false; + ++nshow; } + if (nshow == 1) joint_x = joint_y = false; + + for (var si = series.length - 1, nskip = 0; si >= 0; --si) + series[si].axis_x.join_axes(ref nskip); + + for (var si = series.length - 1, nskip = 0; si >= 0; --si) + series[si].axis_y.join_axes(ref nskip); } - 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; + protected virtual void draw_plarea_border () { + LineStyle().apply(this); + color = border_color; + ctx.rectangle(plarea.x0, plarea.y0, plarea.width, plarea.height); + ctx.stroke (); } - - public virtual void add_active_cursor () { - cursors.append (active_cursor); - is_cursor_active = false; + protected virtual void draw_title () { + var title_height = title.height + title.font.vspacing * 2; + evarea.y0 += title_height; + color = title.color; + ctx.move_to (area.width/2 - title.width/2, title.height + title.font.vspacing); + title.show(); } - - public enum CursorOrientation { - VERTICAL = 0, // default - HORIZONTAL + protected virtual void draw_axes () { + for (var si = series.length - 1, nskip = 0; si >= 0; --si) + series[si].axis_x.draw(ref nskip); + for (var si = series.length - 1, nskip = 0; si >= 0; --si) + series[si].axis_y.draw(ref nskip); } - - public CursorOrientation cursors_orientation = CursorOrientation.VERTICAL; - - public double cursor_max_rm_distance = 32; - - 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 (cursors_orientation) { - case CursorOrientation.VERTICAL: - d = (rel2scr_x(c.x) - rel2scr_x(active_cursor.x)).abs(); - break; - case CursorOrientation.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_max_rm_distance) - cursors.delete_link(cursors.nth(rm_indx)); - is_cursor_active = false; - } - - protected virtual Float128 scr2rel_x (Float128 x) { - return rel_zoom_x_min + (x - plot_area_x_min) / (plot_area_x_max - plot_area_x_min) * (rel_zoom_x_max - rel_zoom_x_min); - } - protected virtual Float128 scr2rel_y (Float128 y) { - return rel_zoom_y_max - (plot_area_y_max - y) / (plot_area_y_max - plot_area_y_min) * (rel_zoom_y_max - rel_zoom_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_area_x_min + (plot_area_x_max - plot_area_x_min) * (x - rel_zoom_x_min) / (rel_zoom_x_max - rel_zoom_x_min); - } - - protected virtual Float128 rel2scr_y(Float128 y) { - return plot_area_y_min + (plot_area_y_max - plot_area_y_min) * (y - rel_zoom_y_min) / (rel_zoom_y_max - rel_zoom_y_min); - } - - protected virtual Point rel2scr_point (Point p) { - return Point (rel2scr_x(p.x), rel2scr_y(p.y)); - } - - public LineStyle cursor_line_style = LineStyle(Color(0.2, 0.2, 0.2, 0.8)); - - 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 (cursors_orientation) { - case CursorOrientation.VERTICAL: - if (c.x <= rel_zoom_x_min || c.x >= rel_zoom_x_max) continue; break; - case CursorOrientation.HORIZONTAL: - if (c.y <= rel_zoom_y_min || c.y >= rel_zoom_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 (cursors_orientation) { - case CursorOrientation.VERTICAL: - points = sort_points (s, s.sort); - break; - case CursorOrientation.HORIZONTAL: - points = sort_points (s, s.sort); - break; - } - - for (var i = 0; i + 1 < points.length; ++i) { - switch (cursors_orientation) { - case CursorOrientation.VERTICAL: - Float128 y = 0.0; - if (vcross(get_scr_point(s, points[i]), get_scr_point(s, points[i+1]), rel2scr_x(c.x), - plot_area_y_min, plot_area_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 CursorOrientation.HORIZONTAL: - Float128 x = 0.0; - if (hcross(get_scr_point(s, points[i]), get_scr_point(s, points[i+1]), - plot_area_x_min, plot_area_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 (cursors_orientation) { - case CursorOrientation.VERTICAL: - show_y = true; - if (!common_x_axes) - 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 CursorOrientation.HORIZONTAL: - if (!common_y_axes) 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; - format_date_time(s.axis_x, 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.size(context); size.x = sz.width; h_x = sz.height; } - if (show_date) { var sz = date_t.size(context); size.x = sz.width; h_x = sz.height; } - if (show_time) { var sz = time_t.size(context); size.x = double.max(size.x, sz.width); h_x += sz.height; } - if (show_y) { var sz = y_t.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_area_x_max, plot_area_y_max); // low and high - var high = Point(plot_area_x_min, plot_area_y_min); // points of the cursor - unowned CursorCross[] ccs = cursors_crossings[cci].crossings; - set_line_style(cursor_line_style); - 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 (common_x_axes) { - switch (s.axis_x.position) { - case Axis.Position.LOW: high.y = plot_area_y_max + s.axis_x.font_indent; break; - case Axis.Position.HIGH: low.y = plot_area_y_min - s.axis_x.font_indent; break; - case Axis.Position.BOTH: - high.y = plot_area_y_max + s.axis_x.font_indent; - low.y = plot_area_y_min - s.axis_x.font_indent; - break; - } - } - if (common_y_axes) { - switch (s.axis_y.position) { - case Axis.Position.LOW: low.x = plot_area_x_min - s.axis_y.font_indent; break; - case Axis.Position.HIGH: high.x = plot_area_x_max + s.axis_y.font_indent; break; - case Axis.Position.BOTH: - low.x = plot_area_x_min - s.axis_y.font_indent; - high.x = plot_area_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 (cursors_orientation) { - case CursorOrientation.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 common X value - if (common_x_axes) { - 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: - format_date_time(s.axis_x, 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.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: - show_text(text_t); - break; - case Axis.Type.DATE_TIME: - if (s.axis_x.date_format != "") show_text(text_t); - 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 != "") show_text(time_text_t); - break; - } - } - break; - case CursorOrientation.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 common Y value - if (common_y_axes) { - 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); - show_text(text_t); - } - 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 (common_x_axes) set_source_rgba (common_axis_color); - show_text(text_t); - } - - if (show_time) { - set_source_rgba(s.axis_x.color); - string date = "", time = ""; - format_date_time(s.axis_x, point.x, out date, out time); - var text_t = new Text(time, s.axis_x.font_style); - var sz = text_t.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 (common_x_axes) set_source_rgba (common_axis_color); - show_text(text_t); - } - - if (show_date) { - set_source_rgba(s.axis_x.color); - string date = "", time = ""; - format_date_time(s.axis_x, point.x, out date, out time); - var text_t = new Text(date, s.axis_x.font_style); - var sz = text_t.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 (common_x_axes) set_source_rgba (common_axis_color); - show_text(text_t); - } - - 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.size(context); - context.move_to (svp.x + size.x / 2 - sz.width, svp.y + sz.height / 2); - if (common_y_axes) set_source_rgba (common_axis_color); - show_text(text_t); - } - } - } - } - - 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 (common_x_axes && cursors_orientation == CursorOrientation.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 (common_y_axes && cursors_orientation == CursorOrientation.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 (common_x_axes) - 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); - format_date_time(s.axis_x, delta, out date, out time); - str = days.to_string() + " + " + time; - break; - } - if (common_y_axes) { - str = s.axis_y.format.printf((LongDouble)delta); - } - return str; - } - - public Chart copy () { - var chart = new Chart (); - chart.active_cursor = this.active_cursor; - chart.axis_rec_npoints = this.axis_rec_npoints; - chart.bg_color = this.bg_color; - chart.border_color = this.border_color; - chart.common_x_axes = this.common_x_axes; - chart.common_y_axes = this.common_y_axes; - 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_line_style = this.cursor_line_style; - chart.cursor_max_rm_distance = this.cursor_max_rm_distance; - chart.cursors = this.cursors.copy(); - chart.cursors_crossings = this.cursors_crossings; - chart.cursors_orientation = this.cursors_orientation; - chart.height = this.height; - chart.is_cursor_active = this.is_cursor_active; - chart.legend = this.legend.copy(); - chart.legend_height = this.legend_height; - chart.legend_line_length = this.legend_line_length; - chart.legend_text_hspace = this.legend_text_hspace; - chart.legend_text_vspace = this.legend_text_vspace; - chart.legend_width = this.legend_width; - chart.marker_size = this.marker_size; - chart.max_font_heights = this.max_font_heights; - chart.plot_area_x_max = this.plot_area_x_max; - chart.plot_area_x_min = this.plot_area_x_min; - chart.plot_area_y_max = this.plot_area_y_max; - chart.plot_area_y_min = this.plot_area_y_min; - chart.rel_zoom_x_min = this.rel_zoom_x_min; - chart.rel_zoom_x_max = this.rel_zoom_x_max; - chart.rel_zoom_y_min = this.rel_zoom_y_min; - chart.rel_zoom_y_max = this.rel_zoom_y_max; - chart.selection_style = this.selection_style; - chart.series = this.series; - chart.show_legend = this.show_legend; - chart.title = this.title.copy(); - chart.title_height = this.title_height; - chart.title_vindent = this.title_vindent; - 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; + protected virtual void draw_series () { + foreach (var s in series) + if (s.zoom_show && s.points.length != 0) + s.draw(); } } } diff --git a/src/Color.vala b/src/Color.vala index 6cbea9a..297ad2e 100644 --- a/src/Color.vala +++ b/src/Color.vala @@ -1,12 +1,46 @@ -namespace Gtk.CairoChart { +namespace CairoChart { + + /** + * R/G/B/A Color. + */ public struct Color { + + /** + * Red component. + */ double red; + + /** + * Green component. + */ double green; + + /** + * Blue component. + */ double blue; + + /** + * Alpha component. + */ double alpha; - public Color (double red = 0.0, double green = 0.0, double blue = 0.0, double alpha = 1.0) { - this.red = red; this.green = green; this.blue = blue; this.alpha = alpha; + /** + * Constructs a new ``Color``. + * @param red red component. + * @param green green component. + * @param blue blue component. + * @param alpha alpha component. + */ + public Color (double red = 0, + double green = 0, + double blue = 0, + double alpha = 1 + ) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; } } } diff --git a/src/Cursor.vala b/src/Cursor.vala new file mode 100644 index 0000000..7a526d9 --- /dev/null +++ b/src/Cursor.vala @@ -0,0 +1,634 @@ +namespace CairoChart { + + /** + * {@link Chart} cursors. + */ + public class Cursors { + + protected unowned Chart chart; + protected List list = new List (); + protected Point active_cursor = Point(); // { get; protected set; default = Point128 (); } + protected bool is_cursor_active = false; // { get; protected set; default = false; } + protected Crossings[] crossings = {}; + + /** + * ``Cursors`` lines orientation. + */ + protected enum Orientation { + /** + * Vertical cursors. + */ + VERTICAL, + + /** + * Horizontal cursors. + */ + HORIZONTAL + } + + /** + * ``Cursors`` style. + */ + public struct Style { + /** + * ``Cursors`` lines orientation. + */ + public Orientation orientation; + + /** + * Maximum distance between mouse and cursor to remove it. + */ + public double select_distance; + + /** + * ``Cursors`` line style. + */ + public LineStyle line_style; + + /** + * Constructs a new ``Style``. + */ + public Style () { + orientation = Orientation.VERTICAL; + select_distance = 32; + line_style = LineStyle(Color(0.2, 0.2, 0.2, 0.8)); + } + } + + /** + * Cursor style. + */ + public Style style = Style(); + + /** + * Value label style. + */ + public LabelStyle label_style = new LabelStyle(); + + /** + * Has crossings. + */ + public bool has_crossings { get { return crossings.length != 0; } protected set {} } + + /** + * Constructs a new ``Chart``. + * @param chart ``Chart`` instance. + */ + public Cursors (Chart chart) { + this.chart = chart; + } + + /** + * Gets a copy of the ``Cursors``. + */ + public Cursors copy () { + var c = new Cursors (chart); + c.list = list.copy(); + c.active_cursor = active_cursor; + c.is_cursor_active = is_cursor_active; + c.style = style; + c.label_style = label_style.copy(); + c.crossings = crossings; + return c; + } + + /** + * Sets active cursor. + * @param p ``Cursor`` position. + * @param remove select for removing or not. + */ + public virtual void set_active (Point p, bool remove = false) { + active_cursor.x = chart.zoom.x0 + (p.x - chart.plarea.x0) / chart.plarea.width * chart.zoom.width; + active_cursor.y = chart.zoom.y1 - (chart.plarea.y1 - p.y) / chart.plarea.height * chart.zoom.height; + is_cursor_active = ! remove; + } + + /** + * Adds active cursor. + */ + public virtual void add_active () { + list.append (active_cursor); + is_cursor_active = false; + } + + /** + * Removes active cursor. + */ + public virtual void remove_active () { + if (list.length() == 0) return; + var distance = 1024.0 * 1024;//width * width; + uint rm_indx = 0; + uint i = 0; + foreach (var c in list) { + double d = distance; + switch (style.orientation) { + case Orientation.VERTICAL: d = (rel2scr_x(c.x) - rel2scr_x(active_cursor.x)).abs(); break; + case 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 < style.select_distance) + list.delete_link(list.nth(rm_indx)); + is_cursor_active = false; + } + + /** + * Gets delta between 2 cursors values. + * @param delta returns delta value. + */ + public bool get_delta (out Float128 delta) { + delta = 0; + if (chart.series.length == 0) return false; + if (list.length() + (is_cursor_active ? 1 : 0) != 2) return false; + if (chart.joint_x && style.orientation == Orientation.VERTICAL) { + Float128 val1 = chart.series[chart.zoom_1st_idx].axis_x.axis_val(rel2scr_x(list.nth_data(0).x)); + Float128 val2 = 0; + if (is_cursor_active) + val2 = chart.series[chart.zoom_1st_idx].axis_x.axis_val(rel2scr_x(active_cursor.x)); + else + val2 = chart.series[chart.zoom_1st_idx].axis_x.axis_val(rel2scr_x(list.nth_data(1).x)); + if (val2 > val1) + delta = val2 - val1; + else + delta = val1 - val2; + return true; + } + if (chart.joint_y && style.orientation == Orientation.HORIZONTAL) { + Float128 val1 = chart.series[chart.zoom_1st_idx].axis_y.axis_val(rel2scr_y(list.nth_data(0).y)); + Float128 val2 = 0; + if (is_cursor_active) + val2 = chart.series[chart.zoom_1st_idx].axis_y.axis_val(rel2scr_y(active_cursor.y)); + else + val2 = chart.series[chart.zoom_1st_idx].axis_y.axis_val(rel2scr_y(list.nth_data(1).y)); + if (val2 > val1) + delta = val2 - val1; + else + delta = val1 - val2; + return true; + } + return false; + } + + /** + * Gets delta formatted string. + */ + public string get_delta_str () { + Float128 delta = 0; + if (!get_delta(out delta)) return ""; + var str = ""; + var s = chart.series[chart.zoom_1st_idx]; + if (chart.joint_x) + switch (s.axis_x.dtype) { + case Axis.DType.NUMBERS: + str = s.axis_x.format.printf((LongDouble)delta); + break; + case Axis.DType.DATE_TIME: + var date = "", time = ""; + int64 days = (int64)(delta / 24 / 3600); + s.axis_x.print_dt(delta, out date, out time); + str = days.to_string() + " + " + time; + break; + } + if (chart.joint_y) { + str = s.axis_y.format.printf((LongDouble)delta); + } + return str; + } + + /** + * Draws cursors. + */ + public virtual void draw () { + if (chart.series.length == 0) return; + + var all_cursors = get_all_cursors(); + calc_cursors_value_positions(); + + for (var cci = 0, max_cci = crossings.length; cci < max_cci; ++cci) { + var low = Point128(chart.plarea.x1, chart.plarea.y1); // low and high + var high = Point128(chart.plarea.x0, chart.plarea.y0); // points of the cursor + unowned Cross[] ccs = crossings[cci].crossings; + style.line_style.apply(chart); + for (var ci = 0, max_ci = ccs.length; ci < max_ci; ++ci) { + var si = ccs[ci].series_index; + var s = chart.series[si]; + var p = ccs[ci].point; + var scrp = s.scr_pnt(p); + if (scrp.x < low.x) low.x = scrp.x; + if (scrp.y < low.y) low.y = scrp.y; + if (scrp.x > high.x) high.x = scrp.x; + if (scrp.y > high.y) high.y = scrp.y; + + if (chart.joint_x) { + switch (s.axis_x.position) { + case Axis.Position.LOW: high.y = chart.plarea.y1 + s.axis_x.font.vspacing; break; + case Axis.Position.HIGH: low.y = chart.plarea.y0 - s.axis_x.font.vspacing; break; + case Axis.Position.BOTH: + high.y = chart.plarea.y1 + s.axis_x.font.vspacing; + low.y = chart.plarea.y0 - s.axis_x.font.vspacing; + break; + } + } + if (chart.joint_y) { + switch (s.axis_y.position) { + case Axis.Position.LOW: low.x = chart.plarea.x0 - s.axis_y.font.hspacing; break; + case Axis.Position.HIGH: high.x = chart.plarea.x1 + s.axis_y.font.hspacing; break; + case Axis.Position.BOTH: + low.x = chart.plarea.x0 - s.axis_y.font.hspacing; + high.x = chart.plarea.x1 + s.axis_y.font.hspacing; + break; + } + } + + chart.ctx.move_to (ccs[ci].scr_point.x, ccs[ci].scr_point.y); + chart.ctx.line_to (ccs[ci].scr_value_point.x, ccs[ci].scr_value_point.y); + } + + var c = all_cursors.nth_data(crossings[cci].cursor_index); + + switch (style.orientation) { + case Orientation.VERTICAL: + if (low.y > high.y) continue; + chart.ctx.move_to (rel2scr_x(c.x), low.y); + chart.ctx.line_to (rel2scr_x(c.x), high.y); + + // show joint X value + if (chart.joint_x) { + var s = chart.series[chart.zoom_1st_idx]; + var x = s.axis_x.axis_val(rel2scr_x(c.x)); + string text = "", time_text = ""; + switch (s.axis_x.dtype) { + case Axis.DType.NUMBERS: text = s.axis_x.format.printf((LongDouble)x); break; + case Axis.DType.DATE_TIME: s.axis_x.print_dt(x, out text, out time_text); break; + } + var text_t = new Text(chart, text, s.axis_x.font, s.axis_x.color); + var time_text_t = new Text(chart, time_text, s.axis_x.font, s.axis_x.color); + var print_y = 0.0; + switch (s.axis_x.position) { + case Axis.Position.LOW: print_y = chart.area.y1 - s.axis_x.font.vspacing + - (chart.legend.position == Legend.Position.BOTTOM ? chart.legend.height : 0); + break; + case Axis.Position.HIGH: + var title_height = chart.title.height + (chart.legend.position == Legend.Position.TOP ? + chart.title.font.vspacing * 2 : chart.title.font.vspacing); + print_y = chart.area.y0 + title_height + s.axis_x.font.vspacing + + (chart.legend.position == Legend.Position.TOP ? chart.legend.height : 0); + switch (s.axis_x.dtype) { + case Axis.DType.NUMBERS: + print_y += text_t.height; + break; + case Axis.DType.DATE_TIME: + print_y += (s.axis_x.date_format == "" ? 0 : text_t.height) + + (s.axis_x.time_format == "" ? 0 : time_text_t.height) + + (s.axis_x.date_format == "" || s.axis_x.time_format == "" ? 0 : s.axis_x.font.vspacing); + break; + } + break; + } + var print_x = s.axis_x.compact_rec_pos (x, text_t); + chart.ctx.move_to (print_x, print_y); + + switch (s.axis_x.dtype) { + case Axis.DType.NUMBERS: + text_t.show(); + break; + case Axis.DType.DATE_TIME: + if (s.axis_x.date_format != "") text_t.show(); + print_x = s.axis_x.compact_rec_pos (x, time_text_t); + chart.ctx.move_to (print_x, print_y - (s.axis_x.date_format == "" ? 0 : text_t.height + s.axis_x.font.vspacing)); + if (s.axis_x.time_format != "") time_text_t.show(); + break; + } + } + break; + case Orientation.HORIZONTAL: + if (low.x > high.x) continue; + chart.ctx.move_to (low.x, rel2scr_y(c.y)); + chart.ctx.line_to (high.x, rel2scr_y(c.y)); + + // show joint Y value + if (chart.joint_y) { + var s = chart.series[chart.zoom_1st_idx]; + var y = s.axis_y.axis_val(rel2scr_y(c.y)); + var text_t = new Text(chart, s.axis_y.format.printf((LongDouble)y, s.axis_y.font)); + var print_y = s.axis_y.compact_rec_pos (y, text_t); + var print_x = 0.0; + switch (s.axis_y.position) { + case Axis.Position.LOW: + print_x = chart.area.x0 + s.axis_y.font.hspacing + + (chart.legend.position == Legend.Position.LEFT ? chart.legend.width : 0); + break; + case Axis.Position.HIGH: + print_x = chart.area.x1 - text_t.width - s.axis_y.font.hspacing + - (chart.legend.position == Legend.Position.RIGHT ? chart.legend.width : 0); + break; + } + chart.ctx.move_to (print_x, print_y); + text_t.show(); + } + break; + } + chart.ctx.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 = chart.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; + + // value label background + chart.color = label_style.bg_color; + chart.ctx.rectangle (svp.x - size.x / 2, svp.y - size.y / 2, size.x, size.y); + chart.ctx.fill(); + // value label frame + label_style.frame_style.apply(chart); + chart.ctx.move_to (svp.x - size.x / 2, svp.y - size.y / 2); + chart.ctx.rel_line_to (size.x, 0); + chart.ctx.rel_line_to (0, size.y); + chart.ctx.rel_line_to (-size.x, 0); + chart.ctx.rel_line_to (0, -size.y); + chart.ctx.stroke(); + + if (show_x) { + chart.color = s.axis_x.color; + var text_t = new Text(chart, s.axis_x.format.printf((LongDouble)point.x), s.axis_x.font); + chart.ctx.move_to (svp.x - size.x / 2, svp.y + text_t.height / 2); + if (chart.joint_x) chart.color = chart.joint_color; + text_t.show(); + } + + if (show_time) { + chart.color = s.axis_x.color; + string date = "", time = ""; + s.axis_x.print_dt(point.x, out date, out time); + var text_t = new Text(chart, time, s.axis_x.font); + var y = svp.y + text_t.height / 2; + if (show_date) y -= text_t.height / 2 + s.axis_x.font.vspacing / 2; + chart.ctx.move_to (svp.x - size.x / 2, y); + if (chart.joint_x) chart.color = chart.joint_color; + text_t.show(); + } + + if (show_date) { + chart.color = s.axis_x.color; + string date = "", time = ""; + s.axis_x.print_dt(point.x, out date, out time); + var text_t = new Text(chart, date, s.axis_x.font); + var y = svp.y + text_t.height / 2; + if (show_time) y += text_t.height / 2 + s.axis_x.font.vspacing / 2; + chart.ctx.move_to (svp.x - size.x / 2, y); + if (chart.joint_x) chart.color = chart.joint_color; + text_t.show(); + } + + if (show_y) { + chart.color = s.axis_y.color; + var text_t = new Text(chart, s.axis_y.format.printf((LongDouble)point.y), s.axis_y.font); + chart.ctx.move_to (svp.x + size.x / 2 - text_t.width, svp.y + text_t.height / 2); + if (chart.joint_y) chart.color = chart.joint_color; + text_t.show(); + } + } + } + } + + /** + * Evaluates crossings. + */ + public void eval_crossings () { + var all_cursors = get_all_cursors(); + + Crossings[] local_cursor_crossings = {}; + + for (var ci = 0, max_ci = all_cursors.length(); ci < max_ci; ++ci) { + var c = all_cursors.nth_data(ci); + switch (style.orientation) { + case Orientation.VERTICAL: if (c.x <= chart.zoom.x0 || c.x >= chart.zoom.x1) continue; break; + case Orientation.HORIZONTAL: if (c.y <= chart.zoom.y0 || c.y >= chart.zoom.y1) continue; break; + } + + Cross[] crossings = {}; + for (var si = 0, max_si = chart.series.length; si < max_si; ++si) { + var s = chart.series[si]; + if (!s.zoom_show) continue; + + var points = Math.sort_points (s, s.sort); + + for (var i = 0; i + 1 < points.length; ++i) { + switch (style.orientation) { + case Orientation.VERTICAL: + Float128 y = 0; + if (Math.vcross(s.scr_pnt(points[i]), s.scr_pnt(points[i+1]), rel2scr_x(c.x), + chart.plarea.y0, chart.plarea.y1, out y)) { + var point = Point128(s.axis_x.axis_val(rel2scr_x(c.x)), s.axis_y.axis_val(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); + Cross cc = {si, point, size, show_x, show_date, show_time, show_y}; + crossings += cc; + } + break; + case Orientation.HORIZONTAL: + Float128 x = 0; + if (Math.hcross(s.scr_pnt(points[i]), s.scr_pnt(points[i+1]), + chart.plarea.x0, chart.plarea.x1, rel2scr_y(c.y), out x)) { + var point = Point128(s.axis_x.axis_val(x), s.axis_y.axis_val(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); + Cross cc = {si, point, size, show_x, show_date, show_time, show_y}; + crossings += cc; + } + break; + } + } + } + if (crossings.length != 0) { + Crossings ccs = {ci, crossings}; + local_cursor_crossings += ccs; + } + } + crossings = local_cursor_crossings; + } + + protected struct Cross { + uint series_index; + Point128 point; + Point size; + bool show_x; + bool show_date; + bool show_time; + bool show_y; + Point scr_point; + Point scr_value_point; + } + + protected struct Crossings { + uint cursor_index; + Cross[] crossings; + } + + protected virtual Float128 rel2scr_x(Float128 x) { + return chart.plarea.x0 + chart.plarea.width * (x - chart.zoom.x0) / chart.zoom.width; + } + + protected virtual Float128 rel2scr_y(Float128 y) { + return chart.plarea.y0 + chart.plarea.height * (y - chart.zoom.y0) / chart.zoom.height; + } + + protected List get_all_cursors () { + var all_cursors = list.copy_deep ((src) => { return src; }); + if (is_cursor_active) + all_cursors.append(active_cursor); + return all_cursors; + } + + protected virtual void scr2cell (int m, int n, Point p, out int i, out int j) { + i = (int)((p.x - chart.plarea.x0) / chart.plarea.width * m); + j = (int)((p.y - chart.plarea.y0) / chart.plarea.height * n); + } + + protected virtual void cell2scr (int m, int n, int i, int j, out Point p) { + p = Point(chart.plarea.x0 + chart.plarea.width * (i + 0.5) / m, + chart.plarea.y0 + chart.plarea.height * (j + 0.5)/ n); + } + + protected virtual void calc_cursors_value_positions () { + // 1. Find maximum width/height of cursors values. + var max_width = 1.0, max_height = 1.0; + for (var ccsi = 0, max_ccsi = crossings.length; ccsi < max_ccsi; ++ccsi) { + for (var cci = 0, max_cci = crossings[ccsi].crossings.length; cci < max_cci; ++cci) { + unowned Cross[] cr = crossings[ccsi].crossings; + max_width = double.max(max_width, cr[cci].size.x + + 4 * double.max(chart.series[cr[cci].series_index].axis_x.font.hspacing, + chart.series[cr[cci].series_index].axis_y.font.hspacing)); + max_height = double.max(max_height, cr[cci].size.y + + 4 * double.max(chart.series[cr[cci].series_index].axis_x.font.vspacing, + chart.series[cr[cci].series_index].axis_y.font.vspacing)); + } + } + + // 2. Calculate 2D-array sizes. + var m = (int.max(1, (int)(chart.plarea.width / max_width))), + n = (int.max(1, (int)(chart.plarea.height / max_height))); + + // 3. Create 2D-array of bool or links to cursors values. + var arr2d_e = new bool[m, n]; + + // 4. Set Busy/Cross Cells + for (var ccsi = 0, max_ccsi = crossings.length; ccsi < max_ccsi; ++ccsi) { + for (var cci = 0, max_cci = crossings[ccsi].crossings.length; cci < max_cci; ++cci) { + unowned Cross[] cr = crossings[ccsi].crossings; + cr[cci].scr_point = chart.series[cr[cci].series_index].scr_pnt (cr[cci].point); + int i = 0, j = 0; + scr2cell(m, n, cr[cci].scr_point, out i, out j); + arr2d_e[i, j] = true; + } + } + + // 5. Calculate positions. + for (var ccsi = 0, max_ccsi = crossings.length; ccsi < max_ccsi; ++ccsi) { + for (var cci = 0, max_cci = crossings[ccsi].crossings.length; cci < max_cci; ++cci) { + unowned Cross[] cr = crossings[ccsi].crossings; + int i = 0, j = 0; + scr2cell(m, n, cr[cci].scr_point, out i, out j); + for (var radius = 1; radius < int.max(m, n); ++radius) { + bool found = false; + + // top, bottom + int[] ll = {int.max(0, j - radius), int.min(n - 1, j + radius)}; + foreach (var l in ll) { + for (var k = int.max(0, i - radius); k <= int.min(m - 1, i + radius); ++k) { + if (k == i) continue; + if (!arr2d_e[k, l]) { + arr2d_e[k, l] = true; + cell2scr(m, n, k, l, out cr[cci].scr_value_point); + found = true; + break; + } + } + if (found) break; + } + if (found) break; + + // left, right + int[] kk = {int.max(0, i - radius), int.min(m - 1, i + radius)}; + foreach (var k in kk) { + for (var l = int.max(0, j - radius); l <= int.min(n - 1, j + radius); ++l) { + if (l == j) continue; + if (!arr2d_e[k, l]) { + arr2d_e[k, l] = true; + cell2scr(m, n, k, l, out cr[cci].scr_value_point); + found = true; + break; + } + } + if (found) break; + } + if (found) break; + } + } + } + } + + 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 (style.orientation) { + case Orientation.VERTICAL: + show_y = true; + if (!chart.joint_x) + switch (s.axis_x.dtype) { + case Axis.DType.NUMBERS: show_x = true; break; + case Axis.DType.DATE_TIME: + if (s.axis_x.date_format != "") show_date = true; + if (s.axis_x.time_format != "") show_time = true; + break; + } + break; + case Orientation.HORIZONTAL: + if (!chart.joint_y) show_y = true; + switch (s.axis_x.dtype) { + case Axis.DType.NUMBERS: show_x = true; break; + case Axis.DType.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, Point128 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.print_dt(p.x, out date, out time); + var date_t = new Text(chart, date, s.axis_x.font, s.axis_x.color); + var time_t = new Text(chart, time, s.axis_x.font, s.axis_x.color); + var x_t = new Text(chart, s.axis_x.format.printf((LongDouble)p.x), s.axis_x.font, s.axis_x.color); + var y_t = new Text(chart, s.axis_y.format.printf((LongDouble)p.y), s.axis_y.font, s.axis_y.color); + var h_x = 0.0, h_y = 0.0; + if (show_x) { size.x = x_t.width; h_x = x_t.height; } + if (show_date) { size.x = date_t.width; h_x = date_t.height; } + if (show_time) { size.x = double.max(size.x, time_t.width); h_x += time_t.height; } + if (show_y) { size.x += y_t.width; h_y = y_t.height; } + if ((show_x || show_date || show_time) && show_y) size.x += s.axis_x.font.hspacing + s.axis_y.font.hspacing; + if (show_date && show_time) h_x += s.axis_x.font.vspacing; + size.y = double.max (h_x, h_y); + } + } +} diff --git a/src/Font.vala b/src/Font.vala new file mode 100644 index 0000000..857975a --- /dev/null +++ b/src/Font.vala @@ -0,0 +1,90 @@ +namespace CairoChart { + + /** + * ``Font`` style. + */ + [Compact] + public class Font : Object { + + /** + * A font family name, encoded in UTF-8. + */ + public virtual string family { get; set; } + + /** + * The new font size, in user space units. + */ + public virtual double size { get; set; } + + /** + * The slant for the font. + */ + public virtual Cairo.FontSlant slant { get; set; } + + /** + * The weight for the font. + */ + public virtual Cairo.FontWeight weight { get; set; } + + /** + * Font/Text orientation. + */ + public virtual Gtk.Orientation orient { get; set; } + + /** + * Vertical spacing. + */ + public double vspacing = 2; + + /** + * Horizontal spacing. + */ + public double hspacing = 2; + + /** + * Both vertical & horizontal spacing (set only). + */ + public virtual double spacing { + protected get { + return 0; + } + set { + vspacing = hspacing = value; + } + default = 2; + } + + /** + * Constructs a new ``Font``. + * @param family a font family name, encoded in UTF-8. + * @param size the new font size, in user space units. + * @param slant the slant for the font. + * @param weight the weight for the font. + * @param orient font/text orientation. + */ + public Font (string family = "Sans", + double size = 10, + Cairo.FontSlant slant = Cairo.FontSlant.NORMAL, + Cairo.FontWeight weight = Cairo.FontWeight.NORMAL, + Gtk.Orientation orient = Gtk.Orientation.HORIZONTAL, + double vspacing = 2, + double hspacing = 2 + ) { + this.family = family; + this.size = size; + this.slant = slant; + this.weight = weight; + this.orient = orient; + } + + /** + * Gets a copy of the ``Font``. + */ + public virtual Font copy () { + var f = new Font(family, size, slant, weight, orient); + f.vspacing = vspacing; + f.hspacing = hspacing; + return f; + } + } +} diff --git a/src/FontStyle.vala b/src/FontStyle.vala deleted file mode 100644 index 685fd0e..0000000 --- a/src/FontStyle.vala +++ /dev/null @@ -1,27 +0,0 @@ -namespace Gtk.CairoChart { - public enum FontOrient { - HORIZONTAL = 0, - VERTICAL - } - - public struct FontStyle { - string family; - Cairo.FontSlant slant; - Cairo.FontWeight weight; - - FontOrient orientation; - double size; - - public FontStyle (string family = "Sans", - Cairo.FontSlant slant = Cairo.FontSlant.NORMAL, - Cairo.FontWeight weight = Cairo.FontWeight.NORMAL, - double size = 10, - FontOrient orientation = FontOrient.HORIZONTAL) { - this.family = family; - this.slant = slant; - this.weight = weight; - this.size = size; - this.orientation = orientation; - } - } -} diff --git a/src/Grid.vala b/src/Grid.vala deleted file mode 100644 index d57691e..0000000 --- a/src/Grid.vala +++ /dev/null @@ -1,22 +0,0 @@ -namespace Gtk.CairoChart { - public class Grid { - /*public enum GridType { - PRICK_LINE = 0, // default - LINE - }*/ - public Color color = Color (0, 0, 0, 0.1); - - public LineStyle line_style = LineStyle (); - - public Grid copy () { - var grid = new Grid (); - grid.color = this.color; - grid.line_style = this.line_style; - return grid; - } - - public Grid () { - line_style.dashes = {2, 3}; - } - } -} diff --git a/src/Label.vala b/src/Label.vala new file mode 100644 index 0000000..0ec8aff --- /dev/null +++ b/src/Label.vala @@ -0,0 +1,46 @@ +namespace CairoChart { + + /** + * ``Cursors`` values style. + */ + public class LabelStyle { + + /** + * Background color. + */ + public Color bg_color; + + /** + * Frame line style. + */ + public LineStyle frame_style; + + /** + * Font style. + */ + public Font font; + + /** + * Constructs a new ``LabelStyle``. + * @param font font style. + * @param bg_color background color. + * @param frame_style frame line style. + */ + public LabelStyle ( + Color bg_color = Color(1, 1, 1, 1), + LineStyle frame_style = LineStyle(Color(0, 0, 0, 0.1)), + Font font = new Font() + ) { + this.bg_color = bg_color; + this.frame_style = frame_style; + this.font = font; + } + + /** + * Gets a copy of the ``LabelStyle``. + */ + public virtual LabelStyle copy () { + return new LabelStyle(bg_color, frame_style, font); + } + } +} diff --git a/src/LabelStyle.vala b/src/LabelStyle.vala deleted file mode 100644 index fc610ee..0000000 --- a/src/LabelStyle.vala +++ /dev/null @@ -1,8 +0,0 @@ -namespace Gtk.CairoChart { - public struct LabelStyle { - FontStyle font_style; - LineStyle frame_line_style; - Color bg_color; - Color frame_color; - } -} diff --git a/src/Legend.vala b/src/Legend.vala index 93e63b9..54a9196 100644 --- a/src/Legend.vala +++ b/src/Legend.vala @@ -1,28 +1,275 @@ -namespace Gtk.CairoChart { +namespace CairoChart { + + /** + * {@link Chart} ``Legend``. + */ public class Legend { + + protected unowned Chart chart; + protected double [] max_font_heights; + + /** + * Show legend? + */ + public bool show = true; + + /** + * ``Legend`` position. + */ public enum Position { - TOP = 0, // default + /** + * Top position. + */ + TOP = 0, + + /** + * Left position. + */ LEFT, + + /** + * Right position. + */ RIGHT, + + /** + * Bottom position. + */ BOTTOM } - public Position position = Position.TOP; - public FontStyle font_style = FontStyle(); - public Color bg_color = Color(1, 1, 1); - public LineStyle border_style = LineStyle (); - public double indent = 5; - public Legend copy () { - var legend = new Legend (); + /** + * Position. + */ + public Position position = Position.TOP; + + /** + * ``Legend`` background color. + */ + public Color bg_color = Color(1, 1, 1); + + /** + * Border line style. + */ + public LineStyle border_style = LineStyle (); + + /** + * Both vertical & horizontal spacing. + */ + public double spacing = 5; + + /** + * ``Legend`` width. + */ + public double width { get; protected set; } + + /** + * ``Legend`` height. + */ + public double height { get; protected set; } + + /** + * {@link Series} line length. + */ + public double line_length = 30; + + /** + * Constructs a new ``Legend``. + * @param chart ``Chart`` instance. + */ + public Legend (Chart chart) { + this.chart = chart; + border_style.color = Color (0, 0, 0, 0.3); + } + + /** + * Gets a copy of the ``Legend``. + */ + public virtual Legend copy () { + var legend = new Legend (chart); legend.position = this.position; - legend.font_style = this.font_style; legend.bg_color = this.bg_color; - legend.indent = this.indent; + legend.spacing = this.spacing; + legend.height = this.height; + legend.line_length = this.line_length; + legend.width = this.width; + legend.show = this.show; + legend.max_font_heights = this.max_font_heights; return legend; } - public Legend () { - border_style.color = Color (0, 0, 0, 0.3); + /** + * Draws the ``Legend``. + */ + public virtual void draw () { + if (!show) return; + process (ProcessType.CALC); + process (ProcessType.DRAW); + } + + protected virtual void draw_rect (out double x0, out double y0) { + x0 = y0 = 0; + if (chart.ctx != null) { + switch (position) { + case Position.TOP: + x0 = (chart.area.width - width) / 2; + var title_height = chart.title.height + (chart.legend.position == Position.TOP ? + chart.title.font.vspacing * 2 : chart.title.font.vspacing); + y0 = title_height; + break; + + case Position.BOTTOM: + x0 = (chart.area.width - width) / 2; + y0 = chart.area.height - height; + break; + + case Position.LEFT: + x0 = 0; + y0 = (chart.area.height - height) / 2; + break; + + case Position.RIGHT: + x0 = chart.area.width - width; + y0 = (chart.area.height - height) / 2; + break; + } + chart.color = bg_color; + chart.ctx.rectangle (x0, y0, width, height); + chart.ctx.fill(); + border_style.apply(chart); + chart.ctx.move_to (x0, y0); + chart.ctx.rel_line_to (width, 0); + chart.ctx.rel_line_to (0, height); + chart.ctx.rel_line_to (-width, 0); + chart.ctx.rel_line_to (0, -height); + chart.ctx.stroke (); + } + } + + protected enum ProcessType { + CALC = 0, // default + DRAW + } + + protected virtual void process (ProcessType process_type) { + var legend_x0 = 0.0, legend_y0 = 0.0; + var heights_idx = 0; + var leg_width_sum = 0.0; + var leg_height_sum = 0.0; + var max_font_h = 0.0; + + double [] mfh = max_font_heights; + + // prepare + switch (process_type) { + case ProcessType.CALC: + width = 0; + height = 0; + mfh = {}; + heights_idx = 0; + break; + case ProcessType.DRAW: + draw_rect(out legend_x0, out legend_y0); + break; + } + + foreach (var s in chart.series) { + if (!s.zoom_show) continue; + + // carry + switch (position) { + case Position.TOP: + case Position.BOTTOM: + var ser_title_width = line_length + s.title.width + s.title.font.hspacing * 2; + if (leg_width_sum + ser_title_width > chart.area.width) { // carry + leg_height_sum += max_font_h; + switch (process_type) { + case ProcessType.CALC: + mfh += max_font_h; + width = double.max(width, leg_width_sum); + break; + case ProcessType.DRAW: + heights_idx++; + break; + } + leg_width_sum = 0; + max_font_h = 0; + } + break; + } + + switch (process_type) { + case ProcessType.DRAW: + var x = legend_x0 + leg_width_sum + s.title.font.hspacing; + var y = legend_y0 + leg_height_sum + mfh[heights_idx] / 2; + + // series title + chart.ctx.move_to (x + line_length, y + s.title.height / 2); + chart.color = s.title.color; + s.title.show(); + + // series line style + chart.ctx.move_to (x, y); + s.line_style.apply(chart); + chart.ctx.rel_line_to (line_length, 0); + chart.ctx.stroke(); + s.marker.draw_at_pos (Point(x + line_length / 2, y)); + break; + } + + switch (position) { + case Position.TOP: + case Position.BOTTOM: + var ser_title_width = line_length + s.title.width + s.title.font.hspacing * 2; + leg_width_sum += ser_title_width; + max_font_h = double.max (max_font_h, s.title.height + s.title.font.vspacing * 2); + break; + + case Position.LEFT: + case Position.RIGHT: + switch (process_type) { + case ProcessType.CALC: + mfh += s.title.height + s.title.font.vspacing * 2; + width = double.max (width, s.title.font.hspacing * 2 + line_length + s.title.width); + break; + case ProcessType.DRAW: + heights_idx++; + break; + } + leg_height_sum += s.title.height + s.title.font.vspacing * 2; + break; + } + } + + // TOP, BOTTOM + switch (position) { + case Position.TOP: + case Position.BOTTOM: + if (leg_width_sum != 0) { + leg_height_sum += max_font_h; + switch (process_type) { + case ProcessType.CALC: + mfh += max_font_h; + width = double.max(width, leg_width_sum); + break; + } + } + break; + } + + switch (process_type) { + case ProcessType.CALC: + height = leg_height_sum; + switch (position) { + case Position.TOP: chart.evarea.y0 += height + spacing; break; + case Position.BOTTOM: chart.evarea.y1 -= height + spacing; break; + case Position.LEFT: chart.evarea.x0 += width + spacing; break; + case Position.RIGHT: chart.evarea.x1 -= width + spacing; break; + } + break; + } + + max_font_heights = mfh; } } } diff --git a/src/Line.vala b/src/Line.vala new file mode 100644 index 0000000..cbd4486 --- /dev/null +++ b/src/Line.vala @@ -0,0 +1,72 @@ +namespace CairoChart { + + /** + * Line Style. + */ + public struct LineStyle { + + /** + * Line color. + */ + Color color; + + /** + * A line width. + */ + double width; + + /** + * An array specifying alternate lengths of on and off stroke portions. + */ + double[]? dashes; + + /** + * An offset into the dash pattern at which the stroke should start. + */ + double dash_offset; + /** + * A line join style. + */ + Cairo.LineJoin join; + + /** + * A line cap style. + */ + Cairo.LineCap cap; + + /** + * Constructs a new ``LineStyle``. + * @param color line color. + * @param width a line width. + * @param dashes an array specifying alternate lengths of on and off stroke portions. + * @param dash_offset an offset into the dash pattern at which the stroke should start. + * @param join a line join style. + * @param cap a line cap style. + */ + public LineStyle (Color color = Color(), + double width = 1, + double[]? dashes = null, + double dash_offset = 0, + Cairo.LineJoin join = Cairo.LineJoin.MITER, + Cairo.LineCap cap = Cairo.LineCap.ROUND + ) { + this.color = color; + this.width = width; + this.dashes = dashes; + this.dash_offset = dash_offset; + this.join = join; + this.cap = cap; + } + + /** + * Applies current style to the {@link Chart} ``Context``. + */ + public void apply (Chart chart) { + chart.color = color; + chart.ctx.set_line_width(width); + chart.ctx.set_dash(dashes, dash_offset); + chart.ctx.set_line_join(join); + chart.ctx.set_line_cap(cap); + } + } +} diff --git a/src/LineStyle.vala b/src/LineStyle.vala deleted file mode 100644 index 4a58d1e..0000000 --- a/src/LineStyle.vala +++ /dev/null @@ -1,24 +0,0 @@ -namespace Gtk.CairoChart { - public struct LineStyle { - double width; - Cairo.LineJoin line_join; - Cairo.LineCap line_cap; - double[]? dashes; - double dash_offset; - Color color; - - public LineStyle (Color color = Color(), - double width = 1, - double[]? dashes = null, double dash_offset = 0, - Cairo.LineJoin line_join = Cairo.LineJoin.MITER, - Cairo.LineCap line_cap = Cairo.LineCap.ROUND - ) { - this.width = width; - this.line_join = line_join; - this.line_cap = line_cap; - this.dashes = dashes; - this.dash_offset = dash_offset; - this.color = color; - } - } -} diff --git a/src/Marker.vala b/src/Marker.vala new file mode 100644 index 0000000..c17206f --- /dev/null +++ b/src/Marker.vala @@ -0,0 +1,100 @@ +namespace CairoChart { + + /** + * {@link Series} Marker. + */ + public class Marker { + + protected unowned Chart chart; + + /** + * ``Marker`` shape. + */ + public enum Shape { + NONE = 0, + SQUARE, + CIRCLE, + TRIANGLE, + PRICLE_SQUARE, + PRICLE_CIRCLE, + PRICLE_TRIANGLE + } + + /** + * ``Marker`` shape. + */ + public Shape shape; + + /** + * ``Marker`` size. + */ + public double size; + + /** + * Constructs a new ``Marker``. + * @param chart ``Chart`` instance. + * @param shape ``Marker`` shape. + * @param size ``Marker`` size. + */ + public Marker (Chart chart, + Shape shape = Shape.NONE, + double size = 8 + ) { + this.chart = chart; + this.shape = shape; + this.size = size; + } + + /** + * Gets a copy of the ``Marker``. + */ + public virtual Marker copy () { + return new Marker (chart, shape, size); + } + + /** + * Draws the ``Marker`` at specific position. + * @param p coordinates. + */ + public virtual void draw_at_pos (Point p) { + chart.ctx.move_to (p.x, p.y); + switch (shape) { + case Shape.SQUARE: + chart.ctx.rectangle (p.x - size / 2, p.y - size / 2, size, size); + chart.ctx.fill(); + break; + + case Shape.CIRCLE: + chart.ctx.arc (p.x, p.y, size / 2, 0, 2 * GLib.Math.PI); + chart.ctx.fill(); + break; + + case Shape.TRIANGLE: + chart.ctx.move_to (p.x - size / 2, p.y - size / 2); + chart.ctx.rel_line_to (size, 0); + chart.ctx.rel_line_to (-size / 2, size); + chart.ctx.rel_line_to (-size / 2, -size); + chart.ctx.fill(); + break; + + case Shape.PRICLE_SQUARE: + chart.ctx.rectangle (p.x - size / 2, p.y - size / 2, size, size); + chart.ctx.stroke(); + break; + + case Shape.PRICLE_CIRCLE: + chart.ctx.arc (p.x, p.y, size / 2, 0, 2 * GLib.Math.PI); + chart.ctx.stroke(); + break; + + case Shape.PRICLE_TRIANGLE: + chart.ctx.move_to (p.x - size / 2, p.y - size / 2); + chart.ctx.rel_line_to (size, 0); + chart.ctx.rel_line_to (-size / 2, size); + chart.ctx.rel_line_to (-size / 2, -size); + chart.ctx.stroke(); + break; + } + } + } +} diff --git a/src/Math.vala b/src/Math.vala new file mode 100644 index 0000000..493da8e --- /dev/null +++ b/src/Math.vala @@ -0,0 +1,161 @@ +namespace CairoChart { + + namespace Math { + + internal Float128 calc_round_step (Float128 aver_step, bool date_time = false) { + Float128 step = 1; + + if (aver_step > 1) { + if (date_time) while (step < aver_step) step *= 60; + if (date_time) while (step < aver_step) step *= 60; + if (date_time) while (step < aver_step) step *= 24; + while (step < aver_step) step *= 10; + if (step / 5 > aver_step) step /= 5; + while (step / 2 > aver_step) step /= 2; + } else if (aver_step > 0) { + while (step / 10 > aver_step) step /= 10; + if (step / 5 > aver_step) step /= 5; + while (step / 2 > aver_step) step /= 2; + } + + return step; + } + + internal bool coord_cross (double a_min, double a_max, double b_min, double b_max) { + if ( a_min < a_max <= b_min < b_max + || b_min < b_max <= a_min < a_max) + return false; + return true; + } + + /*internal bool rect_cross (Cairo.Rectangle r1, Cairo.Rectangle r2) { + return coord_cross(r1.x, r1.x + r1.width, r2.x, r2.x + r2.width) + && coord_cross(r1.y, r1.y + r1.height, r2.y, r2.y + r2.height); + }*/ + + internal bool point_belong (Float128 p, Float128 a, Float128 b) { + if (a > b) { Float128 tmp = a; a = b; b = tmp; } + if (a <= p <= b) return true; + return false; + } + + internal bool x_in_range (double x, double x0, double x1) { + if (x0 <= x <= x1 || x1 <= x <= x0) + return true; + return false; + } + + internal bool y_in_range (double y, double y0, double y1) { + if (y0 <= y <= y1 || y1 <= y <= y0) + return true; + return false; + } + + internal bool point_in_rect (Point p, double x0, double x1, double y0, double y1) { + if (x_in_range(p.x, x0, x1) && y_in_range(p.y, y0, y1)) + return true; + return false; + } + + internal bool hcross (Point a1, Point a2, double h_x1, double h_x2, double h_y, out double x) { + x = 0; + if (a1.y == a2.y) return false; + if (a1.y >= h_y && a2.y >= h_y || a1.y <= h_y && a2.y <= h_y) return false; + x = a1.x + (a2.x - a1.x) * (h_y - a1.y) / (a2.y - a1.y); + if (h_x1 <= x <= h_x2 || h_x2 <= x <= h_x1) + return true; + return false; + } + + internal bool vcross (Point a1, Point a2, double v_x, double v_y1, double v_y2, out double y) { + y = 0; + if (a1.x == a2.x) return false; + if (a1.x >= v_x && a2.x >= v_x || a1.x <= v_x && a2.x <= v_x) return false; + y = a1.y + (a2.y - a1.y) * (v_x - a1.x) / (a2.x - a1.x); + if (v_y1 <= y <= v_y2 || v_y2 <= y <= v_y1) + return true; + return false; + } + + internal delegate int PointComparator(Point128 a, Point128 b); + + internal void sort_points_delegate(Point128[] points, PointComparator compare) { + for(var i = 0; i < points.length; ++i) { + for(var j = i + 1; j < points.length; ++j) { + if(compare(points[i], points[j]) > 0) { + var tmp = points[i]; + points[i] = points[j]; + points[j] = tmp; + } + } + } + } + + internal bool cut_line (Point p_min, Point p_max, Point a, Point b, out Point c, out Point d) { + int ncross = 0; + Float128 x = 0, y = 0; + Point pc[4]; + if (hcross(a, b, p_min.x, p_max.x, p_min.y, out x)) + pc[ncross++] = Point(x, p_min.y); + if (hcross(a, b, p_min.x, p_max.x, p_max.y, out x)) + pc[ncross++] = Point(x, p_max.y); + if (vcross(a, b, p_min.x, p_min.y, p_max.y, out y)) + pc[ncross++] = Point(p_min.x, y); + if (vcross(a, b, p_max.x, p_min.y, p_max.y, out y)) + pc[ncross++] = Point(p_max.x, y); + c = a; + d = b; + if (ncross == 0) { + if ( point_in_rect (a, p_min.x, p_max.x, p_min.y, p_max.y) + && point_in_rect (b, p_min.x, p_max.x, p_min.y, p_max.y)) + return true; + return false; + } + if (ncross >= 2) { + c = pc[0]; d = pc[1]; + return true; + } + if (ncross == 1) { + if (point_in_rect (a, p_min.x, p_max.x, p_min.y, p_max.y)) { + c = a; + d = pc[0]; + return true; + } else if (point_in_rect (b, p_min.x, p_max.x, p_min.y, p_max.y)) { + c = b; + d = pc[0]; + return true; + } + } + return false; + } + + internal Point128[] sort_points (Series s, Series.Sort sort) { + var points = s.points; + switch(sort) { + case Series.Sort.BY_X: + sort_points_delegate(points, (a, b) => { + if (a.x < b.x) return -1; + if (a.x > b.x) return 1; + return 0; + }); + break; + case Series.Sort.BY_Y: + sort_points_delegate(points, (a, b) => { + if (a.y < b.y) return -1; + if (a.y > b.y) return 1; + return 0; + }); + break; + } + return points; + } + + internal int find_arr (G[] arr, G elem) { + for (var i = 0; i < arr.length; ++i) { + if (arr[i] == elem) + return i; + } + return -1; + } + } +} diff --git a/src/Place.vala b/src/Place.vala deleted file mode 100644 index 6ac611d..0000000 --- a/src/Place.vala +++ /dev/null @@ -1,52 +0,0 @@ -namespace Gtk.CairoChart { - public class Place { - double _x_low = 0; - double _x_high = 0; - double _y_low = 0; - double _y_high = 0; - public double x_low { - get { return _x_low; } - set { _x_low = zoom_x_low = value; } - default = 0; - } - public double x_high { - get { return _x_high; } - set { _x_high = zoom_x_high = value; } - default = 0; - } - public double y_low { - get { return _y_low; } - set { _y_low = zoom_y_low = value; } - default = 0; - } - public double y_high { - get { return _y_high; } - set { _y_high = zoom_y_high = value; } - default = 0; - } - public double zoom_x_low = 0; - public double zoom_x_high = 1; - public double zoom_y_low = 0; - public double zoom_y_high = 1; - - public Place copy () { - var place = new Place (); - place.x_low = this.x_low; - place.x_high = this.x_high; - place.y_low = this.y_low; - place.y_high = this.y_high; - return place; - } - - public Place (double x_low = 0, double x_high = 1, double y_low = 0, double y_high = 1) { - this.x_low = x_low; - this.x_high = x_high; - this.y_low = y_low; - this.y_high = y_high; - zoom_x_low = x_low; - zoom_x_high = x_high; - zoom_y_low = y_low; - zoom_y_high = y_high; - } - } -} diff --git a/src/Point.vala b/src/Point.vala index 30bee7b..8ad3920 100644 --- a/src/Point.vala +++ b/src/Point.vala @@ -1,9 +1,51 @@ -namespace Gtk.CairoChart { +namespace CairoChart { + + /** + * 64-bit point. + */ public struct Point { + + /** + * X-coordinate. + */ + double x; + + /** + * Y-coordinate. + */ + double y; + + /** + * Constructs a new ``Point``. + * @param x x-coordinate. + * @param y y-coordinate. + */ + public Point (double x = 0, double y = 0) { + this.x = x; this.y = y; + } + } + + /** + * 128-bit point. + */ + public struct Point128 { + + /** + * X-coordinate. + */ Float128 x; + + /** + * Y-coordinate. + */ Float128 y; - public Point (Float128 x = 0.0, Float128 y = 0.0) { + /** + * Constructs a new ``Point128``. + * @param x x-coordinate. + * @param y y-coordinate. + */ + public Point128 (Float128 x = 0, Float128 y = 0) { this.x = x; this.y = y; } } diff --git a/src/Range.vala b/src/Range.vala new file mode 100644 index 0000000..6ed8bf9 --- /dev/null +++ b/src/Range.vala @@ -0,0 +1,118 @@ +namespace CairoChart { + + /** + * Linear range. + */ + public class Range { + + protected Float128 _min = 0; + protected Float128 _max = 1; + + /** + * Zoomed min bound. + */ + public Float128 zmin = 0; + + /** + * Zoomed max bound. + */ + public Float128 zmax = 1; + + /** + * Low bound. + */ + public virtual Float128 min { + get { + return _min; + } + set { + zmin = _min = value; + } + } + + /** + * High bound. + */ + public virtual Float128 max { + get { + return _max; + } + set { + zmax = _max = value; + } + } + + /** + * ``Range`` value. + */ + public virtual Float128 range { + get { + return _max - _min; + } + set { + zmax = _max = _min + value; + } + } + + /** + * ``Range`` zoomed value. + */ + public virtual Float128 zrange { + get { + return zmax - zmin; + } + set { + zmax = zmin + value; + } + } + + /** + * Constructs a new ``Range``. + */ + public Range () { } + + /** + * Constructs a new ``Range`` with a ``Range`` instance. + * @param range ``Range`` instance. + */ + public Range.with_range (Range range) { + this.min = range.min; + this.max = range.max; + } + + /** + * Constructs a new ``Range`` with absolute coordinates. + * @param min minimum/low limit. + * @param max maximum/high limit. + */ + public Range.with_abs (Float128 min, Float128 max) { + this.min = min; + this.max = max; + } + + /** + * Constructs a new ``Range`` with relative coordinates. + * @param min minimum/low limit. + * @param range length of the ``Range``. + */ + public Range.with_rel (Float128 min, Float128 range) { + this.min = min; + this.range = range; + } + + /** + * Gets a copy of the ``Range``. + */ + public virtual Range copy () { + return new Range.with_range(this); + } + + /** + * Zooms out ``Range``. + */ + public virtual void zoom_out () { + zmin = min; + zmax = max; + } + } +} diff --git a/src/Series.vala b/src/Series.vala index 284921e..624811f 100644 --- a/src/Series.vala +++ b/src/Series.vala @@ -1,65 +1,111 @@ using Cairo; -namespace Gtk.CairoChart { +namespace CairoChart { + /** + * ``Chart`` series. + */ public class Series { - public Point[] points = {}; - public enum Sort { - BY_X = 0, - BY_Y = 1, - NO_SORT - } - public Sort sort = Sort.BY_X; + protected unowned Chart chart { get; protected set; default = null; } - public Axis axis_x = new Axis(); - public Axis axis_y = new Axis(); - - public enum MarkerType { - NONE = 0, // default - SQUARE, - CIRCLE, - TRIANGLE, - PRICLE_SQUARE, - PRICLE_CIRCLE, - PRICLE_TRIANGLE - } - - public Place place = new Place(); - public Text title = new Text (); - public MarkerType marker_type = MarkerType.SQUARE; - - public Grid grid = new Grid (); + /** + * Title of the ``Chart``. + */ + public Text title; + /** + * ``Series`` line style. + */ public LineStyle line_style = LineStyle (); - protected Color _color = Color (0.0, 0.0, 0.0, 1.0); - public Color color { - get { return _color; } - set { - _color = value; - line_style.color = _color; - axis_x.color = _color; - axis_y.color = _color; - grid.color = _color; - grid.color.alpha = 0.5; - grid.line_style.color = _color; - grid.line_style.color.alpha = 0.5; - } - default = Color (0.0, 0.0, 0.0, 1.0); + /** + * 128-bit (X;Y) points. + */ + public Point128[] points = {}; + + /** + * ``Marker`` style. + */ + public Marker marker; + + /** + * Sort style. + */ + public enum Sort { + /** + * Sort by X. + */ + BY_X = 0, + + /** + * Sort by Y. + */ + BY_Y = 1, + + /** + * Do not sort points on draw(). + */ + UNSORTED } + /** + * Sort style. + */ + public Sort sort = Sort.BY_X; + + /** + * X-axis. + */ + public Axis axis_x; + + /** + * Y-axis. + */ + public Axis axis_y; + + /** + * ``Series`` color (set only). + */ + public Color color { + protected get { return Color(); } + set { + line_style.color = value; + axis_x.color = value; + axis_y.color = value; + axis_x.grid_style.color.red = axis_y.grid_style.color.red = value.red; + axis_x.grid_style.color.green = axis_y.grid_style.color.green = value.green; + axis_x.grid_style.color.blue = axis_y.grid_style.color.blue = value.blue; + } + default = Color (0, 0, 0, 1); + } + + /** + * Show the ``Series`` in zoomed area or not. + */ public bool zoom_show = true; - public Series copy () { - var series = new Series (); - series._color = this._color; + /** + * Constructs a new ``Series``. + * @param chart ``Chart`` instance. + */ + public Series (Chart chart) { + this.chart = chart; + title = new Text(chart); + axis_x = new Axis(chart, this, true); + axis_y = new Axis(chart, this, false); + this.marker = new Marker(chart); + } + + /** + * Gets a copy of the ``Series``. + */ + public virtual Series copy () { + var series = new Series (chart); series.axis_x = this.axis_x.copy (); series.axis_y = this.axis_y.copy (); - series.grid = this.grid.copy (); series.line_style = this.line_style; - series.marker_type = this.marker_type; - series.place = this.place.copy(); + series.marker = this.marker; series.points = this.points; series.sort = this.sort; series.title = this.title.copy(); @@ -67,7 +113,58 @@ namespace Gtk.CairoChart { return series; } - public Series () { + /** + * Gets screen point by real ``Series`` (X;Y) value. + * @param p real ``Series`` (X;Y) value. + */ + public virtual Point scr_pnt (Point128 p) { + return Point(axis_x.scr_pos(p.x), axis_y.scr_pos(p.y)); + } + + /** + * Gets real ``Series`` (X;Y) value by plot area screen point. + * @param p (X;Y) screen point. + */ + public virtual Point128 axis_pnt (Point p) { + return Point128 (axis_x.axis_val(p.x), axis_y.axis_val(p.y)); + } + + /** + * Draws the ``Series``. + */ + public virtual void draw () { + var points = Math.sort_points(this, sort); + line_style.apply(chart); + // draw series line + for (int i = 1; i < points.length; ++i) { + Point c, d; + if (Math.cut_line ( + Point(chart.plarea.x0, chart.plarea.y0), + Point(chart.plarea.x1, chart.plarea.y1), + scr_pnt(points[i-1]), + scr_pnt(points[i]), + out c, out d) + ) { + chart.ctx.move_to (c.x, c.y); + chart.ctx.line_to (d.x, d.y); + } + } + chart.ctx.stroke(); + for (int i = 0; i < points.length; ++i) { + var p = scr_pnt(points[i]); + if (Math.point_in_rect (p, chart.plarea.x0, chart.plarea.x1, + chart.plarea.y0, chart.plarea.y1)) + marker.draw_at_pos(p); + } + } + + /** + * Zooms out the ``Series``. + */ + public virtual void zoom_out () { + zoom_show = true; + axis_x.zoom_out(); + axis_y.zoom_out(); } } } diff --git a/src/Text.vala b/src/Text.vala index 77c7ef7..ad2a16f 100644 --- a/src/Text.vala +++ b/src/Text.vala @@ -1,85 +1,142 @@ -namespace Gtk.CairoChart { - [Compact] +namespace CairoChart { + + /** + * ``CairoChart`` Text. + */ public class Text { - public string text = ""; - public FontStyle style = FontStyle (); + + protected unowned Chart chart; + protected string _text; + protected Font _font; + protected Cairo.TextExtents? _ext; + + /** + * ``Text`` string. + */ + public virtual string text { + get { + return _text; + } + set { + _text = value; + _ext = null; + } + } + + /** + * ``Text`` font style. + */ + public virtual Font font { + get { + return _font; + } + set { + _font = value; + _font.notify.connect((s, p) => { + _ext = null; + }); + _ext = null; + } + } + + /** + * ``Text`` color. + */ public Color color = Color(); - public Cairo.TextExtents get_extents (Cairo.Context context) { - context.select_font_face (style.family, - style.slant, - style.weight); - context.set_font_size (style.size); - Cairo.TextExtents extents; - context.text_extents (text, out extents); - return extents; - } - - public double get_width (Cairo.Context context) { - var extents = get_extents (context); - switch (style.orientation) { - case FontOrient.HORIZONTAL: return extents.width; - case FontOrient.VERTICAL: return extents.height; - default: return 0.0; + /** + * Cairo ``Text`` extents. + */ + protected virtual Cairo.TextExtents ext { + get { + if (_ext == null) { + chart.ctx.select_font_face (font.family, font.slant, font.weight); + chart.ctx.set_font_size (font.size); + chart.ctx.text_extents (text, out _ext); + } + return _ext; + } + protected set { } } - public double get_height (Cairo.Context context) { - var extents = get_extents (context); - switch (style.orientation) { - case FontOrient.HORIZONTAL: return extents.height; - case FontOrient.VERTICAL: return extents.width; - default: return 0.0; + /** + * ``Text`` width. + */ + public virtual double width { + get { + switch (font.orient) { + case Gtk.Orientation.HORIZONTAL: return ext.width + ext.x_bearing; + case Gtk.Orientation.VERTICAL: return ext.height; + default: return 0; + } + } + protected set { } } - public double get_x_bearing (Cairo.Context context) { - var extents = get_extents (context); - switch (style.orientation) { - case FontOrient.HORIZONTAL: return extents.x_bearing; - case FontOrient.VERTICAL: return extents.y_bearing; - default: return 0.0; + /** + * ``Text`` height. + */ + public virtual double height { + get { + switch (font.orient) { + case Gtk.Orientation.HORIZONTAL: return ext.height; // + ext.x_bearing ? + case Gtk.Orientation.VERTICAL: return ext.width; // +- ext.y_bearing ? + default: return 0; + } + } + protected set { } } - public struct Size { - double width; - double height; - double x_bearing; - } - - public Size size (Cairo.Context context) { - var sz = Size(); - var extents = get_extents (context); - switch (style.orientation) { - case FontOrient.HORIZONTAL: - sz.width = extents.width; - sz.height = extents.height; - sz.x_bearing = extents.x_bearing; - break; - case FontOrient.VERTICAL: - sz.width = extents.height; - sz.height = extents.width; - sz.x_bearing = extents.y_bearing; - break; - } - return sz; - } - - public Text (string text = "", - FontStyle style = FontStyle(), - Color color = Color()) { + /** + * Constructs a new ``Text``. + * @param chart ``Chart`` instance. + * @param text ``Text`` string. + * @param font ``Text`` font style. + * @param color ``Text`` color. + */ + public Text (Chart chart, + string text = "", + Font font = new Font(), + Color color = Color() + ) { + this.chart = chart; this.text = text; - this.style = style; + this.font = font; this.color = color; } - public Text copy () { - var text = new Text (); + /** + * Gets a copy of the ``Text``. + */ + public virtual Text copy () { + var text = new Text (chart); + text.chart = this.chart; text.text = this.text; - text.style = this.style; + text.font = this.font.copy(); + text._ext = this._ext; text.color = this.color; return text; } + + /** + * Show ``Text``. + */ + public virtual void show () { + if (text == "") return; + chart.ctx.select_font_face(font.family, + font.slant, + font.weight); + chart.ctx.set_font_size(font.size); + if (font.orient == Gtk.Orientation.VERTICAL) { + chart.ctx.rotate(- GLib.Math.PI / 2); + chart.ctx.show_text(text); + chart.ctx.rotate(GLib.Math.PI / 2); + } else { + chart.ctx.show_text(text); + } + } } } diff --git a/src/cairo-chart-float128type.vapi b/src/cairo-chart-float128type.vapi index 04aaa8c..b3387d8 100644 --- a/src/cairo-chart-float128type.vapi +++ b/src/cairo-chart-float128type.vapi @@ -1,6 +1,14 @@ -namespace Gtk.CairoChart { +namespace CairoChart { + + /** + * 128-bit float type. + */ [CCode (cname = "cairo_chart_float128", has_type_id = false, cheader_filename = "cairo-chart-float128type.h")] public struct Float128 : double {} + + /** + * Long Double float type. + */ [CCode (cname = "cairo_chart_long_double", has_type_id = false, cheader_filename = "cairo-chart-float128type.h")] public struct LongDouble : double {} } diff --git a/test/ChartTest.vala b/test/ChartTest.vala index a3e4f15..a9d20ad 100644 --- a/test/ChartTest.vala +++ b/test/ChartTest.vala @@ -1,211 +1,247 @@ using Gtk, CairoChart; void plot_chart1 (Chart chart) { - var s1 = new Series (); - var s2 = new Series (); - var s3 = new Series (); + chart.title.text = "Chart №1"; + var s1 = new Series (chart); + var s2 = new Series (chart); + var s3 = new Series (chart); - s1.title = new Text("Series 1"); s1.color = Color (1, 0, 0); - s1.points = {Point(0, 0), Point(2, 1), Point(1, 3)}; + s1.title = new Text(chart, "Series 1"); s1.color = Color (1, 0, 0); + s1.points = {Point128(0, 0), Point128(2, 1), Point128(1, 3)}; s1.axis_x.position = Axis.Position.HIGH; s1.axis_x.format = "%.3Lf"; - s2.title = new Text("Series 2"); s2.color = Color (0, 1, 0); - s2.points = {Point(5, -3), Point(25, -18), Point(-11, 173)}; - s3.title = new Text("Series 3"); s3.color = Color (0, 0, 1); - s3.points = {Point(9, 17), Point(2, 10), Point(122, 31)}; + s2.title = new Text(chart, "Series 2"); s2.color = Color (0, 1, 0); + s2.points = {Point128(5, -3), Point128(25, -18), Point128(-11, 173)}; + s3.title = new Text(chart, "Series 3"); s3.color = Color (0, 0, 1); + s3.points = {Point128(9, 17), Point128(2, 10), Point128(122, 31)}; s3.axis_y.position = Axis.Position.HIGH; - s1.axis_x.min = 0; s1.axis_x.max = 2; - s1.axis_y.min = 0; s1.axis_y.max = 3; - s1.place.x_low = 0.25; s1.place.x_high = 0.75; - s1.place.y_low = 0.3; s1.place.y_high = 0.9; + s1.axis_x.range.min = 0; s1.axis_x.range.max = 2; + s1.axis_y.range.min = 0; s1.axis_y.range.max = 3; + s1.axis_x.place.min = 0.25; s1.axis_x.place.max = 0.75; + s1.axis_y.place.min = 0.3; s1.axis_y.place.max = 0.9; - s2.axis_x.min = -15; s2.axis_x.max = 30; - s2.axis_y.min = -20; s2.axis_y.max = 200; - s2.place.x_low = 0.5; s2.place.x_high = 1; - s2.place.y_low = 0.0; s2.place.y_high = 0.5; + s2.axis_x.range.min = -15; s2.axis_x.range.max = 30; + s2.axis_y.range.min = -20; s2.axis_y.range.max = 200; + s2.axis_x.place.min = 0.5; s2.axis_x.place.max = 1; + s2.axis_y.place.min = 0.0; s2.axis_y.place.max = 0.5; - s3.axis_x.min = 0; s3.axis_x.max = 130; - s3.axis_y.min = 15; s3.axis_y.max = 35; - s3.place.x_low = 0; s3.place.x_high = 0.5; - s3.place.y_low = 0.5; s3.place.y_high = 1.0; + s3.axis_x.range.min = 0; s3.axis_x.range.max = 130; + s3.axis_y.range.min = 15; s3.axis_y.range.max = 35; + s3.axis_x.place.min = 0; s3.axis_x.place.max = 0.5; + s3.axis_y.place.min = 0.5; s3.axis_y.place.max = 1.0; - s2.marker_type = Series.MarkerType.CIRCLE; - s3.marker_type = Series.MarkerType.PRICLE_TRIANGLE; + s1.marker.shape = Marker.Shape.SQUARE; + s2.marker.shape = Marker.Shape.CIRCLE; + s3.marker.shape = Marker.Shape.PRICLE_TRIANGLE; - s1.axis_x.title = new Text("Series 1: Axis X."); - s1.axis_y.title = new Text("Series 1: Axis Y."); - s2.axis_x.title = new Text("Series 2: Axis X."); - s2.axis_y.title = new Text("Series 2: Axis Y."); - s3.axis_x.title = new Text("Series 3: Axis X."); - s3.axis_y.title = new Text("Series 3: Axis Y."); + s1.axis_x.title = new Text(chart, "Series 1: Axis X."); + s1.axis_y.title = new Text(chart, "Series 1: Axis Y."); + s2.axis_x.title = new Text(chart, "Series 2: Axis X."); + s2.axis_y.title = new Text(chart, "Series 2: Axis Y."); + s3.axis_x.title = new Text(chart, "Series 3: Axis X."); + s3.axis_y.title = new Text(chart, "Series 3: Axis Y."); chart.series = { s1, s2, s3 }; + + chart.title.font.size = 24; + //chart.legend.show = false; + s1.title.font.size = 16; + s2.title.font.size = 16; + s2.title.font.slant = Cairo.FontSlant.ITALIC; + s2.title.font.weight = Cairo.FontWeight.BOLD; + s3.title.font.size = 18; + s1.axis_x.title.font.size = 14; + s2.axis_x.title.font.size = 14; + s2.axis_x.title.font.slant = Cairo.FontSlant.ITALIC; + s2.axis_x.title.font.weight = Cairo.FontWeight.BOLD; + s3.axis_x.title.font.size = 18; + s1.axis_y.title.font.size = 14; + s2.axis_y.title.font.size = 14; + s2.axis_y.title.font.slant = Cairo.FontSlant.ITALIC; + s2.axis_y.title.font.weight = Cairo.FontWeight.BOLD; + s3.axis_y.title.font.size = 18; + s1.axis_x.font.size = 12; + s2.axis_x.font.size = 12; + s3.axis_x.font.size = 12; + s1.axis_y.font.size = 12; + s2.axis_y.font.size = 12; + s3.axis_y.font.size = 14; + s1.marker.size = 6; + s2.marker.size = 8; + s3.marker.size = 7; + s3.line_style.width = 2; } void plot_chart2 (Chart chart) { - var s1 = new Series (); - var s2 = new Series (); - var s3 = new Series (); + var s1 = new Series (chart); + var s2 = new Series (chart); + var s3 = new Series (chart); - s1.title = new Text("Series 1"); s1.color = Color (1, 0, 0); - s1.points = {Point(-12, 0), Point(2, 1), Point(20, 3)}; + s1.title = new Text(chart, "Series 1"); s1.color = Color (1, 0, 0); + s1.points = {Point128(-12, 0), Point128(2, 1), Point128(20, 3)}; s2.axis_y.position = Axis.Position.HIGH; s1.axis_x.format = "%.3Lf"; - s2.title = new Text("Series 2"); s2.color = Color (0, 1, 0); - s2.points = {Point(5, -3), Point(25, -18), Point(-11, 173)}; - s3.title = new Text("Series 3"); s3.color = Color (0, 0, 1); - s3.points = {Point(9, 17), Point(2, 10), Point(-15, 31)}; + s2.title = new Text(chart, "Series 2"); s2.color = Color (0, 1, 0); + s2.points = {Point128(5, -3), Point128(25, -18), Point128(-11, 173)}; + s3.title = new Text(chart, "Series 3"); s3.color = Color (0, 0, 1); + s3.points = {Point128(9, 17), Point128(2, 10), Point128(-15, 31)}; s3.axis_y.position = Axis.Position.HIGH; - s1.axis_x.min = -15; s1.axis_x.max = 30; - s1.axis_y.min = 0; s1.axis_y.max = 3; - s1.place.x_low = 0.0; s1.place.x_high = 1.0; - s1.place.y_low = 0.3; s1.place.y_high = 0.9; + s1.axis_x.range.min = -15; s1.axis_x.range.max = 30; + s1.axis_y.range.min = 0; s1.axis_y.range.max = 3; + s1.axis_x.place.min = 0.0; s1.axis_x.place.max = 1.0; + s1.axis_y.place.min = 0.3; s1.axis_y.place.max = 0.9; - s2.axis_x.min = -15; s2.axis_x.max = 30; - s2.axis_y.min = -20; s2.axis_y.max = 200; - s2.place.x_low = 0.0; s2.place.x_high = 1.0; - s2.place.y_low = 0.0; s2.place.y_high = 0.5; + s2.axis_x.range.min = -15; s2.axis_x.range.max = 30; + s2.axis_y.range.min = -20; s2.axis_y.range.max = 200; + s2.axis_x.place.min = 0.0; s2.axis_x.place.max = 1.0; + s2.axis_y.place.min = 0.0; s2.axis_y.place.max = 0.5; - s3.axis_x.min = -15; s3.axis_x.max = 30; - s3.axis_y.min = 15; s3.axis_y.max = 35; - s3.place.x_low = 0.0; s3.place.x_high = 1.0; - s3.place.y_low = 0.5; s3.place.y_high = 1.0; + s3.axis_x.range.min = -15; s3.axis_x.range.max = 30; + s3.axis_y.range.min = 15; s3.axis_y.range.max = 35; + s3.axis_x.place.min = 0.0; s3.axis_x.place.max = 1.0; + s3.axis_y.place.min = 0.5; s3.axis_y.place.max = 1.0; - s1.marker_type = Series.MarkerType.PRICLE_CIRCLE; - s2.marker_type = Series.MarkerType.PRICLE_SQUARE; + s1.marker.shape = Marker.Shape.PRICLE_CIRCLE; + s2.marker.shape = Marker.Shape.PRICLE_SQUARE; + s3.marker.shape = Marker.Shape.SQUARE; - s1.axis_x.title = new Text("All Series: Axis X."); - s1.axis_y.title = new Text("Series 1: Axis Y."); - s2.axis_x.title = new Text("All Series: Axis X."); - s2.axis_y.title = new Text("Series 2: Axis Y."); - s3.axis_x.title = new Text("All Series: Axis X."); - s3.axis_y.title = new Text("Series 3: Axis Y."); + s1.axis_x.title = new Text(chart, "All Series: Axis X."); + s1.axis_y.title = new Text(chart, "Series 1: Axis Y."); + s2.axis_x.title = new Text(chart, "All Series: Axis X."); + s2.axis_y.title = new Text(chart, "Series 2: Axis Y."); + s3.axis_x.title = new Text(chart, "All Series: Axis X."); + s3.axis_y.title = new Text(chart, "Series 3: Axis Y."); //s1.axis_x.position = s2.axis_x.position = s3.axis_x.position = Axis.Position.HIGH; - //s1.axis_x.type = s2.axis_x.type = s3.axis_x.type = Axis.Type.DATE_TIME; - //s1.axis_x.max = s2.axis_x.max = s3.axis_x.max = 5*24*3600; + //s1.axis_x.dtype = s2.axis_x.dtype = s3.axis_x.dtype = Axis.DType.DATE_TIME; + //s1.axis_x.range.max = s2.axis_x.range.max = s3.axis_x.range.max = 5*24*3600; chart.series = { s1, s2, s3 }; } void plot_chart3 (Chart chart) { - var s1 = new Series (); - var s2 = new Series (); - var s3 = new Series (); + var s1 = new Series (chart); + var s2 = new Series (chart); + var s3 = new Series (chart); - s1.title = new Text("Series 1"); s1.color = Color (1, 0, 0); - s1.points = {Point(0, 70), Point(2, 155), Point(1, -3)}; + s1.title = new Text(chart, "Series 1"); s1.color = Color (1, 0, 0); + s1.points = {Point128(0, 70), Point128(2, 155), Point128(1, -3)}; s1.axis_x.position = Axis.Position.HIGH; s1.axis_y.position = Axis.Position.HIGH; s1.axis_x.format = "%.3Lf"; - s2.title = new Text("Series 2"); s2.color = Color (0, 1, 0); - s2.points = {Point(5, -3), Point(25, -18), Point(-11, 173)}; + s2.title = new Text(chart, "Series 2"); s2.color = Color (0, 1, 0); + s2.points = {Point128(5, -3), Point128(25, -18), Point128(-11, 173)}; s2.axis_y.position = Axis.Position.HIGH; - s3.title = new Text("Series 3"); s3.color = Color (0, 0, 1); - s3.points = {Point(9, -17), Point(2, 10), Point(122, 31)}; + s3.title = new Text(chart, "Series 3"); s3.color = Color (0, 0, 1); + s3.points = {Point128(9, -17), Point128(2, 10), Point128(122, 31)}; s3.axis_y.position = Axis.Position.HIGH; - s1.axis_x.min = 0; s1.axis_x.max = 2; - s1.axis_y.min = -20; s1.axis_y.max = 200; - s1.place.x_low = 0.25; s1.place.x_high = 0.75; - s1.place.y_low = 0.0; s1.place.y_high = 1.0; + s1.axis_x.range.min = 0; s1.axis_x.range.max = 2; + s1.axis_y.range.min = -20; s1.axis_y.range.max = 200; + s1.axis_x.place.min = 0.25; s1.axis_x.place.max = 0.75; + s1.axis_y.place.min = 0.0; s1.axis_y.place.max = 1.0; - s2.axis_x.min = -15; s2.axis_x.max = 30; - s2.axis_y.min = -20; s2.axis_y.max = 200; - s2.place.x_low = 0.5; s2.place.x_high = 1; - s2.place.y_low = 0.0; s2.place.y_high = 1.0; + s2.axis_x.range.min = -15; s2.axis_x.range.max = 30; + s2.axis_y.range.min = -20; s2.axis_y.range.max = 200; + s2.axis_x.place.min = 0.5; s2.axis_x.place.max = 1; + s2.axis_y.place.min = 0.0; s2.axis_y.place.max = 1.0; - s3.axis_x.min = 0; s3.axis_x.max = 130; - s3.axis_y.min = -20; s3.axis_y.max = 200; - s3.place.x_low = 0; s3.place.x_high = 0.5; - s3.place.y_low = 0.0; s3.place.y_high = 1.0; + s3.axis_x.range.min = 0; s3.axis_x.range.max = 130; + s3.axis_y.range.min = -20; s3.axis_y.range.max = 200; + s3.axis_x.place.min = 0; s3.axis_x.place.max = 0.5; + s3.axis_y.place.min = 0.0; s3.axis_y.place.max = 1.0; - s2.marker_type = Series.MarkerType.PRICLE_CIRCLE; - s3.marker_type = Series.MarkerType.TRIANGLE; + s1.marker.shape = Marker.Shape.SQUARE; + s2.marker.shape = Marker.Shape.PRICLE_CIRCLE; + s3.marker.shape = Marker.Shape.TRIANGLE; - s1.axis_x.title = new Text("Series 1: Axis X."); - s1.axis_y.title = new Text("Series 1: Axis Y."); - s2.axis_x.title = new Text("Series 2: Axis X."); - s2.axis_y.title = new Text("Series 2: Axis Y."); - s3.axis_x.title = new Text("Series 3: Axis X."); - s3.axis_y.title = new Text("Series 3: Axis Y."); + s1.axis_x.title = new Text(chart, "Series 1: Axis X."); + s1.axis_y.title = new Text(chart, "All Series: Axis Y."); + s2.axis_x.title = new Text(chart, "Series 2: Axis X."); + s2.axis_y.title = new Text(chart, "All Series: Axis Y."); + s3.axis_x.title = new Text(chart, "Series 3: Axis X."); + s3.axis_y.title = new Text(chart, "All Series: Axis Y."); //s1.axis_y.position = s2.axis_y.position = s3.axis_y.position = Axis.Position.LOW; + chart.cursors.style.orientation = Cursors.Orientation.HORIZONTAL; chart.series = { s1, s2, s3 }; } void plot_chart4 (Chart chart) { - var s1 = new Series (); - var s2 = new Series (); - var s3 = new Series (); - var s4 = new Series (); + var s1 = new Series (chart); + var s2 = new Series (chart); + var s3 = new Series (chart); + var s4 = new Series (chart); - s1.axis_x.type = Axis.Type.DATE_TIME; - s3.axis_x.type = Axis.Type.DATE_TIME; - s4.axis_x.type = Axis.Type.DATE_TIME; + s1.axis_x.dtype = Axis.DType.DATE_TIME; + s3.axis_x.dtype = Axis.DType.DATE_TIME; + s4.axis_x.dtype = Axis.DType.DATE_TIME; s4.axis_x.dsec_signs = 5; var now = new DateTime.now_local().to_unix(); var high = (uint64) (253000000000L); - s1.title = new Text("Series 1"); s1.color = Color (1, 0, 0); - s1.points = {Point(now, 70), Point(now - 100000, 155), Point(now + 100000, 30)}; + s1.title = new Text(chart, "Series 1"); s1.color = Color (1, 0, 0); + s1.points = {Point128(now, 70), Point128(now - 100000, 155), Point128(now + 100000, 30)}; s1.axis_x.position = Axis.Position.HIGH; s1.axis_y.position = Axis.Position.HIGH; - s2.title = new Text("Series 2"); s2.color = Color (0, 1, 0); - s2.points = {Point(5, -3), Point(25, -18), Point(-11, 173)}; + s2.title = new Text(chart, "Series 2"); s2.color = Color (0, 1, 0); + s2.points = {Point128(5, -3), Point128(25, -18), Point128(-11, 173)}; s2.axis_y.position = Axis.Position.HIGH; - s3.title = new Text("Series 3"); s3.color = Color (0, 0, 1); - s3.points = {Point(high - 2 + 0.73, -17), Point(high - 1 + 0.234, 10), Point(high + 1 + 0.411, 31)}; + s3.title = new Text(chart, "Series 3"); s3.color = Color (0, 0, 1); + s3.points = {Point128(high - 2 + 0.73, -17), Point128(high - 1 + 0.234, 10), Point128(high + 1 + 0.411, 31)}; s3.axis_y.position = Axis.Position.HIGH; - s4.title = new Text("Series 4"); s4.color = Color (0.5, 0.3, 0.9); - s4.points = {Point(high + 0.005, -19.05), Point(high + 0.0051, 28), Point(high + 0.0052, 55), Point(high + 0.0053, 44)}; + s4.title = new Text(chart, "Series 4"); s4.color = Color (0.5, 0.3, 0.9); + s4.points = {Point128(high + 0.005, -19.05), Point128(high + 0.0051, 28), Point128(high + 0.0052, 55), Point128(high + 0.0053, 44)}; s4.axis_y.position = Axis.Position.HIGH; - s1.axis_x.min = now - 100000; s1.axis_x.max = now + 100000; - s1.axis_y.min = -20; s1.axis_y.max = 200; - s1.place.x_low = 0.25; s1.place.x_high = 0.75; - s1.place.y_low = 0.0; s1.place.y_high = 1.0; + s1.axis_x.range.min = now - 100000; s1.axis_x.range.max = now + 100000; + s1.axis_y.range.min = -20; s1.axis_y.range.max = 200; + s1.axis_x.place.min = 0.25; s1.axis_x.place.max = 0.75; + s1.axis_y.place.min = 0.0; s1.axis_y.place.max = 1.0; - s2.axis_x.min = -15; s2.axis_x.max = 30; - s2.axis_y.min = -20; s2.axis_y.max = 200; - s2.place.x_low = 0.2; s2.place.x_high = 1; - s2.place.y_low = 0.0; s2.place.y_high = 1.0; + s2.axis_x.range.min = -15; s2.axis_x.range.max = 30; + s2.axis_y.range.min = -20; s2.axis_y.range.max = 200; + s2.axis_x.place.min = 0.2; s2.axis_x.place.max = 1; + s2.axis_y.place.min = 0.0; s2.axis_y.place.max = 1.0; - s3.axis_x.min = high - 2; s3.axis_x.max = high + 1; - s3.axis_y.min = -20; s3.axis_y.max = 200; - s3.place.x_low = 0; s3.place.x_high = 0.8; - s3.place.y_low = 0.0; s3.place.y_high = 1.0; + s3.axis_x.range.min = high - 2; s3.axis_x.range.max = high + 1; + s3.axis_y.range.min = -20; s3.axis_y.range.max = 200; + s3.axis_x.place.min = 0; s3.axis_x.place.max = 0.8; + s3.axis_y.place.min = 0.0; s3.axis_y.place.max = 1.0; - s4.axis_x.min = high + 0.0049; s4.axis_x.max = high + 0.0054; - s4.axis_y.min = -20; s4.axis_y.max = 200; - s4.place.x_low = 0.2; s4.place.x_high = 1.0; - s4.place.y_low = 0.0; s4.place.y_high = 1.0; + s4.axis_x.range.min = high + 0.0049; s4.axis_x.range.max = high + 0.0054; + s4.axis_y.range.min = -20; s4.axis_y.range.max = 200; + s4.axis_x.place.min = 0.2; s4.axis_x.place.max = 1.0; + s4.axis_y.place.min = 0.0; s4.axis_y.place.max = 1.0; - s2.marker_type = Series.MarkerType.PRICLE_CIRCLE; - s3.marker_type = Series.MarkerType.TRIANGLE; - s4.marker_type = Series.MarkerType.PRICLE_SQUARE; + s1.marker.shape = Marker.Shape.SQUARE; + s2.marker.shape = Marker.Shape.PRICLE_CIRCLE; + s3.marker.shape = Marker.Shape.TRIANGLE; + s4.marker.shape = Marker.Shape.PRICLE_SQUARE; - s1.axis_x.title = new Text("Series 1: Axis X."); - s1.axis_y.title = new Text("Series 1: Axis Y."); - s2.axis_x.title = new Text("Series 2: Axis X."); - s2.axis_y.title = new Text("Series 2: Axis Y."); - s3.axis_x.title = new Text("Series 3: Axis X."); - s3.axis_y.title = new Text("Series 3: Axis Y."); - s4.axis_x.title = new Text("Series 4: Axis X."); - s4.axis_y.title = new Text("Series 4: Axis Y."); + s1.axis_x.title = new Text(chart, "Series 1: Axis X."); + s1.axis_y.title = new Text(chart, "All Series: Axis Y."); + s2.axis_x.title = new Text(chart, "Series 2: Axis X."); + s2.axis_y.title = new Text(chart, "All Series: Axis Y."); + s3.axis_x.title = new Text(chart, "Series 3: Axis X."); + s3.axis_y.title = new Text(chart, "All Series: Axis Y."); + s4.axis_x.title = new Text(chart, "Series 4: Axis X."); + s4.axis_y.title = new Text(chart, "All Series: Axis Y."); + + chart.cursors.style.orientation = Cursors.Orientation.HORIZONTAL; chart.series = { s1, s2, s3, s4 }; } bool point_in_chart (Chart chart, double x, double y) { - if (x < chart.plot_area_x_min) return false; - if (x > chart.plot_area_x_max) return false; - if (y < chart.plot_area_y_min) return false; - if (y > chart.plot_area_y_max) return false; + if (x < chart.plarea.x0) return false; + if (x > chart.plarea.x1) return false; + if (y < chart.plarea.y0) return false; + if (y > chart.plarea.y1) return false; return true; } @@ -230,10 +266,10 @@ int main (string[] args) { var chart2 = new Chart(); var chart3 = new Chart(); var chart4 = new Chart(); - var label = new Label ("Chart Test!"); + var label = new Gtk.Label ("Chart Test!"); var button1 = new Button.with_label("Separate axes"); - var button2 = new Button.with_label("Common X axes"); - var button3 = new Button.with_label("Common Y axes"); + var button2 = new Button.with_label("Joint X axes"); + var button3 = new Button.with_label("Joint Y axes"); var button4 = new Button.with_label("Dates/Times"); var button5 = new Button.with_label("rm Axis Titles"); var button6 = new Button.with_label("Dates only"); @@ -245,12 +281,13 @@ int main (string[] args) { plot_chart3 (chart3); plot_chart4 (chart4); - chart1.selection_style = LineStyle(Color(0.3, 0.3, 0.3, 0.7), 1); + chart1.selection_style = LineStyle(Color(0.3, 0.3, 0.3, 0.7)); var da = new DrawingArea(); da.set_events ( Gdk.EventMask.BUTTON_PRESS_MASK |Gdk.EventMask.BUTTON_RELEASE_MASK |Gdk.EventMask.POINTER_MOTION_MASK + |Gdk.EventMask.SCROLL_MASK ); var chart = chart1; @@ -270,9 +307,9 @@ int main (string[] args) { case Legend.Position.LEFT: radio_button3.set_active(true); break; case Legend.Position.BOTTOM: radio_button4.set_active(true); break; } - switch (chart.cursors_orientation) { - case Chart.CursorOrientation.VERTICAL: radio_button7.set_active(true); break; - case Chart.CursorOrientation.HORIZONTAL: radio_button8.set_active(true); break; + switch (chart.cursors.style.orientation) { + case Cursors.Orientation.VERTICAL: radio_button7.set_active(true); break; + case Cursors.Orientation.HORIZONTAL: radio_button8.set_active(true); break; } }); button2.clicked.connect (() => { @@ -283,9 +320,9 @@ int main (string[] args) { case Legend.Position.LEFT: radio_button3.set_active(true); break; case Legend.Position.BOTTOM: radio_button4.set_active(true); break; } - switch (chart.cursors_orientation) { - case Chart.CursorOrientation.VERTICAL: radio_button7.set_active(true); break; - case Chart.CursorOrientation.HORIZONTAL: radio_button8.set_active(true); break; + switch (chart.cursors.style.orientation) { + case Cursors.Orientation.VERTICAL: radio_button7.set_active(true); break; + case Cursors.Orientation.HORIZONTAL: radio_button8.set_active(true); break; } }); button3.clicked.connect (() => { @@ -296,9 +333,9 @@ int main (string[] args) { case Legend.Position.LEFT: radio_button3.set_active(true); break; case Legend.Position.BOTTOM: radio_button4.set_active(true); break; } - switch (chart.cursors_orientation) { - case Chart.CursorOrientation.VERTICAL: radio_button7.set_active(true); break; - case Chart.CursorOrientation.HORIZONTAL: radio_button8.set_active(true); break; + switch (chart.cursors.style.orientation) { + case Cursors.Orientation.VERTICAL: radio_button7.set_active(true); break; + case Cursors.Orientation.HORIZONTAL: radio_button8.set_active(true); break; } }); button4.clicked.connect (() => { @@ -309,9 +346,9 @@ int main (string[] args) { case Legend.Position.LEFT: radio_button4.set_active(true); break; case Legend.Position.BOTTOM: radio_button4.set_active(true); break; } - switch (chart.cursors_orientation) { - case Chart.CursorOrientation.VERTICAL: radio_button7.set_active(true); break; - case Chart.CursorOrientation.HORIZONTAL: radio_button8.set_active(true); break; + switch (chart.cursors.style.orientation) { + case Cursors.Orientation.VERTICAL: radio_button7.set_active(true); break; + case Cursors.Orientation.HORIZONTAL: radio_button8.set_active(true); break; } }); button5.clicked.connect (() => { @@ -393,13 +430,13 @@ int main (string[] args) { radio_button7.toggled.connect ((button) => { if (button.get_active()) { - chart.cursors_orientation = Chart.CursorOrientation.VERTICAL; + chart.cursors.style.orientation = Cursors.Orientation.VERTICAL; da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); } }); radio_button8.toggled.connect ((button) => { if (button.get_active()) { - chart.cursors_orientation = Chart.CursorOrientation.HORIZONTAL; + chart.cursors.style.orientation = Cursors.Orientation.HORIZONTAL; da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); } }); @@ -409,10 +446,10 @@ int main (string[] args) { double sel_x0 = 0, sel_x1 = 0, sel_y0 = 0, sel_y1 = 0; double mov_x0 = 0, mov_y0 = 0; - da.draw.connect((context) => { - chart.context = context; - chart.width = da.get_allocated_width(); - chart.height = da.get_allocated_height(); + da.draw.connect((ctx) => { + chart.ctx = ctx; + chart.area.width = da.get_allocated_width(); + chart.area.height = da.get_allocated_height(); chart.clear(); // user's pre draw operations here... @@ -422,23 +459,23 @@ int main (string[] args) { // user's post draw operations here... if (mouse_state == MouseState.DRAW_SELECTION) - chart.draw_selection (sel_x0, sel_y0, sel_x1, sel_y1); + chart.draw_selection (new Area.with_abs(sel_x0, sel_y0, sel_x1, sel_y1)); // show delta - var str = chart.get_cursors_delta_str(); + var str = chart.cursors.get_delta_str(); if (str != "") { var text = "Δ = " + str; - var text_t = new Text(text); - var w = text_t.get_width(context); - var h = text_t.get_height(context); - var x0 = chart.plot_area_x_max - w - 5; - var y0 = chart.plot_area_y_min + h + 5; - chart.set_source_rgba(chart.legend.bg_color); - context.rectangle (x0, y0 - h, w, h); - context.fill(); - context.move_to (x0, y0); - chart.set_source_rgba(chart.common_axis_color); - context.show_text(text); + var text_t = new Text(chart, text); + var w = text_t.width; + var h = text_t.height; + var x0 = chart.plarea.x1 - w - 5; + var y0 = chart.plarea.y0 + h + 5; + chart.color = chart.legend.bg_color; + ctx.rectangle (x0, y0 - h, w, h); + ctx.fill(); + ctx.move_to (x0, y0); + chart.color = chart.joint_color; + ctx.show_text(text); } return true;//ret; @@ -450,11 +487,11 @@ int main (string[] args) { switch (event.button) { case 1: // start cursor position selection if ((event.state & Gdk.ModifierType.SHIFT_MASK) != 0) { // remove cursor - chart.set_active_cursor (event.x, event.y, true); - chart.remove_active_cursor(); + chart.cursors.set_active (Point(event.x, event.y), true); + chart.cursors.remove_active(); mouse_state = MouseState.FREE; } else { // add cursor - chart.set_active_cursor (event.x, event.y); + chart.cursors.set_active (Point(event.x, event.y)); mouse_state = MouseState.CURSOR_SELECTION; } da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); @@ -482,11 +519,11 @@ int main (string[] args) { switch (event.button) { case 1: // start cursor position selection if ((event.state & Gdk.ModifierType.SHIFT_MASK) != 0) { // remove cursor - //chart.remove_active_cursor (); + //chart.remove_active (); //da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); //mouse_state = MouseState.FREE; } else { // add cursor - chart.add_active_cursor (); + chart.cursors.add_active (); mouse_state = MouseState.FREE; } break; @@ -495,7 +532,7 @@ int main (string[] args) { sel_x1 = event.x; sel_y1 = event.y; if (sel_x1 > sel_x0 && sel_y1 > sel_y0) - chart.zoom_in (sel_x0, sel_y0, sel_x1, sel_y1); + chart.zoom_in (new Area.with_abs(sel_x0, sel_y0, sel_x1, sel_y1)); else chart.zoom_out (); da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); @@ -521,25 +558,32 @@ int main (string[] args) { case MouseState.MOVING_CHART: var delta_x = event.x - mov_x0, delta_y = event.y - mov_y0; - chart.move (delta_x, delta_y); + chart.move (Point(){x = delta_x, y = delta_y}); mov_x0 = event.x; mov_y0 = event.y; da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); break; case MouseState.CURSOR_SELECTION: - chart.set_active_cursor (event.x, event.y); + chart.cursors.set_active (Point(event.x, event.y)); da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); break; } return true; // return ret; }); - da.add_events(Gdk.EventMask.SCROLL_MASK); da.scroll_event.connect((event) => { + switch (event.direction) { + case Gdk.ScrollDirection.UP: + chart.zoom_scroll_in(Point(event.x, event.y)); + break; + case Gdk.ScrollDirection.DOWN: + chart.zoom_scroll_out(Point(event.x, event.y)); + break; + } + da.queue_draw_area(0, 0, da.get_allocated_width(), da.get_allocated_height()); //var ret = chart.scroll_notify_event(event); - return true; // return ret; });