gerbilcharts 0.2.13 → 0.5.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/History.txt +35 -0
  2. data/Manifest.txt +8 -0
  3. data/README.txt +8 -8
  4. data/lib/gerbilcharts/charts.rb +3 -0
  5. data/lib/gerbilcharts/charts/bubble_chart.rb +30 -0
  6. data/lib/gerbilcharts/charts/conversation_ring.rb +31 -0
  7. data/lib/gerbilcharts/charts/line_chart.rb +1 -0
  8. data/lib/gerbilcharts/charts/matrix_chart.rb +32 -0
  9. data/lib/gerbilcharts/charts/square_line_chart.rb +7 -0
  10. data/lib/gerbilcharts/models.rb +1 -0
  11. data/lib/gerbilcharts/models/bucketized_timeseries_graph_model.rb +1 -1
  12. data/lib/gerbilcharts/models/graph_model_group.rb +19 -2
  13. data/lib/gerbilcharts/models/matrix_model.rb +85 -0
  14. data/lib/gerbilcharts/models/monotonous_graph_model.rb +5 -1
  15. data/lib/gerbilcharts/models/presets.rb +18 -11
  16. data/lib/gerbilcharts/models/raw_range.rb +1 -1
  17. data/lib/gerbilcharts/models/round_range.rb +3 -1
  18. data/lib/gerbilcharts/models/round_time_range.rb +19 -29
  19. data/lib/gerbilcharts/models/sampled_timeseries_graph_model.rb +1 -1
  20. data/lib/gerbilcharts/public/brushmetal.css +123 -74
  21. data/lib/gerbilcharts/public/gerbil.js +104 -64
  22. data/lib/gerbilcharts/surfaces.rb +3 -0
  23. data/lib/gerbilcharts/surfaces/bubble_surface.rb +105 -0
  24. data/lib/gerbilcharts/surfaces/chart.rb +4 -2
  25. data/lib/gerbilcharts/surfaces/conversation_ring.rb +64 -0
  26. data/lib/gerbilcharts/surfaces/detailed_legend.rb +35 -13
  27. data/lib/gerbilcharts/surfaces/graph_element.rb +8 -1
  28. data/lib/gerbilcharts/surfaces/grid.rb +8 -2
  29. data/lib/gerbilcharts/surfaces/horizontal_axis.rb +12 -3
  30. data/lib/gerbilcharts/surfaces/horizontal_time_axis.rb +5 -4
  31. data/lib/gerbilcharts/surfaces/impulse_surface.rb +1 -1
  32. data/lib/gerbilcharts/surfaces/legend.rb +43 -6
  33. data/lib/gerbilcharts/surfaces/mark_band.rb +17 -1
  34. data/lib/gerbilcharts/surfaces/matrix_surface.rb +87 -0
  35. data/lib/gerbilcharts/surfaces/pie_surface.rb +109 -96
  36. data/lib/gerbilcharts/surfaces/rect.rb +18 -0
  37. data/lib/gerbilcharts/surfaces/stacked_area_surface.rb +42 -2
  38. data/lib/gerbilcharts/surfaces/stacked_grid.rb +1 -3
  39. data/lib/gerbilcharts/surfaces/title_panel.rb +7 -2
  40. data/lib/gerbilcharts/surfaces/tracker.rb +4 -1
  41. data/lib/gerbilcharts/surfaces/vertical_axis.rb +2 -2
  42. data/lib/gerbilcharts/svgdc.rb +1 -0
  43. data/lib/gerbilcharts/svgdc/css_inliner.rb +2 -2
  44. data/lib/gerbilcharts/svgdc/svg_ellipse.rb +21 -0
  45. data/lib/gerbilcharts/svgdc/svg_polygon.rb +19 -0
  46. data/lib/gerbilcharts/svgdc/svgdc.rb +4 -1
  47. data/lib/gerbilcharts/version.rb +2 -3
  48. data/test/test_bar.rb +1 -1
  49. data/test/test_bubble.rb +52 -0
  50. data/test/test_conversation.rb +34 -0
  51. data/test/test_lines.rb +3 -3
  52. data/test/test_matrix.rb +34 -0
  53. data/test/test_noob.rb +9 -6
  54. data/test/test_pie.rb +2 -2
  55. data/test/test_ranges.rb +15 -2
  56. data/test/test_sa.rb +88 -0
  57. metadata +14 -2
@@ -27,6 +27,13 @@ class Rect
27
27
  @top += t
28
28
  @bottom -= b
29
29
  end
30
+
31
+ def deflate(n)
32
+ @left+=n
33
+ @top+=n
34
+ @bottom-=n
35
+ @right-=n
36
+ end
30
37
 
31
38
  def initfrom(r)
32
39
  @left=r.left
@@ -74,6 +81,17 @@ class Rect
74
81
  def height
75
82
  @bottom-@top
76
83
  end
84
+
85
+ def translate_x(newx)
86
+ w=width
87
+ @left=newx
88
+ @right=@left+w
89
+ end
90
+
91
+ def offset_x(delta)
92
+ @left+=delta
93
+ @right+=delta
94
+ end
77
95
 
78
96
 
79
97
  def to_s
@@ -9,22 +9,61 @@ class StackedAreaSurface < Surface
9
9
  end
10
10
 
11
11
  def int_render(g)
12
- rx = parent.modelgroup.effective_round_range_x
12
+ range_options_x = parent.get_global_option(:scaling_x,:auto)
13
+
14
+ rx = parent.modelgroup.effective_range_x(range_options_x)
13
15
  ry = parent.modelgroup.cumulative_sweep_round_range_y0
14
16
 
15
17
  # ajax if used
16
18
  if parent.usesAjax?
17
19
  set_ajaxSurfaceContext(rx.rmax,ry.rmax,"SA")
18
20
  end
21
+
22
+
23
+ # draw reference if any
24
+ if parent.modelgroup.has_ref_model?
25
+ ref_model = parent.modelgroup.ref_model
26
+
27
+ y_zero = scale_y 0,ry
28
+ g.begin_polygon
29
+ firstpoint=true
30
+ xpos,ypos=0,0
31
+ last_x=nil
32
+ ref_model.each_tuple do |x,y|
33
+
34
+ xpos = scale_x x,rx
35
+ ypos = scale_y y,ry
36
+
37
+ if firstpoint
38
+ g.polygon_point xpos,y_zero
39
+ firstpoint=false
40
+ end
41
+
42
+ if last_x && (x-last_x) > parent.modelgroup.sweep_interval
43
+ g.polygon_point scale_x(last_x,rx) ,y_zero
44
+ g.polygon_point scale_x(x,rx) ,y_zero
45
+ end
46
+ last_x=x
47
+
48
+ g.polygon_point xpos,ypos
49
+ end
50
+ g.polygon_point xpos,y_zero
51
+ g.end_polygon(:id => "ref_mod")
52
+ end
19
53
 
20
54
  # prepare models for sweeping
21
55
  sweep_pos= rx.rmin
22
56
  sweep_to = rx.rmax
23
57
  polygons = []
24
58
  modnames = []
59
+
60
+ klass_poly = parent.get_global_option(:squarize,false) ?
61
+ GerbilCharts::SVGDC::SVGSquarizedPolygon :
62
+ GerbilCharts::SVGDC::SVGPolygon
63
+
25
64
  parent.modelgroup.each_model do | mod|
26
65
  mod.begin_sweep
27
- polygons << GerbilCharts::SVGDC::SVGPolygon.new
66
+ polygons << klass_poly.new
28
67
  modnames << mod.name
29
68
  end
30
69
 
@@ -36,6 +75,7 @@ class StackedAreaSurface < Surface
36
75
  acc_y = 0
37
76
  parent.modelgroup.each_model_with_index do | mod, i|
38
77
  acc_y += mod.sweep(sweep_pos)
78
+
39
79
  xpos = scale_x sweep_pos,rx
40
80
  ypos = scale_y acc_y,ry
41
81
 
@@ -5,11 +5,9 @@ module GerbilCharts::Surfaces
5
5
  class StackedGrid < Grid
6
6
 
7
7
  protected
8
-
9
8
  def grid_range_y
10
- return parent.modelgroup.cumulative_round_range_y0
9
+ return parent.modelgroup.cumulative_sweep_round_range_y0
11
10
  end
12
-
13
11
  end
14
12
 
15
13
  end
@@ -21,10 +21,15 @@ class TitlePanel < Panel
21
21
  if @just == :left
22
22
  g.textout(@bounds.left, @bounds.top+16, parent.modelgroup.name, opts)
23
23
  else
24
- opts.store( "text-anchor", "end" )
25
- g.textout(@bounds.right, @bounds.top+16, parent.modelgroup.name, opts)
24
+ g.textout(@bounds.right, @bounds.top+16, parent.modelgroup.name, opts.merge("text-anchor" => "end"))
26
25
  end
27
26
 
27
+ xoff=0
28
+ parent.get_global_option(:toolhrefs,[]).each do |tool|
29
+ topts = {:class => "titletool"}
30
+ g.textout(@bounds.left + xoff, @bounds.top+29, tool[0] , topts.merge(:href => tool[1]) )
31
+ xoff =xoff+ 7*(tool[0].length )
32
+ end
28
33
  end
29
34
 
30
35
  def align_to_anchor(anc)
@@ -20,10 +20,12 @@ class Tracker < GraphElement
20
20
  # render the elements directly
21
21
  def render_direct(xfrag)
22
22
 
23
+
23
24
  # calculate scaling factors
24
25
  range_options_x = parent.get_global_option(:scaling_x,:auto)
25
26
  rx = parent.modelgroup.effective_range_x(range_options_x)
26
27
 
28
+ return unless rx.rmin.is_a? Time
27
29
 
28
30
  xfrag.g(:id=> 'gtrackerrect', :visibility=>'hidden') {
29
31
  xfrag.rect(:id=>"trackerrect", :class=>"trackerrect",
@@ -52,12 +54,13 @@ class Tracker < GraphElement
52
54
  }
53
55
  }
54
56
 
55
- raise "Time Tracker expects X-Axis to be a time interval " unless rx.rmin.is_a? Time
56
57
 
57
58
  xfrag.g(:id=>"gtrackerdata", :visibility=>'hidden',
58
59
  :gerb_fromts=>rx.rmin.tv_sec,
59
60
  :gerb_seconds=>rx.rmax.tv_sec-rx.rmin.tv_sec,
60
61
  :gerb_scale =>(rx.delta)/parent.anchor.bounds.width,
62
+ :gerb_tzoffset => Time.new.utc_offset,
63
+ :gerb_tzname => Time.new.zone,
61
64
  :gerb_selts=>1,
62
65
  :gerb_selsecs=>1)
63
66
 
@@ -34,13 +34,13 @@ class VerticalAxis < Axis
34
34
 
35
35
  ry.each_label do |val,label|
36
36
  yp = scale_y val,ry
37
- yp_label=yp
37
+ yp_label=yp + 4
38
38
 
39
39
  # make sure the edge ones are visible
40
40
  yp_label = max(yp_label,@bounds.top+10)
41
41
 
42
42
  g.textout(@bounds.right-4, yp_label, label, {:class => "axislabel", "text-anchor" => "end"})
43
- g.line(@bounds.right-3,yp,@bounds.right+2,yp, {:class => "axistickmajor"})
43
+ g.line(@bounds.right-1,yp,@bounds.right+1,yp, {:class => "axistickmajor"})
44
44
  end
45
45
 
46
46
  end
@@ -23,3 +23,4 @@ require 'gerbilcharts/svgdc/svg_text'
23
23
  require 'gerbilcharts/svgdc/svg_arc'
24
24
  require 'gerbilcharts/svgdc/svg_arc'
25
25
  require 'gerbilcharts/svgdc/css_inliner'
26
+ require 'gerbilcharts/svgdc/svg_ellipse'
@@ -22,8 +22,8 @@ class CssInliner
22
22
  parts.each { |p| p.strip!; p.squeeze!(' ');}
23
23
 
24
24
  case parts[0][0]
25
- when 35; @selector_table.store(parts[0][1..-1],parts[1])
26
- when 46; @class_table.store(parts[0][1..-1],parts[1])
25
+ when '#'; @selector_table.store(parts[0][1..-1],parts[1])
26
+ when '.'; @class_table.store(parts[0][1..-1],parts[1])
27
27
  end
28
28
  end
29
29
  end
@@ -0,0 +1,21 @@
1
+ module GerbilCharts::SVGDC
2
+
3
+ # = SVGEllpise
4
+ # Draws a circle at x, y, and radius rx, ry
5
+ #
6
+ class SVGEllipse < SVGShape
7
+
8
+ attr_accessor :x,:y,:rx,:ry
9
+
10
+ def initialize(x,y,rx,ry)
11
+ @x,@y,@rx,@ry=x,y,rx,ry
12
+ super()
13
+ end
14
+
15
+ def render(xfrag)
16
+ h = { :cx => @x, :cy => @y, :rx => @rx, :ry => @ry }
17
+ xfrag.ellipse(h.merge(render_attributes))
18
+ end
19
+ end
20
+
21
+ end
@@ -31,4 +31,23 @@ class SVGPolygon < SVGShape
31
31
 
32
32
  end
33
33
 
34
+
35
+ class SVGSquarizedPolygon < SVGPolygon
36
+
37
+
38
+ def addpoint(x,y)
39
+ @lastpt ||= []
40
+
41
+ unless @lastpt.empty?
42
+ xf,yf = "%.2f"%x, "%.2f"%@lastpt[1]
43
+ @operstring << "#{xf},#{yf} "
44
+ xf,yf = "%.2f"%x, "%.2f"%y
45
+ @operstring << "#{xf},#{yf} "
46
+ end
47
+
48
+ @lastpt = [x,y]
49
+ end
50
+
51
+ end
52
+
34
53
  end
@@ -204,7 +204,7 @@ class SVGDC
204
204
  # render tooltips optional
205
205
  if @use_tooltips
206
206
  doc.g(:id=>'ToolTip', :opacity=>'0.8', :visibility=>'hidden', "pointer-events"=>'none') {
207
- doc.rect(:id=>'tipbox', :x=>'0', :y=>'5', :width=>'88', :height=> '20', :rx=> '2', :ry=> '2', :fill=>'white', :stroke=>'black')
207
+ doc.rect(:id=>'tipbox', :x=>'0', :y=>'5', :width=>'88', :height=> '40', :rx=> '2', :ry=> '2', :fill=>'white', :stroke=>'black')
208
208
  doc.text(:id=>'tipText', :x=>'5', :y=>'20', "font-family"=> 'Arial', "font-size"=>'12') {
209
209
  doc.tspan(:id=>'tipTitle', :x=>'5', "font-weight"=>'bold') {
210
210
  doc.cdata!("")
@@ -350,6 +350,9 @@ class SVGDC
350
350
  rectangle(r.left, r.top, r.width, r.height, opt)
351
351
  end
352
352
 
353
+ def circle_r(r, opt={} )
354
+ circle( (r.left + r.right)/2, (r.bottom+r.top)/2, [r.width,r.height].min/2, opt)
355
+ end
353
356
 
354
357
  def select_presentation(opt={})
355
358
  @curr_presentation_context.store(:pen,opt[:pen]) if opt[:pen]
@@ -1,9 +1,8 @@
1
1
  module GerbilCharts
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 2
5
- TINY = 13
6
-
4
+ MINOR = 5
5
+ TINY = 9
7
6
  STRING = [MAJOR, MINOR, TINY].join('.')
8
7
  end
9
8
  end
data/test/test_bar.rb CHANGED
@@ -26,7 +26,7 @@ class TestBar < Test::Unit::TestCase
26
26
  ["VIVEK", 145,112, 22, 45, 18, 170]
27
27
  ]
28
28
  )
29
- mychart = GerbilCharts::Charts::BarChart.new( :width => 550, :height => 200,
29
+ mychart = GerbilCharts::Charts::BarChart.new( :width => 300, :height => 200,
30
30
  :style => 'inline:brushmetal.css', :auto_tooltips => true ,
31
31
  :javascripts => ['/tmp/gerbil.js', '/tmp/prototype.js'],
32
32
  :filter => 'LikeButton'
@@ -0,0 +1,52 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ require 'trafgen'
4
+
5
+ # tests some easy to use mods
6
+ # newbies enter here
7
+ class TestChartsNoob < Test::Unit::TestCase
8
+
9
+
10
+ # alerts
11
+ def test_alerts
12
+
13
+ mychart = GerbilCharts::Charts::BubbleChart.new( :width => 650, :height => 200,
14
+ :style => 'inline:brushmetal.css',
15
+ :javascripts => ['inline:/tmp/gerbil.js','inline:/tmp/prototype.js'] )
16
+
17
+
18
+ tend = Time.now
19
+ tbegin = tend - 3600*24
20
+
21
+ model1 = GerbilCharts::Models::BucketizedTimeSeriesGraphModel.new( "eth0", 3600 )
22
+ TimeSeriesDataGenerator.new(tbegin,tend,300,1000, 5000 ).each_tuple do |t,v|
23
+ model1.add(t,v)
24
+ end
25
+
26
+ model2 = GerbilCharts::Models::BucketizedTimeSeriesGraphModel.new( "wan1", 3600 )
27
+ TimeSeriesDataGenerator.new(tbegin,tend,300,2, 20).each_tuple do |t,v|
28
+ model2.add(t,v)
29
+ end
30
+
31
+ model3 = GerbilCharts::Models::BucketizedTimeSeriesGraphModel.new( "fiber0", 3600 )
32
+ TimeSeriesDataGenerator.new(tbegin,tend,1800,0, 10).each_tuple do |t,v|
33
+ model3.add(t,v)
34
+ end
35
+
36
+ modelgroup = GerbilCharts::Models::GraphModelGroup.new( "External Traffic")
37
+ modelgroup.add model2
38
+ modelgroup.add model1
39
+ modelgroup.add model3
40
+
41
+
42
+ mychart.modelgroup=modelgroup
43
+
44
+ mychart.render('/tmp/n_bubble.svg')
45
+
46
+ end
47
+
48
+
49
+
50
+ end
51
+
52
+
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ require 'trafgen'
4
+
5
+ # test conversation bar chart
6
+ class TestConversation < Test::Unit::TestCase
7
+
8
+ def test_monthly_sales
9
+ modelgroup = GerbilCharts::Models::MatrixModel.new("Conversation Ring Chart")
10
+
11
+ modelgroup.add("192.168.1.25","192.168.1.20",500)
12
+ modelgroup.add("192.168.1.25","192.168.1.21",7500)
13
+ modelgroup.add("192.168.1.25","192.168.1.22",6500)
14
+ modelgroup.add("192.168.1.25","192.168.1.23",5500)
15
+ modelgroup.add("192.168.1.25","192.168.1.24",4500)
16
+ modelgroup.add("192.168.1.25","192.168.1.26",10500)
17
+ modelgroup.add("192.168.1.25","192.168.1.27",500)
18
+ modelgroup.add("192.168.1.25","192.168.1.28",2500)
19
+ modelgroup.add("192.168.1.25","192.168.1.29",3500)
20
+ modelgroup.add("192.168.1.25","192.168.1.30",500)
21
+
22
+ modelgroup.calc_matrix
23
+
24
+ mychart = GerbilCharts::Charts::ConversationRing.new( :width => 450, :height => 250,
25
+ :style => 'inline:brushmetal.css', :auto_tooltips => true ,
26
+ :javascripts => ['/tmp/gerbil.js', '/tmp/prototype.js'],
27
+ :filter => 'LikeButton'
28
+ )
29
+
30
+ mychart.modelgroup = modelgroup
31
+ mychart.render('/tmp/con_monthly_sales.svg')
32
+
33
+ end
34
+ end
data/test/test_lines.rb CHANGED
@@ -35,9 +35,9 @@ class TestLines < Test::Unit::TestCase
35
35
  # test a line chart
36
36
  def test_line_1
37
37
 
38
- mychart = GerbilCharts::Charts::AreaChart.new( :width => 450, :height => 250,
39
- :javascripts => ['inline:/tmp/gerbil.js' ],
40
- :auto_tooltips => true, :style => 'inline:brushmetal.css')
38
+ mychart = GerbilCharts::Charts::AreaChart.new( :width => 750, :height => 250,
39
+ :javascripts => ['inline:gerbil.js' ],
40
+ :auto_tooltips => false, :style => 'inline:brushmetal.css' )
41
41
  mychart.setmodelgroup(@modgroup)
42
42
  mychart.render('/tmp/sq_linechart1.svg')
43
43
 
@@ -0,0 +1,34 @@
1
+ require File.dirname(__FILE__) + '/test_helper.rb'
2
+
3
+ require 'trafgen'
4
+
5
+ # test conversation bar chart
6
+ class TestMatrix < Test::Unit::TestCase
7
+
8
+ def test_monthly_sales
9
+ modelgroup = GerbilCharts::Models::MatrixModel.new("Traffic Matrix")
10
+
11
+ modelgroup.add("192.168.1.25","192.168.1.20",12500)
12
+ modelgroup.add("192.168.1.26","192.168.1.21",7500)
13
+ modelgroup.add("192.168.1.25","192.168.1.22",6500)
14
+ modelgroup.add("192.168.1.27","192.168.1.23",5500)
15
+ modelgroup.add("192.168.1.28","192.168.1.24",4500)
16
+ modelgroup.add("192.168.1.29","192.168.1.26",10500)
17
+ modelgroup.add("192.168.1.25","192.168.1.27",500)
18
+ modelgroup.add("192.168.1.25","192.168.1.28",2500)
19
+ modelgroup.add("192.168.1.30","192.168.1.29",3500)
20
+ modelgroup.add("192.168.1.52","192.168.1.30",500)
21
+
22
+ modelgroup.calc_matrix
23
+
24
+ mychart = GerbilCharts::Charts::MatrixChart.new( :width => 1000, :height => 400,
25
+ :style => 'inline:brushmetal.css', :auto_tooltips => true ,
26
+ :javascripts => ['/tmp/gerbil.js', '/tmp/prototype.js'],
27
+ :filter => 'LikeButton'
28
+ )
29
+
30
+ mychart.modelgroup = modelgroup
31
+ mychart.render('/tmp/matrix_monthly_sales.svg')
32
+
33
+ end
34
+ end
data/test/test_noob.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  require File.dirname(__FILE__) + '/test_helper.rb'
2
2
 
3
- require 'trafgen'
3
+ require './trafgen'
4
4
 
5
5
  # tests some easy to use mods
6
6
  # newbies enter here
@@ -11,15 +11,16 @@ class TestChartsNoob < Test::Unit::TestCase
11
11
  # use a simple timeseries model
12
12
  def test_monthly_sales
13
13
 
14
- mychart = GerbilCharts::Charts::LineChart.new( :width => 350, :height => 200, :style => 'brushmetal.css',
15
- :circle_data_points => true )
14
+ mychart = GerbilCharts::Charts::SquareLineChart.new( :width => 350, :height => 200, :style => 'inline:brushmetal.css',
15
+ :circle_data_points => true, :scaling_y => :auto,
16
+ :javascripts => ['/tmp/gerbil.js','/tmp/prototype.js'] )
16
17
 
17
18
  modelgroup = GerbilCharts::Models::SimpleTimeSeriesModelGroup.new(
18
19
  :title => "Sales figures",
19
20
  :timeseries => (1..6).collect { |month| Time.local(2008,month) },
20
21
  :models => [ ["Bruce", 1, 10, 18, 28, 80, 122],
21
- ["Rex" , 112,22, 45, 70, 218, 309],
22
- ["Buzo" , 0, 23, 25, 40, 18, 59]
22
+ ["Rex" , 112,22, 45, 70, 218, 309],
23
+ ["Buzo" , 0, 23, 25, 40, 18, 59]
23
24
  ]
24
25
  )
25
26
 
@@ -69,7 +70,9 @@ class TestChartsNoob < Test::Unit::TestCase
69
70
 
70
71
 
71
72
  # demo stacked area
72
- mysachart = GerbilCharts::Charts::StackedAreaChart.new( :width => 450, :height => 200, :style => 'brushmetal.css')
73
+ mysachart = GerbilCharts::Charts::StackedAreaChart.new( :width => 450, :height => 200, :style => 'inline:brushmetal.css',
74
+ :scaling_y => :auto, :auto_tooltips => true ,
75
+ :javascripts => ['/tmp/gerbil.js' , '/tmp/prototype.js' ])
73
76
  mysachart.modelgroup=modelgroup
74
77
  mysachart.render('/tmp/n_daily_traffic_sa.svg')
75
78