- commit
- 7b5e2b849230ae9f8e87332b8a72978c8f8b33e9
- parent
- 1451363f6d2cd2fe61e23c56127ef07a306f8e15
- Author
- Tobias Bengfort <tobias.bengfort@posteo.de>
- Date
- 2019-02-19 12:41
init
Diffstat
| A | chart.py | 257 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
1 files changed, 257 insertions, 0 deletions
diff --git a/chart.py b/chart.py
@@ -0,0 +1,257 @@
-1 1 from xml.sax.saxutils import escape
-1 2
-1 3 COLORS = ['#e41a1c', '#377eb8', '#4daf4a', '#984ea3', '#ff7f00', '#ffff33']
-1 4
-1 5
-1 6 def round_max(value):
-1 7 s = str(value)
-1 8 head = int(s[0])
-1 9 tail = 10 ** (len(s) - 1)
-1 10 head += 1
-1 11 if head & 1:
-1 12 head += 1
-1 13 return head * tail
-1 14
-1 15
-1 16 class BaseRenderer:
-1 17 stacked = False
-1 18
-1 19 def __init__(
-1 20 self, height=480, width=640, padding=5, colors=COLORS, ui_color='#333'
-1 21 ):
-1 22 self.height = height
-1 23 self.width = width
-1 24 self.padding = padding
-1 25 self.colors = colors
-1 26 self.ui_color = ui_color
-1 27
-1 28 self.char_width = 10
-1 29 self.char_padding = 4
-1 30 self.x_labels = self.char_width * 5
-1 31 self.y_labels = 20
-1 32 self.y_legend = 20
-1 33
-1 34 def get_color(self, i):
-1 35 return self.colors[i % len(self.colors)]
-1 36
-1 37 def attrs(self, **kwargs):
-1 38 return ''.join(
-1 39 ' {}="{}"'.format(key.replace('_', '-'), escape(str(value)))
-1 40 for key, value in kwargs.items()
-1 41 )
-1 42
-1 43 def element(self, tag, content=None, **attrs):
-1 44 if content:
-1 45 if '\n' in content:
-1 46 lines = content.strip().split('\n')
-1 47 content = '\n' + '\n'.join('\t' + l for l in lines) + '\n'
-1 48 return '<{tag}{attrs}>{content}</{tag}>\n'.format(
-1 49 tag=tag,
-1 50 attrs=self.attrs(**attrs),
-1 51 content=content,
-1 52 )
-1 53 else:
-1 54 return '<{tag}{attrs} />\n'.format(
-1 55 tag=tag,
-1 56 attrs=self.attrs(**attrs),
-1 57 )
-1 58
-1 59 def line(self, x1, x2, y1, y2, color, **kwargs):
-1 60 return self.element(
-1 61 'line', x1=x1, x2=x2, y1=y1, y2=y2, stroke=color, **kwargs)
-1 62
-1 63 def text(self, s, x, y, **kwargs):
-1 64 return self.element(
-1 65 'text', escape(str(s)), x=x, y=y, fill=self.ui_color, **kwargs)
-1 66
-1 67 def rect(self, x, y, width, height, title=None, **kwargs):
-1 68 content = None
-1 69 if title:
-1 70 content = self.element('title', escape(str(title)))
-1 71 return self.element(
-1 72 'rect', content, x=x, y=y, width=width, height=height, **kwargs)
-1 73
-1 74 def path(self, points, **kwargs):
-1 75 d = 'M {},{} L'.format(*points[0])
-1 76 for x, y in points[1:]:
-1 77 d += ' {},{}'.format(x, y)
-1 78 return self.element('path', d=d, **kwargs)
-1 79
-1 80 def get_title(self, rows, legend, i, j):
-1 81 key = rows[i]['label']
-1 82 if legend:
-1 83 key += ' - ' + legend[j]
-1 84 return '{}: {}'.format(key, rows[i]['values'][j])
-1 85
-1 86 def render_axes(self, rows, max_value):
-1 87 s = ''
-1 88 s += self.line(0, 0, 0, self.height, self.ui_color)
-1 89 s += self.line(0, self.width, self.height, self.height, self.ui_color)
-1 90
-1 91 for y, value in [
-1 92 (self.height, 0),
-1 93 (self.height / 2, max_value // 2),
-1 94 (0, max_value),
-1 95 ]:
-1 96 s += self.text(value, -self.char_padding, y, **{
-1 97 'dominant-baseline': 'middle',
-1 98 'text-anchor': 'end',
-1 99 })
-1 100
-1 101 width = self.width / len(rows)
-1 102 y = self.height + self.y_labels / 2
-1 103 for i, row in enumerate(rows):
-1 104 x = (i + 0.5) * width
-1 105 s += self.text(row['label'], x, y, **{
-1 106 'dominant-baseline': 'middle',
-1 107 'text-anchor': 'middle',
-1 108 })
-1 109
-1 110 return s
-1 111
-1 112 def render_legend(self, legend):
-1 113 s = ''
-1 114
-1 115 width = 2 * self.char_padding - self.char_width
-1 116 for label in legend:
-1 117 width += self.char_width + self.char_padding # square
-1 118 width += len(label) * self.char_width # text
-1 119 width += self.char_width # margin
-1 120
-1 121 x = self.width - width
-1 122
-1 123 s += self.rect(x, -self.y_legend, width, self.y_legend, **{
-1 124 'fill': 'none',
-1 125 'stroke': self.ui_color,
-1 126 })
-1 127 x += self.char_padding
-1 128
-1 129 y = -self.y_legend / 2
-1 130 for i, label in enumerate(legend):
-1 131 # square
-1 132 size = self.char_width
-1 133 s += self.rect(x, y - size / 2, size, size, fill=self.get_color(i))
-1 134 x += self.char_width + self.char_padding
-1 135 # text
-1 136 s += self.text(label, x, y, **{
-1 137 'dominant-baseline': 'middle',
-1 138 })
-1 139 x += len(label) * self.char_width
-1 140 # margin
-1 141 x += self.char_width
-1 142
-1 143 return self.element('g', s)
-1 144
-1 145 def render_rows(self, rows, legend, max_value):
-1 146 raise NotImplementedError
-1 147
-1 148 def get_view_box(self):
-1 149 p = 2 * self.padding
-1 150 x = -(self.padding + self.x_labels)
-1 151 y = -(self.padding + self.y_legend)
-1 152 width = self.width + p + self.x_labels
-1 153 height = self.height + p + self.y_legend + self.y_legend
-1 154 return '{} {} {} {}'.format(x, y, width, height)
-1 155
-1 156 def render(self, data):
-1 157 if self.stacked:
-1 158 max_value = max(sum(row['values']) for row in data['rows'])
-1 159 else:
-1 160 max_value = max(max(row['values']) for row in data['rows'])
-1 161 max_value = round_max(max_value)
-1 162
-1 163 content = ''
-1 164 content += self.render_axes(data['rows'], max_value)
-1 165 if 'legend' in data:
-1 166 content += self.render_legend(data['legend'])
-1 167 content += self.render_rows(data['rows'], data['legend'], max_value)
-1 168
-1 169 return self.element(
-1 170 'svg',
-1 171 content,
-1 172 xmlns='http://www.w3.org/2000/svg',
-1 173 viewBox=self.get_view_box(),
-1 174 )
-1 175
-1 176
-1 177 class ColumnRenderer(BaseRenderer):
-1 178 def render_rows(self, rows, legend, max_value):
-1 179 s = ''
-1 180 n = len(rows)
-1 181 k = len(rows[0]['values'])
-1 182 width = self.width / n / (k + 2)
-1 183 for i, row in enumerate(rows):
-1 184 group = ''
-1 185 for j, value in enumerate(row['values']):
-1 186 height = self.height * value / max_value
-1 187 x = width * (i * (k + 2) + j + 1)
-1 188 group += self.rect(
-1 189 x,
-1 190 self.height - height - 1,
-1 191 width,
-1 192 height,
-1 193 fill=self.get_color(j),
-1 194 stroke='white',
-1 195 title=self.get_title(rows, legend, i, j),
-1 196 )
-1 197 s += self.element('g', group)
-1 198 return s
-1 199
-1 200
-1 201 class StackedColumnRenderer(BaseRenderer):
-1 202 stacked = True
-1 203
-1 204 def render_rows(self, rows, legend, max_value):
-1 205 s = ''
-1 206 n = len(rows)
-1 207 width = self.width / n
-1 208 for i, row in enumerate(rows):
-1 209 group = ''
-1 210 y = self.height - 1
-1 211 for j, value in enumerate(row['values']):
-1 212 height = self.height * value / max_value
-1 213 x = width * (i + 0.5)
-1 214 y -= height
-1 215 group += self.rect(x - width / 6, y, width / 3, height, **{
-1 216 'fill': self.get_color(j),
-1 217 'stroke': 'white',
-1 218 'title': self.get_title(rows, legend, i, j),
-1 219 })
-1 220 s += self.element('g', group)
-1 221 return s
-1 222
-1 223
-1 224 class LineRenderer(BaseRenderer):
-1 225 def render_rows(self, rows, legend, max_value):
-1 226 s = ''
-1 227 k = len(rows[0]['values'])
-1 228 width = self.width / len(rows)
-1 229 for j in range(k):
-1 230 points = []
-1 231 for i, row in enumerate(rows):
-1 232 x = width * (i + 0.5)
-1 233 y = self.height * row['values'][j] / max_value
-1 234 points.append((x, self.height - y))
-1 235 s += self.path(points, fill='none', stroke=self.get_color(j))
-1 236 return s
-1 237
-1 238
-1 239 class StackedAreaRenderer(BaseRenderer):
-1 240 stacked = True
-1 241
-1 242 def render_rows(self, rows, legend, max_value):
-1 243 s = ''
-1 244 k = len(rows[0]['values'])
-1 245 width = self.width / len(rows)
-1 246 prev = [(width * (i + 0.5), 1) for i in range(len(rows))]
-1 247 for j in range(k):
-1 248 points = []
-1 249 for i, row in enumerate(rows):
-1 250 x = width * (i + 0.5)
-1 251 y = self.height * row['values'][j] / max_value
-1 252 points.append((x, prev[i][1] + y))
-1 253 s += self.path([
-1 254 (x, self.height - y) for x, y in points + list(reversed(prev))
-1 255 ], fill=self.get_color(j), stroke='white')
-1 256 prev = points
-1 257 return s