diff --git a/README.md b/README.md index d815531..7895031 100644 --- a/README.md +++ b/README.md @@ -662,6 +662,24 @@ chart.add_unaware_timestamp_collection_by_index(0, unaware_timestamp_collection, Et voilĂ  ! +## CAUTIONS + +### Removing LiveChart.Chart from Gtk.Widget + +Removing LiveChart.Chart from Gtk.Widget without stopping auto-refresh causes memory leak. + +```vala +var window = new Gtk.Window(); +var chart = new LiveChart.Chart(); +window.add(chart); + +//... +chart.refresh_every(-1); //Don't forget to stop auto-refresh if your app replaces the LiveChart.Chart widget. +window.remove(chart); + +``` + + ## How LiveChart versions works ? * For each new feature, the `minor` version number will be bumped diff --git a/examples/live-chart.vala b/examples/live-chart.vala index 07e073c..5244bdf 100644 --- a/examples/live-chart.vala +++ b/examples/live-chart.vala @@ -57,7 +57,12 @@ public class Example : Gtk.Window { heat.add(heat_value); return true; }); - + + Timeout.add(20000, () => { + chart.remove_serie(rss); + return false; + }); + var export_button = new Gtk.Button.with_label("Export to PNG"); export_button.clicked.connect (() => { try { diff --git a/meson.build b/meson.build index 6a4a55e..0e74d6f 100644 --- a/meson.build +++ b/meson.build @@ -1,13 +1,25 @@ -project('live-chart', ['vala', 'c'], version: '1.9.1') +project('live-chart', ['vala', 'c'], version: '1.9.1-RO') cc = meson.get_compiler('c') libm = cc.find_library('m', required: true) -gtk = dependency('gtk+-3.0', version: '>= 3.22') +#setting up GEE gee = dependency('gee-0.8') - vala_args = ['--target-glib=2.50'] +#setting up gtk +gtk_major = 3 + +if gtk_major == 3 + gtk = dependency('gtk+-3.0', version: '>= 3.22') + vala_args += ['--define=GTK3'] +endif + +if gtk_major == 4 + gtk = dependency('gtk4') + vala_args += ['--define=GTK4'] +endif + if meson.version().version_compare('>= 0.47') if get_option('debug') == true vala_args += ['--ccode', '--debug'] diff --git a/src/axis.vala b/src/axis.vala index 7a4b6b8..85badaf 100644 --- a/src/axis.vala +++ b/src/axis.vala @@ -10,8 +10,8 @@ namespace LiveChart { public Path lines = new Path(); public XAxis() { - axis.color = {0.5, 0.5, 0.5, 0.5}; - lines.color = {0.5, 0.5, 0.5, 0.2}; + axis.color = {0.5f, 0.5f, 0.5f, 0.5f}; + lines.color = {0.5f, 0.5f, 0.5f, 0.2f}; } public double get_ratio() { @@ -51,8 +51,8 @@ namespace LiveChart { public YAxis(string unit = "") { this.unit = unit; ticks = get_ticks(); - axis.color = {0.5, 0.5, 0.5, 0.5}; - lines.color = {0.5, 0.5, 0.5, 0.2}; + axis.color = {0.5f, 0.5f, 0.5f, 0.5f}; + lines.color = {0.5f, 0.5f, 0.5f, 0.2f}; bounds.notify["upper"].connect(() => { this.ticks = get_ticks(); }); diff --git a/src/background.vala b/src/background.vala index 8bf0517..b485966 100644 --- a/src/background.vala +++ b/src/background.vala @@ -23,10 +23,10 @@ namespace LiveChart { [Version (deprecated = true, deprecated_since = "1.8.0", replacement = "Background.color")] public Gdk.RGBA main_color { get; set; default= Gdk.RGBA() { - red = 0.1, - green = 0.1, - blue = 0.1, - alpha = 1.0 + red = 0.1f, + green = 0.1f, + blue = 0.1f, + alpha = 1.0f }; } diff --git a/src/chart.vala b/src/chart.vala index 822e0d8..affe9a4 100644 --- a/src/chart.vala +++ b/src/chart.vala @@ -17,25 +17,48 @@ namespace LiveChart { public Series series; private uint source_timeout = 0; - + private double play_ratio = 1.0; + + private int64 prev_time; + public Chart(Config config = new Config()) { this.config = config; + +#if GTK3 this.size_allocate.connect((allocation) => { this.config.height = allocation.height; this.config.width = allocation.width; }); - this.draw.connect(render); - +#endif +#if GTK4 + this.set_draw_func((_, ctx, width, height) => { + this.config.height = height; + this.config.width = width; + this.render(_, ctx); + }); +#endif this.refresh_every(100); series = new Series(this); + this.destroy.connect(() => { + refresh_every(-1); + remove_all_series(); + }); } public void add_serie(Serie serie) { this.series.register(serie); } + public void remove_serie(Serie serie){ + this.series.remove_serie(serie); + } + + public void remove_all_series(){ + this.series.remove_all(); + } + [Version (deprecated = true, deprecated_since = "1.7.0", replacement = "Retrieve the Serie from Chart.series (or from the serie you created) and add the value using serie.add")] public void add_value(Serie serie, double value) { serie.add(value); @@ -72,26 +95,36 @@ namespace LiveChart { } public void to_png(string filename) throws Error { +#if GTK3 var window = this.get_window(); if (window == null) { throw new ChartError.EXPORT_ERROR("Chart is not realized yet"); } var pixbuff = Gdk.pixbuf_get_from_window(window, 0, 0, window.get_width(), window.get_height()); pixbuff.savev(filename, "png", {}, {}); +#endif } - public void refresh_every(int ms) { + public void refresh_every(int ms, double play_ratio = 1.0) { + this.play_ratio = play_ratio; if (source_timeout != 0) { GLib.Source.remove(source_timeout); + source_timeout = 0; + } + if(ms > 0){ + this.prev_time = GLib.get_monotonic_time() / 1000; + source_timeout = Timeout.add(ms, () => { + var now = GLib.get_monotonic_time() / 1000; + config.time.current += (int64)((now - this.prev_time) * this.play_ratio); + this.prev_time = now; + this.queue_draw(); + return true; + }); } - source_timeout = Timeout.add(ms, () => { - this.queue_draw(); - return true; - }); } private bool render(Gtk.Widget _, Context ctx) { - + ctx.set_antialias(Cairo.Antialias.NONE); config.configure(ctx, legend); this.background.draw(ctx, config); diff --git a/src/config.vala b/src/config.vala index c855686..84b2095 100644 --- a/src/config.vala +++ b/src/config.vala @@ -38,11 +38,28 @@ namespace LiveChart { public int width; public int height; } + + public class Config { - + private int _width = 0; public int width { - get; set; default = 0; + get{ + return _width; + } + set{ + if(_width != value){ + //i = config.width - config.padding.right; i > config.padding.left; i -= config.x_axis.tick_length + if(x_axis.tick_length <= 0.0){ + time.head_offset = -1.0; + } + else{ + var tmp = value / x_axis.tick_length; + time.head_offset = tmp * x_axis.tick_interval * 1000.0; + } + } + _width = value; + } } public int height { @@ -53,7 +70,16 @@ namespace LiveChart { public YAxis y_axis = new YAxis(); public XAxis x_axis = new XAxis(); - + + public struct TimeSeek { + int64 current; + double head_offset; + } + public TimeSeek time = { + GLib.get_real_time() / 1000, + 0 + }; + internal Gee.ArrayList categories; public Boundaries boundaries() { diff --git a/src/font.vala b/src/font.vala index 3f6a382..71e5f9a 100644 --- a/src/font.vala +++ b/src/font.vala @@ -14,7 +14,7 @@ namespace LiveChart { face = "Sans serif"; slant = FontSlant.NORMAL; weight = FontWeight.NORMAL; - color = {0.4, 0.4, 0.4, 1.0}; + color = {0.4f, 0.4f, 0.4f, 1.0f}; } public void configure(Context ctx) { diff --git a/src/grid.vala b/src/grid.vala index 539d17f..e75accb 100644 --- a/src/grid.vala +++ b/src/grid.vala @@ -14,10 +14,10 @@ namespace LiveChart { public bool visible { get; set; default = true; } public Gdk.RGBA main_color { get; set; default= Gdk.RGBA() { - red = 0.4, - green = 0.4, - blue = 0.4, - alpha = 1.0 + red = 0.4f, + green = 0.4f, + blue = 0.4f, + alpha = 1.0f }; } @@ -67,7 +67,8 @@ namespace LiveChart { } protected void render_vgrid(Context ctx, Config config) { - var time = new DateTime.now().to_unix(); + // var time = new DateTime.now().to_unix(); + var time = config.time.current / 1000; for (double i = config.width - config.padding.right; i > config.padding.left; i -= config.x_axis.tick_length) { if (config.x_axis.lines.visible) { config.x_axis.lines.configure(ctx); diff --git a/src/legend.vala b/src/legend.vala index ee4a88f..d318524 100644 --- a/src/legend.vala +++ b/src/legend.vala @@ -15,17 +15,26 @@ namespace LiveChart { height=0 }; public Gdk.RGBA main_color { - get; set; default= Gdk.RGBA() { - red = 1.0, - green = 1.0, - blue = 1.0, - alpha = 1.0 + get; set; + default= Gdk.RGBA() { + red = 1.0f, + green = 1.0f, + blue = 1.0f, + alpha = 1.0f }; } public void add_legend(Serie serie) { series.add(serie); } - + public void remove_legend(Serie serie){ + if(series.contains(serie)){ + series.remove(serie); + } + } + public void remove_all_legend(){ + series.clear(); + } + public abstract void draw(Context ctx, Config config); public BoundingBox get_bounding_box() { return bounding_box; diff --git a/src/path.vala b/src/path.vala index 87d0a58..2a0949e 100644 --- a/src/path.vala +++ b/src/path.vala @@ -16,7 +16,7 @@ namespace LiveChart { public Gdk.RGBA color { get; set; } public bool visible {get; set; } - public Path(double width = 0.5, Gdk.RGBA color = {1.0, 1.0, 1.0, 1.0}, bool visible = true, Dash? dash = null) { + public Path(double width = 0.5, Gdk.RGBA color = {1.0f, 1.0f, 1.0f, 1.0f}, bool visible = true, Dash? dash = null) { this.width = width; this.color = color; this.visible = true; diff --git a/src/points.vala b/src/points.vala index 0d4795d..1847a57 100644 --- a/src/points.vala +++ b/src/points.vala @@ -55,11 +55,27 @@ namespace LiveChart { var boundaries = config.boundaries(); Points points = new Points(); - if (values.size > 0) { - var last_value = values.last(); - points.realtime_delta = (((GLib.get_real_time() / 1000) - last_value.timestamp) * config.x_axis.get_ratio()) / 1000; + if (values.size > 1) { + /// \note SortedSet.sub_set won't work as I expected correctly. + SortedSet renderee = null; + TimestampedValue border = {(double)config.time.current + 1, 0.0}; + renderee = values.head_set(border); - foreach (TimestampedValue value in values) { + if(config.time.head_offset >= 0.0 && renderee.size > 0){ + border.timestamp -= config.time.head_offset; + if(renderee.first().timestamp < border.timestamp){ + renderee = renderee.tail_set(border); + } + } + + //var renderee = values; + if(renderee.size <= 2){ + return points; + } + var last_value = renderee.last(); + //points.realtime_delta = (((GLib.get_real_time() / 1000) - last_value.timestamp) * config.x_axis.get_ratio()) / 1000; + points.realtime_delta = ((config.time.current - last_value.timestamp) * config.x_axis.get_ratio()) / 1000; + foreach (TimestampedValue value in renderee) { var point = Points.value_to_point(last_value, value, config, boundaries, points.realtime_delta); points.add(point); } diff --git a/src/series.vala b/src/series.vala index 1b01c7f..3109e44 100644 --- a/src/series.vala +++ b/src/series.vala @@ -2,23 +2,27 @@ using Gee; namespace LiveChart { public class Series : Object { - + private Gee.Map signals = new Gee.HashMap(); private Gee.ArrayList series = new Gee.ArrayList(); - private Chart chart; + private unowned Chart chart; public Series(Chart chart) { this.chart = chart; } public Serie register(Serie serie) { + if(signals.has_key(serie)){ + return serie; + } this.series.add(serie); //if values were added to serie before registration serie.get_values().foreach((value) => {chart.config.y_axis.update_bounds(value.value); return true;}); if(chart.legend != null) chart.legend.add_legend(serie); - serie.value_added.connect((value) => { + var sh = serie.value_added.connect((value) => { chart.config.y_axis.update_bounds(value); }); + signals[serie] = sh; return serie; } @@ -35,9 +39,38 @@ namespace LiveChart { } throw new ChartError.SERIE_NOT_FOUND("Serie with name %s not found".printf(name)); } - + + public void remove_serie(Serie serie){ + if(signals.has_key(serie) && series.contains(serie)){ + var sh = signals[serie]; + serie.disconnect(sh); + series.remove(serie); + signals.unset(serie); + } + if(chart.legend != null){ + chart.legend.remove_legend(serie); + } + } + + public void remove_all(){ + foreach(var entry in signals){ + entry.key.disconnect(entry.value); + } + signals.clear(); + series.clear(); + if(chart.legend != null){ + chart.legend.remove_all_legend(); + } + } + public Iterator iterator() { return series.list_iterator(); } + + ~Series(){ + foreach(var entry in signals){ + entry.key.disconnect(entry.value); + } + } } } \ No newline at end of file diff --git a/src/static/static_chart.vala b/src/static/static_chart.vala index db87f94..88b03ce 100644 --- a/src/static/static_chart.vala +++ b/src/static/static_chart.vala @@ -15,12 +15,20 @@ namespace LiveChart.Static { public StaticChart(Config config = new Config()) { this.config = config; +#if GTK3 this.size_allocate.connect((allocation) => { this.config.height = allocation.height; this.config.width = allocation.width; }); - this.draw.connect(render); +#endif +#if GTK4 + this.set_draw_func((_, ctx, width, height) => { + this.config.height = height; + this.config.width = width; + this.render(_, ctx); + }); +#endif series = new StaticSeries(this); } diff --git a/src/static/static_grid.vala b/src/static/static_grid.vala index bef2a4c..94a354b 100644 --- a/src/static/static_grid.vala +++ b/src/static/static_grid.vala @@ -15,10 +15,10 @@ namespace LiveChart.Static { public bool visible { get; set; default = true; } public Gdk.RGBA main_color { get; set; default= Gdk.RGBA() { - red = 0.4, - green = 0.4, - blue = 0.4, - alpha = 1.0 + red = 0.4f, + green = 0.4f, + blue = 0.4f, + alpha = 1.0f }; } diff --git a/src/values.vala b/src/values.vala index f6faaa1..01bed5e 100644 --- a/src/values.vala +++ b/src/values.vala @@ -7,22 +7,50 @@ namespace LiveChart { public double value; } - public class Values : LinkedList { - + public class Values : TreeSet { public Bounds bounds { get; construct set; } private int buffer_size; - + + private static int cmp(TimestampedValue? a, TimestampedValue? b){ + double r = a.timestamp - b.timestamp; + if(r < 0.0){ + return -1; + } + if(r > 0.0){ + return 1; + } + return 0; + } public Values(int buffer_size = 1000) { + base(cmp); this.bounds = new Bounds(); this.buffer_size = buffer_size; } - +/* + public new TimestampedValue @get(int index){ + assert (index >= 0); + assert (index < buffer_size); + int i = 0; + TimestampedValue ret = {}; + this.foreach((v) => { + if(i == index){ + ret = v; + return false; + } + i++; + return true; + }); + return ret; + } + +*/ public new void add(TimestampedValue value) { if (this.size == buffer_size) { - this.remove_at(0); + //this.remove_at(0); + this.remove(this.first()); } bounds.update(value.value); base.add(value); diff --git a/tests/chart.vala b/tests/chart.vala index 8b94556..642b9a4 100644 --- a/tests/chart.vala +++ b/tests/chart.vala @@ -1,4 +1,19 @@ - +private LiveChart.TimestampedValue get_at(int index, LiveChart.Values values){ + assert(values.size < index); + assert(index >= 0); + + LiveChart.TimestampedValue ret = {}; + int i = 0; + + values.foreach((v) => { + if(i == index){ + ret = v; + return false; + } + return true; + }); + return ret; +} private void register_chart() { Test.add_func("/LiveChart/Chart/serie/add_value#should_update_bounds_when_adding_a_value", () => { @@ -88,13 +103,20 @@ private void register_chart() { //then assert(serie.get_values().size == 3); + /* assert(serie.get_values().get(0).value == 5); assert(serie.get_values().get(1).value == 10); assert(serie.get_values().get(2).value == 15); assert(serie.get_values().get(2).timestamp == now); assert(serie.get_values().get(1).timestamp == now - 5000); assert(serie.get_values().get(0).timestamp == now - 10000); - + */ + assert(get_at(0, serie.get_values()).value == 5); + assert(get_at(1, serie.get_values()).value == 10); + assert(get_at(2, serie.get_values()).value == 15); + assert(get_at(2, serie.get_values()).timestamp == now); + assert(get_at(1, serie.get_values()).timestamp == now - 5000); + assert(get_at(0, serie.get_values()).timestamp == now - 10000); assert(chart.config.y_axis.get_bounds().lower == 5); assert(chart.config.y_axis.get_bounds().upper == 15); }); @@ -115,7 +137,7 @@ private void register_chart() { //then assert(serie.get_values().size == 1); - assert(serie.get_values().get(0).value == 100); + assert(serie.get_values().first().value == 100); }); //Deprecated @@ -135,7 +157,7 @@ private void register_chart() { //then assert(serie.get_values().size == 1); - assert(serie.get_values().get(0).value == 100); + assert(serie.get_values().first().value == 100); }); Test.add_func("/LiveChart/Chart/add_unaware_timestamp_collection_by_index", () => { @@ -162,13 +184,20 @@ private void register_chart() { //then assert(serie.get_values().size == 3); +/* assert(serie.get_values().get(0).value == 5); assert(serie.get_values().get(1).value == 10); assert(serie.get_values().get(2).value == 15); assert(serie.get_values().get(2).timestamp == now); assert(serie.get_values().get(1).timestamp == now - 5000); assert(serie.get_values().get(0).timestamp == now - 10000); - +*/ + assert(get_at(0, serie.get_values()).value == 5); + assert(get_at(1, serie.get_values()).value == 10); + assert(get_at(2, serie.get_values()).value == 15); + assert(get_at(2, serie.get_values()).timestamp == now); + assert(get_at(1, serie.get_values()).timestamp == now - 5000); + assert(get_at(0, serie.get_values()).timestamp == now - 10000); assert(chart.config.y_axis.get_bounds().lower == 5); assert(chart.config.y_axis.get_bounds().upper == 15); }); diff --git a/tests/serie.vala b/tests/serie.vala index c32d89a..735438f 100644 --- a/tests/serie.vala +++ b/tests/serie.vala @@ -52,7 +52,7 @@ private void register_serie() { //then assert(serie.get_values().size == 1); - assert(serie.get_values().get(0).value == 100.0); - assert(serie.get_values().get(0).timestamp == 5); + assert(serie.get_values().first().value == 100.0); + assert(serie.get_values().first().timestamp == 5); }); } \ No newline at end of file diff --git a/tests/values.vala b/tests/values.vala index f9c0b18..2be1ddd 100644 --- a/tests/values.vala +++ b/tests/values.vala @@ -7,12 +7,14 @@ private void register_values() { //when values.add({0, 1}); - values.add({1, 10}); values.add({2, 100}); + values.add({1, 10}); //then assert(values.size == 2); - assert(values.get(0) == LiveChart.TimestampedValue(){timestamp = 1, value = 10}); - assert(values.get(1) == LiveChart.TimestampedValue(){timestamp = 2, value = 100}); + //assert(values.get(0) == LiveChart.TimestampedValue(){timestamp = 1, value = 10}); + //assert(values.get(1) == LiveChart.TimestampedValue(){timestamp = 2, value = 100}); + assert(values.first() == LiveChart.TimestampedValue(){timestamp = 1, value = 10}); + assert(values.last() == LiveChart.TimestampedValue(){timestamp = 2, value = 100}); }); } \ No newline at end of file