From a7371afc9c9838b0c7372142093cecb35cff8daa Mon Sep 17 00:00:00 2001 From: dsc Date: Mon, 7 May 2012 12:52:35 -0700 Subject: [PATCH] DataSet UI now wired up properly, woo. --- data/graphs/ohai.json | 119 +-------------------------------------- lib/dataset/data-view.co | 8 ++- lib/dataset/dataset-model.co | 61 +++++++++++++++++--- lib/dataset/dataset-view.co | 28 ++++----- lib/dataset/metric-edit-view.co | 2 + lib/dataset/metric-model.co | 9 ++- lib/graph/graph-edit-view.co | 18 +++--- lib/graph/graph-model.co | 30 ++++++---- lib/template/metric-edit.jade | 2 +- 9 files changed, 108 insertions(+), 169 deletions(-) diff --git a/data/graphs/ohai.json b/data/graphs/ohai.json index c15f14c..f27e9dc 100644 --- a/data/graphs/ohai.json +++ b/data/graphs/ohai.json @@ -1,118 +1 @@ -{ - "id": "ohai", - "slug": "ohai", - "name": "ohai~", - "desc": "A graph for the testing of great justice.", - "notes": "", - "width": "auto", - "height": 250, - "parents": ["root"], - "dataset": "/data/datasources/rc/rc_page_requests.csv", - "data": { - "metrics": [{ - "source_id": "rc_active_editors_count", - "source_col": 1, - "label": "Total Active Editors", - "color": "#E62F74" - }, { - "source_id": "rc_very_active_editors_count", - "source_col": 1, - "label": "Total Very Active Editors", - "color": "#244792" - }, { - "source_id": "rc_edits_count", - "source_col": 1, - "label": "Total Edits", - "color": "#FF6458" - }] - }, - "chartType": "dygraphs", - "options": { - "animatedZooms": true, - "avoidMinZero": false, - "axis": null, - "axisLabelColor": "#666666", - "axisLabelFontSize": 11, - "axisLabelFormatter": null, - "axisLabelWidth": 50, - "axisLineColor": "#AAAAAA", - "axisLineWidth": 0.3, - "axisTickSize": 3, - "colorSaturation": 1, - "colorValue": 0.5, - "colors": ["#FF0097", "#EF8158", "#83BB32", "#182B53", "#4596FF", "#553DC9", "#AD3238", "#00FFBC", "#F1D950"], - "connectSeparatedPoints": false, - "customBars": false, - "dateWindow": null, - "delimiter": ",", - "digitsAfterDecimal": 2, - "displayAnnotations": false, - "drawPoints": true, - "drawXAxis": true, - "drawXGrid": true, - "drawYAxis": true, - "drawYGrid": true, - "errorBars": false, - "file": null, - "fillAlpha": 0.15, - "fillGraph": false, - "fractions": false, - "gridLineColor": "#D8D8D8", - "gridLineWidth": 0.3, - "hideOverlayOnMouseOut": false, - "highlightCircleSize": 4, - "includeZero": false, - "interactionModel": null, - "isZoomedIgnoreProgrammaticZoom": false, - "labels": null, - "labelsDiv": null, - "labelsDivStyles": null, - "labelsDivWidth": 250, - "labelsKMB": true, - "labelsKMG2": false, - "labelsSeparateLines": true, - "labelsShowZeroValues": true, - "legend": "always", - "logscale": true, - "maxNumberWidth": 30, - "panEdgeFraction": null, - "pixelsPerLabel": null, - "pixelsPerXLabel": null, - "pixelsPerYLabel": null, - "pointSize": 1, - "rangeSelectorHeight": 40, - "rangeSelectorPlotFillColor": "#A7B1C4", - "rangeSelectorPlotStrokeColor": "#808FAB", - "rightGap": 20, - "rollPeriod": 1, - "showLabelsOnHighlight": true, - "showRangeSelector": false, - "showRoller": false, - "sigFigs": null, - "sigma": 2, - "stackedGraph": false, - "stepPlot": false, - "strokePattern": null, - "strokeWidth": 4, - "ticker": null, - "title": null, - "titleHeight": 18, - "valueFormatter": null, - "valueRange": null, - "visibility": null, - "wilsonInterval": true, - "xAxisHeight": null, - "xAxisLabelFormatter": null, - "xAxisLabelWidth": 55, - "xLabelHeight": 18, - "xValueFormatter": null, - "xValueParser": null, - "xlabel": null, - "y2label": null, - "yAxisLabelFormatter": null, - "yAxisLabelWidth": 50, - "yLabelWidth": 18, - "yValueFormatter": null, - "ylabel": null - } -} +{"options":{"animatedZooms":true,"avoidMinZero":false,"axis":null,"axisLabelColor":"#666666","axisLabelFontSize":11,"axisLabelFormatter":null,"axisLabelWidth":50,"axisLineColor":"#AAAAAA","axisLineWidth":0.3,"axisTickSize":3,"colorSaturation":1,"colorValue":0.5,"colors":["#FF0097","#EF8158","#83BB32","#182B53","#4596FF","#553DC9","#AD3238","#00FFBC","#F1D950"],"connectSeparatedPoints":false,"customBars":false,"dateWindow":null,"delimiter":",","digitsAfterDecimal":2,"displayAnnotations":false,"drawPoints":true,"drawXAxis":true,"drawXGrid":true,"drawYAxis":true,"drawYGrid":true,"errorBars":false,"file":null,"fillAlpha":0.15,"fillGraph":false,"fractions":false,"gridLineColor":"#D8D8D8","gridLineWidth":0.3,"hideOverlayOnMouseOut":false,"highlightCircleSize":4,"includeZero":false,"interactionModel":null,"isZoomedIgnoreProgrammaticZoom":false,"labels":null,"labelsDiv":null,"labelsDivStyles":null,"labelsDivWidth":250,"labelsKMB":true,"labelsKMG2":false,"labelsSeparateLines":true,"labelsShowZeroValues":true,"legend":"always","logscale":true,"maxNumberWidth":30,"panEdgeFraction":null,"pixelsPerLabel":null,"pixelsPerXLabel":null,"pixelsPerYLabel":null,"pointSize":1,"rangeSelectorHeight":40,"rangeSelectorPlotFillColor":"#A7B1C4","rangeSelectorPlotStrokeColor":"#808FAB","rightGap":20,"rollPeriod":1,"showLabelsOnHighlight":true,"showRangeSelector":false,"showRoller":false,"sigFigs":null,"sigma":2,"stackedGraph":false,"stepPlot":false,"strokePattern":null,"strokeWidth":4,"ticker":null,"title":null,"titleHeight":18,"valueFormatter":null,"valueRange":null,"visibility":null,"wilsonInterval":true,"xAxisHeight":null,"xAxisLabelFormatter":null,"xAxisLabelWidth":55,"xLabelHeight":18,"xValueFormatter":null,"xValueParser":null,"xlabel":null,"y2label":null,"yAxisLabelFormatter":null,"yAxisLabelWidth":50,"yLabelWidth":18,"yValueFormatter":null,"ylabel":null},"slug":"ohai","name":"ohai~","desc":"A graph for the testing of great justice.","notes":"","width":"auto","height":250,"chartType":"dygraphs","parents":["root"],"id":"ohai","data":{"palette":null,"lines":[],"id":"ohai","metrics":[{"index":0,"label":"Total Active Editors","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_active_editors_count","source_col":1,"color":"#E62F74","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1},{"index":0,"label":"Total Very Active Editors","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_very_active_editors_count","source_col":1,"color":"#244792","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1},{"index":0,"label":"","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_new_article_count","source_col":1,"color":"#FF6458","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1,"source-url":"/data/datasources/rc/rc_very_active_editors_count.csv"},{"index":3,"label":"New Editors","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_new_editors_count","source_col":1,"color":"rgb(253,174,97)","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1,"source-url":"/data/datasources/rc/rc_very_active_editors_count.csv"},{"index":4,"label":"","type":"int","timespan":{"start":null,"end":null,"step":null},"disabled":false,"source_id":"rc_binary_files","source_col":10,"color":"rgb(254,224,139)","visible":true,"format_value":null,"format_axis":null,"transforms":[],"scale":1}]},"dataset":"/data/datasources/rc/rc_page_requests.csv"} \ No newline at end of file diff --git a/lib/dataset/data-view.co b/lib/dataset/data-view.co index d36cc61..b947e9a 100644 --- a/lib/dataset/data-view.co +++ b/lib/dataset/data-view.co @@ -47,7 +47,7 @@ DataView = exports.DataView = BaseView.extend do # {{{ @addSubview @dataset_view .on 'add-metric', @onMetricsChanged, this .on 'remove-metric', @onMetricsChanged, this - .on 'edit-metric', @editMetric, this + .on 'select-metric', @selectMetric, this @render() @triggerReady() @@ -87,6 +87,7 @@ DataView = exports.DataView = BaseView.extend do # {{{ .on 'metric-update', @onUpdateMetric, this .on 'metric-change', @onUpdateMetric, this @metric_views.push @addSubview view + @renderSubviews() metric removeMetric: (metric) -> @@ -96,14 +97,15 @@ DataView = exports.DataView = BaseView.extend do # {{{ @removeSubview view metric - editMetric: (metric) -> - console.log "#this.editMetric!", metric + selectMetric: (metric) -> + # console.log "#this.selectMetric!", metric @metric_views.invoke 'hide' @metric_edit_view = @metric_views.findByModel metric @metric_edit_view?.show() _.delay @onMetricsChanged, 10 onMetricsChanged: -> + return unless @dataset_view oldMinHeight = parseInt @$el.css 'min-height' newMinHeight = Math.max do @dataset_view.$el.height() diff --git a/lib/dataset/dataset-model.co b/lib/dataset/dataset-model.co index 5d0fa33..44e3a4c 100644 --- a/lib/dataset/dataset-model.co +++ b/lib/dataset/dataset-model.co @@ -34,16 +34,16 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ metrics : [] - constructor: function DataSet - BaseModel ... + constructor: function DataSet (attributes={}, opts) + @metrics = new MetricList attributes.metrics + BaseModel.call this, attributes, opts initialize : -> BaseModel::initialize ... - @metrics = new MetricList @attributes.metrics + @set 'metrics', @metrics, {+silent} @on 'change:metrics', @onMetricChange, this - load: (opts={}) -> @resetReady() if opts.force return this if @loading or @ready @@ -67,6 +67,34 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ @triggerReady() this + # refreshSubModels: -> + # # @set 'metrics', @metrics.toJSON(), {+silent} + # @set 'metrics', _.pluck(@metrics.models, 'attributes'), {+silent} + # this + + /** + * Override to handle the case where one of our rich sub-objects is attempted + * to be overridden with a native object. + */ + set: (key, value, opts) -> + # return DataSet.__super__.set ... unless @metrics + + if _.isObject(key) and key? + [values, opts] = [key, value] + else + values = { "#key": value } + opts or= {} + + for key, value in values + continue unless key is 'metrics' and _.isArray value + @metrics.reset value + delete values[key] + unless opts.silent + DataSet.__super__.set.call this, 'metrics', value, {+silent} + DataSet.__super__.set.call this, 'metrics', @metrics, opts + + DataSet.__super__.set.call this, values, opts + /* * * * TimeSeriesData interface * * * {{{ */ @@ -74,18 +102,21 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ * @returns {Array} The reified dataset, materialized to a list of rows including timestamps. */ getData: -> + return [] unless @ready _.zip ...@getColumns() /** * @returns {Array} List of all columns (including date column). */ getColumns: -> - [ @getDateColumn() ].concat @getDataColumns() + return [] unless @ready + _.compact [ @getDateColumn() ].concat @getDataColumns() /** * @returns {Array} The date column. */ getDateColumn: -> + return [] unless @ready dates = @metrics.onlyOk().invoke 'getDateColumn' maxLen = _.max _.pluck dates, 'length' _.find dates, -> it.length is maxLen @@ -94,33 +125,45 @@ DataSet = exports.DataSet = BaseModel.extend do # {{{ * @returns {Array} List of all columns except the date column. */ getDataColumns: -> + return [] unless @ready @metrics.onlyOk().invoke 'getData' /** * @returns {Array} List of column labels. */ getLabels: -> + return [] unless @ready [ 'Date' ].concat @metrics.onlyOk().invoke 'getLabel' - # }}} - getColors: -> + return [] unless @ready @metrics.onlyOk().pluck 'color' + # }}} + + newMetric: -> index = @metrics.length @metrics.add m = new Metric { index, color:ColorBrewer.Spectral[11][index] } + # @get 'metrics' .push m.attributes + # @trigger 'change:metrics', this, @metrics, 'metrics' + # @trigger 'change', this, @metrics, 'metrics' m onMetricChange: -> console.log "#this.onMetricChange! ready=#{@ready}" @resetReady() - @metrics.reset @get 'metrics' @load() - # TODO: toJSON() must ensure columns in MetricList are ordered by index + + # XXX: toJSON() must ensure columns in MetricList are ordered by index # ...in theory, MetricList.comparator now does this + # toJSON: -> + # @refreshSubModels() + # json = DataSet.__super__.toJSON ... + # json.metrics = json.metrics.map -> it.toJSON?() or it + # json # }}} diff --git a/lib/dataset/dataset-view.co b/lib/dataset/dataset-view.co index e19efcb..34d7ff7 100644 --- a/lib/dataset/dataset-view.co +++ b/lib/dataset/dataset-view.co @@ -14,7 +14,7 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ events: 'click .new-metric-button' : 'newMetric' - 'click .metrics .dataset-metric' : 'editMetric' + 'click .metrics .dataset-metric' : 'selectMetric' views_by_cid : {} active_view : null @@ -43,25 +43,24 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ addMetric: (metric) -> console.log "#this.addMetric!", metric - if metric.view - @removeSubview metric.view.remove() + if @views_by_cid[metric.cid] + @removeSubview that delete @views_by_cid[metric.cid] view = @addSubview new DataSetMetricView {model:metric, @graph_id} @views_by_cid[metric.cid] = view - @$ '.metrics' .append view.render().el - - # @render() + # @$ '.metrics' .append view.render().el @trigger 'add-metric', metric, view, this + @render() view removeMetric: (metric) -> console.log "#this.removeMetric!", metric - if view = metric.view - @removeSubview view.remove() + if view = @views_by_cid[metric.cid] + @removeSubview view delete @views_by_cid[metric.cid] @trigger 'remove-metric', metric, view, this - metric.view + view addAllMetrics: -> console.log "#this.addAllMetrics! --> #{@model.metrics}" @@ -69,18 +68,17 @@ DataSetView = exports.DataSetView = BaseView.extend do # {{{ @model.metrics.each @addMetric, this this - editMetric: (metric) -> - # console.log "#this.editMetric!", metric + selectMetric: (metric) -> if metric instanceof [jQuery.Event, Event] metric = $ metric.currentTarget .data 'model' view = @active_view = @views_by_cid[metric.cid] - console.log "#this.editMetric!", metric + # console.log "#this.selectMetric!", metric @$ '.metrics .dataset-metric' .removeClass 'metric-active' view.$el.addClass 'metric-active' view.$el.find '.activity-arrow' .css 'font-size', 2+view.$el.height() - @trigger 'edit-metric', metric, view, this + @trigger 'select-metric', metric, view, this this @@ -120,8 +118,8 @@ DataSetMetricView = exports.DataSetMetricView = BaseView.extend do # {{{ 'disabled' if m.disabled, ]).map( -> "metric-#it" ).join ' ' source : - if m.source_id and m.source_col_name - "#{m.source_id}.#{m.source_col_name}" + if m.source_id and m.source_col + "#{m.source_id}[#{m.source_col}]" else 'No source' timespan : diff --git a/lib/dataset/metric-edit-view.co b/lib/dataset/metric-edit-view.co index f0d787c..4b5be60 100644 --- a/lib/dataset/metric-edit-view.co +++ b/lib/dataset/metric-edit-view.co @@ -52,6 +52,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ update: -> MetricEditView.__super__.update ... + @$ '.metric-label' .attr 'placeholder', @model.getPlaceholderLabel() # Update the color picker @$ '.color-swatch' @@ -85,6 +86,7 @@ MetricEditView = exports.MetricEditView = BaseView.extend do # {{{ onSourceMetricChange: (metric) -> console.log "#this.onSourceMetricChange!", metric + @$ '.metric-label' .attr 'placeholder', @model.getPlaceholderLabel() @trigger 'metric-change', @model, this this diff --git a/lib/dataset/metric-model.co b/lib/dataset/metric-model.co index e3b6ce6..7083738 100644 --- a/lib/dataset/metric-model.co +++ b/lib/dataset/metric-model.co @@ -64,7 +64,7 @@ Metric = exports.Metric = BaseModel.extend do # {{{ getPlaceholderLabel: -> col = @get 'source_col' - name = "#{@source.get 'shortName'}, #{@source.getColumnName col}" if @source and col > 0 + name = "#{@source.get 'shortName'}, #{@source.getColumnName col}" if @source and col >= 0 name or @NEW_METRIC_LABEL getSourceColumnName: -> @@ -73,9 +73,12 @@ Metric = exports.Metric = BaseModel.extend do # {{{ load: (opts={}) -> - source_id = @get 'source_id' + source_id = @get 'source_id' @resetReady() if opts.force or @source?.id is not source_id - return this if not source_id or @loading or @ready + return this if @loading or @ready + + unless source_id and @get('source_col') >= 0 + return @triggerReady() console.log "#this.load()..." @updateId() diff --git a/lib/graph/graph-edit-view.co b/lib/graph/graph-edit-view.co index 54656cf..eec0146 100644 --- a/lib/graph/graph-edit-view.co +++ b/lib/graph/graph-edit-view.co @@ -104,7 +104,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ ### Graph Data UI - @data_view = @addSubview new DataView { model:@model.get('data'), graph_id:@id } + @data_view = @addSubview new DataView { model:@model.dataset, graph_id:@id } @data_view .on 'start-waiting', @wait, this .on 'stop-waiting', @unwait, this @@ -275,7 +275,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ valueFormatter : @numberFormatterHTML # console.log "#this.render!", dataset - _.dump options, 'options' + # _.dump options, 'options' # Always rerender the chart to sidestep the case where we need to push defaults into # dygraphs to reset the current option state. @@ -308,7 +308,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ # @renderDetails() # @attachSubviews() # _.invoke @subviews, 'render' - BaseView::render ... + GraphEditView.__super__.render ... @renderChart() # @updateURL() @trigger 'render', this @@ -321,11 +321,11 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ * Update the page URL using HTML5 History API */ updateURL: -> - data = @toJSON() + json = @toJSON() title = "#{@model.get('name') or 'New Graph'} | Edit Graph | GraphKit" url = @toURL('edit') - # console.log 'History.pushState', JSON.stringify(data), title, url - History.pushState data, title, url + # console.log 'History.pushState', JSON.stringify(json), title, url + History.pushState json, title, url ### }}} @@ -397,7 +397,7 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ # \toptions: #{JSON.stringify options} # \t^opts: #{JSON.stringify _.intersection _.keys(changes), _.keys(options)} # """ - @chart?.updateOptions file:that if changes?.dataset + # @chart?.updateOptions file:that if changes?.dataset @chartOptions options, {+silent} if changes?.options onScaffoldChange: (scaffold, value, key, field) -> @@ -435,8 +435,8 @@ GraphEditView = exports.GraphEditView = BaseView.extend do # {{{ onDetailsSubmit: -> console.log "#this.onDetailsSubmit!" - data = @$ 'form.graph-details' .formData() - @model.set data + details = @$ 'form.graph-details' .formData() + @model.set details false onOptionsSubmit: -> diff --git a/lib/graph/graph-model.co b/lib/graph/graph-model.co index 8790325..2d231f5 100644 --- a/lib/graph/graph-model.co +++ b/lib/graph/graph-model.co @@ -98,6 +98,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ # Insert submodels in place of JSON @dataset = new DataSet {id:@id, ...@get 'data'} + # .on 'change', @onDataSetChange, this @set 'data', @dataset, {+silent} @trigger 'init', this @@ -121,9 +122,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{ next.ok() success : @unwaitAnd (model, res) ~> # console.log "#{this}.fetch() --> success!", res + # Update the DataSet model with the new values @dataset.set @get 'data' - @trigger 'change:data', this, @dataset, 'data' - @trigger 'change', this, @dataset, 'data' + @set 'data', @dataset, {+silent} next.ok res # Load Parents... @@ -166,9 +167,11 @@ Graph = exports.Graph = BaseModel.extend do # {{{ Seq @dataset.metrics.models .parEach_ (next, metric) -> metric.once 'ready', next.ok .load() - .parEach_ (next, metric) -> - metric.source - .on 'load-data-success', next.ok .loadData() + .parEach_ (next, metric) ~> + unless metric.source + console.warn "#{this}.loadData() -- Skipping metric #metric with invalid source!", metric + return next.ok() + metric.source.on 'load-data-success', next.ok .loadData() .seq ~> console.log "#{this}.loadData() complete!" @loading = false @@ -176,6 +179,9 @@ Graph = exports.Graph = BaseModel.extend do # {{{ @triggerReady 'dataReady', 'data-ready' this + onDataSetChange: -> + console.log "#this.onDataSetChange!" + @set 'data', @dataset, {+silent} ### Accessors @@ -184,7 +190,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ if _.startsWith key, 'options.' @getOption key.slice(8) else - (@..__super__ or BaseModel::).get.call this, key + Graph.__super__.get.call this, key set: (key, value, opts) -> @@ -195,7 +201,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ values = { "#key": value } values = @parse values - setter = (@..__super__ or BaseModel::).set + setter = Graph.__super__.set # Merge options in, firing granulated change events if values.options @@ -215,7 +221,7 @@ Graph = exports.Graph = BaseModel.extend do # {{{ - ### Chart Option Accessors ### + ### Chart Option Accessors {{{ hasOption: (key) -> @getOption(key) is void @@ -266,8 +272,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{ options - - ### Serialization + # }}} + ### Serialization {{{ parse: (data) -> data = JSON.parse data if typeof data is 'string' @@ -300,8 +306,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{ toJSON: (opts={}) -> opts = {+keepDefaults, +keepUnchanged} import opts # use jQuery's deep-copy implementation -- XXX: Deep-copy no longer necessary thanks to @getOptions() - # json = $.extend true, {}, @attributes json = _.clone(@attributes) import { options:@getOptions(opts) } + # { data: ...json } toKVPairs: (opts={}) -> @@ -346,6 +352,8 @@ Graph = exports.Graph = BaseModel.extend do # {{{ toPermalink: -> "#{root.location.protocol}//#{window.location.host}#{@toLink()}" + # }}} + ### Graph Cache for parent-lookup new ModelCache Graph diff --git a/lib/template/metric-edit.jade b/lib/template/metric-edit.jade index 8b2a9d3..a95e2a7 100644 --- a/lib/template/metric-edit.jade +++ b/lib/template/metric-edit.jade @@ -7,7 +7,7 @@ section.metric-edit-ui input.metric-color(type='hidden', id="#{graph_id}_metric", name="color", value=color) .color-swatch.input-append.color(data-color="#{color}", data-color-format="hex") input.metric-color(type='hidden', id="#{graph_id}_metric_color", name="color", value=color) - span.add-on: i(style="background-color: #{color};") + span.add-on: i(style="background-color: #{color};", title="#{color}") input.metric-label(type='text', id="#{graph_id}_metric_label", name='label', placeholder='#{placeholder_label}', value=label) -- 1.7.0.4