Improve the calendar heatmap (#4800)

* Improve xAxis ticks, thinner bottom margin (#4756)

* Improve xAxis ticks, thinner bottom margin

* Moving utils folder

* Add isTruthy

* Addressing comments

* Set cell_padding to 0

* merging db migrations
This commit is contained in:
Maxime Beauchemin 2018-04-12 17:16:45 -07:00 committed by GitHub
parent b04359003e
commit 6fd4ff45ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 3969 additions and 45 deletions

View File

@ -153,6 +153,10 @@ class Chart extends React.PureComponent {
this.props.actions.chartRenderingFailed(e, this.props.chartKey);
}
verboseMetricName(metric) {
return this.props.datasource.verbose_map[metric] || metric;
}
render_template(s) {
const context = {
width: this.width(),

View File

@ -230,6 +230,7 @@ export const controls = {
default: colorPrimary,
renderTrigger: true,
},
legend_position: {
label: t('Legend Position'),
description: t('Choose the position of the legend'),
@ -324,11 +325,17 @@ export const controls = {
label: t('Linear Color Scheme'),
choices: [
['fire', 'fire'],
['blue_white_yellow', 'blue/white/yellow'],
['white_black', 'white/black'],
['black_white', 'black/white'],
['dark_blue', 'light/dark blue'],
['pink_grey', 'pink/white/grey'],
['greens', 'greens'],
['purples', 'purples'],
['oranges', 'oranges'],
['blue_white_yellow', 'blue/white/yellow'],
['red_yellow_blue', 'red/yellowish/blue'],
['brown_white_green', 'brown/white/green'],
['purple_white_green', 'purple/white/green'],
],
default: 'blue_white_yellow',
clearable: false,
@ -1006,6 +1013,46 @@ export const controls = {
'relative to the time granularity selected'),
},
cell_size: {
type: 'TextControl',
isInt: true,
default: 10,
validators: [v.integer],
renderTrigger: true,
label: t('Cell Size'),
description: t('The size of the square cell, in pixels'),
},
cell_padding: {
type: 'TextControl',
isInt: true,
validators: [v.integer],
renderTrigger: true,
default: 0,
label: t('Cell Padding'),
description: t('The distance between cells, in pixels'),
},
cell_radius: {
type: 'TextControl',
isInt: true,
validators: [v.integer],
renderTrigger: true,
default: 0,
label: t('Cell Radius'),
description: t('The pixel radius'),
},
steps: {
type: 'TextControl',
isInt: true,
validators: [v.integer],
renderTrigger: true,
default: 10,
label: t('Color Steps'),
description: t('The number color "steps"'),
},
grid_size: {
type: 'TextControl',
label: t('Grid Size'),
@ -1462,6 +1509,14 @@ export const controls = {
description: t('Whether to display the numerical values within the cells'),
},
show_metric_name: {
type: 'CheckboxControl',
label: t('Show Metric Names'),
renderTrigger: true,
default: true,
description: t('Whether to display the metric name as a title'),
},
x_axis_showminmax: {
type: 'CheckboxControl',
label: t('X bounds'),

View File

@ -977,17 +977,34 @@ export const visTypes = {
label: t('Query'),
expanded: true,
controlSetRows: [
['metric'],
['domain_granularity', 'subdomain_granularity'],
['metrics'],
],
},
{
label: t('Options'),
label: t('Chart Options'),
expanded: true,
controlSetRows: [
['domain_granularity'],
['subdomain_granularity'],
['linear_color_scheme'],
['cell_size', 'cell_padding'],
['cell_radius', 'steps'],
['y_axis_format', 'x_axis_time_format'],
['show_legend', 'show_values'],
['show_metric_name', null],
],
},
],
controlOverrides: {
y_axis_format: {
label: t('Number Format'),
},
x_axis_time_format: {
label: t('Time Format'),
},
show_values: {
default: false,
},
},
},
box_plot: {

View File

@ -122,6 +122,42 @@ export const spectrums = {
'#FAFAFA',
'#666666',
],
greens: [
'#ffffcc',
'#78c679',
'#006837',
],
purples: [
'#f2f0f7',
'#9e9ac8',
'#54278f',
],
oranges: [
'#fef0d9',
'#fc8d59',
'#b30000',
],
red_yellow_blue: [
'#d7191c',
'#fdae61',
'#ffffbf',
'#abd9e9',
'#2c7bb6',
],
brown_white_green: [
'#a6611a',
'#dfc27d',
'#f5f5f5',
'#80cdc1',
'#018571',
],
purple_white_green: [
'#7b3294',
'#c2a5cf',
'#f7f7f7',
'#a6dba0',
'#008837',
],
};
/**

View File

@ -48,7 +48,6 @@
"bootstrap-slider": "^10.0.0",
"brace": "^0.10.0",
"brfs": "^1.4.3",
"cal-heatmap": "3.6.2",
"classnames": "^2.2.5",
"d3": "^3.5.17",
"d3-cloud": "^1.2.1",

View File

@ -0,0 +1,145 @@
/* Cal-HeatMap CSS */
.cal-heatmap-container {
display: block;
}
.cal-heatmap-container .graph
{
font-family: "Lucida Grande", Lucida, Verdana, sans-serif;
}
.cal-heatmap-container .graph-label
{
fill: #999;
font-size: 10px
}
.cal-heatmap-container .graph, .cal-heatmap-container .graph-legend rect {
shape-rendering: crispedges
}
.cal-heatmap-container .graph-rect
{
fill: #ededed
}
.cal-heatmap-container .graph-subdomain-group rect:hover
{
stroke: #000;
stroke-width: 1px
}
.cal-heatmap-container .subdomain-text {
font-size: 8px;
fill: #999;
pointer-events: none
}
.cal-heatmap-container .hover_cursor:hover {
cursor: pointer
}
.cal-heatmap-container .qi {
background-color: #999;
fill: #999
}
/*
Remove comment to apply this style to date with value equal to 0
.q0
{
background-color: #fff;
fill: #fff;
stroke: #ededed
}
*/
.cal-heatmap-container .q1
{
background-color: #dae289;
fill: #dae289
}
.cal-heatmap-container .q2
{
background-color: #cedb9c;
fill: #9cc069
}
.cal-heatmap-container .q3
{
background-color: #b5cf6b;
fill: #669d45
}
.cal-heatmap-container .q4
{
background-color: #637939;
fill: #637939
}
.cal-heatmap-container .q5
{
background-color: #3b6427;
fill: #3b6427
}
.cal-heatmap-container rect.highlight
{
stroke:#444;
stroke-width:1
}
.cal-heatmap-container text.highlight
{
fill: #444
}
.cal-heatmap-container rect.highlight-now
{
stroke: red
}
.cal-heatmap-container text.highlight-now
{
fill: red;
font-weight: 800
}
.cal-heatmap-container .domain-background {
fill: none;
shape-rendering: crispedges
}
.ch-tooltip {
padding: 10px;
background: #222;
color: #bbb;
font-size: 12px;
line-height: 1.4;
width: 140px;
position: absolute;
z-index: 99999;
text-align: center;
border-radius: 2px;
box-shadow: 2px 2px 2px rgba(0,0,0,0.2);
display: none;
box-sizing: border-box;
}
.ch-tooltip::after{
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
content: "";
padding: 0;
display: block;
bottom: -6px;
left: 50%;
margin-left: -6px;
border-width: 6px 6px 0;
border-top-color: #222;
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,14 @@
.cal_heatmap .slice_container {
.slice_container.cal_heatmap {
padding: 10px;
position: static !important;
overflow: auto !important;
}
.cal_heatmap .slice_container .ch-tooltip {
margin-left: 20px;
margin-top: 5px;
}
.graph-legend rect {
stroke: #aaa;
stroke-location: inside;
}

View File

@ -1,38 +1,83 @@
// JS
import d3 from 'd3';
// CSS
require('./cal_heatmap.css');
require('../node_modules/cal-heatmap/cal-heatmap.css');
import { colorScalerFactory } from '../javascripts/modules/colors';
import CalHeatMap from '../vendor/cal-heatmap/cal-heatmap';
import '../vendor/cal-heatmap/cal-heatmap.css';
import { d3TimeFormatPreset, d3FormatPreset } from '../javascripts/modules/utils';
import './cal_heatmap.css';
import { UTC } from '../javascripts/modules/dates';
const CalHeatMap = require('cal-heatmap');
const UTCTS = uts => UTC(new Date(uts)).getTime();
function calHeatmap(slice, payload) {
const div = d3.select(slice.selector);
const fd = slice.formData;
const steps = fd.steps;
const valueFormatter = d3FormatPreset(fd.y_axis_format);
const timeFormatter = d3TimeFormatPreset(fd.x_axis_time_format);
const container = d3.select(slice.selector).style('height', slice.height());
container.selectAll('*').remove();
const div = container.append('div');
const data = payload.data;
div.selectAll('*').remove();
const cal = new CalHeatMap();
const subDomainTextFormat = fd.show_values ? (date, value) => valueFormatter(value) : null;
const cellPadding = fd.cell_padding !== '' ? fd.cell_padding : 2;
const cellRadius = fd.cell_radius || 0;
const cellSize = fd.cell_size || 10;
const timestamps = data.timestamps;
const extents = d3.extent(Object.keys(timestamps), key => timestamps[key]);
const step = (extents[1] - extents[0]) / 5;
// Trick to convert all timestamps to UTC
const metricsData = {};
Object.keys(data.data).forEach((metric) => {
metricsData[metric] = {};
Object.keys(data.data[metric]).forEach((ts) => {
metricsData[metric][UTCTS(ts * 1000) / 1000] = data.data[metric][ts];
});
});
Object.keys(metricsData).forEach((metric) => {
const calContainer = div.append('div');
if (fd.show_metric_name) {
calContainer.append('h4').text(slice.verboseMetricName(metric));
}
const timestamps = metricsData[metric];
const extents = d3.extent(Object.keys(timestamps), key => timestamps[key]);
const step = (extents[1] - extents[0]) / (steps - 1);
const colorScale = colorScalerFactory(fd.linear_color_scheme, null, null, extents);
const legend = d3.range(steps).map(i => extents[0] + (step * i));
const legendColors = legend.map(colorScale);
const cal = new CalHeatMap();
try {
cal.init({
start: data.start,
start: UTCTS(data.start),
data: timestamps,
itemSelector: slice.selector,
itemSelector: calContainer[0][0],
legendVerticalPosition: 'top',
cellSize,
cellPadding,
cellRadius,
legendCellSize: cellSize,
legendCellPadding: 2,
legendCellRadius: cellRadius,
tooltip: true,
domain: data.domain,
subDomain: data.subdomain,
range: data.range,
browsing: true,
legend: [extents[0], extents[0] + step, extents[0] + (step * 2), extents[0] + (step * 3)],
legend,
legendColors: {
colorScale,
min: legendColors[0],
max: legendColors[legendColors.length - 1],
empty: 'white',
},
displayLegend: fd.show_legend,
itemName: '',
valueFormatter,
timeFormatter,
subDomainTextFormat,
});
} catch (e) {
slice.error(e);
}
});
}
module.exports = calHeatmap;

View File

@ -1761,12 +1761,6 @@ cacache@^10.0.1:
unique-filename "^1.1.0"
y18n "^3.2.1"
cal-heatmap@3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/cal-heatmap/-/cal-heatmap-3.6.2.tgz#961a7f4686b3bdcf7104d951b6ff1dd58c0c62d1"
dependencies:
d3 "^3.0.6"
call-matcher@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/call-matcher/-/call-matcher-1.0.1.tgz#5134d077984f712a54dad3cbf62de28dce416ca8"
@ -2652,7 +2646,7 @@ d3-zoom@^1.3.0:
d3-selection "1"
d3-transition "1"
d3@3, d3@^3.0.6, d3@^3.5.17, d3@^3.5.5, d3@^3.5.6:
d3@3, d3@^3.5.17, d3@^3.5.5, d3@^3.5.6:
version "3.5.17"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"

View File

@ -0,0 +1,22 @@
"""empty message
Revision ID: 5ccf602336a0
Revises: ('130915240929', 'c9495751e314')
Create Date: 2018-04-12 16:00:47.639218
"""
# revision identifiers, used by Alembic.
revision = '5ccf602336a0'
down_revision = ('130915240929', 'c9495751e314')
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
pass

View File

@ -0,0 +1,56 @@
"""cal_heatmap_metric_to_metrics
Revision ID: bf706ae5eb46
Revises: f231d82b9b26
Create Date: 2018-04-10 11:19:47.621878
"""
from alembic import op
import json
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, Text
from superset import db
from superset.legacy import cast_form_data
Base = declarative_base()
# revision identifiers, used by Alembic.
revision = 'bf706ae5eb46'
down_revision = 'f231d82b9b26'
class Slice(Base):
"""Declarative class to do query in upgrade"""
__tablename__ = 'slices'
id = Column(Integer, primary_key=True)
datasource_type = Column(String(200))
viz_type = Column(String(200))
slice_name = Column(String(200))
params = Column(Text)
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind)
slices = session.query(Slice).filter_by(viz_type='cal_heatmap').all()
slice_len = len(slices)
for i, slc in enumerate(slices):
try:
params = json.loads(slc.params or '{}')
params['metrics'] = [params.get('metric')]
del params['metric']
slc.params = json.dumps(params, indent=2, sort_keys=True)
session.merge(slc)
session.commit()
print('Upgraded ({}/{}): {}'.format(i, slice_len, slc.slice_name))
except Exception as e:
print(slc.slice_name + ' error: ' + str(e))
session.close()
def downgrade():
pass

View File

@ -0,0 +1,22 @@
"""empty message
Revision ID: c9495751e314
Revises: ('30bb17c0dc76', 'bf706ae5eb46')
Create Date: 2018-04-10 20:46:57.890773
"""
# revision identifiers, used by Alembic.
revision = 'c9495751e314'
down_revision = ('30bb17c0dc76', 'bf706ae5eb46')
from alembic import op
import sqlalchemy as sa
def upgrade():
pass
def downgrade():
pass

View File

@ -5,16 +5,15 @@ Revises: e68c4473c581
Create Date: 2018-03-20 19:47:54.991259
"""
# revision identifiers, used by Alembic.
revision = 'f231d82b9b26'
down_revision = 'e68c4473c581'
from alembic import op
import sqlalchemy as sa
from superset.utils import generic_find_uq_constraint_name
# revision identifiers, used by Alembic.
revision = 'f231d82b9b26'
down_revision = 'e68c4473c581'
conv = {
'uq': 'uq_%(table_name)s_%(column_0_name)s',
}
@ -44,8 +43,12 @@ def upgrade():
[column, 'datasource_id'],
)
def downgrade():
bind = op.get_bind()
insp = sa.engine.reflection.Inspector.from_engine(bind)
# Restore the size of the metric_name column.
with op.batch_alter_table('metrics', naming_convention=conv) as batch_op:
batch_op.alter_column(

View File

@ -735,12 +735,18 @@ class CalHeatmapViz(BaseViz):
def get_data(self, df):
form_data = self.form_data
df.columns = ['timestamp', 'metric']
timestamps = {str(obj['timestamp'].value / 10**9):
obj.get('metric') for obj in df.to_dict('records')}
data = {}
records = df.to_dict('records')
for metric in self.metrics:
data[metric] = {
str(obj[DTTM_ALIAS].value / 10**9): obj.get(metric)
for obj in records
}
start = utils.parse_human_datetime(form_data.get('since'))
end = utils.parse_human_datetime(form_data.get('until'))
if not start or not end:
raise Exception("Please provide both time bounds (Since and Until)")
domain = form_data.get('domain_granularity')
diff_delta = rdelta.relativedelta(end, start)
diff_secs = (end - start).total_seconds()
@ -757,7 +763,7 @@ class CalHeatmapViz(BaseViz):
range_ = diff_secs // (60 * 60) + 1
return {
'timestamps': timestamps,
'data': data,
'start': start,
'domain': domain,
'subdomain': form_data.get('subdomain_granularity'),
@ -765,9 +771,10 @@ class CalHeatmapViz(BaseViz):
}
def query_obj(self):
qry = super(CalHeatmapViz, self).query_obj()
qry['metrics'] = [self.form_data['metric']]
return qry
d = super(CalHeatmapViz, self).query_obj()
fd = self.form_data
d['metrics'] = fd.get('metrics')
return d
class NVD3Viz(BaseViz):