asciidoctor-chart 1.0.0.alpha.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f04e36b08cd8bcd45da1387e52efb5f982f92aceb1b14912130158eb4c7f34a1
4
- data.tar.gz: fc325aa365a1b8ccf11fcc3e10cb6efd52fb0a447280e72c37d8db77eb412f94
3
+ metadata.gz: d19c56f42982d8d5b332f7e3f9daa2456a46e892e19c34c474b26d916ac9c06f
4
+ data.tar.gz: 5f9c0366c859481c33078b6b6bb7ec66b311a5ed0a7b3ff816a464869d6bc35b
5
5
  SHA512:
6
- metadata.gz: dba17ce4d883a862ad885833dbe8785fbc9d3d334a35fc97a31fb0d05bf11442a2cb086cd80a23a43b15be9031d6e96dce0f48e5042c035f365dd0b1a7102d5a
7
- data.tar.gz: d55676125f54e5898e966fe0f4241c01dc24bc7bb8cfdf0a8f72601eba1999e8f69080d61efbe19dac9b7a966e658609162f9cf1fdeeede3882aae9e68b3e35b
6
+ metadata.gz: 2de58fded66f3709cfa02645bc8d0aff0b5199555f66c3108d624b5990e207dca2a3cdf49e1065a3875349df84988b75084c69ddfc2e1c60df3d4449583e7da2
7
+ data.tar.gz: 3cf74600a24dc2ae35d3b13cc7a0ea5ea1e9c64a9c44ceef9ae6ec9bcabcbd77794968b0e49f471554477cc4247b4a2e76f7f639d4b36c8932880595ad69d928
data/CHANGELOG.adoc ADDED
@@ -0,0 +1,38 @@
1
+ = Asciidoctor Chart Changelog
2
+ :icons: font
3
+ :uri-repo: https://github.com/asciidoctor/asciidoctor-chart
4
+
5
+ This document provides a high-level view of the changes introduced in Asciidoctor Chart by release.
6
+ For an even more detailed look at what has changed, refer to the {uri-repo}/commits/[commit history] on GitHub.
7
+
8
+ The format is based on https://keepachangelog.com/en/1.0.0/[Keep a Changelog],
9
+ and this project adheres to https://semver.org/spec/v2.0.0.html[Semantic Versioning].
10
+
11
+ == Unreleased
12
+
13
+
14
+ == 1.0.0 (2022-10-08)
15
+
16
+ === ✨ Added
17
+
18
+ * Introduce a changelog (#30)
19
+ * Add support for using the JavaScript libraries offline (#25)
20
+ * Add captioned title on charts (#18)
21
+ * Improved documentation
22
+ * Add attributes to define chart resources directory (#26)
23
+ * Introduce visual regression tests (#34)
24
+
25
+ === 🎨 Changed
26
+
27
+ * Use 0.11.x as default version for Chartist.js instead of latest (#28)
28
+ * Update c3.js defaults to latest version 0.7.20 (#32)
29
+ * Upgrade Chart.js from 1.0.2 to 3.7.0 and update code accordingly (#17)
30
+ * Use https protocol (instead of http) when loading scripts from cdnjs.cloudflare.com (#17)
31
+ * Apply RuboCop to asciidoctor-chart.gemspec (#17)
32
+ * Enable multi-factor authentication when publishing to https://rubygems.org/ (#17)
33
+
34
+ === 🐞 Fixed
35
+
36
+ * Fix an issue to allow more than one Chart.js graphs per page (#13)
37
+ * Include assets (styles, scripts) when an chart engine is used (#24)
38
+ * Fix an issue with Chart.js where text was overlapping the graph when width was specified (#17)
data/README.adoc CHANGED
@@ -1,15 +1,101 @@
1
1
  = Asciidoctor Chart
2
+ // Aliases:
3
+ :project-name: Asciidoctor Chart
4
+ :project-handle: asciidoctor-chart
5
+ // Variables:
6
+ :release-version: 1.0.0.alpha.1
7
+ :uri-repo: https://github.com/asciidoctor/asciidoctor-chart
2
8
  // Settings:
3
9
  :idprefix:
4
10
  :idseparator: -
5
11
 
6
- image:https://github.com/asciidoctor/asciidoctor-chart/workflows/Ruby/badge.svg[link=https://github.com/asciidoctor/asciidoctor-chart/actions?query=workflow%3ARuby]
12
+ image:https://github.com/asciidoctor/asciidoctor-chart/workflows/CI/badge.svg[link=https://github.com/asciidoctor/asciidoctor-chart/actions?query=workflow%3ACI]
13
+ image:https://img.shields.io/gem/v/asciidoctor-chart?include_prereleases[link=https://rubygems.org/search?query=asciidoctor-chart]
14
+
15
+ A set of Asciidoctor extensions that adds a chart block and block macro for including charts powered by https://c3js.org/[C3.js], https://gionkunz.github.io/chartist-js/[Chartist], or https://www.chartjs.org/[Chart.js] in your AsciiDoc document.
16
+
17
+ == Prerequisites
18
+
19
+ All that's needed is Ruby 2.5 or better (or JRuby 9.2 or better) and a few Ruby gems (including at least Asciidoctor 2.0.0), which we explain how to install in the next section.
20
+
21
+ To check if you have Ruby available, use the `ruby` command to query the version installed:
22
+
23
+ $ ruby -e 'puts RUBY_VERSION'
24
+
25
+ Make sure this command reports a Ruby version that's at least 2.5.
26
+ If so, you may proceed.
27
+
28
+ == Getting Started
29
+
30
+ You can get {project-name} by <<install-the-published-gem,installing the published gem>>.
31
+ ifndef::env-site[You can also <<development,run the code from source>> if you want to use a development version or participate in development.]
32
+
33
+ === Install the Published Gem
34
+
35
+ To install {project-name}, first make sure you have satisfied the <<Prerequisites,prerequisites>>.
36
+ Then, install the gem from RubyGems.org using the following command:
37
+
38
+ $ gem install asciidoctor-chart --pre
39
+
40
+ === Enable the Extension
41
+
42
+ Assuming all the required gems install properly, you can enable the extension using `--require` option (or `-r` for short) from the Asciidoctor CLI:
43
+
44
+ $ asciidoctor --require asciidoctor-chart my-doc.adoc
45
+
46
+ === Usage
47
+
48
+ Line chart powered by C3.js (default) declared as a literal block::
49
+ +
50
+ ----
51
+ [chart,line]
52
+ ....
53
+ January,February,March,April,May,June,July
54
+ 28,48,40,19,86,27,90
55
+ 65,59,80,81,56,55,40
56
+ ....
57
+ ----
58
+ +
59
+ image::./examples/chart-c3js.png[]
60
+
61
+ Line chart powered by Chart.js declared as a block macro with a CSV file as target::
62
+ +
63
+ ----
64
+ chart::sample-data.csv[line,engine="chartjs"]
65
+ ----
66
+ +
67
+ image::./examples/chart-chartjs.png[]
68
+
69
+ For more examples, see {uri-repo}/blob/master/examples/example.adoc[example.adoc].
70
+
71
+ === Configuration
72
+
73
+ [cols="1s,1,3"]
74
+ |===
75
+ |Attribute{nbsp}Name |Value(s)|Description
76
+
77
+ |c3jsdir
78
+ |<file\|URL>
79
+ |Overrides c3.js directory, where the following files `c3.min.css` and `c3.min.js` are expected. Default is `https://cdnjs.cloudflare.com/ajax/libs/c3/0.7.20/`.
80
+
81
+ |chartjsdir
82
+ |<file\|URL>
83
+ |Overrides chart.js directory, where the following file `chart.min.js` is expected. Default is `https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.7.0/`.
84
+
85
+ |chartistdir
86
+ |<file\|URL>
87
+ |Overrides chartist.js directory, where the following files `chartist.min.css` and `chartist.min.js` are expected. Default is `https://cdn.jsdelivr.net/chartist.js/0.11.x/`.
88
+
89
+
90
+ |d3jsdir
91
+ |<file\|URL>
92
+ |Overrides d3.js directory, where the following file `d3.min.js` is expected. Default is `https://cdnjs.cloudflare.com/ajax/libs/d3/5.16.0/`.
93
+ |===
7
94
 
8
- A set of Asciidoctor extensions that adds a chart block and block macro for including charts powered by powered by c3js, chartist, or chartjs in your AsciiDoc document.
9
95
 
10
96
  == Authors
11
97
 
12
- Asciidoctor Chart was written by https://github.com/mogztter/[Guillaume Grossetie].
98
+ {project-name} was written by https://github.com/mogztter/[Guillaume Grossetie].
13
99
 
14
100
  == Copyright
15
101
 
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  begin
2
4
  require_relative 'lib/asciidoctor/chart/version'
3
5
  rescue LoadError
@@ -8,31 +10,36 @@ Gem::Specification.new do |s|
8
10
  s.name = 'asciidoctor-chart'
9
11
  s.version = Asciidoctor::Chart::VERSION
10
12
  s.summary = 'Adds a chart block and block macro to AsciiDoc'
11
- s.description = 'A set of Asciidoctor extensions that add a chart block and block macro to AsciiDoc for including charts in your AsciiDoc document.'
13
+ s.description = "A set of Asciidoctor extensions that add a chart block and block macro to AsciiDoc
14
+ for including charts in your AsciiDoc document."
12
15
  s.authors = ['Guillaume Grossetie']
13
16
  s.email = 'ggrossetie@gmail.com'
14
17
  s.homepage = 'https://asciidoctor.org'
15
18
  s.license = 'MIT'
16
- # NOTE required ruby version is informational only; it's not enforced since it can't be overridden and can cause builds to break
17
- #s.required_ruby_version = '>= 2.5.0'
19
+ # NOTE: required ruby version is informational only;
20
+ # it's not enforced since it can't be overridden and can cause builds to break
21
+ # s.required_ruby_version = '>= 2.7.0'
18
22
  s.metadata = {
19
23
  'bug_tracker_uri' => 'https://github.com/asciidoctor/asciidoctor-chart/issues',
20
- #'changelog_uri' => 'https://github.com/asciidoctor/asciidoctor-chart/blob/master/CHANGELOG.adoc',
21
- 'mailing_list_uri' => 'http://discuss.asciidoctor.org',
22
- 'source_code_uri' => 'https://github.com/asciidoctor/asciidoctor-chart'
24
+ # 'changelog_uri' => 'https://github.com/asciidoctor/asciidoctor-chart/blob/master/CHANGELOG.adoc',
25
+ 'community_chat_uri' => 'https://asciidoctor.zulipchat.com',
26
+ 'source_code_uri' => 'https://github.com/asciidoctor/asciidoctor-chart',
27
+ 'rubygems_mfa_required' => 'true'
23
28
  }
24
29
 
25
- # NOTE the logic to build the list of files is designed to produce a usable package even when the git command is not available
30
+ # NOTE: the logic to build the list of files is designed to produce a usable package
31
+ # even when the git command is not available
26
32
  begin
27
- files = (result = `git ls-files -z`.split ?\0).empty? ? Dir['**/*'] : result
28
- rescue
33
+ files = (result = `git ls-files -z`.split "\0").empty? ? Dir['**/*'] : result
34
+ rescue StandardError
29
35
  files = Dir['**/*']
30
36
  end
31
- s.files = files.grep %r/^(?:(?:data|lib)\/.+|(?:CHANGELOG|LICENSE|NOTICE|README)\.adoc|\.yardopts|#{s.name}\.gemspec)$/
32
- s.executables = (files.grep %r/^bin\//).map {|f| File.basename f }
37
+ s.files = files.grep %r{^(?:(?:data|lib)/.+|(?:CHANGELOG|LICENSE|NOTICE|README)\.adoc|\.yardopts|#{s.name}\.gemspec)$}
38
+ s.executables = (files.grep %r{^bin/}).map {|f| File.basename f }
33
39
  s.require_paths = ['lib']
34
40
 
35
41
  s.add_runtime_dependency 'asciidoctor', '~> 2.0'
42
+ s.add_runtime_dependency 'tilt', '~> 2.0.0'
36
43
  s.add_development_dependency 'rake', '~> 13.0.0'
37
44
  s.add_development_dependency 'rspec', '~> 3.9.0'
38
45
  end
@@ -12,10 +12,8 @@ module Asciidoctor
12
12
  read_data = parent.read_asset data_path, warn_on_failure: true, normalize: true
13
13
  return if read_data.nil? || read_data.empty?
14
14
 
15
- engine = Backend.resolve_engine attrs, parent.document
16
15
  raw_data = PlainRubyCSV.parse read_data
17
- html = Backend.process engine, attrs, raw_data
18
- create_pass_block parent, html, attrs, subs: nil
16
+ Asciidoctor::Chart::ChartBlock.new parent, raw_data, attrs
19
17
  end
20
18
  end
21
19
  end
@@ -10,10 +10,8 @@ module Asciidoctor
10
10
  parse_content_as :raw
11
11
 
12
12
  def process parent, reader, attrs
13
- engine = Backend.resolve_engine attrs, parent.document
14
13
  raw_data = PlainRubyCSV.parse reader.source
15
- html = Backend.process engine, attrs, raw_data
16
- create_pass_block parent, html, attrs, subs: nil
14
+ Asciidoctor::Chart::ChartBlock.new parent, raw_data, attrs
17
15
  end
18
16
  end
19
17
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Chart
5
+ class ChartBlock < Asciidoctor::Block
6
+ def initialize(parent, data, attrs)
7
+ engine = parent.document.get_engine attrs
8
+ caption = attrs.delete 'caption'
9
+ title = attrs.delete 'title'
10
+ block_attributes = attrs.merge({
11
+ 'id' => attrs['id'] || "chart#{PlainRubyRandom.uuid}",
12
+ 'type' => attrs['type'] || 'line',
13
+ 'engine' => engine,
14
+ 'data-raw' => data
15
+ })
16
+ super parent, :chart, { source: nil, attributes: block_attributes, subs: nil }
17
+ @title = title
18
+ assign_caption(caption, 'figure')
19
+ end
20
+ end
21
+
22
+ module ChartBlockTracker
23
+ attr_reader :x_chart
24
+
25
+ def self.extended instance
26
+ instance.instance_variable_set :@x_chart, {
27
+ engines: Set.new
28
+ }
29
+ end
30
+
31
+ def get_engine attrs
32
+ engine = if attrs.key? 'engine'
33
+ attrs['engine'].downcase
34
+ elsif @attributes.key? 'chart-engine'
35
+ @attributes['chart-engine'].downcase
36
+ else
37
+ 'c3js'
38
+ end
39
+ @x_chart[:engines].add(engine)
40
+
41
+ engine
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,231 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Chart
5
+ module Converter
6
+ class Html5C3jsConverter < Asciidoctor::Chart::Converter::Base
7
+ def convert_line_chart node
8
+ data, labels = prepare_data node
9
+ chart_generate_script = chart_line_script node, data, labels
10
+ <<~HTML
11
+ #{chart_block_element node}
12
+ #{chart_generate_script}
13
+ HTML
14
+ end
15
+
16
+ def convert_bar_chart node
17
+ data, labels = prepare_data node
18
+ <<~HTML
19
+ #{chart_block_element node}
20
+ #{chart_bar_script node, data, labels}
21
+ HTML
22
+ end
23
+
24
+ def convert_step_chart node
25
+ data, labels = prepare_data node
26
+ <<~HTML
27
+ #{chart_block_element node}
28
+ #{chart_step_script node, data, labels}
29
+ HTML
30
+ end
31
+
32
+ def convert_spline_chart node
33
+ data, labels = prepare_data node
34
+ <<~HTML
35
+ #{chart_block_element node}
36
+ #{chart_spline_script node, data, labels}
37
+ HTML
38
+ end
39
+
40
+ def convert_pie_chart node
41
+ raw_data = node.attr 'data-raw', []
42
+ <<~HTML
43
+ #{chart_block_element node}
44
+ #{chart_pie_script node, raw_data}
45
+ HTML
46
+ end
47
+
48
+ private
49
+
50
+ def prepare_data node
51
+ raw_data = node.attr 'data-raw', []
52
+ return [[], []] if raw_data.length <= 1 # question: should we warn?
53
+
54
+ labels = raw_data[0]
55
+ raw_data.shift
56
+ raw_data.map.with_index {|row, index| row.unshift index.to_s }
57
+ [raw_data, labels]
58
+ end
59
+
60
+ def chart_bar_script node, data, labels
61
+ chart_height = get_chart_height node
62
+ chart_width = get_chart_width node
63
+ axis_x_label = get_axis_x_label node
64
+ axis_y_label = get_axis_y_label node
65
+ data_names = get_data_names node
66
+ <<~HTML
67
+ <script>
68
+ c3.generate({
69
+ bindto: '##{node.attr 'id'}',
70
+ size: { height: #{chart_height}, width: #{chart_width} },
71
+ data: {
72
+ columns: #{data.to_s},
73
+ type: 'bar',
74
+ names: #{data_names.to_s}
75
+ },
76
+ axis: {
77
+ x: {
78
+ type: 'category',
79
+ categories: #{labels.to_s},
80
+ label: '#{axis_x_label}'
81
+ },
82
+ y: {
83
+ label: '#{axis_y_label}'
84
+ }
85
+ }
86
+ })
87
+ </script>
88
+ HTML
89
+ end
90
+
91
+ def chart_line_script node, data, labels
92
+ chart_height = get_chart_height node
93
+ chart_width = get_chart_width node
94
+ axis_x_label = get_axis_x_label node
95
+ axis_y_label = get_axis_y_label node
96
+ data_names = get_data_names node
97
+ <<~HTML
98
+ <script>
99
+ c3.generate({
100
+ bindto: '##{node.attr 'id'}',
101
+ size: { height: #{chart_height}, width: #{chart_width} },
102
+ data: {
103
+ columns: #{data.to_s},
104
+ names: #{data_names.to_s}
105
+ },
106
+ axis: {
107
+ x: {
108
+ type: 'category',
109
+ categories: #{labels.to_s},
110
+ label: '#{axis_x_label}'
111
+ },
112
+ y: {
113
+ label: '#{axis_y_label}'
114
+ }
115
+ }
116
+ })
117
+ </script>
118
+ HTML
119
+ end
120
+
121
+ def chart_step_script node, data, labels
122
+ chart_height = get_chart_height node
123
+ chart_width = get_chart_width node
124
+ axis_x_label = get_axis_x_label node
125
+ axis_y_label = get_axis_y_label node
126
+ data_names = get_data_names node
127
+ <<~HTML
128
+ <script>
129
+ c3.generate({
130
+ bindto: '##{node.attr 'id'}',
131
+ size: { height: #{chart_height}, width: #{chart_width} },
132
+ data: {
133
+ columns: #{data.to_s},
134
+ type: 'step',
135
+ names: #{data_names.to_s}
136
+ },
137
+ axis: {
138
+ x: {
139
+ type: 'category',
140
+ categories: #{labels.to_s},
141
+ label: '#{axis_x_label}'
142
+ },
143
+ y: {
144
+ label: '#{axis_y_label}'
145
+ }
146
+ }
147
+ })
148
+ </script>
149
+ HTML
150
+ end
151
+
152
+ def chart_spline_script node, data, labels
153
+ chart_height = get_chart_height node
154
+ chart_width = get_chart_width node
155
+ axis_x_label = get_axis_x_label node
156
+ axis_y_label = get_axis_y_label node
157
+ data_names = get_data_names node
158
+ <<~HTML
159
+ <script>
160
+ c3.generate({
161
+ bindto: '##{node.attr 'id'}',
162
+ size: { height: #{chart_height}, width: #{chart_width} },
163
+ data: {
164
+ columns: #{data.to_s},
165
+ type: 'spline',
166
+ names: #{data_names.to_s}
167
+ },
168
+ axis: {
169
+ x: {
170
+ type: 'category',
171
+ categories: #{labels.to_s},
172
+ label: '#{axis_x_label}'
173
+ },
174
+ y: {
175
+ label: '#{axis_y_label}'
176
+ }
177
+ }
178
+ })
179
+ </script>
180
+ HTML
181
+ end
182
+
183
+ def chart_pie_script node, raw_data
184
+ chart_height = get_chart_height node
185
+ chart_width = get_chart_width node
186
+ <<~HTML
187
+ <script>
188
+ c3.generate({
189
+ bindto: '##{node.attr 'id'}',
190
+ size: { height: #{chart_height}, width: #{chart_width} },
191
+ data: {
192
+ columns: #{raw_data.to_s},
193
+ type: 'pie'
194
+ }
195
+ })
196
+ </script>
197
+ HTML
198
+ end
199
+
200
+ def chart_block_element node
201
+ title_element = node.title? ? %(\n <div class="title">#{node.captioned_title}</div>) : ''
202
+ %(<div class="chartblock">
203
+ <div class="content c3js-content">
204
+ <div id="#{node.attr 'id'}"></div>
205
+ </div>#{title_element}
206
+ </div>)
207
+ end
208
+
209
+ def get_chart_height node
210
+ node.attr 'height', '400'
211
+ end
212
+
213
+ def get_chart_width node
214
+ node.attr 'width', '600'
215
+ end
216
+
217
+ def get_axis_x_label node
218
+ node.attr?('axis-x-label') ? CGI.unescapeHTML(node.attr('axis-x-label')) : ''
219
+ end
220
+
221
+ def get_axis_y_label node
222
+ node.attr?('axis-y-label') ? CGI.unescapeHTML(node.attr('axis-y-label')) : ''
223
+ end
224
+
225
+ def get_data_names node
226
+ node.attr?('data-names') ? CGI.unescapeHTML(node.attr('data-names')) : '{}'
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Asciidoctor
4
+ module Chart
5
+ module Converter
6
+ class Html5ChartistConverter < Asciidoctor::Chart::Converter::Base
7
+ def convert_line_chart node
8
+ data, labels = prepare_data node
9
+ <<~HTML
10
+ #{chart_block_element node}
11
+ #{chart_line_script node, data, labels}
12
+ HTML
13
+ end
14
+
15
+ def convert_bar_chart node
16
+ data, labels = prepare_data node
17
+ <<~HTML
18
+ #{chart_block_element node}
19
+ #{chart_bar_script node, data, labels}
20
+ HTML
21
+ end
22
+
23
+ private
24
+
25
+ def prepare_data node
26
+ raw_data = node.attr 'data-raw', []
27
+ return [[], []] if raw_data.length <= 1 # question: should we warn?
28
+
29
+ labels = raw_data[0]
30
+ raw_data.shift
31
+ [raw_data, labels]
32
+ end
33
+
34
+ def chart_block_element node
35
+ title_element = node.title? ? %(\n <div class="title">#{node.captioned_title}</div>) : ''
36
+ %(<div class="chartblock">
37
+ <div class="content chartist-content">
38
+ <div id="#{node.attr 'id'}" class="ct-chart"></div>
39
+ </div>#{title_element}
40
+ </div>)
41
+ end
42
+
43
+ def chart_line_script node, data, labels
44
+ chart_height = node.attr 'height', '400'
45
+ chart_width = node.attr 'width', '600'
46
+ <<~HTML
47
+ <script>
48
+ var options = {
49
+ height: '#{chart_height}',
50
+ width: '#{chart_width}',
51
+ colors: ["#72B3CC", "#8EB33B"]
52
+ }
53
+ var data = {
54
+ labels: #{labels.to_s},
55
+ series: #{data.to_s}
56
+ }
57
+ new Chartist.Line('##{node.attr 'id'}', data, options)
58
+ </script>
59
+ HTML
60
+ end
61
+
62
+ def chart_bar_script node, data, labels
63
+ chart_height = node.attr 'height', '400'
64
+ <<~HTML
65
+ <script>
66
+ var options = {
67
+ height: '#{chart_height}',
68
+ colors: ["#72B3CC", "#8EB33B"]
69
+ }
70
+ var data = {
71
+ labels: #{labels.to_s},
72
+ series: #{data.to_s}
73
+ }
74
+ new Chartist.Bar('##{node.attr 'id'}', data, options)
75
+ </script>
76
+ HTML
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end