From ff573ae0f2e77e96a3bb08ef9e611309b09309a4 Mon Sep 17 00:00:00 2001 From: declerambaul Date: Wed, 27 Jun 2012 18:41:05 +0200 Subject: [PATCH] Created a D3ChartType ChartType, abstracting the different d3 elements (line,bar) as subclasses of D3ChartElement. --- lib/chart/chart-type.co | 2 + lib/chart/index.co | 5 +- lib/chart/type/d3-chart.co | 129 ++++++++++++++++ lib/chart/type/d3/d3-bar-chart-type.co | 241 ----------------------------- lib/chart/type/d3/d3-bar-element.co | 78 ++++++++++ lib/chart/type/d3/d3-chart-element.co | 79 ++++++++++ lib/chart/type/d3/d3-chart-type.co | 121 --------------- lib/chart/type/d3/d3-geo-chart-type.co | 185 ---------------------- lib/chart/type/d3/d3-geo-element.co | 185 ++++++++++++++++++++++ lib/chart/type/d3/d3-line-chart-type.co | 255 ------------------------------- lib/chart/type/d3/d3-line-element.co | 84 ++++++++++ lib/chart/type/d3/index.co | 8 +- lib/util/formatters.co | 24 ++-- www/modules.yaml | 10 +- 14 files changed, 584 insertions(+), 822 deletions(-) create mode 100644 lib/chart/type/d3-chart.co delete mode 100644 lib/chart/type/d3/d3-bar-chart-type.co create mode 100644 lib/chart/type/d3/d3-bar-element.co create mode 100644 lib/chart/type/d3/d3-chart-element.co delete mode 100644 lib/chart/type/d3/d3-chart-type.co delete mode 100644 lib/chart/type/d3/d3-geo-chart-type.co create mode 100644 lib/chart/type/d3/d3-geo-element.co delete mode 100644 lib/chart/type/d3/d3-line-chart-type.co create mode 100644 lib/chart/type/d3/d3-line-element.co diff --git a/lib/chart/chart-type.co b/lib/chart/chart-type.co index 2030a90..1aa4b9e 100644 --- a/lib/chart/chart-type.co +++ b/lib/chart/chart-type.co @@ -38,6 +38,7 @@ class exports.ChartType extends ReadyEmitter * Register a new chart type. */ @register = (Subclass) -> + console.log "ChartType.register(#Subclass)" KNOWN_CHART_TYPES[ Subclass::typeName ] = Subclass /** @@ -53,6 +54,7 @@ class exports.ChartType extends ReadyEmitter * @returns {ChartType} */ @create = (model, view) -> + console.log "ChartType.create(#model) ->", model return null unless Type = @lookup model new Type model, view diff --git a/lib/chart/index.co b/lib/chart/index.co index a961cba..c5fd1e2 100644 --- a/lib/chart/index.co +++ b/lib/chart/index.co @@ -1,7 +1,8 @@ chart_type = require 'kraken/chart/chart-type' chart_option = require 'kraken/chart/option' dygraphs = require 'kraken/chart/type/dygraphs' -d3_charts = require 'kraken/chart/type/d3' +d3_chart = require 'kraken/chart/type/d3-chart' +d3_elements = require 'kraken/chart/type/d3' exports import chart_type import chart_option \ - import dygraphs import d3_charts + import dygraphs import d3_chart import d3_elements diff --git a/lib/chart/type/d3-chart.co b/lib/chart/type/d3-chart.co new file mode 100644 index 0000000..99d0b6d --- /dev/null +++ b/lib/chart/type/d3-chart.co @@ -0,0 +1,129 @@ +d3 = require 'd3' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ChartType, +} = require 'kraken/chart/chart-type' +{ D3ChartElement, +} = require 'kraken/chart/type/d3/d3-chart-element' + + +root = do -> this + + +class exports.D3ChartType extends ChartType + __bind__ : <[ determineSize ]> + SPEC_URL : '/schema/d3/d3-chart.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'd3-chart' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + + + -> super ... + + + getData: -> + @model.dataset.getColumns() + + + transform: -> + dataset = @model.dataset + options = @model.getOptions() import @determineSize() + options import do + colors : dataset.getColors() + labels : dataset.getLabels() + options + + + renderChart: (data, viewport, options, lastChart) -> + ### Starting with http://bost.ocks.org/mike/chart/ + + margin = {top: 20, right: 20, bottom: 20, left: 20} + width = 760 + height = 320 + xScale = d3.time.scale() + yScale = d3.scale.linear() + + dates = data[0] + cols = data.slice(1) + + # Calculate extents using all the data points (but not dates) + # allValues = d3.merge @model.dataset.getDataColumns() + allValues = d3.merge cols + + # Update the x-scale with the extents of the dates. + xScale + .domain d3.extent dates + .range [ 0, width - margin.left - margin.right ] + + # Update the y-scale with the extents of the data. + yScale + .domain d3.extent allValues + .range [ height - margin.top - margin.bottom, 0 ] + + # Select the svg element, if it exists. + svg = d3.select viewport.0 .selectAll "svg" + .data [cols] + + # ...Otherwise, create the skeletal chart. + enterFrame = svg.enter() + .append "svg" .append "g" + .attr "class", "frame" + + # Update chart dimensions. + svg .attr "width", width + .attr "height", height + + frame = svg.select "g.frame" + .attr "transform", "translate(#{margin.left},#{margin.top})" + .attr "width", width - margin.left - margin.right + .attr "height", height - margin.top - margin.bottom + + + # x-axis. + # TODO move axis to separate chart-type + enterFrame.append "g" + .attr "class", "x axis time" + + + xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) + frame.select ".x.axis.time" + .attr "transform", "translate(0,#{yScale.range()[0]})" + .call xAxis + + + + # this is wrong. should work using d3 datajoins. + # i.e. use the enter/exit function to add/remove + # metrics from the graph + for i,metric in @model.dataset.metrics.models + # metric defined charttype + chartElement = metric.get "chartElement" + # otherwise the graph defined charttype + # FOR NOW take line as default + chartElement ?= 'd3-line' # @model.get "chartType" + + + # create d3 chart element and render it + chEl = D3ChartElement.create chartElement + console.log chEl + chEl.renderChartElement metric, frame ,xScale, yScale + + + svg + + + diff --git a/lib/chart/type/d3/d3-bar-chart-type.co b/lib/chart/type/d3/d3-bar-chart-type.co deleted file mode 100644 index 0a257ff..0000000 --- a/lib/chart/type/d3/d3-bar-chart-type.co +++ /dev/null @@ -1,241 +0,0 @@ -d3 = require 'd3' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' - -root = do -> this - - -class exports.BarChartType extends ChartType - __bind__ : <[ determineSize ]> - SPEC_URL : '/schema/d3/d3-bar.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-bar' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - -> super ... - - getData: -> - @model.dataset.getColumns() - - - transform: -> - dataset = @model.dataset - options = @model.getOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - options - - - renderChartType: (metric, svgEl ,xScale, yScale) -> - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - - - ### Render the line path - metricBars = root.metricBars = svgEl.append "g" - .attr "class", "metric bars "+metric.get 'label' - - data = d3.zip metric.getDateColumn(),metric.getData() - - ### Render Bars - barWidth = svgEl.attr('width')/data.length - barHeight = (d) -> svgEl.attr('height')-Y(d) - - metricBars.selectAll "bar" - .data data - .enter().append "rect" - .attr "class", (d, i) -> "metric bar #i" - .attr "x", X - .attr "y", Y - .attr "height", barHeight - .attr "width", -> barWidth - .attr "fill", metric.get 'color' - .attr "stroke", "white" - .style "opacity", "0.4" - .style "z-index", -10 - - - # adding event listeners - chT = this - metricBars.selectAll ".metric.bar" - .on "mouseover", (d, i) -> - - svgEl.append "text" - .attr "class", "mf" - .attr "dx", 50 - .attr "dy", 100 - .style "font-size", "0px" - .transition() - .duration(800) - .text "Uh boy, the target would be: "+chT.numberFormatter(d[1]).toString() - .style "font-size", "25px" - .on "mouseout", (d, i) -> - - svgEl.selectAll ".mf" - .transition() - .duration(300) - .text "BUMMER!!!" - .style "font-size", "0px" - .remove() - - - - svgEl - - renderChart: (data, viewport, options, lastChart) -> - ### Starting with http://bost.ocks.org/mike/chart/ - - margin = {top: 20, right: 20, bottom: 20, left: 20} - width = 760 - height = 320 - xScale = d3.time.scale() - yScale = d3.scale.linear() - - dates = data[0] - cols = data.slice(1) - - # Calculate extents using all the data points (but not dates) - # allValues = d3.merge @model.dataset.getDataColumns() - allValues = d3.merge cols - - - # Update the x-scale with the extents of the dates. - xScale - .domain d3.extent dates - .range [ 0, width - margin.left - margin.right ] - - # Update the y-scale with the extents of the data. - yScale - .domain d3.extent allValues - .range [ height - margin.top - margin.bottom, 0 ] - - # Select the svg element, if it exists. - svg = d3.select viewport.0 .selectAll "svg" - .data [cols] - - # ...Otherwise, create the skeletal chart. - enterFrame = svg.enter() - .append "svg" .append "g" - .attr "class", "frame" - enterFrame.append "g" - .attr "class", "x axis time" - - # Update chart dimensions. - svg .attr "width", width - .attr "height", height - frame = svg.select "g.frame" - .attr "transform", "translate(#{margin.left},#{margin.top})" - - # Update the x-axis. - xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) - frame.select ".x.axis.time" - .attr "transform", "translate(0,#{yScale.range()[0]})" - .call xAxis - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - - ### Render Bars - barWidth = svg.attr('width')/dates.length - barHeight = (d) -> svg.attr('height')-Y(d) - - bars = frame.selectAll "g.bars" - .data cols.map -> d3.zip dates, it - bars.enter().append "g" - .attr "class", (col, i) -> "metric bars #i" - bars.exit().remove() - - bars.selectAll ".bar" - .data op.first - .enter().append "rect" - .attr "class", "bar" - .attr "x", X - .attr "y", Y - .attr "height", barHeight - .attr "width", -> barWidth - # TODO grab color from graph spec - .attr "fill", "red" - .attr "stroke", "white" - - - ### Mouse Lens - lens = root.lens = frame.selectAll "g.lens" - .data [[]] - gLens = lens.enter().append "g" - .attr "class", "lens" - .style "z-index", 1e9 - gInner = gLens.append "g" - .attr "transform", "translate(1.5em,0)" - gInner.append "circle" - .attr "r", "1.5em" - # .style "opacity", "0.4" - # .style "fill", "white" - .style "fill", "rgba(255, 255, 255, 0.4)" - .style "stroke", "white" - .style "stroke-width", "3px" - gInner.append "text" - .attr "y", "0.5em" - .attr "text-anchor", "middle" - .style "fill", "white" - .style "font", "12px Helvetica" - .style "font-weight", "bold" - - - mf = frame.selectAll "g.mf" - .data ["mf"] - .enter().append "g" - .attr "class", "mf" - .append "text" - .attr "class", "yoyo" - .attr "dx", 50 - .attr "dy", 100 - - - - bars.selectAll ".bar" - .on "mouseover", (d, i) -> - el = root.el = el # DOM element of event - # {r,g,b} = color = d3.rgb options.colors[i] - mf - .transition() - .duration(300) - .ease("exp") - .text "Uh boy, the target would be:"+d[1] - .style "font-size", "25px" - - - .on "mouseout", (d, i) -> - mf - .transition() - .duration(1000) - .text "BUMMER!!!" - .style "font-size", "0px" - - - - # {x:lineX, y:lineY} = root.pt = line.indexToPoint idx - # lens = frame.select "g.lens" - # .attr "transform", "translate(#lineX, #lineY)" - # lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" - # lens.select "text" .text Y - - - - svg diff --git a/lib/chart/type/d3/d3-bar-element.co b/lib/chart/type/d3/d3-bar-element.co new file mode 100644 index 0000000..dcb300a --- /dev/null +++ b/lib/chart/type/d3/d3-bar-element.co @@ -0,0 +1,78 @@ +d3 = require 'd3' + +{ _, op, +} = require 'kraken/util' +{ D3ChartElement +} = require 'kraken/chart/type/d3/d3-chart-element' + +_fmt = require 'kraken/util/formatters' + +root = do -> this + +class exports.BarChartType extends D3ChartElement + __bind__ : <[ ]> + SPEC_URL : '/schema/d3/d3-bar.json' + + # NOTE: D3ChartElement.register() must come AFTER `typeName` declaration. + chartElement : 'd3-bar' + D3ChartElement.register this + + -> super ... + + renderChartElement: (metric, svgEl ,xScale, yScale) -> + + X = (d, i) -> xScale d[0] + Y = (d, i) -> yScale d[1] + + + ### Render the line path + metricBars = root.metricBars = svgEl.append "g" + .attr "class", "metric bars "+metric.get 'label' + + data = d3.zip metric.getDateColumn(),metric.getData() + + ### Render Bars + barWidth = svgEl.attr('width')/data.length + barHeight = (d) -> svgEl.attr('height')-Y(d) + + metricBars.selectAll "bar" + .data data + .enter().append "rect" + .attr "class", (d, i) -> "metric bar #i" + .attr "x", X + .attr "y", Y + .attr "height", barHeight + .attr "width", -> barWidth + .attr "fill", metric.get 'color' + .attr "stroke", "white" + .style "opacity", "0.4" + .style "z-index", -10 + + + # adding event listeners + chT = this + metricBars.selectAll ".metric.bar" + .on "mouseover", (d, i) -> + + svgEl.append "text" + .attr "class", "mf" + .attr "dx", 50 + .attr "dy", 100 + .style "font-size", "0px" + .transition() + .duration(800) + .text "Uh boy, the target would be: "+_fmt.numberFormatter(d[1]).toString() + .style "font-size", "25px" + .on "mouseout", (d, i) -> + + svgEl.selectAll ".mf" + .transition() + .duration(300) + .text "BUMMER!!!" + .style "font-size", "0px" + .remove() + + + + svgEl + diff --git a/lib/chart/type/d3/d3-chart-element.co b/lib/chart/type/d3/d3-chart-element.co new file mode 100644 index 0000000..bc93ed1 --- /dev/null +++ b/lib/chart/type/d3/d3-chart-element.co @@ -0,0 +1,79 @@ +d3 = require 'd3' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ReadyEmitter, +} = require 'kraken/util/event' +# Base = require 'kraken/base/base' + + +root = do -> this + +/** + * Map of known libraries by name. + * @type Object + */ +KNOWN_CHART_ELEMENTS = exports.KNOWN_CHART_ELEMENTS = {} + +class exports.D3ChartElement extends ReadyEmitter + __bind__ : <[ ]> + SPEC_URL : '/schema/d3/d3-chart.json' + + + ### Class Methods + + /** + * Register a new d3 element + */ + @register = (Subclass) -> + console.log "D3ChartElement.register(#Subclass)" + KNOWN_CHART_ELEMENTS[ Subclass::chartElement ] = Subclass + + /** + * Look up a `charttype` by `typeName`. + */ + @lookup = (name) -> + name = name.get('chartElement') if name instanceof Backbone.Model + KNOWN_CHART_ELEMENTS[name] + + /** + * Look up a chart type by name, returning a new instance + * with the given model (and, optionally, view). + * @returns {D3ChartElement} + */ + @create = (name) -> + console.log "D3ChartElement.create(#name)" + return null unless Type = @lookup name + + new Type + + + () -> + _.bindAll this, ...@__bind__ # TODO: roll up MRO + @loadSpec() unless @ready + super ... + + + + /** + * Load the corresponding chart specification, which includes + * info about valid options, along with their types and defaults. + */ + loadSpec: -> + return this if @ready + proto = @constructor:: + jQuery.ajax do + url : @SPEC_URL + dataType : 'json' + success : (spec) ~> + proto.spec = spec + proto.options_ordered = spec + proto.options = _.synthesize spec, -> [it.name, it] + proto.ready = true + @triggerReady() + error: ~> console.error "Error loading #{@typeName} spec! #it" + this + + + diff --git a/lib/chart/type/d3/d3-chart-type.co b/lib/chart/type/d3/d3-chart-type.co deleted file mode 100644 index 8f73c54..0000000 --- a/lib/chart/type/d3/d3-chart-type.co +++ /dev/null @@ -1,121 +0,0 @@ -d3 = require 'd3' -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' - -root = do -> this - - -class exports.D3ChartType extends ChartType - __bind__ : <[ determineSize ]> - SPEC_URL : '/schema/d3/d3-chart.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-chart' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - # constructor: function D3ChartType - -> super ... - - - - getData: -> - @model.dataset.getColumns() - - - transform: -> - dataset = @model.dataset - options = @model.getOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - options - - - renderChart: (data, viewport, options, lastChart) -> - ### Starting with http://bost.ocks.org/mike/chart/ - - margin = {top: 20, right: 20, bottom: 20, left: 20} - width = 760 - height = 320 - xScale = d3.time.scale() - yScale = d3.scale.linear() - - dates = data[0] - cols = data.slice(1) - - # Calculate extents using all the data points (but not dates) - # allValues = d3.merge @model.dataset.getDataColumns() - allValues = d3.merge cols - - # Update the x-scale with the extents of the dates. - xScale - .domain d3.extent dates - .range [ 0, width - margin.left - margin.right ] - - # Update the y-scale with the extents of the data. - yScale - .domain d3.extent allValues - .range [ height - margin.top - margin.bottom, 0 ] - - # Select the svg element, if it exists. - svg = d3.select viewport.0 .selectAll "svg" - .data [cols] - - # ...Otherwise, create the skeletal chart. - enterFrame = svg.enter() - .append "svg" .append "g" - .attr "class", "frame" - - # Update chart dimensions. - svg .attr "width", width - .attr "height", height - - frame = svg.select "g.frame" - .attr "transform", "translate(#{margin.left},#{margin.top})" - .attr "width", width - margin.left - margin.right - .attr "height", height - margin.top - margin.bottom - - - # x-axis. - # TODO move axis to separate chart-type - enterFrame.append "g" - .attr "class", "x axis time" - - - xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) - frame.select ".x.axis.time" - .attr "transform", "translate(0,#{yScale.range()[0]})" - .call xAxis - - for i,metric in @model.dataset.metrics.models - # metric defined charttype - chartType = metric.get "chartType" - # otherwise the graph defined charttype - # FOR NOW take line as default - chartType or= 'd3-line' # @model.get "chartType" - - - # create d3 charttype and render it - model = ChartType.create chartType - model.renderChartType metric, frame ,xScale, yScale - - - svg - - - diff --git a/lib/chart/type/d3/d3-geo-chart-type.co b/lib/chart/type/d3/d3-geo-chart-type.co deleted file mode 100644 index 2238b31..0000000 --- a/lib/chart/type/d3/d3-geo-chart-type.co +++ /dev/null @@ -1,185 +0,0 @@ -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' - - - - -class exports.GeoWorldChartType extends ChartType - __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> - SPEC_URL : '/schema/d3/d3-geo-world.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-geo-world' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - - -> super ... - - - transform: -> - options = @model.getOptions() import @determineSize() - # options.colors.palette = ["black", "red"] if options.colors.palette? - options.colors.scaleDomain = d3.extent if options.colors.scaleDomain? - options - - - getProjection : (type) -> - switch type - case 'mercator' 'albers' 'albersUsa' - d3.geo[type]() - case 'azimuthalOrtho' - d3.geo.azimuthal() - .mode 'orthographic' - case 'azimuthalStereo' - d3.geo.azimuthal() - .mode 'stereographic' - default - throw new Error "Invalid map projection type '#type'!" - - - renderChart: (data, viewport, options, lastChart) -> - {width, height} = options - - fill = @fill = (data, options) -> - d3.scale[ options.colors.scale ]() - .domain options.colors.scaleDomain - .range options.colors.palette - - quantize = @quantize = (data, options) -> - (d) -> - if data[d.properties.name]? - return fill data[d.properties.name].editors - else - # console.log 'Country '+d.properties.name+' not in data' - return fill "rgb(0,0,0)" - - projection = @projection = @getProjection(options.map.projection) - .scale width - .translate [width/2, height/2] - - path = d3.geo.path() - .projection projection - - # path objects - feature = map.selectAll ".feature" - infobox = d3.select '.infobox' - - - move = -> - projection - .translate d3.event.translate - .scale d3.event.scale - feature.attr "d", path - - zoom = d3.behavior.zoom() - .translate projection.translate() - .scale projection.scale() - .scaleExtent [height,height*8] - .on "zoom", move - - - #### - - chart = d3.select viewport.0 - .append "svg:svg" - .attr "width", width - .attr "height", height - .append "svg:g" - .attr "transform", "translate(0,0)" - .call zoom - - # rectangle - map.append "svg:rect" - .attr "class", "frame" - .attr "width", width - .attr "height", height - - - ### infobox - infobox.select '#ball' - .append "svg:svg" - .attr "width", "100%" - .attr "height", "20px" - .append "svg:rect" - .attr "width", "60%" - .attr "height", "20px" - .attr "fill", '#f40500' - - setInfoBox = (d) -> - name = d.properties.name - ae = 0 - e5 = 0 - e100 = 0 - - if data[name]? - ae = parseInt data[name].editors - e5 = parseInt data[name].editors5 - e100 = parseInt data[name].editors100 - - infobox.select '#country' .text name - infobox.select '#ae' .text ae - infobox.select '#e5' .text e5+" ("+(100.0*e5/ae).toPrecision(3)+"%)" - infobox.select '#e100' .text e100+" ("+(100.0*e100/ae).toPrecision(3)+"%)" - - xy = d3.svg.mouse this - infobox.style "left", xy[0]+'px' - infobox.style "top", xy[1]+'px' - infobox.style "display", "block" - - - worldmap = -> - d3.json do - "/data/geo/maps/world-countries.json" - (json) -> - feature := feature - .data json.features - .enter().append "svg:path" - .attr "class", "feature" - .attr "d", path - .attr "fill", quantize - .attr "id", (d) -> d.properties.name - .on "mouseover", setInfoBox - .on "mouseout", -> infobox.style "display", "none" - - - - - -data = null -main = -> - jQuery.ajax do - url : "/data/geo/data/en_geo_editors.json" - dataType : 'json' - success : (res) -> - # result will be the returned JSON - data := res - - # delete & hide spinner - jQuery '.geo-spinner' .spin(false).hide() - - # load the world map - worldmap() - - # adding bootstrap tooltips - # $ '.page-header' .tooltip title:"for the header it works but is useless" - # $ '.feature' .tooltip title:"here it doesn't work" - - console.log 'Loaded geo coding map!' - error : (err) -> console.error err - - diff --git a/lib/chart/type/d3/d3-geo-element.co b/lib/chart/type/d3/d3-geo-element.co new file mode 100644 index 0000000..2238b31 --- /dev/null +++ b/lib/chart/type/d3/d3-geo-element.co @@ -0,0 +1,185 @@ +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ ChartType, +} = require 'kraken/chart/chart-type' + + + + +class exports.GeoWorldChartType extends ChartType + __bind__ : <[ dygNumberFormatter dygNumberFormatterHTML ]> + SPEC_URL : '/schema/d3/d3-geo-world.json' + + # NOTE: ChartType.register() must come AFTER `typeName` declaration. + typeName : 'd3-geo-world' + ChartType.register this + + + /** + * Hash of role-names to the selector which, when applied to the view, + * returns the correct element. + * @type Object + */ + roles : + viewport : '.viewport' + legend : '.graph-legend' + + + + -> super ... + + + transform: -> + options = @model.getOptions() import @determineSize() + # options.colors.palette = ["black", "red"] if options.colors.palette? + options.colors.scaleDomain = d3.extent if options.colors.scaleDomain? + options + + + getProjection : (type) -> + switch type + case 'mercator' 'albers' 'albersUsa' + d3.geo[type]() + case 'azimuthalOrtho' + d3.geo.azimuthal() + .mode 'orthographic' + case 'azimuthalStereo' + d3.geo.azimuthal() + .mode 'stereographic' + default + throw new Error "Invalid map projection type '#type'!" + + + renderChart: (data, viewport, options, lastChart) -> + {width, height} = options + + fill = @fill = (data, options) -> + d3.scale[ options.colors.scale ]() + .domain options.colors.scaleDomain + .range options.colors.palette + + quantize = @quantize = (data, options) -> + (d) -> + if data[d.properties.name]? + return fill data[d.properties.name].editors + else + # console.log 'Country '+d.properties.name+' not in data' + return fill "rgb(0,0,0)" + + projection = @projection = @getProjection(options.map.projection) + .scale width + .translate [width/2, height/2] + + path = d3.geo.path() + .projection projection + + # path objects + feature = map.selectAll ".feature" + infobox = d3.select '.infobox' + + + move = -> + projection + .translate d3.event.translate + .scale d3.event.scale + feature.attr "d", path + + zoom = d3.behavior.zoom() + .translate projection.translate() + .scale projection.scale() + .scaleExtent [height,height*8] + .on "zoom", move + + + #### + + chart = d3.select viewport.0 + .append "svg:svg" + .attr "width", width + .attr "height", height + .append "svg:g" + .attr "transform", "translate(0,0)" + .call zoom + + # rectangle + map.append "svg:rect" + .attr "class", "frame" + .attr "width", width + .attr "height", height + + + ### infobox + infobox.select '#ball' + .append "svg:svg" + .attr "width", "100%" + .attr "height", "20px" + .append "svg:rect" + .attr "width", "60%" + .attr "height", "20px" + .attr "fill", '#f40500' + + setInfoBox = (d) -> + name = d.properties.name + ae = 0 + e5 = 0 + e100 = 0 + + if data[name]? + ae = parseInt data[name].editors + e5 = parseInt data[name].editors5 + e100 = parseInt data[name].editors100 + + infobox.select '#country' .text name + infobox.select '#ae' .text ae + infobox.select '#e5' .text e5+" ("+(100.0*e5/ae).toPrecision(3)+"%)" + infobox.select '#e100' .text e100+" ("+(100.0*e100/ae).toPrecision(3)+"%)" + + xy = d3.svg.mouse this + infobox.style "left", xy[0]+'px' + infobox.style "top", xy[1]+'px' + infobox.style "display", "block" + + + worldmap = -> + d3.json do + "/data/geo/maps/world-countries.json" + (json) -> + feature := feature + .data json.features + .enter().append "svg:path" + .attr "class", "feature" + .attr "d", path + .attr "fill", quantize + .attr "id", (d) -> d.properties.name + .on "mouseover", setInfoBox + .on "mouseout", -> infobox.style "display", "none" + + + + + +data = null +main = -> + jQuery.ajax do + url : "/data/geo/data/en_geo_editors.json" + dataType : 'json' + success : (res) -> + # result will be the returned JSON + data := res + + # delete & hide spinner + jQuery '.geo-spinner' .spin(false).hide() + + # load the world map + worldmap() + + # adding bootstrap tooltips + # $ '.page-header' .tooltip title:"for the header it works but is useless" + # $ '.feature' .tooltip title:"here it doesn't work" + + console.log 'Loaded geo coding map!' + error : (err) -> console.error err + + diff --git a/lib/chart/type/d3/d3-line-chart-type.co b/lib/chart/type/d3/d3-line-chart-type.co deleted file mode 100644 index e0974f7..0000000 --- a/lib/chart/type/d3/d3-line-chart-type.co +++ /dev/null @@ -1,255 +0,0 @@ -d3 = require 'd3' -ColorBrewer = require 'colorbrewer' - -{ _, op, -} = require 'kraken/util' -{ ChartType, -} = require 'kraken/chart/chart-type' - -root = do -> this - - -class exports.LineChartType extends ChartType - __bind__ : <[ determineSize ]> - SPEC_URL : '/schema/d3/d3-line.json' - - # NOTE: ChartType.register() must come AFTER `typeName` declaration. - typeName : 'd3-line' - ChartType.register this - - - /** - * Hash of role-names to the selector which, when applied to the view, - * returns the correct element. - * @type Object - */ - roles : - viewport : '.viewport' - legend : '.graph-legend' - - - -> super ... - - - getData: -> - @model.dataset.getColumns() - - - transform: -> - dataset = @model.dataset - options = @model.getOptions() import @determineSize() - options import do - colors : dataset.getColors() - labels : dataset.getLabels() - options - - renderChartType: (metric, svgEl ,xScale, yScale) -> - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - line = d3.svg.line().x(X).y(Y) - - ### Render the line path - metricLine = root.metricLine = svgEl.append "g" - .attr "class", "g metric line "+metric.get 'label' - - data = d3.zip metric.getDateColumn(),metric.getData() - - metricLine.selectAll "path.line" - .data d3.zip data.slice(0,-1), data.slice(1) - .enter().append "path" - .attr "d", line - .attr "class", (d, i) -> "metric line segment #i" - .style "stroke", metric.get 'color' - - - ### Mouse Lens - lens = root.lens = svgEl.selectAll "g.lens" - .data [[]] - gLens = lens.enter().append "g" - .attr "class", "lens" - .style "z-index", 1e9 - gInner = gLens.append "g" - .attr "transform", "translate(1.5em,0)" - gInner.append "circle" - .attr "r", "1.5em" - # .style "opacity", "0.4" - # .style "fill", "white" - .style "fill", "rgba(255, 255, 255, 0.4)" - .style "stroke", "white" - .style "stroke-width", "3px" - gInner.append "text" - .attr "y", "0.5em" - .attr "text-anchor", "middle" - .style "fill", "black" - .style "font", "12px Helvetica" - .style "font-weight", "bold" - - # event listeners - chT = this - metricLine.selectAll ".line.segment" - .on "mouseover", (d, i) -> - - {r,g,b} = color = d3.rgb metric.get 'color' - lineX = (X(d[0])+X(d[1]))/2 - lineY = (Y(d[0])+Y(d[1]))/2 - - - lens = svgEl.select "g.lens" - .attr "transform", "translate(#lineX, #lineY)" - lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" - lens.select "text" .text -> chT.numberFormatter(d[0][1]).toString() - - - svgEl - - - renderChart: (data, viewport, options, lastChart) -> - ### Starting with http://bost.ocks.org/mike/chart/ - - margin = {top: 20, right: 20, bottom: 20, left: 20} - width = 760 - height = 320 - xScale = d3.time.scale() - yScale = d3.scale.linear() - - dates = data[0] - cols = data.slice(1) - - # Calculate extents using all the data points (but not dates) - # allValues = d3.merge @model.dataset.getDataColumns() - allValues = d3.merge cols - - - # Update the x-scale with the extents of the dates. - xScale - .domain d3.extent dates - .range [ 0, width - margin.left - margin.right ] - - # Update the y-scale with the extents of the data. - yScale - .domain d3.extent allValues - .range [ height - margin.top - margin.bottom, 0 ] - - # Select the svg element, if it exists. - svg = d3.select viewport.0 .selectAll "svg" - .data [cols] - - # ...Otherwise, create the skeletal chart. - enterFrame = svg.enter() - .append "svg" .append "g" - .attr "class", "frame" - enterFrame.append "g" - .attr "class", "x axis time" - - # Update chart dimensions. - svg .attr "width", width - .attr "height", height - frame = svg.select "g.frame" - .attr "transform", "translate(#{margin.left},#{margin.top})" - - # Update the x-axis. - xAxis = d3.svg.axis().scale(xScale).orient("bottom").tickSize(6, 0) - frame.select ".x.axis.time" - .attr "transform", "translate(0,#{yScale.range()[0]})" - .call xAxis - - - ### Render the line paths - lines = root.lines = frame.selectAll "path.line" - .data cols.map -> d3.zip dates, it - lines.enter().append "path" - .attr "class", "metric line" - lines.exit().remove() - - X = (d, i) -> xScale d[0] - Y = (d, i) -> yScale d[1] - line = d3.svg.line().x(X).y(Y) - lines.attr "d", line - .attr "class", (col, i) -> "metric line metric#i" - .style "stroke", (col, i) -> options.colors[i] - .each (col, i) -> - {width} = bbox = @getBBox() - # Add line-to-data position conversions - @indexAtX = d3.scale.quantize() - .domain [0, width] - .range d3.range col.length - @indexToPoint = (idx) -> - @pathSegList.getItem idx - - ### Render Points - points = frame.selectAll "g.points" - .data cols - points.enter().append "g" - .attr "class", (col, i) -> "points points#i" - .property "line", (col, i) -> $(this).parent().find('path.metric.line')[i] - points.exit().remove() - - points.selectAll ".point" - .data op.first - .enter().append "circle" - .attr "class", "point" - .attr "r", "2px" - .property "line", -> $ this .parentsUntil('svg', 'g')[0].line - .style "fill", -> $ this.line .css 'stroke' - .attr "transform", (d, i) -> - {x,y} = @line.indexToPoint i - "translate(#x, #y)" - - - ### Mouse Lens - lens = root.lens = frame.selectAll "g.lens" - .data [[]] - gLens = lens.enter().append "g" - .attr "class", "lens" - .style "z-index", 1e9 - gInner = gLens.append "g" - .attr "transform", "translate(1.5em,0)" - gInner.append "circle" - .attr "r", "1.5em" - # .style "opacity", "0.4" - # .style "fill", "white" - .style "fill", "rgba(255, 255, 255, 0.4)" - .style "stroke", "white" - .style "stroke-width", "3px" - gInner.append "text" - .attr "y", "0.5em" - .attr "text-anchor", "middle" - .style "fill", "white" - .style "font", "12px Helvetica" - .style "font-weight", "bold" - - lines.on "mouseover", (col, i) -> - line = root.line = this # DOM element of event - {r,g,b} = color = d3.rgb options.colors[i] - - # quantize mouse x-location to get for closest data-point (index into data array) - [x,y] = root.pos = d3.mouse line - idx = root.idx = line.indexAtX x - {x:lineX, y:lineY} = root.pt = line.indexToPoint idx - - lens = frame.select "g.lens" - .attr "transform", "translate(#lineX, #lineY)" - lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" - lens.select "text" .text -> col[idx][1] - - - points.on "mouseover", (d, i) -> - line = root.line = this.line # this is the DOM element of point - {r,g,b} = color = d3.rgb options.colors[i] - - # quantize mouse x-location to get for closest data-point (index into data array) - [x,y] = root.pos = d3.mouse line - idx = root.idx = line.indexAtX x - {x:lineX, y:lineY} = root.pt = line.indexToPoint idx - - lens = frame.select "g.lens" - .attr "transform", "translate(#lineX, #lineY)" - lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" - lens.select "text" .text -> d[idx] - - - svg - - - diff --git a/lib/chart/type/d3/d3-line-element.co b/lib/chart/type/d3/d3-line-element.co new file mode 100644 index 0000000..aad93d9 --- /dev/null +++ b/lib/chart/type/d3/d3-line-element.co @@ -0,0 +1,84 @@ +d3 = require 'd3' +ColorBrewer = require 'colorbrewer' + +{ _, op, +} = require 'kraken/util' +{ D3ChartElement +} = require 'kraken/chart/type/d3/d3-chart-element' + +_fmt = require 'kraken/util/formatters' + +root = do -> this + +class exports.LineChartElement extends D3ChartElement + __bind__ : <[ ]> + SPEC_URL : '/schema/d3/d3-line.json' + + # NOTE: D3ChartElement.register() must come AFTER `typeName` declaration. + chartElement : 'd3-line' + D3ChartElement.register this + + -> super ... + + renderChartElement: (metric, svgEl ,xScale, yScale) -> + + X = (d, i) -> xScale d[0] + Y = (d, i) -> yScale d[1] + line = d3.svg.line().x(X).y(Y) + + ### Render the line path + metricLine = root.metricLine = svgEl.append "g" + .attr "class", "g metric line "+metric.get 'label' + + data = d3.zip metric.getDateColumn(),metric.getData() + + metricLine.selectAll "path.line" + .data d3.zip data.slice(0,-1), data.slice(1) + .enter().append "path" + .attr "d", line + .attr "class", (d, i) -> "metric line segment #i" + .style "stroke", metric.get 'color' + + + ### Mouse Lens + lens = root.lens = svgEl.selectAll "g.lens" + .data [[]] + gLens = lens.enter().append "g" + .attr "class", "lens" + .style "z-index", 1e9 + gInner = gLens.append "g" + .attr "transform", "translate(1.5em,0)" + gInner.append "circle" + .attr "r", "1.5em" + # .style "opacity", "0.4" + # .style "fill", "white" + .style "fill", "rgba(255, 255, 255, 0.4)" + .style "stroke", "white" + .style "stroke-width", "3px" + gInner.append "text" + .attr "y", "0.5em" + .attr "text-anchor", "middle" + .style "fill", "black" + .style "font", "12px Helvetica" + .style "font-weight", "bold" + + # event listeners + metricLine.selectAll ".line.segment" + .on "mouseover", (d, i) -> + + {r,g,b} = color = d3.rgb metric.get 'color' + lineX = (X(d[0])+X(d[1]))/2 + lineY = (Y(d[0])+Y(d[1]))/2 + + + lens = svgEl.select "g.lens" + .attr "transform", "translate(#lineX, #lineY)" + lens.select "circle" .style "fill", "rgba(#r, #g, #b, 0.4)" + lens.select "text" .text -> _fmt.numberFormatter(d[0][1]).toString() + + svgEl + + + + + diff --git a/lib/chart/type/d3/index.co b/lib/chart/type/d3/index.co index 3baf40d..a18badd 100644 --- a/lib/chart/type/d3/index.co +++ b/lib/chart/type/d3/index.co @@ -1,6 +1,6 @@ -d3chart = require 'kraken/chart/type/d3/d3-chart-type' -line = require 'kraken/chart/type/d3/d3-line-chart-type' -bar = require 'kraken/chart/type/d3/d3-bar-chart-type' -# geo = require 'kraken/chart/type/d3/d3-geo-chart-type' +d3chart = require 'kraken/chart/type/d3/d3-chart-element' +line = require 'kraken/chart/type/d3/d3-line-element' +bar = require 'kraken/chart/type/d3/d3-bar-element' +# geo = require 'kraken/chart/type/d3/d3-geo-element' exports import line import bar import d3chart # import geo diff --git a/lib/util/formatters.co b/lib/util/formatters.co index b30650c..f2c3b95 100644 --- a/lib/util/formatters.co +++ b/lib/util/formatters.co @@ -30,10 +30,17 @@ _fmt = do * * @param {Number} n Number to format. * @param {Number} [digits=2] Number of digits after the decimal to always display. - * @returns {String} Formatted number. + * @param {Boolean} [abbrev=true] Expand number suffixes if false. + * @returns {Object} Formatted number parts. */ - _numberFormatter: (n, digits=2) -> - for [suffix, d] of [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + numberFormatter: (n, digits=2, abbrev=true) -> + suffixes = do + if abbrev + [['B', 1000000000], ['M', 1000000], ['K', 1000], ['', NaN]] + else + [['Billion', 1000000000], ['Million', 1000000], ['', NaN]] + + for [suffix, d] of suffixes break if isNaN d if n >= d n = n / d @@ -42,12 +49,9 @@ _fmt = do parts = s.split '.' whole = _.rchop parts[0], 3 .join ',' fraction = '.' + parts.slice(1).join '.' - { n, digits, whole, fraction, suffix } - - - numberFormatter: (n, digits=2) -> - { whole, fraction, suffix } = _fmt._numberFormatter n, digits - "#whole#fraction#suffix" + { n, digits, whole, fraction, suffix, toString: -> + "#{@whole}#{@fraction}#{if abbrev then '' else ' '}#{@suffix}" + } numberFormatterHTML: (n, digits=2) -> { whole, fraction, suffix } = _fmt._numberFormatter n, digits @@ -61,4 +65,4 @@ _fmt = do -exports = _fmt +module.exports = exports = _fmt diff --git a/www/modules.yaml b/www/modules.yaml index 64c93c2..0f884a3 100644 --- a/www/modules.yaml +++ b/www/modules.yaml @@ -80,10 +80,11 @@ dev: - timeseries - csv - index - - op + - op - backbone - parser - cascade + - formatters - index - base: - scaffold: @@ -112,11 +113,12 @@ dev: - index - type: - d3: - - d3-chart-type - - d3-line-chart-type - - d3-bar-chart-type + - d3-chart-element + - d3-line-element + - d3-bar-element # - d3-geo-chart-type - index + - d3-chart - dygraphs - index - chart-type -- 1.7.0.4