app/assets/js/components/SparklinePlus.coffee (130 lines of code) (raw):

{svg, g, path, circle, line, rect, text, div} = React.DOM SparklinePlus = React.createClass render: () -> points = @props.points if (points.length > 0) (svg { className: "sparkline-plus" style: { height: @props.height } }, [ (g {}, [ (rect { x: 0 y: 0 height: @props.height width: @state.width fill: 'white' onMouseMove: @calculateActivePoint onMouseLeave: @disableDetail }) (sparkline { points: points width: @state.width - 50 height: @sparklineHeight() stroke: "black" }) if @props.additionalLine?.props.points.length > 0 React.addons.cloneWithProps(@props.additionalLine, { width: @state.width - 50 height: @sparklineHeight() }) (circle { cx: @x()(points[points.length - 1].x) cy: @y()(points[points.length - 1].y) r: 2 fill: "black" }) (text { x: @state.width - 45 y: @y()(points[points.length - 1].y) + 10 style: { fontSize: '1em' } }, points[points.length - 1].y + @props.unit) if @state.showDetail hoverDetail({ detailPoint: @state.detailPoint x: @x y: @y height: @props.height unit: @props.unit }) ]) ]) else (div {}, ["No data"]) getInitialState: () -> { width: 10 showDetail: false } calculateActivePoint: (e) -> dt = @x().invert(e.clientX - @getDOMNode().getBoundingClientRect().left).getTime() lessThan = (point for point in @props.points when point.x < dt) biggestLessThan = lessThan[lessThan.length - 1] smallestGreaterThan = (point for point in @props.points when point.x > dt)[0] nearestPoint = if !smallestGreaterThan? || (biggestLessThan? && dt - biggestLessThan.x <= smallestGreaterThan.x) biggestLessThan else smallestGreaterThan @setState({ detailPoint: nearestPoint showDetail: true }) disableDetail: () -> @setState({ showDetail: false }) deriveWidth: () -> @setState({ width: @getDOMNode().parentNode.offsetWidth }) componentDidMount: () -> @deriveWidth() window.addEventListener('resize', @deriveWidth) componentWillUnmount: () -> window.removeEventListener('resize', @deriveWidth) x: () -> d3.time.scale().range([0, @state.width - 50]).domain(d3.extent(@props.points, (d) -> d.x)) y: () -> d3.scale.linear().range([@sparklineHeight(),12]).domain(d3.extent(@props.points, (d) -> d.y)) sparklineHeight: () -> @props.height - 10 Sparkline = React.createClass render: () -> (path { d : "M#{@x()(@props.points[0].x)},#{@y()(@props.points[0].y)}" + ("L#{@x()(p.x)},#{@y()(p.y)}" for p in @props.points).join('') fill: 'none' stroke: @props.stroke }) x: () -> d3.time.scale().range([0, @props.width]).domain(d3.extent(@props.points, (d) -> d.x)) y: () -> d3.scale.linear().range([@props.height,12]).domain(d3.extent(@props.points, (d) -> d.y)) sparkline = React.createFactory(Sparkline) HoverDetail = React.createClass render: () -> (g {}, [ (line { x1: @props.x()(@props.detailPoint.x) x2: @props.x()(@props.detailPoint.x) y1: 0 y2: @props.height stroke: 'black' }) (text { x: @props.x()(@props.detailPoint.x) - 3 y: 0 style: { textAnchor: 'end' alignmentBaseline: 'hanging' } }, d3.time.format('%H:%M')(new Date(@props.detailPoint.x))) (text { x: @props.x()(@props.detailPoint.x) + 3 y: 0 style: { textAnchor: 'start' alignmentBaseline: 'hanging' } }, "" + d3.format(' ,')(@props.detailPoint.y) + @props.unit) ]) hoverDetail = React.createFactory(HoverDetail) window.sparklinePlus = React.createFactory(SparklinePlus) window.sparkline = React.createFactory(Sparkline)