daisho
Version:
Modular dashboard framework
406 lines (334 loc) • 11 kB
text/coffeescript
import Tween from 'es-tween'
import randomColor from 'randomcolor'
import Dynamic from '../dynamic'
import d3 from './d3'
import html from '../../templates/graphics/chart'
# # http://big-elephants.com/2014-06/unrolling-line-charts-d3js/
# getSmoothInterpolation = (lineFn, data) ->
# (d, i, a) ->
# interpolate = d3.scalelinear().domain([
# 0
# 1
# ]).range([
# 1
# data.length + 1
# ])
# (t) ->
# flooredX = Math.floor(interpolate(t))
# weight = interpolate(t) - flooredX
# interpolatedLine = data.slice(0, flooredX)
# if flooredX > 0 and flooredX < 31
# weightedLineAverage = data[flooredX].y * weight + data[flooredX - 1].y * (1 - weight)
# interpolatedLine.push [interpolate(t) - 1, weightedLineAverage]
# lineFn interpolatedLine
# --Chart--
# A chart supports a model with many series with x/y values.
class Chart extends Dynamic
tag: 'daisho-graphics-chart'
html: html
margin:
top: 40
right: 40
bottom: 50
left: 90
width: 0
height: 400
yMin: 10
interpolationTime: 3000
redrawTime: 300
# SVG Bits
svg: null
chart: null
xA: null
yA: null
xAxis: null
yAxis: null
lines: null
points: null
notes: null
legend: null
lineWidth: 3
pointRadius: 6
# Update?
dataHash: ''
colorSeed: 10
colors: null
# from Dynamic
refreshTiming: 'after'
tips: null
nextColor: ()->
x = Math.sin(++) * 10000
return randomColor(seed: Math.floor((x - Math.floor(x)) * 1000))#.replace new RegExp('-', 'g'), ''
init: ->
super()
= []
= []
'mount', =>
= svg = d3.select
.select 'svg'
= d3.timeParse '%Y-%m-%dT%H:%M:%S%Z'
= chart = svg.append 'g'
.attr 'transform', 'translate(' + .left + ',' + .top + ')'
= .append 'g'
.classed 'lines', true
= .append 'g'
.classed 'points-group', true
= .append 'g'
.classed 'notes', true
= chart.append 'g'
.classed 'axis', true
.classed 'x-axis', true
.append 'text'
= chart.append 'g'
.classed 'axis', true
.classed 'y-axis', true
.append 'text'
= svg.append("g")
.classed 'legend', true
.attr 'transform', 'translate(50,30)'
= d3.scaleTime()
= d3.scaleLinear()
_refresh: ->
width = || $().parent().width()
height =
if width <= 0 || height <= 0
return
.attr 'width', width
.attr 'height', height
serieses = .get()
return if !serieses[0]
=
.length = 0
width -= .left + .right
height -= .top + .bottom
xs = []
ys = []
xScale =
yScale =
xScale.rangeRound [0, width]
.ticks d3.timeDay.every 1
yScale.rangeRound [height, 0]
for i, series of serieses
if series.type == 'line' || series.type == 'bar'
xs = xs.concat series.xs
ys = ys.concat series.ys
ysBuf = ys.map serieses[0].fmt.y
ysBuf.push
xScale.domain d3.extent xs.map(serieses[0].fmt.x), (x)=> return x
yScale.domain d3.extent ysBuf, (y)-> return y
# redraw/remove
if &&
.transition()
.duration
.call .scale(xScale)
.transition()
.duration
.call .scale(yScale)
else
= d3.axisBottom(xScale).tickFormat serieses[0].axis.x.ticks
.call
.attr 'transform', 'translate(0,' + height + ')'
.select 'text'
.attr 'fill', '#000'
.attr 'x', width
.attr 'y', -12
.attr 'dy', '0.71em'
.attr 'text-anchor', 'end'
.text series.axis.x.name
= d3.axisLeft(yScale).tickFormat serieses[0].axis.y.ticks
.call
.select 'text'
.attr 'fill', '#000'
.attr 'transform', 'rotate(-90)'
.attr 'y', 6
.attr 'dy', '0.71em'
.attr 'text-anchor', 'end'
.text series.axis.y.name
.selectAll '*'
.attr 'opacity', 1
.transition()
.duration
.attr 'opacity', 0
.attr 'd', lineFn
.remove()
.selectAll '*'
.attr 'opacity', 1
.transition()
.duration
.attr 'opacity', 0
.remove()
.selectAll '*'
.attr 'opacity', 1
.transition()
.duration
.attr 'opacity', 0
.remove()
notes = []
do =>
for tip in
tip.hide()
= []
for i, series of serieses
if series.xs.length == 0 || series.ys.length == 0
continue
# line renderer
if series.type == 'line'
xys = series.xs.map (x, j)->
return [x, series.ys[j]]
lineFn = d3.line()
.x (d) => return xScale
.y (d) -> return yScale series.fmt.y(d[1] || 0)
line = .append 'path'
.classed 'line', true
.classed 'line-' + series.series, true
color =
.push color
line.datum xys
.attr 'fill', 'none'
.attr 'stroke', color
.attr 'stroke-linejoin', 'round'
.attr 'stroke-linecap', 'round'
.attr 'stroke-width',
.attr 'd', lineFn
do (series, line, color)=>
lineLength = line.node().getTotalLength()
tip = d3.tip()
.attr 'class', 'tip tip-' + series.series
.offset [-10, 0]
.html (d) ->
return """
<div class='tip-group'>
<span class='tip-label'>#{ series.axis.x.name }:</span>
<span class='tip-value' style='color:#{ color }'>#{ series.tip.x(series.fmt.x(d[0] || 0)) }</span>
</div>
<div class='tip-group'>
<span class='tip-label'>#{ series.axis.y.name }:</span>
<pre class='tip-value' style='color:#{ color }'>#{ series.tip.y(series.fmt.y(d[1] || 0)) }</pre>
</div>
"""
.push tip
# line stroke tween
# http://stackoverflow.com/questions/32789314/unrolling-line-in-d3js-linechart
point = .append 'g'
.classed 'points', true
.classed 'points-' + series.series, true
point.call tip
line
.attr 'stroke-dashoffset', lineLength
.attr 'stroke-dasharray', lineLength + ' ' + lineLength
.transition()
.duration
.attrTween 'stroke-dashoffset', (ds)=>
j = 0
len = ds.length
lineInterpolator = d3.interpolate lineLength, 0
return (t)=>
if t >= j / len && ds[j]
show = false
p = point.append 'circle'
.classed 'point', true
.classed 'point-' + series.series, true
.datum ds[j]
.attr 'stroke', color
.attr 'stroke-width', 0
.attr 'stroke-opacity', 0
.attr 'fill', color
.attr 'cx', (d)=> return xScale
.attr 'cy', (d)-> yScale series.fmt.y(d[1] || 0)
.on 'mouseover', tip.show
.on 'mouseout', (e)->
if !show
tip.hide(e)
.on 'click', (e)->
show = !show
if show
tip.show(e)
else
tip.hide(e)
p
.transition()
.duration
.attrTween 'r', (d)=>
return d3.interpolate 0,
j++
return lineInterpolator t
# line renderer
# else if series.type == 'bar'
# else if series.type == 'note'
# 1 == 1
# do a thing
# Aggregate data like legends and notes go after here
notes = {}
maxes = []
for series in serieses
if series.type == 'notes'
xs = series.xs
ys = series.ys
for i, x of xs
if notes[x]
notes[x].push ys[i]
else
notes[x] = [ys[i]]
else
xs = series.xs
ys = series.ys
for i, x of xs
if !maxes[x]? || maxes[x] < ys[i]
maxes[x] = ys[i]
for x, ys of notes
datum = [x, maxes[x]]
do (datum, ys)=>
tip = d3.tip()
.attr 'class', 'tip tip-notes'
.offset [-10, 0]
.html (d) ->
return """
<div class='tip-group'>
<span class='tip-label'>#{ serieses[0].axis.x.name }:</span>
<span class='tip-value'>#{ serieses[0].tip.x(series.fmt.x(d[0] || 0)) }</span>
</div>
<div class='tip-group'>
<span class='tip-label'>Notes:</span>
<pre class='tip-value'>#{ ys.join '\n' }</pre>
</div>
"""
.push tip
point = .append 'circle'
.classed 'point', true
.classed 'point-notes', true
point.call tip
show = false
point.datum datum
.attr 'stroke', '#048ba8'
.attr 'stroke-width', 0
.attr 'stroke-opacity', 0
.attr 'fill', '#048ba8'
.attr 'cx', (d)=> return xScale
.attr 'cy', (d)-> yScale(serieses[0].fmt.y(d[1] || 0)) - 20
.on 'mouseover', tip.show
.on 'mouseout', tip.hide
# .on 'mouseout', (e)->
# # if !show
# tip.hide(e)
# .on 'click', (e)->
# show = !show
# if show
# tip.show(e)
# else
# tip.hide(e)
point.transition()
.duration
.attrTween 'r', (d)=>
return d3.interpolate 0, * 1.5
ordinal = d3.scaleOrdinal()
.domain serieses.map((s)-> return s.series).filter (s)-> return !!s
.range
.attr 'transform', 'translate(' + width + ',' + .top + ')'
legendOrdinal = d3.legendColor()
.shape 'path', d3.symbol().type(d3.symbolCircle).size(150)()
.shapePadding 10
# .cellFilter (d)-> return d.label !== 'e'
.scale ordinal
.call legendOrdinal
export default Chart