# Livescript transpiles to javascript, and is easier on the eyes and brain. # http://livescript.net const GOLDEN_RATIO = 2 / (1 + Math.sqrt(5)) export class TimeSeries # Time in x-axis # Data in y-axis (@title, @data, @container, @options = {}) -> # title : string # data : list of {x: <datetime>, y: <float>} console.log "Create time series '#{@title}'" @init() init: -> console.log "Initialize time series '#{@title}'", @data, @options @margin = { top: 30, right: 20, bottom: 30, left: 60 } @xScale = d3.scaleTime().domain(d3.extent(@data, (d) -> d.x)) @yScale = d3.scaleLinear().domain(d3.extent(@data, (d) -> d.y)) #console.info("Y domain #{@title}", d3.extent(@data, (d) -> d.y)) @xAxis = d3.axisBottom() .ticks(7, ",f") .tickFormat(d3.timeFormat("%Y-%m-%d")) @yAxis = d3.axisLeft() .ticks(10) @line = d3.line() .x((d) ~> @xScale(d.x)) .y((d) ~> @yScale(d.y)) @svg = d3.select(@container).append('svg') @plotWrapper = @svg.append('g') @plotWrapper.attr('transform', 'translate(' + @margin.left + ',' + @margin.top + ')') @path = @plotWrapper.append('path') .datum(@data) .classed('line', true) @mouseCanvas = @plotWrapper.append("rect") .style("fill", "none") .style("pointer-events", "all") @mouseCanvas .on("mouseover", @onMouseOver) .on("mouseout", @onMouseOut) .on("mousemove", @onMouseMove) @plotWrapper.append('g').classed('x axis', true) @plotWrapper.append('g').classed('y axis', true) @yAxisText = @plotWrapper.append("text") .attr("transform", "rotate(-90)") .attr("dy", "1em") .style("text-anchor", "middle") .text(@title) @focus = @plotWrapper.append('g').style("display", "none") @cursorCircle = @focus.append("circle") .attr("class", "cursor-circle") .attr("r", 3) dx = 8 @cursorValueShadow = @focus.append("text") .attr("class", "cursor-text cursor-text-shadow") .attr("dx", dx) .attr("dy", "-.3em") @cursorValue = @focus.append("text") .attr("class", "cursor-text cursor-value") .attr("dx", dx) .attr("dy", "-.3em") @cursorDateShadow = @focus.append("text") .attr("class", "cursor-text cursor-text-shadow") .attr("dx", dx) .attr("dy", "1em") @cursorDate = @focus.append("text") .attr("class", "cursor-text cursor-date") .attr("dx", dx) .attr("dy", "1em") @resize() resize: -> width = jQuery(@container).width() - @margin.left - @margin.right height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width @plotWidth = width @plotHeight = height console.log("Resize time series #{@title} : #{width} x #{height}") @xScale.range([0, width]); @yScale.range([height, 0]); @svg.attr('width', width + @margin.right + @margin.left) .attr('height', height + @margin.top + @margin.bottom) @path.attr('d', @line) # wip, do we need to put this in resize ? @xAxis.scale(@xScale) @yAxis.scale(@yScale) #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f") @xAxis.ticks(Math.floor(width / 60)) @yAxis.ticks(Math.floor(height / 18)) @svg.select('.x.axis') .attr('transform', 'translate(0,' + height + ')') .call(@xAxis) @svg.select('.y.axis') .call(@yAxis) @yAxisText.attr("y", 0 - @margin.left) .attr("x", 0 - (height / 2)) @mouseCanvas.attr("width", width) .attr("height", height) this onMouseMove: ~> x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0]) if @options.onMouseMove? @options.onMouseMove(x) else @moveCursor(x) onMouseOver: ~> if @options.onMouseOver? @options.onMouseOver() else @showCursor() onMouseOut: ~> if @options.onMouseOut? @options.onMouseOut() else @hideCursor() showCursor: -> @focus.style("display", null) hideCursor: -> @focus.style("display", "none") bisectDate: d3.bisector((d) -> d.x).left # /!\ complex timeFormat: d3.timeFormat("%Y-%m-%d %Hh") moveCursor: (x0) -> i = @bisectDate(@data, x0, 1) d0 = @data[i - 1] d1 = @data[i] return unless d1 and d0 d = if x0 - d0.x > d1.x - x0 then d1 else d0 xx = @xScale(d.x) yy = @yScale(d.y) transform = "translate(#{xx}, #{yy})" mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false # console.log("xx", xx) dx = 8 dx = -1 * dx if mirrored @cursorCircle.attr("transform", transform) @cursorValue.attr("transform", transform).text(d.y) .attr('text-anchor', if mirrored then 'end' else 'start') .attr("dx", dx) @cursorValueShadow.attr("transform", transform).text(d.y) .attr('text-anchor', if mirrored then 'end' else 'start') .attr("dx", dx) @cursorDate.attr("transform", transform).text(@timeFormat(d.x)) .attr('text-anchor', if mirrored then 'end' else 'start') .attr("dx", dx) @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x)) .attr('text-anchor', if mirrored then 'end' else 'start') .attr("dx", dx) this export class Orbits # View of the solar system from above (@orbiters, @data, @container, @options = {}) -> console.log "Create orbits" @init() init: -> console.log "Initialize orbits", @data, @options @margin = { top: 30, right: 20, bottom: 30, left: 60 } @extremum = 1.11 * d3.max(@data, (d) -> Math.max(Math.abs(d.x), Math.abs(d.y)) ) @xScale = d3.scaleLinear().domain([-1 * @extremum, @extremum]) @yScale = d3.scaleLinear().domain([-1 * @extremum, @extremum]) @xAxis = d3.axisBottom().ticks(10) @yAxis = d3.axisLeft().ticks(10) @svg = d3.select(@container).append('svg') @plotWrapper = @svg.append('g') @plotWrapper.attr('transform', 'translate(' + @margin.left + ',' + @margin.top + ')') @plotWrapper.append('g').classed('x axis', true) @plotWrapper.append('g').classed('y axis', true) @sun = @plotWrapper.append("svg:circle") @sun.append('svg:title').text("Sol") @sun.attr("r", 17).style("fill", "yellow") for slug, config of @orbiters @initOrbiter(slug, config) @resize() orbitersElements: {} initOrbiter: (slug, config) -> if slug of @orbitersElements then throw new Error("Second init of #{slug}") # The order is important, as it will define the default z-order orbit_ellipse = @plotWrapper.append("svg:ellipse") .classed('orbit orbit_ellipse', true) orbiter = @plotWrapper.append("svg:image") .attr('xlink:href', config['img']) .attr('width', '32px').attr('height', '32px') orbit_line = d3.line() .x((d) ~> @xScale(d.x)) .y((d) ~> @yScale(d.y)) orbit_section = @plotWrapper.append('path') .datum(@data) # fixme .classed('orbit orbit_section', true) @orbitersElements[slug] = orbiter: orbiter orbit_ellipse: orbit_ellipse orbit_section: orbit_section orbit_line: orbit_line this resize: -> width = jQuery(@container).width() - @margin.left - @margin.right height = 1.0 * width console.log("Resize orbits : #{width} x #{height}") @xScale.range([0, width]); @yScale.range([height, 0]); @svg.attr('width', width + @margin.right + @margin.left) .attr('height', height + @margin.top + @margin.bottom) @sun.attr("cx", width / 2).attr("cy", height / 2) for slug, config of @orbiters @resizeOrbiter(slug, config) @xAxis.scale(@xScale) @yAxis.scale(@yScale) @svg.select('.x.axis') .attr('transform', 'translate(0,' + height + ')') .call(@xAxis) @svg.select('.y.axis') .call(@yAxis) this resizeOrbiter: (slug, config) -> width = jQuery(@container).width() - @margin.left - @margin.right height = 1.0 * width console.log("Resize orbiter #{slug}") el = @orbitersElements[slug] el['orbit_section'].attr('d', el['orbit_line']) a = config['orbit']['a'] b = config['orbit']['b'] c = Math.sqrt(a*a - b*b) cx = (width / 2) - c cy = (height / 2) @yScale.range([0, height]) el['orbit_ellipse'].attr('cx', cx).attr('cy', cy) .attr('rx', @xScale(a) - @xScale(0)) .attr('ry', @yScale(b) - @yScale(0)) .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')') @yScale.range([height, 0]) data = @data el['orbiter'].attr('x', @xScale(data[data.length - 1].x) - 16) el['orbiter'].attr('y', @yScale(data[data.length - 1].y) - 16) this