Created
February 15, 2013 22:10
-
-
Save karlin/4963981 to your computer and use it in GitHub Desktop.
A CodePen by Karlin Fox. kpi - bullet graph of some key performance metrics
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div style="float:right"> | |
<input id="metric" value="week_billable"/> | |
<input id="h" type="range" min="0" max="120"/> | |
</div> | |
<div class="header"></div> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Kpi = (-> | |
label = (node, attrs) -> | |
node.append('text') | |
.attr('class', attrs.class) | |
.attr('y', attrs.y) | |
.attr('x', attrs.x) | |
.text(if typeof attrs.text is 'function' then attrs.text else ((d) -> d[attrs.text])) | |
rect = (node, attrs) -> | |
anode = node.append('rect') | |
.attr('class', attrs.class) | |
if attrs.bounds? | |
anode.attr('x', attrs.bounds[0]) | |
.attr('y', attrs.bounds[1]) | |
.attr('width', attrs.bounds[2]) | |
.attr('height', attrs.bounds[3]) | |
else | |
if attrs.x? then anode.attr('x', attrs.x) | |
if attrs.y? then anode.attr('y', attrs.y) | |
if attrs.width? then anode.attr('width', attrs.width) | |
anode.attr('height', attrs.height) if attrs.height? | |
if attrs.title? | |
anode.append('title').text(attrs.title) | |
anode | |
kpi_graphs = (home) -> | |
draw_graphs = (kpi_inputs) -> | |
kpi_labels = [ | |
label: 'Hours' | |
label_line2: 'this week' | |
id: 'week' | |
week_billable_label: 'billable' | |
week_total_label: 'total' | |
, | |
id: 'quarter' | |
label_line2: 'this quarter' | |
label: 'Utilization' | |
] | |
WEEK = 0 | |
QUARTER = 1 | |
graph_inputs = [] | |
$.each(kpi_inputs, (i, k) -> | |
graph_inputs.push($.extend({}, k, kpi_labels[i])) | |
) | |
dims = | |
graph_padding: 10 | |
label_padding: 4 | |
numeric_padding: 12 | |
graph_height: 24 | |
intergraph_padding: 60 | |
hours_max: 40 | |
utilization_max: 100 | |
graph_width: 280 | |
dims.graph_unit_height = dims.graph_height / 3 | |
# Create our scales | |
hrs_scale = d3.scale.linear() | |
.domain([0,dims.hours_max]) | |
.range([0,dims.graph_width]) | |
ulz_scale = d3.scale.linear() | |
.domain([0,dims.utilization_max]) | |
.range([0,dims.graph_width]) | |
# Add axes with ticks and labels | |
week_axis = d3.svg.axis() | |
.scale(hrs_scale) | |
.orient('bottom') | |
.tickValues([0,8,16,24,32,40]) | |
.tickSubdivide(1) | |
quarter_axis = d3.svg.axis() | |
.scale(ulz_scale) | |
.orient('bottom') | |
.tickValues([0,25,50,75,100]) | |
# create the SVG element | |
vis = d3.select(home).append('svg') | |
.attr('width', 620) | |
.attr('height', 115) | |
.attr('id', 'kpi') | |
.attr('shape-rendering', 'crisp-edges') | |
$('svg#kpi').append( | |
"<defs> | |
<style type='text/css'> | |
<![CDATA[ | |
@font-face { | |
font-family: DINOT-Light; | |
src: url('css/DINOT.woff'); | |
} | |
]]> | |
</style> | |
</defs>") | |
# group all the graphs | |
graphs = vis.append('g').selectAll('g.bullet-graph') | |
.data(graph_inputs).enter() | |
.append('g') | |
.attr('class', 'bullet-graph') | |
# add a group for each graph with a named class | |
graphs.append('g').attr('class', (d) -> d.id) | |
# | |
# Week graph | |
# | |
week = graphs.select('g.week') | |
label week, | |
class: 'label' | |
y: dims.graph_height / 2 | |
x: -dims.label_padding | |
text: 'label' | |
label week, | |
class: 'small-label' | |
y: dims.graph_height | |
x: -dims.label_padding | |
text: 'label_line2' | |
rect week, | |
class: 'week-total-avg' | |
bounds: [0, 0, ((d) -> hrs_scale(d.week_total_avg)), dims.graph_height] | |
title: '12-week average of total hours per week' | |
rect week, | |
class: 'week-billable-avg' | |
height: dims.graph_height | |
title: '12-week average of billable hours per week' | |
rect week, | |
class: 'week-total' | |
bounds: [0, dims.graph_unit_height, ((d) -> hrs_scale(d.week_total)), dims.graph_unit_height] | |
title: 'Total hours punched this week' | |
rect week, | |
class: 'week-billable' | |
bounds: [0, dims.graph_unit_height, ((d) -> hrs_scale(d.week_billable)), dims.graph_unit_height] | |
title: 'Billable hours punched this week' | |
rect week, | |
class: 'week-target' | |
bounds: [ | |
((d) -> hrs_scale(d.week_billable_possible)), dims.graph_unit_height / 2, | |
2, dims.graph_height-dims.graph_unit_height | |
] | |
title: 'Billable hours possible this week' | |
label(week, | |
class: 'numeric-label billable' | |
y: dims.graph_height / 2+3 | |
).attr('dy', '0.20em') | |
label(week, | |
class: 'numeric-label total' | |
y: dims.graph_height / 2+3 | |
).attr('dy', '0.20em') | |
label week, | |
class: 'small-label legend billable' | |
y: dims.graph_height + 18 | |
text: 'week_billable_label' | |
label week, | |
class: 'small-label legend total' | |
y: dims.graph_height+18 | |
text: 'week_total_label' | |
adjust_week_graph_for = (t) -> | |
t.select('.week-billable').attr('width', (d) -> hrs_scale(d.week_billable)) | |
t.select('.week-total').attr('width', (d) -> hrs_scale(d.week_total)) | |
t.select('.week-billable-avg').attr 'width', (d) -> hrs_scale(d.week_billable_avg) | |
t.select('.week-total-avg').attr 'width', (d) -> hrs_scale(d.week_total_avg) | |
t.select('.week-target').attr('x', (d) -> hrs_scale(d.week_billable_possible)) | |
scaled_max_x = (d) -> | |
x = 0 | |
(x = n if n > x) for n in [d.week_total_avg, d.week_billable, d.week_total, d.week_billable_avg, d.week_billable_possible] | |
hrs_scale(x) | |
after_billable = (d) -> | |
dims.numeric_padding*2 + | |
scaled_max_x(d) + | |
Math.max($('text.legend.billable')[0].getComputedTextLength?(), $('text.billable.numeric-label')[0].getComputedTextLength?()) | |
t.select('text.billable.numeric-label').text((d) -> d.week_billable) | |
t.select('text.total.numeric-label').text((d) -> d.week_total) | |
# t.select('rect.label-container') | |
# .attr('x', (d) -> hrs_scale(d.week_total_avg) + dims.numeric_padding/2) | |
t.selectAll('text.billable').attr('x', (d) -> scaled_max_x(d) + dims.numeric_padding) | |
t.selectAll('text.total').attr('x', after_billable) | |
adjust_week_graph_for week | |
week.append('g').attr('class', 'axis') | |
.attr('transform', "translate(0,#{dims.graph_height})") | |
.call(week_axis) | |
# create the update functions and return their container | |
kpi_inputs[WEEK].recalc_graph = -> | |
graph_inputs[WEEK][metric] = kpi_inputs[WEEK][metric] for metric in [ | |
'week_billable', | |
'week_total', | |
'week_billable_avg', | |
'week_total_avg', | |
'week_billable_possible' | |
] | |
adjust_week_graph_for week.transition() | |
# | |
# Quarter graph | |
# | |
quarter = graphs.selectAll('g.quarter') | |
.attr('transform', "translate(0, #{dims.intergraph_padding})") | |
label quarter, | |
class: 'label utilization' | |
y: dims.graph_height / 2 | |
x: -dims.label_padding | |
text: 'label' | |
label quarter, | |
class: 'small-label' | |
y: dims.graph_height | |
x: -dims.label_padding | |
text: 'label_line2' | |
rect quarter, | |
class: 'quarter-utilization-goal' | |
height: dims.graph_height | |
width: (d) -> ulz_scale(d.quarter_utilization_goal) | |
rect quarter, | |
class: 'quarter-co-utilization' | |
height: dims.graph_height | |
title: 'Company-wide utilization this fiscal quarter' | |
rect quarter, | |
class: 'quarter-utilization' | |
height: dims.graph_unit_height | |
y: dims.graph_unit_height | |
title: 'Utilization this fiscal quarter' | |
label(quarter, | |
class: 'numeric-label utilization' | |
y: dims.graph_height / 2+3 | |
).attr('dy', '0.20em') | |
adjust_quarter_graph_for = (t) -> | |
max_x = (d) -> Math.max(d.quarter_utilization, Math.max(d.quarter_utilization_goal, d.quarter_co_utilization)) | |
t.select('text.utilization.numeric-label').attr('x', (d) -> ulz_scale(max_x(d)) + dims.numeric_padding) | |
t.select('.quarter-utilization').attr('width', (d) -> ulz_scale(d.quarter_utilization)) | |
t.select('.quarter-co-utilization').attr('width', (d) -> ulz_scale(d.quarter_co_utilization)) | |
t.selectAll('text.numeric-label.utilization').text((d) -> "#{d.quarter_utilization}%") | |
adjust_quarter_graph_for quarter | |
quarter.append('g') | |
.attr('class', 'axis') | |
.attr('transform', "translate(0,#{dims.graph_height})") | |
.call(quarter_axis) | |
# bump the whole thing right to accomodate the labels, pad the top | |
right_padding = dims.graph_padding + parseInt($('text.label.utilization')[0].getComputedTextLength?()) | |
graphs.attr('transform', | |
"translate(#{right_padding},#{dims.graph_padding})") | |
q_inputs = kpi_inputs[QUARTER] | |
q_inputs.recalc_graph = -> | |
graph_inputs[QUARTER].quarter_utilization = q_inputs.quarter_utilization | |
graph_inputs[QUARTER].quarter_co_utilization = q_inputs.quarter_co_utilization | |
adjust_quarter_graph_for(quarter.transition()) | |
kpi_inputs | |
graphs_in_header = kpi_graphs 'div.header' | |
KPI = graphs_in_header [ | |
week_billable: 24.25 | |
week_total: 25 | |
week_billable_avg: 34.3 | |
week_total_avg: 45.875 | |
week_billable_possible: 40 | |
, | |
quarter_utilization: 99 | |
quarter_co_utilization: 62 | |
quarter_utilization_goal: 100 | |
] | |
update_for = (x) -> | |
(new_vals) -> | |
for k, v of new_vals | |
for p in x when p?[k] | |
p[k] = v | |
p.recalc_graph() | |
update_kpi_with = update_for KPI | |
update_kpi_with | |
)() | |
$('input#h').on 'change', -> | |
new_data = {} | |
new_data[$('#metric').val()] = $(this).val() | |
Kpi new_data |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* Week */ | |
rect.week-billable { | |
fill: #1e3332; | |
} | |
rect.week-total { | |
fill: #e5f4f3; | |
} | |
rect.week-total-avg { | |
fill: #b0dfde; | |
} | |
rect.week-billable-avg { | |
fill: #79ccc7; | |
} | |
rect.week-target { | |
fill: #1e3332; | |
} | |
/* Quarter */ | |
rect.quarter-co-utilization { | |
fill: #f9aa99; | |
} | |
rect.quarter-utilization-goal { | |
fill: #fbddd6; | |
} | |
rect.quarter-utilization { | |
fill: #3e2a26; | |
} | |
/* Labels */ | |
text.label { | |
fill: #596665; | |
font-size: 16px; | |
font-family: DINOT; | |
text-anchor: end; | |
dominant-baseline: top; | |
} | |
text.numeric-label { | |
font-family: DINOT; | |
text-anchor: start; | |
font-size: 23px; | |
} | |
text.numeric-label.total { | |
font-size: 14px; | |
fill: #aaa; | |
} | |
text.small-label { | |
font-size: 12px; | |
font-family: DINOT-Light; | |
text-anchor: end; | |
fill: #ccc; | |
} | |
text.legend { | |
text-anchor: start; | |
} | |
/* Axes */ | |
.axis { | |
fill: #ccc; | |
stroke: none; | |
stroke-width: 0; | |
fill:none; | |
} | |
.axis line { | |
shape-rendering: crispEdges; | |
stroke-width: 1px; | |
stroke: #ccc; | |
} | |
.axis text { | |
text-rendering: geometricPrecision; | |
font-family: DINOT-Light; | |
fill: #ccc; | |
font-size: 12px; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment