From bc7dddf031537bc5a658573c63f7bc9d73e633cb Mon Sep 17 00:00:00 2001 From: Stas Vilchik Date: Fri, 4 Jul 2014 17:06:25 +0600 Subject: [PATCH] SONAR-5207 Treemap as a metric widget skeleton --- .../org/sonar/plugins/core/CorePlugin.java | 2 +- .../dashboards/GlobalDefaultDashboard.java | 8 +- .../plugins/core/widgets/TreemapWidget.java | 2 +- .../MeasureFilterAsTreemapWidget.java | 55 +++++++++++ .../measures/MeasureFilterTreemapWidget.java | 50 ---------- .../widgets/measure_filter_treemap.html.erb | 85 +++++++++------- .../GlobalDefaultDashboardTest.java | 4 +- sonar-server/Gruntfile.coffee | 2 + .../src/main/coffee/widgets/base.coffee | 12 ++- .../src/main/coffee/widgets/treemap.coffee | 96 +++++++++++++++++++ .../src/main/coffee/widgets/word-cloud.coffee | 11 +-- sonar-server/src/main/less/style.less | 42 ++++++++ 12 files changed, 266 insertions(+), 103 deletions(-) create mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterAsTreemapWidget.java delete mode 100644 plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterTreemapWidget.java create mode 100644 sonar-server/src/main/coffee/widgets/treemap.coffee diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java index 22e37e6bf34..484ff746d68 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/CorePlugin.java @@ -216,7 +216,7 @@ public final class CorePlugin extends SonarPlugin { HotspotMetricWidget.class, TreemapWidget.class, MeasureFilterListWidget.class, - MeasureFilterTreemapWidget.class, + MeasureFilterAsTreemapWidget.class, WelcomeWidget.class, DocumentationCommentsWidget.class, DuplicationsWidget.class, diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java index f7933cfbbf4..5e204b18537 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboard.java @@ -27,8 +27,8 @@ import org.sonar.core.measure.db.MeasureFilterDto; import org.sonar.plugins.core.measurefilters.MyFavouritesFilter; import org.sonar.plugins.core.measurefilters.ProjectFilter; import org.sonar.plugins.core.widgets.WelcomeWidget; +import org.sonar.plugins.core.widgets.measures.MeasureFilterAsTreemapWidget; import org.sonar.plugins.core.widgets.measures.MeasureFilterListWidget; -import org.sonar.plugins.core.widgets.measures.MeasureFilterTreemapWidget; /** * Projects global dashboard for Sonar @@ -75,10 +75,10 @@ public final class GlobalDefaultDashboard extends DashboardTemplate { .setProperty(MeasureFilterListWidget.PAGE_SIZE_PROPERTY, "20"); dashboard - .addWidget(MeasureFilterTreemapWidget.ID, 2) + .addWidget(MeasureFilterAsTreemapWidget.ID, 2) .setProperty(MeasureFilterListWidget.FILTER_PROPERTY, filter.getId().toString()) - .setProperty(MeasureFilterTreemapWidget.SIZE_METRIC_PROPERTY, "ncloc") - .setProperty(MeasureFilterTreemapWidget.COLOR_METRIC_PROPERTY, "coverage"); + .setProperty(MeasureFilterAsTreemapWidget.SIZE_METRIC_PROPERTY, "ncloc") + .setProperty(MeasureFilterAsTreemapWidget.COLOR_METRIC_PROPERTY, "coverage"); } } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java index 6ce87b3be75..c941ef8ca36 100644 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/TreemapWidget.java @@ -33,6 +33,6 @@ import org.sonar.api.web.WidgetPropertyType; public class TreemapWidget extends CoreWidget { public TreemapWidget() { // do not use the id "treemap" to avoid conflict with the same CSS class - super("treemap-widget", "Treemap of Components", "/org/sonar/plugins/core/widgets/treemap.html.erb"); + super("treemap-widget", "Treemap of Components", "/Users/Stas/Projects/sonar/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/treemap.html.erb"); } } diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterAsTreemapWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterAsTreemapWidget.java new file mode 100644 index 00000000000..ae959dd25ce --- /dev/null +++ b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterAsTreemapWidget.java @@ -0,0 +1,55 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.plugins.core.widgets.measures; + +import org.sonar.api.web.*; +import org.sonar.plugins.core.widgets.CoreWidget; +import org.sonar.plugins.core.widgets.WidgetConstants; + +import static org.sonar.api.web.WidgetScope.GLOBAL; + +@WidgetCategory({"Filters"}) +@WidgetScope(GLOBAL) +@WidgetProperties({ + @WidgetProperty(key = MeasureFilterAsTreemapWidget.FILTER_PROPERTY, type = WidgetPropertyType.FILTER, + optional = false), + @WidgetProperty(key = MeasureFilterAsTreemapWidget.CHART_TITLE_PROPERTY, type = WidgetPropertyType.STRING), + @WidgetProperty(key = MeasureFilterAsTreemapWidget.SIZE_METRIC_PROPERTY, type = WidgetPropertyType.METRIC, + optional = true, options = { WidgetConstants.FILTER_OUT_NEW_METRICS }), + @WidgetProperty(key = MeasureFilterAsTreemapWidget.COLOR_METRIC_PROPERTY, type = WidgetPropertyType.METRIC, + optional = true, options = { WidgetConstants.FILTER_OUT_NEW_METRICS, "type:PERCENT,RATING,LEVEL" }), + @WidgetProperty(key = MeasureFilterAsTreemapWidget.HEIGHT_PERCENTS_PROPERTY, type = WidgetPropertyType.INTEGER, + optional = true, defaultValue = "55", description = "Height in percents of width"), + @WidgetProperty(key = MeasureFilterAsTreemapWidget.MAX_ITEMS_PROPERTY, type = WidgetPropertyType.INTEGER, + defaultValue = "100") +}) +public class MeasureFilterAsTreemapWidget extends CoreWidget { + public static final String FILTER_PROPERTY = "filter"; + public static final String SIZE_METRIC_PROPERTY = "sizeMetric"; + public static final String COLOR_METRIC_PROPERTY = "colorMetric"; + public static final String HEIGHT_PERCENTS_PROPERTY = "heightInPercents"; + public static final String CHART_TITLE_PROPERTY = "chartTitle"; + public static final String MAX_ITEMS_PROPERTY = "maxItems"; + public static final String ID = "measure_filter_treemap"; + + public MeasureFilterAsTreemapWidget() { + super(ID, "Measure Filter as Treemap", "/Users/Stas/Projects/sonar/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/measure_filter_treemap.html.erb"); + } +} diff --git a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterTreemapWidget.java b/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterTreemapWidget.java deleted file mode 100644 index 29bc578b684..00000000000 --- a/plugins/sonar-core-plugin/src/main/java/org/sonar/plugins/core/widgets/measures/MeasureFilterTreemapWidget.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.plugins.core.widgets.measures; - -import org.sonar.api.web.*; -import org.sonar.plugins.core.widgets.CoreWidget; -import org.sonar.plugins.core.widgets.WidgetConstants; - -import static org.sonar.api.web.WidgetScope.GLOBAL; - -@WidgetCategory({"Filters"}) -@WidgetScope(GLOBAL) -@WidgetProperties({ - @WidgetProperty(key = MeasureFilterTreemapWidget.FILTER_PROPERTY, type = WidgetPropertyType.FILTER, optional = false), - @WidgetProperty(key = MeasureFilterTreemapWidget.SIZE_METRIC_PROPERTY, type = WidgetPropertyType.METRIC, optional = true, options = {WidgetConstants.FILTER_OUT_NEW_METRICS}), - @WidgetProperty(key = MeasureFilterTreemapWidget.COLOR_METRIC_PROPERTY, type = WidgetPropertyType.METRIC, optional = true, - options = {WidgetConstants.FILTER_OUT_NEW_METRICS,"type:PERCENT,RATING,LEVEL"}), - @WidgetProperty(key = MeasureFilterTreemapWidget.HEIGHT_PERCENTS_PROPERTY, type = WidgetPropertyType.INTEGER, optional = true, defaultValue = "55", - description = "Height in percents of width"), - @WidgetProperty(key = MeasureFilterListWidget.DISPLAY_FILTER_DESCRIPTION, type = WidgetPropertyType.BOOLEAN, defaultValue = "false") -}) -public class MeasureFilterTreemapWidget extends CoreWidget { - public static final String FILTER_PROPERTY = "filter"; - public static final String SIZE_METRIC_PROPERTY = "sizeMetric"; - public static final String COLOR_METRIC_PROPERTY = "colorMetric"; - public static final String HEIGHT_PERCENTS_PROPERTY = "heightInPercents"; - public static final String DISPLAY_FILTER_DESCRIPTION = "displayFilterDescription"; - public static final String ID = "measure_filter_treemap"; - - public MeasureFilterTreemapWidget() { - super(ID, "Measure Filter as Treemap", "/org/sonar/plugins/core/widgets/measure_filter_treemap.html.erb"); - } -} diff --git a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/measure_filter_treemap.html.erb b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/measure_filter_treemap.html.erb index 8d1021f5ade..67d2bd13cfb 100644 --- a/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/measure_filter_treemap.html.erb +++ b/plugins/sonar-core-plugin/src/main/resources/org/sonar/plugins/core/widgets/measure_filter_treemap.html.erb @@ -1,41 +1,56 @@ <% - filter_id = widget_properties['filter'] - size_metric = widget_properties['sizeMetric'] - color_metric = widget_properties['colorMetric'] - filter = MeasureFilter.find_by_id(filter_id.to_i) if filter_id - if filter - url_options = {:controller => 'measures', :action => 'filter', :id => filter.id} - filter.load_criteria_from_data - filter.set_criteria_value(:display, 'treemap') - if size_metric - filter.set_criteria_value(:tmSize, size_metric.key) - url_options[:tmSize]=size_metric.key - end - if color_metric - filter.set_criteria_value(:tmColor, color_metric.key) - url_options[:tmColor]=color_metric.key - end - filter.set_criteria_value(:tmHeight, widget_properties['heightInPercents']) + containerId = 'treemap-widget' + widget.id.to_s + chartTitle = widget_properties['chartTitle'] + filterId = widget_properties['filter'].to_i + maxItems = widget_properties['maxItems'].to_i - - if !filter.require_authentication? || logged_in? - filter.execute(self, :user => current_user) - - @widget_title = link_to h(filter.name), url_options + filter = MeasureFilter.find_by_id(filterId.to_i) + @widget_title = link_to h(filter.name), {:controller => 'measures', :action => 'filter', :id => filter.id, :display => 'list'} %> - <% if widget_properties['displayFilterDescription'] && !filter.description.blank? %> -
- <%= h filter.description -%> -
+
+ + + + <% if chartTitle %> +

<%= h(chartTitle) -%>

<% end %> + +
- <%= render :partial => 'measures/display_treemap', :locals => {:edit_mode => false, :widget_id => widget.id, :filter => filter} %> -<% - end - else -%> -

<%= message 'measure_filter.widget.unknown_filter_warning' -%>

-<% - end -%> + + + diff --git a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java index 49770b62dd7..b5f79bb88de 100644 --- a/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java +++ b/plugins/sonar-core-plugin/src/test/java/org/sonar/plugins/core/dashboards/GlobalDefaultDashboardTest.java @@ -29,8 +29,8 @@ import org.sonar.plugins.core.CorePlugin; import org.sonar.plugins.core.measurefilters.MyFavouritesFilter; import org.sonar.plugins.core.measurefilters.ProjectFilter; import org.sonar.plugins.core.widgets.WelcomeWidget; +import org.sonar.plugins.core.widgets.measures.MeasureFilterAsTreemapWidget; import org.sonar.plugins.core.widgets.measures.MeasureFilterListWidget; -import org.sonar.plugins.core.widgets.measures.MeasureFilterTreemapWidget; import java.util.List; @@ -77,7 +77,7 @@ public class GlobalDefaultDashboardTest { assertThat(secondColumn).hasSize(2); assertThat(secondColumn.get(0).getId()).isEqualTo(MeasureFilterListWidget.ID); assertThat(secondColumn.get(0).getProperty("filter")).isEqualTo("101"); - assertThat(secondColumn.get(1).getId()).isEqualTo(MeasureFilterTreemapWidget.ID); + assertThat(secondColumn.get(1).getId()).isEqualTo(MeasureFilterAsTreemapWidget.ID); assertThat(secondColumn.get(1).getProperty("filter")).isEqualTo("101"); } diff --git a/sonar-server/Gruntfile.coffee b/sonar-server/Gruntfile.coffee index 0f06e25c577..e49048f7eff 100644 --- a/sonar-server/Gruntfile.coffee +++ b/sonar-server/Gruntfile.coffee @@ -94,6 +94,7 @@ module.exports = (grunt) -> '<%= pkg.assets %>js/widgets/pie-chart.js' '<%= pkg.assets %>js/widgets/histogram.js' '<%= pkg.assets %>js/widgets/word-cloud.js' + '<%= pkg.assets %>js/widgets/treemap.js' '<%= pkg.assets %>js/top-search.js' '<%= pkg.assets %>js/sortable.js' '<%= pkg.assets %>js/common/inputs.js' @@ -125,6 +126,7 @@ module.exports = (grunt) -> '<%= pkg.assets %>js/widgets/pie-chart.js' '<%= pkg.assets %>js/widgets/histogram.js' '<%= pkg.assets %>js/widgets/word-cloud.js' + '<%= pkg.assets %>js/widgets/treemap.js' '<%= pkg.assets %>js/top-search.js' '<%= pkg.assets %>js/sortable.js' '<%= pkg.assets %>js/common/inputs.js' diff --git a/sonar-server/src/main/coffee/widgets/base.coffee b/sonar-server/src/main/coffee/widgets/base.coffee index 133c15e464d..5531ceb2d92 100644 --- a/sonar-server/src/main/coffee/widgets/base.coffee +++ b/sonar-server/src/main/coffee/widgets/base.coffee @@ -2,6 +2,9 @@ window.SonarWidgets ?= {} class BaseWidget lineHeight: 20 + colorLow: '#d62728' + colorHigh: '#85bb43' + colorUnknown: '#777' constructor: -> @@ -47,4 +50,11 @@ class BaseWidget @ -window.SonarWidgets.BaseWidget = BaseWidget \ No newline at end of file + tooltip: (d) -> + title = d.longName + title += "\n#{@colorMetric.name}: #{@colorMetric.formattedValue d}" if @colorMetric.value(d)? + title += "\n#{@sizeMetric.name}: #{@sizeMetric.formattedValue d}" if @sizeMetric.value(d)? + title + + +window.SonarWidgets.BaseWidget = BaseWidget diff --git a/sonar-server/src/main/coffee/widgets/treemap.coffee b/sonar-server/src/main/coffee/widgets/treemap.coffee new file mode 100644 index 00000000000..7cb065d02b4 --- /dev/null +++ b/sonar-server/src/main/coffee/widgets/treemap.coffee @@ -0,0 +1,96 @@ +class Treemap extends window.SonarWidgets.BaseWidget + sizeLow: 10 + sizeHigh: 24 + + + constructor: -> + @addField 'width', null + @addField 'height', null + @addField 'maxResultsReached', false + super + + + getNodes: -> + @treemap.nodes(children: @components()).filter (d) -> !d.children + + + renderTreemap: -> + @treemap = d3.layout.treemap() + @treemap.value (d) => + @sizeMetric.value d + @cells = @box.selectAll('.treemap-cell').data @getNodes() + + cellsEnter = @cells.enter().append 'div' + cellsEnter.classed 'treemap-cell', true + cellsEnter.attr 'title', (d) => @tooltip d + cellsEnter.style 'color', (d) => + if @colorMetric.value(d)? then @color @colorMetric.value(d) else @colorUnknown + cellsEnter.style 'font-size', (d) => "#{@size @sizeMetric.value d}px" + + cellsLink = cellsEnter.append('a').classed 'treemap-detach', true + cellsLink.attr 'target', '_blank' + cellsLink.attr 'href', (d) => + url = @options().baseUrl + encodeURIComponent(d.key) + url += '?metric=' + encodeURIComponent(@colorMetric.key) if d.qualifier == 'CLA' || d.qualifier == 'FIL' + url + cellsLink.append('i').attr 'class', (d) -> "icon-qualifier-#{d.qualifier.toLowerCase()}" + + @cellsInner = cellsEnter.append('div').classed 'treemap-inner', true + @cellsInner.text (d) -> d.longName + @cellsInner.style 'border-color', (d) => + if @colorMetric.value(d)? then @color @colorMetric.value(d) else @colorUnknown + + @attachEvents cellsEnter + + + attachEvents: (cells) -> + + + positionCells: -> + @cells.style 'left', (d) -> "#{d.x}px" + @cells.style 'top', (d) -> "#{d.y}px" + @cells.style 'width', (d) -> "#{d.dx}px" + @cells.style 'height', (d) -> "#{d.dy}px" + @cells.classed 'treemap-cell-small', (d) -> d.dy < 60 + @cellsInner.style 'line-height', (d) -> "#{d.dy}px" + + + render: (container) -> + box = d3.select(container).append('div') + box.classed 'sonar-d3', true + @box = box.append('div').classed 'treemap-container', true + + # Configure metrics + @addMetric 'colorMetric', 0 + @addMetric 'sizeMetric', 1 + + # Configure scales + @color = d3.scale.linear().domain([0, 100]) + if @colorMetric.direction == 1 + @color.range [@colorLow, @colorHigh] + else + @color.range [@colorHigh, @colorLow] + + sizeDomain = d3.extent @components(), (d) => @sizeMetric.value d + @size = d3.scale.linear().domain(sizeDomain).range [@sizeLow, @sizeHigh] + + # Show maxResultsReached message + if @maxResultsReached() + maxResultsReachedLabel = box.append('div').text @options().maxItemsReachedMessage + maxResultsReachedLabel.classed 'max-results-reached-message', true + + @renderTreemap() + super + + + update: -> + @width @box.property 'offsetWidth' + @height (@width() / 100.0 * @options().heightInPercents) + @box.style 'height', "#{@height()}px" + @treemap.size [@width(), @height()] + @cells.data @getNodes() + @positionCells() + + + +window.SonarWidgets.Treemap = Treemap diff --git a/sonar-server/src/main/coffee/widgets/word-cloud.coffee b/sonar-server/src/main/coffee/widgets/word-cloud.coffee index 1e09b55a6d5..1d98fd1add0 100644 --- a/sonar-server/src/main/coffee/widgets/word-cloud.coffee +++ b/sonar-server/src/main/coffee/widgets/word-cloud.coffee @@ -1,7 +1,4 @@ class WordCloud extends window.SonarWidgets.BaseWidget - colorLow: '#d62728' - colorHigh: '#1f77b4' - colorUnknown: '#777' sizeLow: 10 sizeHigh: 24 @@ -22,11 +19,7 @@ class WordCloud extends window.SonarWidgets.BaseWidget url = @options().baseUrl + encodeURIComponent(d.key) url += '?metric=' + encodeURIComponent(@colorMetric.key) if d.qualifier == 'CLA' || d.qualifier == 'FIL' url - wordsEnter.attr 'title', (d) => - title = d.longName - title += " | #{@colorMetric.name}: #{@colorMetric.formattedValue d}" if @colorMetric.value(d)? - title += " | #{@sizeMetric.name}: #{@sizeMetric.formattedValue d}" if @sizeMetric.value(d)? - title + wordsEnter.attr 'title', (d) => @tooltip d words.style 'color', (d) => if @colorMetric.value(d)? then @color @colorMetric.value(d) else @colorUnknown @@ -67,4 +60,4 @@ class WordCloud extends window.SonarWidgets.BaseWidget -window.SonarWidgets.WordCloud = WordCloud \ No newline at end of file +window.SonarWidgets.WordCloud = WordCloud diff --git a/sonar-server/src/main/less/style.less b/sonar-server/src/main/less/style.less index d359113276f..aedda2cd0e9 100644 --- a/sonar-server/src/main/less/style.less +++ b/sonar-server/src/main/less/style.less @@ -2799,6 +2799,48 @@ div.rule-title { fill: #777; } +.sonar-d3 .treemap-container { + position: relative; +} + +.sonar-d3 .treemap-cell { + position: absolute; + border-right: 1px solid #fff; + border-bottom: 1px solid #fff; + .box-sizing(border-box); +} + +.sonar-d3 .treemap-inner { + position: absolute; + z-index: 1; + top: 0; left: 0; bottom: 0; right: 0; + padding: 0 5px; + border: 1px solid; + text-align: center; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sonar-d3 .treemap-detach { + position: absolute; + z-index: 2; + top: 5px; right: 5px; + line-height: 16px; + color: inherit; + opacity: 0.5; + + &:hover { opacity: 1; } +} + +.sonar-d3 .treemap-cell-small { + .treemap-inner { text-indent: -9999px; } + .treemap-detach { + top: 50%; left: 50%; + margin: -8px 0 0 -8px; + } +} + /* ------------------- Admin pages ------------------- */ -- 2.39.5