Download:
child 323:81a30615a4bb
parent 321:b4d13f27821e
322:38b1d78b2242
Anton Shestakov <av6@dwimlabs.net>, Mon, 16 Oct 2017 23:21:11 +0800
viewer: pie charts with check stats

5 файлов изменено, 167 вставок(+), 28 удалений(-) [+]
candolint/handlers.py file | annotate | diff | comparison | revisions
static/candolint.css file | annotate | diff | comparison | revisions
static/candolint.js file | annotate | diff | comparison | revisions
templates/check.html file | annotate | diff | comparison | revisions
tests/test_viewer.py file | annotate | diff | comparison | revisions
--- a/candolint/handlers.py Mon Oct 16 20:37:16 2017 +0800
+++ b/candolint/handlers.py Mon Oct 16 23:21:11 2017 +0800
@@ -1,6 +1,7 @@
from __future__ import absolute_import, division
import traceback
+from collections import OrderedDict
from difflib import unified_diff
from math import ceil
@@ -203,6 +204,36 @@
else:
return {}
+ def get_files_and_codes(self, check, lines):
+ if not check.success or not (check.errors or check.warnings):
+ return {}, {}
+
+ files = {'errors': OrderedDict(), 'warnings': OrderedDict()}
+ codes = {'errors': {}, 'warnings': {}}
+ for number, line in enumerate(lines, 1):
+ if line.get('cls') not in ('error', 'warning'):
+ continue
+ kind = line['cls'] + 's'
+ for key, data in (('filename', files), ('code', codes)):
+ if key in line:
+ value = line[key]
+ if value not in data[kind]:
+ data[kind][value] = {'count': 0, 'line': number}
+ data[kind][value]['count'] += 1
+ files = {
+ kind: [
+ {'filename': k, 'count': v['count'], 'line': v['line']}
+ for k, v in files[kind].items()
+ ] for kind in files
+ }
+ codes = {
+ kind: [
+ {'code': k, 'count': v['count'], 'line': v['line']}
+ for k, v in sorted(codes[kind].items(), key=lambda i: (i[1], i[0]), reverse=True)
+ ] for kind in codes
+ }
+ return files, codes
+
def get_html(self, project, check, lines):
adapter = project.get_adapter()
for line in lines:
@@ -210,7 +241,8 @@
line['link'] = adapter.get_line_url(check.change, line)
reference = self.get_reference_check(check)
chart = self.get_chart_data(check)
- self.render('check.html', project=project, check=check, lines=lines, adapter=adapter, reference=reference, chart=chart)
+ files, codes = self.get_files_and_codes(check, lines)
+ self.render('check.html', project=project, check=check, lines=lines, adapter=adapter, reference=reference, chart=chart, files=files, codes=codes)
class CompareHandler(BaseHandler):
--- a/static/candolint.css Mon Oct 16 20:37:16 2017 +0800
+++ b/static/candolint.css Mon Oct 16 23:21:11 2017 +0800
@@ -90,6 +90,7 @@
max-height: 130px;
}
+.pie-tooltip,
.chart-tooltip {
background: white;
border: 1px solid #000;
--- a/static/candolint.js Mon Oct 16 20:37:16 2017 +0800
+++ b/static/candolint.js Mon Oct 16 23:21:11 2017 +0800
@@ -171,3 +171,87 @@
});
render();
})(jQuery, d3);
+
+(function($, d3) {
+ if ($('#check-files-data, #check-codes-data').length === 0) { return; }
+ if ($('#check-files-pie, #check-codes-pie').length === 0) { return; }
+
+ var $tooltip = $('.pie-tooltip');
+ var pie = d3.pie()
+ .sort(null)
+ .sortValues(null)
+ .value(function(d) { return d.count; });
+
+ var render = function(title, svg, $data, formatTooltip) {
+ if ($data.length === 0) { return; }
+
+ var data = JSON.parse($data.text());
+ var width = +svg.attr('width');
+ var height = +svg.attr('height');
+ var radius = Math.min(width, height) / 2;
+ var thickness = radius / 3.5;
+
+ var g = svg.append('g')
+ .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
+
+ g.append('text')
+ .attr('fill', '#000')
+ .attr('text-anchor', 'middle')
+ .attr('dominant-baseline', 'middle')
+ .text(title);
+
+ var outerArc = d3.arc()
+ .outerRadius(radius)
+ .innerRadius(radius - thickness);
+
+ var innerArc = d3.arc()
+ .outerRadius(radius - thickness)
+ .innerRadius(radius - thickness * 2);
+
+ if (data.errors.length === 0) {
+ innerArc = d3.arc()
+ .outerRadius(radius)
+ .innerRadius(radius - thickness * 2);
+ }
+
+ var config = [
+ {name: 'warnings', color: '#faa732', arc: innerArc},
+ {name: 'errors', color: '#da314b', arc: outerArc}
+ ];
+
+ $.each(config, function(i, series) {
+ var pg = g.append('g')
+ .attr('fill', series.color)
+ .attr('stroke-width', 2)
+ .attr('stroke', 'white');
+
+ pg.selectAll('a')
+ .data(pie(data[series.name]))
+ .enter()
+ .append('a')
+ .attr('href', function(d) { return '#l' + d.data.line; })
+ .append('path')
+ .attr('d', series.arc)
+ .on('mousemove', function(d) {
+ $tooltip.removeClass('uk-hidden').css({
+ left: d3.event.layerX + 10,
+ top: d3.event.layerY + 10
+ });
+ })
+ .on('mouseover', function(d) {
+ $tooltip.text(formatTooltip(d));
+ })
+ .on('mouseout', function(d) {
+ $tooltip.addClass('uk-hidden');
+ });
+ });
+ };
+
+ render('files', d3.select('#check-files-pie'), $('#check-files-data'), function(d) {
+ return d.data.filename + ': ' + d.data.count;
+ });
+
+ render('codes', d3.select('#check-codes-pie'), $('#check-codes-data'), function(d) {
+ return d.data.code + ': ' + d.data.count;
+ });
+})(jQuery, d3);
--- a/templates/check.html Mon Oct 16 20:37:16 2017 +0800
+++ b/templates/check.html Mon Oct 16 23:21:11 2017 +0800
@@ -68,34 +68,51 @@
(<a href="{{ project.get_url() }}/{{ check.ordinal }}/compare/{{ reference.ordinal }}">compare</a>).
</p>
{% end %}
- {% if chart %}
- <div class="uk-position-relative uk-margin-top chart-container">
- <svg id="check-chart"></svg>
- <table class="uk-display-inline-block uk-position-absolute uk-position-z-index uk-hidden chart-tooltip">
- <tr>
- <td>
- <div class="uk-display-inline-block legend-dot" data-dot="warnings"></div>
- </td>
- <td>Warnings:</td>
- <td data-point="warnings">0</td>
- </tr>
- <tr>
- <td>
- <div class="uk-display-inline-block legend-dot" data-dot="errors"></div>
- </td>
- <td>Errors:</td>
- <td data-point="errors">0</td>
- </tr>
- <tr>
- <td>
- <div class="uk-display-inline-block legend-dot" data-dot="duration"></div>
- </td>
- <td>Duration:</td>
- <td data-point="duration">0</td>
- </tr>
- </table>
+ {% if chart or files or codes %}
+ <div class="uk-flex uk-flex-middle uk-margin-top">
+ {% if chart %}
+ <div class="uk-position-relative uk-flex-item-auto chart-container">
+ <svg id="check-chart"></svg>
+ <table class="uk-display-inline-block uk-position-absolute uk-position-z-index uk-text-nowrap uk-hidden chart-tooltip">
+ <tr>
+ <td>
+ <div class="uk-display-inline-block legend-dot" data-dot="warnings"></div>
+ </td>
+ <td>Warnings:</td>
+ <td data-point="warnings">0</td>
+ </tr>
+ <tr>
+ <td>
+ <div class="uk-display-inline-block legend-dot" data-dot="errors"></div>
+ </td>
+ <td>Errors:</td>
+ <td data-point="errors">0</td>
+ </tr>
+ <tr>
+ <td>
+ <div class="uk-display-inline-block legend-dot" data-dot="duration"></div>
+ </td>
+ <td>Duration:</td>
+ <td data-point="duration">0</td>
+ </tr>
+ </table>
+ </div>
+ <script type="application/json" id="check-chart-data">{% raw json_encode(chart) %}</script>
+ {% end %}
+ {% if files or codes %}
+ <div class="uk-position-relative uk-flex-item-none uk-margin-left uk-margin-right">
+ {% if files.get('errors') or files.get('warnings') %}
+ <svg id="check-files-pie" width="120" height="120"></svg>
+ <script type="application/json" id="check-files-data">{% raw json_encode(files) %}</script>
+ {% end %}
+ {% if codes.get('errors') or codes.get('warnings') %}
+ <svg id="check-codes-pie" width="120" height="120"></svg>
+ <script type="application/json" id="check-codes-data">{% raw json_encode(codes) %}</script>
+ {% end %}
+ <div class="uk-display-inline-block uk-position-absolute uk-position-z-index uk-text-nowrap uk-hidden pie-tooltip"></div>
+ </div>
+ {% end %}
</div>
- <script type="application/json" id="check-chart-data">{% raw json_encode(chart) %}</script>
{% end %}
{% include ui/check-log.html %}
</div>
--- a/tests/test_viewer.py Mon Oct 16 20:37:16 2017 +0800
+++ b/tests/test_viewer.py Mon Oct 16 23:21:11 2017 +0800
@@ -65,6 +65,7 @@
'link_start': 0,
'text': 'test.py:20:80: E501 line too long (82 > 79 characters)',
'filename': 'test.py',
+ 'code': 'E501',
'link_end': 10,
'cls': 'warning'
}, {
@@ -161,6 +162,10 @@
assert ('<time datetime="2016-07-19T22:23+0800"'
' title="2016-07-19 22:23 +0800">'
'2016-07-19 22:23 +0800</time>') in response.body
+ assert 'id="check-files-data"' in response.body
+ assert 'id="check-codes-data"' in response.body
+ assert '"filename": "test.py"' in response.body
+ assert '"code": "E501"' in response.body
response = self.fetch('/example.com/alice/test-viewer/latest/raw')
assert response.code == 200