Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tk canvas backend #58

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -125,3 +125,69 @@ 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

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
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.
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
@@ -6,3 +6,7 @@ group :test do
gem 'rake'
gem 'rspec'
end

group :tk_canvas, optional: true do
gem 'tk'
end
52 changes: 51 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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,37 @@ export GKS_FONTPATH="/home/sameer/Downloads/gr"
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
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.
9 changes: 9 additions & 0 deletions examples/tk_canvas_wrapper/README.md
Original file line number Diff line number Diff line change
@@ -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

26 changes: 26 additions & 0 deletions examples/tk_canvas_wrapper/plot_window_raw_tk.rb
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions examples/tk_canvas_wrapper/plot_window_tk_component.rb
Original file line number Diff line number Diff line change
@@ -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
296 changes: 296 additions & 0 deletions examples/tk_canvas_wrapper/tk_plot_demo.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
#!/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|
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|
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
2 changes: 2 additions & 0 deletions lib/rubyplot.rb
Original file line number Diff line number Diff line change
@@ -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
3 changes: 2 additions & 1 deletion lib/rubyplot/artist/figure.rb
Original file line number Diff line number Diff line change
@@ -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

2 changes: 2 additions & 0 deletions lib/rubyplot/backend.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require_relative 'backend/base'
if ENV["RUBYPLOT_BACKEND"] == "GR"
require_relative 'backend/gr_wrapper'
elsif ENV["RUBYPLOT_BACKEND"] == "TK_CANVAS"
require_relative 'backend/tk_canvas_wrapper'
elsif ENV["RUBYPLOT_BACKEND"] = "MAGICK"
require_relative 'backend/magick_wrapper'
end
3 changes: 3 additions & 0 deletions lib/rubyplot/backend/base.rb
Original file line number Diff line number Diff line change
@@ -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.
#
369 changes: 369 additions & 0 deletions lib/rubyplot/backend/tk_canvas_wrapper.rb
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion rubyplot.gemspec
Original file line number Diff line number Diff line change
@@ -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