From 2d2ae9ea0d5e2c6add40f40594514f8f5567cea1 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Wed, 17 Feb 2021 09:52:53 +0100 Subject: [PATCH 1/7] First version of TkCanvasWrapper backend Allows for figures to be shown interactively in Tk applications --- examples/tk_canvas_wrapper/README.md | 9 + .../tk_canvas_wrapper/plot_window_raw_tk.rb | 26 ++ .../plot_window_tk_component.rb | 33 ++ examples/tk_canvas_wrapper/tk_plot_demo.rb | 274 +++++++++++++ lib/rubyplot.rb | 2 + lib/rubyplot/artist/figure.rb | 3 +- lib/rubyplot/backend.rb | 4 +- lib/rubyplot/backend/base.rb | 3 + lib/rubyplot/backend/tk_canvas_wrapper.rb | 369 ++++++++++++++++++ rubyplot.gemspec | 1 + 10 files changed, 722 insertions(+), 2 deletions(-) create mode 100644 examples/tk_canvas_wrapper/README.md create mode 100644 examples/tk_canvas_wrapper/plot_window_raw_tk.rb create mode 100644 examples/tk_canvas_wrapper/plot_window_tk_component.rb create mode 100755 examples/tk_canvas_wrapper/tk_plot_demo.rb create mode 100644 lib/rubyplot/backend/tk_canvas_wrapper.rb diff --git a/examples/tk_canvas_wrapper/README.md b/examples/tk_canvas_wrapper/README.md new file mode 100644 index 0000000..aefff6b --- /dev/null +++ b/examples/tk_canvas_wrapper/README.md @@ -0,0 +1,9 @@ +# Examples using TkCanvasWrapper + +Some example figures, extracted from the Tutorial, drawn in windows +using the TkCanvasWrapper. + +## Running + + bundle exec tk_plot_demo.rb + diff --git a/examples/tk_canvas_wrapper/plot_window_raw_tk.rb b/examples/tk_canvas_wrapper/plot_window_raw_tk.rb new file mode 100644 index 0000000..4e64a98 --- /dev/null +++ b/examples/tk_canvas_wrapper/plot_window_raw_tk.rb @@ -0,0 +1,26 @@ +# Minimal method to show a window with the figure using raw Tk +# +def plot_window(root = false) + figure = Rubyplot::Artist::Figure.new(height: 20, width: 20) + + window = root ? + TkRoot.new { title "Plot Demo" } : + TkToplevel.new { title "Plot Demo" } + content = Tk::Tile::Frame.new(window).grid(sticky: 'nsew', column: 1, row: 1) + TkGrid.columnconfigure window, 1, weight: 1 + TkGrid.rowconfigure window, 1, weight: 1 + canvas = TkCanvas.new(content) { width 600; height 600 } + canvas.grid sticky: 'nwes', column: 1, row: 1 + refresh_button = Tk::Tile::Button.new(content) do + text "Refresh!" + command { Tk.update; figure.show(canvas) } + end.grid(column: 1, row: 2, sticky: 'es') + TkGrid.columnconfigure content, 1, weight: 1 + TkGrid.rowconfigure content, 1, weight: 1 + TkGrid.rowconfigure content, 2, weight: 0 + + yield figure + + Tk.update + figure.show(canvas) +end diff --git a/examples/tk_canvas_wrapper/plot_window_tk_component.rb b/examples/tk_canvas_wrapper/plot_window_tk_component.rb new file mode 100644 index 0000000..1246d0e --- /dev/null +++ b/examples/tk_canvas_wrapper/plot_window_tk_component.rb @@ -0,0 +1,33 @@ +# Minimal method to show a window with the figure using TkComponent +require "tk_component" + +class PlotComponent < TkComponent::Base + attr_accessor :chart + + def initialize(options = {}) + super + @chart = options[:chart] + end + + def render(p, parent_component) + p.vframe(sticky: 'wens', x_flex: 1, y_flex: 1) do |vf| + @canvas = vf.canvas(sticky: 'wens', width: 600, height: 600, x_flex: 1, y_flex: 1) + vf.button(text: "Redraw", sticky: 'e', on_click: ->(e) { chart.show(@canvas.native_item) }) + end + end + + def component_did_build + Tk.update + chart.show(@canvas.native_item) + end +end + +def plot_window(root = false) + window = TkComponent::Window.new(title: "Plot Demo", root: root) + figure = Rubyplot::Artist::Figure.new(height: 20, width: 20) + + yield figure + + component = PlotComponent.new(chart: figure) + window.place_root_component(component) +end diff --git a/examples/tk_canvas_wrapper/tk_plot_demo.rb b/examples/tk_canvas_wrapper/tk_plot_demo.rb new file mode 100755 index 0000000..f1ed73a --- /dev/null +++ b/examples/tk_canvas_wrapper/tk_plot_demo.rb @@ -0,0 +1,274 @@ +#!/usr/bin/env ruby + +# Shows several figures, each in its own window, using Tk +# Figures are extracted from other examples + +require "bundler/setup" + +ENV["RUBYPLOT_BACKEND"] = "TK_CANVAS" + +require "rubyplot" + +# Code to create a window and draw the figure inside +# Uncomment only one of the lines below +# +# - The first uses just raw Tk +# +# - The second uses TkComponent. For that, you'll have to include the +# 'tk_component' gem into your project + +require_relative "./plot_window_raw_tk" +# require_relative "./plot_window_tk_component" + +Rubyplot.set_backend(:tk_canvas) + +# Here come the examples, extracted directly from the Tutorial + +plot_window(true) do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.bar! do |p| + p.data [23, 13, 45, 67, 5] # Data as given as heights of bars + p.color = :neon_red # Colour of the bars + p.spacing_ratio = 0.3 # Ratio of space the bars don't occupy out of the maximum space allotted to each bar + # Each bar is allotted equal space, so maximum space for each bar is total space divided by the number of bars + p.label = "Points"# Label for this data + end + + axes00.title = "A bar plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.area! do |p| + p.data [1, 2, 3, 4, 5, 6], [3, 2, 5, 5, 7, 4] # Data as height of consecutive points i.e. y coordinates + p.color = :black # Color of the area + p.label = "Stock A"# Label for this data + p.stacked true # stacked option makes area plot opaque i.e. opacity = 1 + # Opacity of the area plot is set to 0.3 for visibility if not stacked + end + axes00.area! do |p| + p.data [1, 2, 3, 4, 5, 6], [2, 1, 3, 4, 5, 1] # Data as height of consecutive points i.e. y coordinates + p.color = :yellow # Color of the area + p.label = "Stock B"# Label for this data + p.stacked true # stacked option makes area plot opaque i.e. opacity = 1 + # Opacity of the area plot is set to 0.3 for visibility if not stacked + end + + axes00.title = "An area plot" + axes00.x_title = "Time" + axes00.y_title = "Value" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.scatter! do |p| + p.data [1, 2, 3, 4, 5],[12, 55, 4, 10, 24] # Data as arrays of x coordinates and y coordinates + # i.e. the points are (1,12), (2,55), (3,4), (4,10), (5,24) + p.marker_type = :diamond # Type of marker + p.marker_fill_color = :lemon # Colour to be filled inside the marker + p.marker_size = 2 # Size of the marker, unit is 15*pixels + p.marker_border_color = :black # Colour of the border of the marker + p.label = "Diamonds"# Label for this data + end + + axes00.title = "A scatter plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.bubble! do |p| + p.data [12, 4, 25, 7, 19], [50, 30, 75, 12, 25], [0.5, 0.7, 0.4, 0.5, 1] # Data as arrays of x coordinates, y coordinates and sizes + # Size units are 27.5*pixel + p.color = :blue # Colour of the bubbles + p.label = "Bubbles 1"# Label for this data + # Opacity of the bubbles is set to 0.5 for visibility + end + axes00.bubble! do |p| + p.data [1, 7, 20, 27, 17], [41, 30, 48, 22, 5], [0.5, 1, 0.8, 0.9, 1] # Data as arrays of x coordinates, y coordinates and sizes + # Size units are 27.5*pixel + p.color = :red # Colour of the bubbles + p.label = "Bubbles 2"# Label for this data + # Opacity of the bubbles is set to 0.5 for visibility + end + + + axes00.title = "A bubble plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.histogram! do |p| + p.data 100.times.map{ rand(10) } # Data as an array of values + p.color = :electric_lime # Colour of the bars + p.label = "Counts"# Label for this data + # bins are not given so they are decided by Rubyplot + end + + axes00.title = "A histogram" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.candle_stick! do |p| + p.lows = [100, 110, 120, 130, 120, 110] # Array for minimum values for sticks + p.highs = [140, 150, 160, 170, 160, 150] # Array for maximum value for sticks + p.opens = [110, 120, 130, 140, 130, 120] # Array for minimum value for bars + p.closes = [130, 140, 150, 160, 150, 140] # Array for maximum value for bars + p.border_color = :black # Colour of the border of the bars + p.color = :yellow # Colour of the bars + p.label = "Data"# Label for this data + end + + axes00.title = "A candle-stick plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.error_bar! do |p| + p.data [1,2,3,4], [1,4,9,16] # Arrays for x coordinates and y coordinates + p.xerr = [0.5,1.0,1.5,0.3] # X error for each point + p.yerr = [0.6,0.2,0.8,0.1] # Y error for each point + p.color = :red # Colour of the line + p.label = "Values"# Label for this data + end + + axes00.title = "An error-bar plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.box_plot! do |p| + p.data [ +[60,70,80,70,50], +[100,40,20,80,70], +[30, 10] +] # Array of arrays for data for each box + p.color = :blue # Colours of the boxes + p.whiskers = 0.3 # whiskers for determining outliers + p.outlier_marker_type = :hglass # Type of the outlier marker + p.outlier_marker_color = :yellow # Fill colour of the outlier marker + # Border colour of the outlier marker is set to black + p.outlier_marker_size = 1 # Size of the outlier marker + p.label = "Data"# Label for this data + end + + axes00.title = "A box plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + # Step 3 + axes00 = figure.add_subplot! 0,0 + axes00.bar! do |p| + p.data [1, 2, 3, 4, 5] # Data as height of bars + p.color = :lemon # Colour of the bars + p.spacing_ratio = 0.2 # Ratio of space the bars don't occupy out of the maximum space allotted to each bar + # Each bar is allotted equal space, so maximum space for each bar is total space divided by the number of bars + p.label = "Stock 1"# Label for this data + end + # Spacing ratio declared first is considered + axes00.bar! do |p| + p.data [5, 4, 3, 2, 1] # Data as height of bars + p.color = :blue # Colour of the bars + p.spacing_ratio = 0.2 # Ratio of space the bars don't occupy out of the maximum space allotted to each bar + # Each bar is allotted equal space, so maximum space for each bar is total space divided by the number of bars + p.label = "Stock 2"# Label for this data + end + axes00.bar! do |p| + p.data [3, 5, 7, 5, 3] # Data as height of bars + p.color = :red # Colour of the bars + p.spacing_ratio = 0.2 # Ratio of space the bars don't occupy out of the maximum space allotted to each bar + # Each bar is allotted equal space, so maximum space for each bar is total space divided by the number of bars + p.label = "Stock 3"# Label for this data + end + + + axes00.title = "A multi bar plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.box_plot! do |p| + p.data [ +[60,70,80,70,50], +[100,40,20,80,70], +[30, 10] +] # Array of arrays for data for each box + p.color = :lemon # Colours of the boxes + p.whiskers = 0.3 # whiskers for determining outliers + p.outlier_marker_type = :hglass # Type of the outlier marker + p.outlier_marker_color = :yellow # Fill colour of the outlier marker + # Border colour of the outlier marker is set to black + p.outlier_marker_size = 1 # Size of the outlier marker + p.label = "Data"# Label for this data + end + axes00.box_plot! do |p| + p.data [ +[10, 30, 90, 30, 20], +[120, 140, 150, 120, 75], +[70, 90] +] # Array of arrays for data for each box + p.color = :red # Colours of the boxes + p.whiskers = 0.1 # whiskers for determining outliers + p.outlier_marker_type = :plus # Type of the outlier marker + p.outlier_marker_color = :blue # Fill colour of the outlier marker + # Border colour of the outlier marker is set to black + p.outlier_marker_size = 1 # Size of the outlier marker + p.label = "Data"# Label for this data + end + + axes00.title = "A multi box plot" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +plot_window do |figure| + axes00 = figure.add_subplot! 0,0 + axes00.plot! do |p| + d = (0..360).step(5).to_a + p.data d, d.map { |a| Math.sin(a * Math::PI / 180) } # Data as arrays of x coordinates and y coordinates + p.marker_type = :circle # Type of marker + p.marker_fill_color = :white # Colour to be filled inside the marker + p.marker_size = 0.5 # Size of the marker, unit is 15*pixels + p.marker_border_color = :black # Colour of the border of the marker + p.line_type = :solid # Type of the line + p.line_color = :black # Colour of the line + p.line_width = 2 # Width of the line + # p.fmt = 'b.-' # fmt argument to specify line type, marker type and colour in short + # fmt argument overwrites line type, marker type and all the colours i.e. marker_fill_color, marker_border_color, line_color + # line type, marker type and colour can be in any order + p.label = "sine" # Label for this data + end + + axes00.title = "A plot function example" + axes00.x_title = "X-axis" + axes00.y_title = "Y-axis" + axes00.square_axes = false +end + +Tk.mainloop diff --git a/lib/rubyplot.rb b/lib/rubyplot.rb index 770bfaf..a9a827a 100644 --- a/lib/rubyplot.rb +++ b/lib/rubyplot.rb @@ -174,6 +174,8 @@ def set_backend b @backend = Rubyplot::Backend::MagickWrapper.new when :gr @backend = Rubyplot::Backend::GRWrapper.new + when :tk_canvas + @backend = Rubyplot::Backend::TkCanvasWrapper.new end end end diff --git a/lib/rubyplot/artist/figure.rb b/lib/rubyplot/artist/figure.rb index 397f021..90d16d8 100644 --- a/lib/rubyplot/artist/figure.rb +++ b/lib/rubyplot/artist/figure.rb @@ -98,8 +98,9 @@ def write(file_name, device: :file) print_on_device(file_name, device) end - def show + def show(show_context = nil) Rubyplot.backend.output_device = Rubyplot.iruby_inline ? :iruby : :window + Rubyplot.backend.show_context = show_context print_on_device(nil, Rubyplot.backend.output_device) end diff --git a/lib/rubyplot/backend.rb b/lib/rubyplot/backend.rb index 81c4e2b..7b95fbf 100644 --- a/lib/rubyplot/backend.rb +++ b/lib/rubyplot/backend.rb @@ -1,8 +1,10 @@ require_relative 'backend/base' if ENV["RUBYPLOT_BACKEND"] == "GR" require_relative 'backend/gr_wrapper' -elsif ENV["RUBYPLOT_BACKEND"] = "MAGICK" +elsif ENV["RUBYPLOT_BACKEND"] == "MAGICK" require_relative 'backend/magick_wrapper' +elsif ENV["RUBYPLOT_BACKEND"] == "TK_CANVAS" + require_relative 'backend/tk_canvas_wrapper' end diff --git a/lib/rubyplot/backend/base.rb b/lib/rubyplot/backend/base.rb index 681bc2e..dff74e5 100644 --- a/lib/rubyplot/backend/base.rb +++ b/lib/rubyplot/backend/base.rb @@ -7,6 +7,9 @@ class Base attr_accessor :active_axes, :figure, :output_device + # Only needed by some backends + attr_accessor :show_context + # Write text anywhere on the canvas. abs_x and abs_y should be specified in terms # of Rubyplot Artist Co-ordinates. # diff --git a/lib/rubyplot/backend/tk_canvas_wrapper.rb b/lib/rubyplot/backend/tk_canvas_wrapper.rb new file mode 100644 index 0000000..e666080 --- /dev/null +++ b/lib/rubyplot/backend/tk_canvas_wrapper.rb @@ -0,0 +1,369 @@ +require 'tk' + +module Rubyplot + module Backend + class TkCanvasWrapper < Base + def tk_canvas + @show_context + end + + def show + end + + # Write text anywhere on the canvas. abs_x and abs_y should be specified in terms + # of Rubyplot Artist Co-ordinates. + # + # @param text [String] String of text to write. + # @param abs_x [Numeric] X co-ordinate of the text in Rubyplot Artist Co-ordinates. + # @param abs_y [Numeric] Y co-ordinate of the text in Rubyplot Aritst Co-ordinates. + # @param font_color [Symbol] Color of the font from Rubyplot::Colors. + # @param font [Symbol] Name of the font. + # @param size [Numeric] Size of the font. + # @param font_weight [Symbol] Measure of 'bigness' of the font. + # @param rotation [Numeric] Angle between 0 and 360 degrees signifying rotation of text. + # @param halign [Symbol] Horizontal alignment of the text from Artist::Text::HAlignment. + # @param valign [Symbol] Vertical alignment of the text from Artist::Text::VAlignment + def draw_text(text,color:,font: nil,size:, + font_weight: nil, halign: nil, valign: nil, + abs_x:, abs_y:,rotation: nil, stroke: nil, abs: true) + x = x_to_tk(abs_x, abs: abs) + y = y_to_tk(abs_y, abs: abs) + TkcText.new(tk_canvas, x, y, text: text, anchor: text_align_to_tk(halign, valign), + font: font_to_tk(font, size, font_weight), fill: color_to_tk(color), angle: rotation_to_tk(rotation)) + end + + # Draw a rectangle with optional fill. + # + # @param x1 [Numeric] Lower left X co-ordinate. + # @param y1 [Numeric] Lower left Y co-ordinate. + # @param x2 [Numeric] Upper right X co-ordinate. + # @param x2 [Numeric] Upper right Y co-ordinate. + def draw_rectangle(x1:,y1:,x2:,y2:, border_color: nil, fill_color: nil, + border_width: nil, border_type: nil, abs: false) + x1 = x_to_tk(x1, abs: abs) + x2 = x_to_tk(x2, abs: abs) + y1 = y_to_tk(y1, abs: abs) + y2 = y_to_tk(y2, abs: abs) + TkcRectangle.new(tk_canvas, x1, y1, x2, y2, + fill: color_to_tk(fill_color), outline: color_to_tk(border_color), + width: width_to_tk(border_width), dash: line_type_to_tk(border_type)) + end + + # Draw multiple markers as specified by co-ordinates. + # + # @param x [[Numeric]] Array of X co-ordinates. + # @param y [[Numeric]] Array of Y co-ordinates. + # @param marker_type [Symbol] A marker type from Rubyplot::MARKERS. + # @param marker_color [Symbol] A color from Rubyplot::Color. + # @param marker_size [Numeric] Size of the marker. + def draw_markers(x:, y:, type:, fill_color:, border_color: nil, size:) + (0..x.size - 1).each do |idx| + draw_marker(x: x[idx], y: y[idx], type: type, + fill_color: fill_color, border_color: border_color, size: size[idx]) + end + end + + # Draw a circle.n + def draw_circle(x:, y:, radius:, border_width:, border_color:, border_type:, + fill_color:, fill_opacity:) + x = x_to_tk(x) + y = y_to_tk(y) + radius = distance_to_tk(radius) + TkcOval.new(tk_canvas, x - radius / 2, y - radius / 2, x + radius / 2, y + radius / 2, + fill: color_to_tk(fill_color), outline: color_to_tk(border_color), + width: width_to_tk(border_width), dash: line_type_to_tk(border_type), + stipple: opacity_to_tk_stipple(fill_opacity)) + end + + # Draw a polygon and fill it with color. Co-ordinates are specified in (x,y) + # pairs in the coords Array. + # + # @param x [Array] Array containing X co-ordinates. + # @param y [Array] Array containting Y co-ordinates. + # @param border_width [Numeric] Widht of the border. + def draw_polygon(x:, y:, border_width:, border_type:, border_color:, fill_color:, + fill_opacity:) + TkcPolygon.new(tk_canvas, *points_to_tk(x, y), + fill: color_to_tk(fill_color), outline: color_to_tk(border_color), + width: width_to_tk(border_width), dash: line_type_to_tk(border_type), + stipple: opacity_to_tk_stipple(fill_opacity)) + end + + def draw_lines(x:, y:, width:, type:, color:, opacity:) + TkcLine.new(tk_canvas, *points_to_tk(x, y), + fill: color_to_tk(color), + width: width_to_tk(width), dash: line_type_to_tk(type), + stipple: opacity_to_tk_stipple(opacity)) + end + + def draw_arrow(x1:, y1:, x2:, y2:, size:, style:) + x1 = x_to_tk(x1) + x2 = x_to_tk(x2) + y1 = y_to_tk(y1) + y2 = y_to_tk(y3) + TkcLine.new(tk_canvas, x1, y1, x2, y2, + fill: color_to_tk(color), arrow: 'last', + arrowshape: arrow_to_tk(size, style)) + end + + def init_output_device file_name, device: :file + raise NotImplementedError if device == :file + tk_canvas.delete('all') + @axes_map = {} + end + + def stop_output_device + draw_axes + end + + def draw_x_axis(minor_ticks:, origin:, major_ticks:, minor_ticks_count:, major_ticks_count:) + if @axes_map[active_axes.object_id].nil? + @axes_map[@active_axes.object_id]={ + axes: @active_axes, + x_origin: origin, + minor_ticks: minor_ticks, + major_ticks: major_ticks, + minor_ticks_count: minor_ticks_count, + major_ticks_count: major_ticks_count + } + else + @axes_map[@active_axes.object_id].merge!( + x_origin: origin, + minor_ticks: minor_ticks, + major_ticks: major_ticks, + minor_ticks_count: minor_ticks_count, + major_ticks_count: major_ticks_count + ) + end + end + + def draw_y_axis(minor_ticks:, origin:, major_ticks:, minor_ticks_count:, major_ticks_count:) + if @axes_map[@active_axes.object_id].nil? + @axes_map[@active_axes.object_id]={ + axes: @active_axes, + y_origin: origin, + minor_ticks: minor_ticks, + major_ticks: major_ticks, + minor_ticks_count: minor_ticks_count, + major_ticks_count: major_ticks_count + } + else + @axes_map[@active_axes.object_id].merge!( + y_origin: origin, + minor_ticks: minor_ticks, + major_ticks: major_ticks, + minor_ticks_count: minor_ticks_count, + major_ticks_count: major_ticks_count + ) + end + end + + def draw_axes + @axes_map.each_value do |v| + axes = v[:axes] + @active_axes = axes + TkcLine.new(tk_canvas, + x_to_tk(axes.x_range[1]), y_to_tk(v[:y_origin]), + x_to_tk(v[:x_origin]), y_to_tk(v[:y_origin]), + x_to_tk(v[:x_origin]), y_to_tk(axes.y_range[1]), + fill: 'black', width: 2) + # Drawing ticks + # X major ticks + axes.x_axis.major_ticks.each do |x_major_tick| + TkcLine.new(tk_canvas, + x_to_tk(x_major_tick.coord), y_to_tk(v[:y_origin]), + x_to_tk(x_major_tick.coord), y_to_tk(v[:y_origin]) + x_major_tick.tick_size * 10.0, + fill: 'black', width: 2) + TkcText.new(tk_canvas, + x_to_tk(x_major_tick.coord), y_to_tk(v[:y_origin]) + 20.0, + text: x_major_tick.label, anchor: 'center', + font: 'TkCaptionFont', fill: 'black') + end + # X minor ticks + axes.x_axis.minor_ticks.each do |x_minor_tick| + TkcLine.new(tk_canvas, + x_to_tk(x_minor_tick.coord), y_to_tk(v[:y_origin]), + x_to_tk(x_minor_tick.coord), y_to_tk(v[:y_origin]) + x_minor_tick.tick_size * 10.0, + fill: 'black', width: 2) + end + # Y major ticks + axes.y_axis.major_ticks.each do |y_major_tick| + TkcLine.new(tk_canvas, + x_to_tk(v[:x_origin]) - y_major_tick.tick_size * 10.0, y_to_tk(y_major_tick.coord), + x_to_tk(v[:x_origin]), y_to_tk(y_major_tick.coord), + fill: 'black', width: 2) + TkcText.new(tk_canvas, + x_to_tk(v[:x_origin]) - 15.0, y_to_tk(y_major_tick.coord), + text: y_major_tick.label, anchor: 'e', + font: 'TkCaptionFont', fill: 'black') + end + # Y minor ticks + axes.y_axis.minor_ticks.each do |y_minor_tick| + TkcLine.new(tk_canvas, + x_to_tk(v[:x_origin]) - y_minor_tick.tick_size * 10.0, y_to_tk(y_minor_tick.coord), + x_to_tk(v[:x_origin]), y_to_tk(y_minor_tick.coord), + fill: 'black', width: 2) + end + end + end + + private + + def x_to_tk(x, abs: false) + x_factor = tk_canvas.winfo_width.to_f / @canvas_width.to_f + if abs + (@canvas_width.to_f * x.to_f / @figure.max_x.to_f) * x_factor + else + raw_x = ((x.to_f - @active_axes.x_range[0].to_f) / (@active_axes.x_range[1].to_f - @active_axes.x_range[0].to_f)) * @canvas_width.to_f * x_factor + add_margin_x(raw_x) + end + end + + def y_to_tk(y, abs: false) + y_factor = tk_canvas.winfo_height.to_f / @canvas_height.to_f + if abs + (@canvas_height.to_f * (@figure.max_y.to_f - y.to_f) / @figure.max_y.to_f) * y_factor + else + raw_y = ((@active_axes.y_range[1].to_f - y.to_f) / (@active_axes.y_range[1].to_f - @active_axes.y_range[0].to_f)) * @canvas_height.to_f * y_factor + add_margin_y(raw_y) + end + end + + def add_margin_x(x) + x_factor = tk_canvas.winfo_width.to_f / @canvas_width.to_f + x_shift = (@active_axes.abs_x + @active_axes.left_margin) * @canvas_width / @figure.max_x + x_shift *= x_factor + real_width = @active_axes.width - (@active_axes.left_margin + @active_axes.right_margin) + margin_factor = real_width.to_f / @figure.max_x + (x + x_shift) * margin_factor + end + + def add_margin_y(y) + y_factor = tk_canvas.winfo_height.to_f / @canvas_height.to_f + y_shift = ((@active_axes.height * (@figure.nrows - 1)) - (@active_axes.abs_y - @figure.bottom_spacing) + @figure.top_spacing + @active_axes.top_margin) * @canvas_height / @figure.max_y + y_shift *= y_factor + real_height = @active_axes.height - (@active_axes.bottom_margin + @active_axes.top_margin) + margin_factor = real_height.to_f / @figure.max_y + (y + y_shift) * margin_factor + end + + def distance_to_tk(d, abs: false) + x_to_tk(d, abs: abs) + end + + def width_to_tk(w) + w + end + + def size_to_tk(s) + s * 10.0 + end + + def points_to_tk(x, y) + (0..x.size - 1).map do |idx| + [x_to_tk(x[idx]), y_to_tk(y[idx])] + end.flatten + end + + def color_to_tk(color) + Rubyplot::Color::COLOR_INDEX[color] + end + + TEXT_HALIGNMENT_MAP = { + normal: 'w', + left: 'w', + center: 'ew', + right: 'e' + }.freeze + + TEXT_VALIGNMENT_MAP = { + normal: 's', + top: 'n', + cap: 's', + half: 'ns', + base: 's', + bottom: 's' + }.freeze + + def text_align_to_tk(halign, valign) + TEXT_VALIGNMENT_MAP[valign] + TEXT_HALIGNMENT_MAP[halign] + end + + def font_to_tk(font, size, font_weight) + if font == :times_roman && + size == 25.0 && + font_weight.nil? + # Default values for axes + # Let's use a better font + return 'TkMenuFont' + end + if font == :times_roman && + size == 20.0 && + font_weight.nil? + # Default values for caption + # Let's use a better font + return 'TkCaptionFont' + end + "#{font} #{size.to_i} #{font_weight}" + end + + def rotation_to_tk(rotation) + (rotation || 0.0) * -1.0 + end + + def line_type_to_tk(type) + nil + end + + def arrow_to_tk(size, style) + nil + end + + def opacity_to_tk_stipple(opacity) + if opacity == 1.0 + return nil + elsif opacity >= 0.75 + return 'gray75' + elsif opacity >= 0.5 + return 'gray50' + elsif opacity >= 0.25 + return 'gray25' + else + return 'gray12' + end + end + + MARKER_PROCS = { + dot: ->(canvas, x, y, border_color, fill_color, size) { + }, + circle: ->(canvas, x, y, border_color, fill_color, size) { + TkcOval.new(canvas, x - size / 2, y - size / 2, x + size / 2, y + size / 2, + fill: fill_color, outline: border_color) + }, + diamond: ->(canvas, x, y, border_color, fill_color, size) { + TkcPolygon.new(canvas, + x - size / 2, y, + x, y - size / 2, + x + size / 2, y, + x, y + size / 2, + fill: fill_color, outline: border_color) + } + } + + def draw_marker(x:, y:, type:, fill_color:, border_color:, size:) + x = x_to_tk(x) + y = y_to_tk(y) + size = size_to_tk(size) + fill_color = color_to_tk(fill_color) + if (res = type.to_s.match(/\Asolid_(.*)/)) + type = r.captures.first.to_sym + border_color = fill_color + else + border_color = color_to_tk(border_color) + end + code = MARKER_PROCS[type] || MARKER_PROCS[:circle] + code.call(tk_canvas, x, y, border_color, fill_color, size) + end + end + end +end diff --git a/rubyplot.gemspec b/rubyplot.gemspec index e8a20e6..995c7c5 100644 --- a/rubyplot.gemspec +++ b/rubyplot.gemspec @@ -30,4 +30,5 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'parallel_tests' spec.add_runtime_dependency 'rmagick', '>= 2.13.4' + spec.add_runtime_dependency "tk", "~> 0.3.0" end From acfddbec429a1b571cb0f7b359d748f1309556e1 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Sat, 27 Feb 2021 19:21:20 +0100 Subject: [PATCH 2/7] Added Readme notes for TkCanvas backend --- README.md | 40 +++++++++++++++++++++++++++- examples/tk_canvas_wrapper/README.md | 2 +- rubyplot.gemspec | 2 +- 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 200c0a5..5a53817 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,26 @@ An advanced plotting library for Ruby. It aims to allow you to visualize anything, anywhere with a flexible, extensible, Ruby-like API. -# Usage +# Backends + +Rubyplot can use different backends to render its plots: + +- `magick` Using ImageMagick +- `gr` Using the GR framework +- `tk_canvas` Interactive drawing in a Canvas object using Ruby/Tk + +Call `Rubyplot.set_backend` at the start of your program to select +the desired backend. + +Pick the desired one: + +``` ruby +Rubyplot.set_backend(:magick) +Rubyplot.set_backend(:gr) +Rubyplot.set_backend(:tk_canvas) +``` + +# Installing GR Install the GR framework from the [website](https://gr-framework.org/c.html). @@ -17,6 +36,25 @@ export GKS_FONTPATH="/home/sameer/Downloads/gr" export RUBYPLOT_BACKEND="GR" ``` +# Installing Tk + +In addition to install the `tk` gem in your project, you need to +install the Tcl/Tk runtime in your system. The instructions depending +on the OS but it is a safe bet to install the Community Edition of +Active Tcl from https://www.activestate.com/ + +You have more details about installing Tk in different systems in the +excelent [TkDocs](https://tkdocs.com/tutorial/install.html) website. + +If you want to have a more modern way of interacting with Tk from +Ruby, you can use +[TkComponent](https://github.com/josepegea/tk_component) and +[TkInspect](https://github.com/josepegea/tk_inspect). + +# Examples + +See the [examples](./examples) to see some how-to code. + # Short term priorities Check milestones in GitHub for more information. diff --git a/examples/tk_canvas_wrapper/README.md b/examples/tk_canvas_wrapper/README.md index aefff6b..4ea9a8d 100644 --- a/examples/tk_canvas_wrapper/README.md +++ b/examples/tk_canvas_wrapper/README.md @@ -6,4 +6,4 @@ using the TkCanvasWrapper. ## Running bundle exec tk_plot_demo.rb - + diff --git a/rubyplot.gemspec b/rubyplot.gemspec index 995c7c5..9b2f727 100644 --- a/rubyplot.gemspec +++ b/rubyplot.gemspec @@ -8,7 +8,7 @@ Rubyplot::DESCRIPTION = "An advanced plotting library for Ruby." Gem::Specification.new do |spec| spec.name = 'rubyplot' spec.version = Rubyplot::VERSION - spec.authors = ['Arafat Khan', 'Pranav Garg', 'John Woods', 'Pjotr Prins', 'Sameer Deshmukh'] + spec.authors = ['Arafat Khan', 'Pranav Garg', 'John Woods', 'Pjotr Prins', 'Sameer Deshmukh', 'Josep Egea'] spec.email = ['sameer.deshmukh93@gmail.com'] # add other author ids spec.summary = %q{An advaced plotting library for Ruby.} spec.description = Rubyplot::DESCRIPTION From b6c9cb35d700286a99bfe5b22e9c180a01330e29 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Sat, 27 Feb 2021 20:57:39 +0100 Subject: [PATCH 3/7] Added notes to Contributing docs --- CONTRIBUTING.md | 55 ++++++++++++++++++++++ examples/tk_canvas_wrapper/tk_plot_demo.rb | 22 +++++++++ 2 files changed, 77 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 51e97b5..38416a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -125,3 +125,58 @@ Name of output file in case of wanting to write to file. ``` export GKS_FILEPATH="hello.png" ``` + +# Tk Canvas backend notes + +Tk is a multiplatform GUI toolkit that works with Ruby, in addition to +TCL, Python and Perl. + +Tk allows to create multiplatform GUI applications that need little or +no code changes to run in UNIX, macOS and Windows. + +You can learn more about using Tk with Ruby in +[TkDocs](https://tkdocs.com). + +If you want to have a more modern way of interacting with Tk from +Ruby, you can use +[TkComponent](https://github.com/josepegea/tk_component) and +[TkInspect](https://github.com/josepegea/tk_inspect). + +## Installing Tk + +In addition to install the `tk` gem in your project, you need to +install the Tcl/Tk runtime in your system. The instructions depending +on the OS but it is a safe bet to install the Community Edition of +Active Tcl from https://www.activestate.com/ + +You have more details about installing Tk in different systems in the +excelent [TkDocs](https://tkdocs.com/tutorial/install.html) website. + +## Units + +Given that this backend only works with screen output, all measures +are interpreted as pixels. + +## Limitations of Tk + +Some rendering features of ruby_plot are not available in Tk. When +those are used, the results are downgraded as gracefully as possible. + +Right now these are the unsuported features: + +- Opacity: Tk doesn't support opacity. It does support "stipple" which + is a set of different density fill patterns, that don't completely + overwrite the background. TkCanvas tries to adapt the required + opacity to the nearest stipple for the closest result. Take into + account, though, that not all Tk implementations support + stipple. For instance, macOS implementations ignore it and always + use 100% opacity. + +- Marker types: Tk doesn't support all the marker types defined in + ruby_plot. They could be generated manually, but right now only + those that are a 1-to-1 match are implemented. + +## Testing + +Given that this backend doesn't generate image files and are meant to +generate interactive images, the current tests don't apply. diff --git a/examples/tk_canvas_wrapper/tk_plot_demo.rb b/examples/tk_canvas_wrapper/tk_plot_demo.rb index f1ed73a..3e7ec47 100755 --- a/examples/tk_canvas_wrapper/tk_plot_demo.rb +++ b/examples/tk_canvas_wrapper/tk_plot_demo.rb @@ -40,6 +40,28 @@ axes00.square_axes = false end +plot_window do |figure| + axes = figure.add_subplot! 0,0 + [ + ["Charles", [20, 10, 5, 12, 11, 6, 10, 7], :silver], + ["Adam", [5, 10, 20, 6, 9, 12, 14, 8], :black], + ["Daniel", [19, 9, 6, 11, 12, 7, 15, 8], :orangeish] + ].each do |label, data, color| + axes.stacked_bar! do |p| + p.data data + p.label = label + p.color = color + p.spacing_ratio = 0.6 + end + end + axes.title = "Income." + axes.x_title = "X title" + axes.y_title = "Y title" + axes.x_ticks = ['Jan', 'Feb', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'] + axes.y_ticks = ['5', '10', '15', '20', '25', '30'] +end + plot_window do |figure| axes00 = figure.add_subplot! 0,0 axes00.area! do |p| From 5e7030f603346048b18640142467d2d15924a463 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Sat, 27 Feb 2021 21:03:21 +0100 Subject: [PATCH 4/7] Keep ImageMagick as default backend when no ENV variable present --- lib/rubyplot/backend.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rubyplot/backend.rb b/lib/rubyplot/backend.rb index 7b95fbf..a6c64fc 100644 --- a/lib/rubyplot/backend.rb +++ b/lib/rubyplot/backend.rb @@ -1,10 +1,10 @@ require_relative 'backend/base' if ENV["RUBYPLOT_BACKEND"] == "GR" require_relative 'backend/gr_wrapper' -elsif ENV["RUBYPLOT_BACKEND"] == "MAGICK" - require_relative 'backend/magick_wrapper' elsif ENV["RUBYPLOT_BACKEND"] == "TK_CANVAS" require_relative 'backend/tk_canvas_wrapper' +elsif ENV["RUBYPLOT_BACKEND"] = "MAGICK" + require_relative 'backend/magick_wrapper' end From ef051723619f0bfcd12d78ee7b44f159f781fde7 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Sun, 28 Feb 2021 17:01:42 +0100 Subject: [PATCH 5/7] Changes to travis.yml to require Tk libs --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 68a3a04..fa53b8d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,9 @@ language: ruby +addons: + apt: + packages: tk-dev rvm: - '2.0' - '2.1' From 2cc09aa7c9b34ecbc58f4efe868323e4f3ad7551 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Sun, 28 Feb 2021 18:43:27 +0100 Subject: [PATCH 6/7] Removed the hard dependency on 'tk' gem That way only projects that need it will have to deal with install Tk (that also includes tests) --- .travis.yml | 3 --- Gemfile | 4 ++++ README.md | 12 ++++++++++++ rubyplot.gemspec | 1 - 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index fa53b8d..68a3a04 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,6 @@ language: ruby -addons: - apt: - packages: tk-dev rvm: - '2.0' - '2.1' diff --git a/Gemfile b/Gemfile index fe47135..792c764 100644 --- a/Gemfile +++ b/Gemfile @@ -6,3 +6,7 @@ group :test do gem 'rake' gem 'rspec' end + +group :development do + gem 'tk' +end diff --git a/README.md b/README.md index 5a53817..986feba 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,18 @@ export RUBYPLOT_BACKEND="GR" # Installing Tk +This gem is not including `tk` in its dependencies so you don't have +to install it if you're not going to use the `tk_canvas` backend. + +If you will need to use this backend, you will need to add a reference +to `tk` in your `Gemfile` in addition to referring to `rubyplot`. + +``` ruby +gem 'tk' +``` + +Running `bundle install` after this change will install it. + In addition to install the `tk` gem in your project, you need to install the Tcl/Tk runtime in your system. The instructions depending on the OS but it is a safe bet to install the Community Edition of diff --git a/rubyplot.gemspec b/rubyplot.gemspec index 9b2f727..c4110f0 100644 --- a/rubyplot.gemspec +++ b/rubyplot.gemspec @@ -30,5 +30,4 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'parallel_tests' spec.add_runtime_dependency 'rmagick', '>= 2.13.4' - spec.add_runtime_dependency "tk", "~> 0.3.0" end From e2b3ad9a7052d83b9f5d13b7ff44c161b813ddf6 Mon Sep 17 00:00:00 2001 From: Josep Egea Date: Sun, 28 Feb 2021 19:08:08 +0100 Subject: [PATCH 7/7] Made `tk` gem really optional --- CONTRIBUTING.md | 11 +++++++++++ Gemfile | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38416a4..454d13c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -144,6 +144,17 @@ Ruby, you can use ## Installing Tk +The `Gemfile` includes the `tk` gem inside an `optional` group, in +order to not force users not interested in TkCanvas to deal with the +installation of Tk. + +If you do want to use TkCanvas while developing this gem, you'll need +to run `bundle install` specifying the `tk_canvas` group explicitly: + +```ruby +bundle install --with tk_canvas +``` + In addition to install the `tk` gem in your project, you need to install the Tcl/Tk runtime in your system. The instructions depending on the OS but it is a safe bet to install the Community Edition of diff --git a/Gemfile b/Gemfile index 792c764..83ac2e7 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,6 @@ group :test do gem 'rspec' end -group :development do +group :tk_canvas, optional: true do gem 'tk' end