Commit | Line | Data |
---|---|---|
846cf979 | 1 | #!/usr/bin/env python |
2150109a JD |
2 | # |
3 | # Copyright (c) 2012 Pierre-Francois Carpentier <carpentier.pf@gmail.com> | |
4 | # | |
5 | # https://github.com/kakwa/py-ascii-graph/ | |
6 | # | |
7 | # Permission is hereby granted, free of charge, to any person obtaining | |
8 | # a copy of this software and associated documentation files (the | |
9 | # "Software"), to deal in the Software without restriction, including | |
10 | # without limitation the rights to use, copy, modify, merge, publish, | |
11 | # distribute, sublicense, and/or sell copies of the Software, and to | |
12 | # permit persons to whom the Software is furnished to do so, subject to | |
13 | # the following conditions: | |
14 | # | |
15 | # The above copyright notice and this permission notice shall be | |
16 | # included in all copies or substantial portions of the Software. | |
17 | # | |
18 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
19 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
20 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
21 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
22 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
23 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
24 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
846cf979 JD |
25 | |
26 | from __future__ import unicode_literals | |
27 | import sys | |
17818137 | 28 | import os |
846cf979 | 29 | |
846cf979 | 30 | |
8054fc4c SG |
31 | class Pyasciigraph: |
32 | def __init__(self, line_length=79, min_graph_length=50, | |
33 | separator_length=2): | |
846cf979 | 34 | """Constructor of Pyasciigraph |
8054fc4c | 35 | |
846cf979 | 36 | :param int line_length: the max number of char on a line |
8054fc4c | 37 | if any line cannot be shorter, |
846cf979 JD |
38 | it will go over this limit |
39 | :param int min_graph_length: the min number of char used by the graph | |
40 | :param int separator_length: the length of field separator | |
41 | """ | |
42 | self.line_length = line_length | |
43 | self.separator_length = separator_length | |
44 | self.min_graph_length = min_graph_length | |
45 | ||
46 | def _u(self, x): | |
47 | if sys.version < '3': | |
48 | import codecs | |
49 | return codecs.unicode_escape_decode(x)[0] | |
50 | else: | |
51 | return x | |
52 | ||
53 | def _get_maximum(self, data): | |
54 | all_max = {} | |
55 | all_max['value_max_length'] = 0 | |
56 | all_max['info_max_length'] = 0 | |
57 | all_max['max_value'] = 0 | |
58 | ||
59 | for (info, value) in data: | |
60 | if value > all_max['max_value']: | |
61 | all_max['max_value'] = value | |
62 | ||
63 | if len(info) > all_max['info_max_length']: | |
64 | all_max['info_max_length'] = len(info) | |
8054fc4c | 65 | |
846cf979 JD |
66 | if len(str(value)) > all_max['value_max_length']: |
67 | all_max['value_max_length'] = len(str(value)) | |
68 | return all_max | |
69 | ||
70 | def _gen_graph_string(self, value, max_value, graph_length, start_value): | |
40fbd9cc JD |
71 | if max_value == 0: |
72 | number_of_square = int(value * graph_length) | |
73 | else: | |
74 | number_of_square = int(value * graph_length / max_value) | |
846cf979 JD |
75 | number_of_space = int(start_value - number_of_square) |
76 | return '█' * number_of_square + self._u(' ') * number_of_space | |
77 | ||
17818137 JD |
78 | def _console_size(self): |
79 | TERMSIZE = 80 | |
80 | return int(os.environ.get('COLUMNS', TERMSIZE)) - 1 | |
81 | ||
952b1e1f | 82 | def _gen_info_string(self, info, start_info, line_length, info_before): |
846cf979 | 83 | number_of_space = (line_length - start_info - len(info)) |
952b1e1f JD |
84 | if info_before: |
85 | return self._u(' ') * number_of_space + info | |
86 | else: | |
87 | return info + self._u(' ') * number_of_space | |
846cf979 | 88 | |
09071fb9 JD |
89 | def _gen_value_string(self, value, start_value, start_info, unit, count): |
90 | if not count: | |
91 | v = str("%0.02f" % value) | |
92 | else: | |
93 | # we don't want to add .00 to count values (only integers) | |
94 | v = str(value) | |
846cf979 | 95 | number_space = start_info -\ |
8054fc4c | 96 | start_value -\ |
8e05871d | 97 | len(v) -\ |
8054fc4c | 98 | self.separator_length |
846cf979 | 99 | |
8054fc4c | 100 | return ' ' * number_space +\ |
8e05871d | 101 | v + str(unit) +\ |
8054fc4c | 102 | ' ' * self.separator_length |
846cf979 JD |
103 | |
104 | def _sanitize_string(self, string): | |
8054fc4c | 105 | # get the type of a unicode string |
846cf979 JD |
106 | unicode_type = type(self._u('t')) |
107 | input_type = type(string) | |
108 | if input_type is str: | |
ddefe9a6 | 109 | if sys.version_info.major < 3: # pragma: no cover |
db1cc4db | 110 | info = string |
8054fc4c | 111 | else: |
846cf979 JD |
112 | info = string |
113 | elif input_type is unicode_type: | |
114 | info = string | |
115 | elif input_type is int or input_type is float: | |
ddefe9a6 | 116 | if sys.version_info.major < 3: # pragma: no cover |
db1cc4db | 117 | info = string |
846cf979 JD |
118 | else: |
119 | info = str(string) | |
120 | return info | |
121 | ||
122 | def _sanitize_data(self, data): | |
123 | ret = [] | |
124 | for item in data: | |
125 | ret.append((self._sanitize_string(item[0]), item[1])) | |
126 | return ret | |
127 | ||
952b1e1f | 128 | def graph(self, label, data, sort=0, with_value=True, unit="", |
09071fb9 | 129 | info_before=False, count=False): |
846cf979 | 130 | """function generating the graph |
8054fc4c | 131 | |
846cf979 JD |
132 | :param string label: the label of the graph |
133 | :param iterable data: the data (list of tuple (info, value)) | |
134 | info must be "castable" to a unicode string | |
135 | value must be an int or a float | |
136 | :param int sort: flag sorted | |
137 | 0: not sorted (same order as given) (default) | |
138 | 1: increasing order | |
139 | 2: decreasing order | |
140 | :param boolean with_value: flag printing value | |
141 | True: print the numeric value (default) | |
142 | False: don't print the numeric value | |
143 | :rtype: a list of strings (each lines) | |
144 | ||
145 | """ | |
146 | result = [] | |
147 | san_data = self._sanitize_data(data) | |
148 | san_label = self._sanitize_string(label) | |
149 | ||
150 | if sort == 1: | |
8054fc4c SG |
151 | san_data = sorted(san_data, key=lambda value: value[1], |
152 | reverse=False) | |
846cf979 | 153 | elif sort == 2: |
8054fc4c SG |
154 | san_data = sorted(san_data, key=lambda value: value[1], |
155 | reverse=True) | |
846cf979 JD |
156 | |
157 | all_max = self._get_maximum(san_data) | |
8054fc4c | 158 | |
846cf979 | 159 | real_line_length = max(self.line_length, len(label)) |
8054fc4c | 160 | |
846cf979 | 161 | min_line_length = self.min_graph_length +\ |
8054fc4c SG |
162 | 2 * self.separator_length +\ |
163 | all_max['value_max_length'] +\ | |
164 | all_max['info_max_length'] | |
846cf979 JD |
165 | |
166 | if min_line_length < real_line_length: | |
8054fc4c | 167 | # calcul of where to start info |
846cf979 | 168 | start_info = self.line_length -\ |
8054fc4c SG |
169 | all_max['info_max_length'] |
170 | # calcul of where to start value | |
846cf979 | 171 | start_value = start_info -\ |
8054fc4c SG |
172 | self.separator_length -\ |
173 | all_max['value_max_length'] | |
174 | # calcul of where to end graph | |
846cf979 | 175 | graph_length = start_value -\ |
8054fc4c | 176 | self.separator_length |
846cf979 | 177 | else: |
8054fc4c | 178 | # calcul of where to start value |
846cf979 | 179 | start_value = self.min_graph_length +\ |
8054fc4c SG |
180 | self.separator_length |
181 | # calcul of where to start info | |
846cf979 | 182 | start_info = start_value +\ |
8054fc4c SG |
183 | all_max['value_max_length'] +\ |
184 | self.separator_length | |
185 | # calcul of where to end graph | |
846cf979 | 186 | graph_length = self.min_graph_length |
8054fc4c | 187 | # calcul of the real line length |
846cf979 JD |
188 | real_line_length = min_line_length |
189 | ||
17818137 | 190 | real_line_length = min(real_line_length, self._console_size()) |
846cf979 | 191 | result.append(san_label) |
8054fc4c | 192 | result.append(self._u('#') * real_line_length) |
846cf979 JD |
193 | |
194 | for item in san_data: | |
195 | info = item[0] | |
196 | value = item[1] | |
197 | ||
198 | graph_string = self._gen_graph_string( | |
8054fc4c SG |
199 | value, |
200 | all_max['max_value'], | |
201 | graph_length, | |
202 | start_value) | |
846cf979 | 203 | |
452b4312 JD |
204 | if with_value: |
205 | value_string = self._gen_value_string( | |
206 | value, | |
207 | start_value, | |
09071fb9 | 208 | start_info, unit, count) |
452b4312 JD |
209 | else: |
210 | value_string = "" | |
846cf979 JD |
211 | |
212 | info_string = self._gen_info_string( | |
8054fc4c SG |
213 | info, |
214 | start_info, | |
952b1e1f JD |
215 | real_line_length, info_before) |
216 | if info_before: | |
217 | new_line = info_string + " " + graph_string + value_string | |
218 | else: | |
219 | new_line = graph_string + value_string + info_string | |
846cf979 JD |
220 | result.append(new_line) |
221 | ||
222 | return result | |
223 | ||
224 | if __name__ == '__main__': | |
8054fc4c SG |
225 | test = [('long_label', 423), ('sl', 1234), ('line3', 531), |
226 | ('line4', 200), ('line5', 834)] | |
846cf979 | 227 | graph = Pyasciigraph() |
8054fc4c | 228 | for line in graph.graph('test print', test): |
846cf979 | 229 | print(line) |