From 4f1869bc8eaa8d26215a7d70d174beedf1f6f2a5 Mon Sep 17 00:00:00 2001 From: deecay Date: Mon, 22 Oct 2018 05:50:39 +0900 Subject: [PATCH 1/4] Treemap visualization in d3 --- client/app/visualizations/treemap/index.js | 268 ++++++++++++++++++ .../treemap/treemap-editor.html | 122 ++++++++ .../app/visualizations/treemap/treemap.less | 48 ++++ 3 files changed, 438 insertions(+) create mode 100644 client/app/visualizations/treemap/index.js create mode 100644 client/app/visualizations/treemap/treemap-editor.html create mode 100644 client/app/visualizations/treemap/treemap.less diff --git a/client/app/visualizations/treemap/index.js b/client/app/visualizations/treemap/index.js new file mode 100644 index 0000000000..f68949c0a9 --- /dev/null +++ b/client/app/visualizations/treemap/index.js @@ -0,0 +1,268 @@ +import d3 from 'd3'; +import angular from 'angular'; +import chroma from 'chroma-js'; +import { each, debounce, min, max } from 'lodash'; +import { ColorPalette } from '@/visualizations/chart/plotly/utils'; +import { formatSimpleTemplate } from '@/lib/value-format'; +import editorTemplate from './treemap-editor.html'; + +import './treemap.less'; + +function createTreemap(element, data, scope, $sanitize) { + const margin = { + top: 20, right: 50, bottom: 20, left: 50, + }; + const width = element.clientWidth - margin.right - margin.left; + const height = 460 - margin.top - margin.bottom; + const defaultColor = d3.scale.category20c(); + + if ((width <= 0) || (height <= 0)) { + return; + } + + const svg = d3.select(element).append('svg') + .attr('width', width + margin.right + margin.left) + .attr('height', height + margin.top + margin.bottom) + .append('g') + .attr('transform', 'translate(-.5,-.5)'); + + const tooltip = d3.select('visualization-renderer').append('div').attr('class', 'treemap-tooltip'); + + const rootTree = data; + + function datalabelText(item) { + return $sanitize(formatSimpleTemplate(scope.options.datalabel.template, item)); + } + + function getTreeAttr(array, attr) { + return array.reduce((r, a) => { + r.push(a[attr]); + if (a.children && Array.isArray(a.children)) { + r = r.concat(getTreeAttr(a.children, attr)); + } + return r; + }, []); + } + + function update(source) { + const zMin = min(getTreeAttr(source, scope.options.colorColumn)); + const zMax = max(getTreeAttr(source, scope.options.colorColumn)); + + const chromaColor = chroma.scale([ + scope.options.customColor.min, + scope.options.customColor.max, + ]).domain([zMin, zMax]); + + function getColor(item) { + if (scope.options.customColor.enabled) { + if (scope.options.customColor.encoding === 'category') { + return defaultColor(item[scope.options.colorColumn]); + } + return chromaColor(item[scope.options.colorColumn]); + } + let name = ''; + if (!item.parent) { + name = ''; + } else { + name = item.children ? item.name : item.parent.name; + } + return defaultColor(name); + } + + function ascSorter(a, b) { + return a.value - b.value; + } + + function descSorter(a, b) { + return b.value - a.value; + } + + d3.select('svg') + .attr('height', height + margin.top + margin.bottom) + .attr('width', width + margin.left + margin.right); + + const treemap = d3.layout.treemap() + .size([width, height]) + .padding(scope.options.cellPadding) + .sort((a, b) => (scope.options.sortReverse ? descSorter(a, b) : ascSorter(a, b))) + .value(d => d[scope.options.sizeColumn]); + + const cell = svg.data(source).selectAll('g') + .data(treemap.nodes) + .enter() + .append('g') + .attr('class', 'cell') + .attr('transform', () => 'translate(' + margin.left + ',' + margin.top + ')'); + + const rects = cell.append('rect') + .attr('id', d => 'rect-' + d['@@rownum']) + .attr('x', d => d.x) + .attr('y', d => d.y) + .attr('width', d => d.dx) + .attr('height', d => d.dy) + .attr('stroke', 'white') + .attr('stroke-width', 0.5) + .attr('fill', d => getColor(d)); + + if (scope.options.tooltip.enabled) { + rects + .on('mousemove', (d) => { + tooltip.html($sanitize(formatSimpleTemplate(scope.options.tooltip.template, d))); + tooltip.style('display', 'block'); + + const tooltipWidth = Number(tooltip.style('width').slice(0, -2)); + const tooltipHeight = Number(tooltip.style('height').slice(0, -2)); + const posTop = margin.top + d.y + d.dy / 2 - tooltipHeight / 2; + const posLeft = margin.left + d.x + d.dx / 2; + + tooltip.style('top', posTop + 'px'); + + const rightEnd = posLeft + tooltipWidth; + if (rightEnd > element.clientWidth) { + tooltip.style('left', element.clientWidth - tooltipWidth + 'px'); + } else { + tooltip.style('left', posLeft + 'px'); + } + }) + .on('mouseout', () => { + tooltip.style('display', 'none'); + }); + } + + if (scope.options.datalabel.enabled) { + cell.append('clipPath') + .attr('id', d => 'clip-' + d['@@rownum']) + .append('use') + .attr('transform', 'translate(-2,0)') + .attr('xlink:href', d => '#rect-' + d['@@rownum'] + ''); + + cell.append('text') + .attr('class', 'treemap-text') + .attr('clip-path', d => 'url(#clip-' + d['@@rownum'] + ')') + .attr('x', d => d.x + 3) + .attr('y', d => d.y + 10) + .html(d => (d.children ? null : datalabelText(d))); + } + } + update(rootTree); +} + +function treemapRenderer($sanitize) { + return { + restrict: 'E', + scope: { + queryResult: '=', + options: '=', + }, + template: '
', + replace: false, + link($scope, element) { + function refreshData() { + const queryData = angular.copy($scope.queryResult.getData()); + + if (!queryData) { return; } + + // eslint-disable-next-line prefer-arrow-callback + const dataMap = queryData.reduce(function makeDatamap(map, node) { + map[node[$scope.options.childColumn]] = node; + return map; + }, {}); + + each(queryData, (item, index) => { + item['@@child'] = item[$scope.options.childColumn]; + item['@@parent'] = item[$scope.options.parentColumn]; + item['@@size'] = item[$scope.options.sizeColumn]; + item['@@rownum'] = index; + }); + + const treeData = []; + // eslint-disable-next-line prefer-arrow-callback + queryData.forEach(function makeTree(node) { + // add to parent + const parent = dataMap[node[$scope.options.parentColumn]]; + if (parent) { + // create child array if it doesn't exist + (parent.children || (parent.children = [])) + // add node to child array + .push(node); + } else { + // parent is null or missing + treeData.push(node); + } + }); + if (treeData) { + const container = element[0].querySelector('.treemap-visualization-container'); + angular.element(container).empty(); + createTreemap(container, treeData, $scope, $sanitize); + } + } + + $scope.$watch('queryResult && queryResult.getData()', refreshData); + $scope.$watch('options', refreshData, true); + $scope.handleResize = debounce(refreshData, 50); + }, + }; +} + +function treemapEditor() { + return { + restrict: 'E', + template: editorTemplate, + scope: { + queryResult: '=', + options: '=?', + }, + link($scope) { + $scope.colors = ColorPalette; + $scope.currentTab = 'general'; + + $scope.colorEncodings = { + value: 'value', + category: 'category', + }; + + $scope.templateHint = ` +
All query result columns can be referenced using {{ column_name }} syntax.
+
Use special names to access additional properties:
+
{{ @@parent }} parent node;
+
{{ @@child }} child node;
+
{{ @@size }} size;
+
This syntax is applicable to tooltip and popup templates.
+ `; + }, + }; +} + +export default function init(ngModule) { + ngModule.directive('treemapRenderer', treemapRenderer); + ngModule.directive('treemapEditor', treemapEditor); + + ngModule.config((VisualizationProvider) => { + const editTemplate = ''; + + VisualizationProvider.registerVisualization({ + type: 'TREEMAP', + name: 'Treemap', + renderTemplate: '', + editorTemplate: editTemplate, + defaultOptions: { + cellPadding: 1, + tooltip: { + enabled: true, + template: '{{ @@child }} : {{ @@size }}', + }, + datalabel: { + enabled: true, + template: '{{ @@child }}', + }, + customColor: { + enabled: false, + encoding: 'value', + min: '#356AFF', + max: '#50F5ED', + }, + }, + }); + }); +} + diff --git a/client/app/visualizations/treemap/treemap-editor.html b/client/app/visualizations/treemap/treemap-editor.html new file mode 100644 index 0000000000..751775234e --- /dev/null +++ b/client/app/visualizations/treemap/treemap-editor.html @@ -0,0 +1,122 @@ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+ +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+
+ +
+
+
+
+ +
+
+ +
+ +
+
+
+ +
+ +
+
+
+
+
+
+ + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + +
+
+
+
+
+
diff --git a/client/app/visualizations/treemap/treemap.less b/client/app/visualizations/treemap/treemap.less new file mode 100644 index 0000000000..b5a03bd884 --- /dev/null +++ b/client/app/visualizations/treemap/treemap.less @@ -0,0 +1,48 @@ +.node { + cursor: pointer; +} + +.node circle { + fill: #fff; + stroke: steelblue; + stroke-width: 3px; +} + +.node text { font: 12px sans-serif; } + +.link { + fill: none; + stroke: #ccc; + stroke-width: 2px; +} + +rect { + stroke: #fff; +} + +text { + font: 10px sans-serif; +} + +.treemap-tooltip { + display: none; + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + opacity: 0.9; +} + +.treemap-text { + pointer-events: none; +} + From a2654d532c4837773ba92aae08bb8ea17f92930e Mon Sep 17 00:00:00 2001 From: deecay Date: Tue, 27 Nov 2018 23:34:05 +0900 Subject: [PATCH 2/4] Fix column name bug --- client/app/visualizations/treemap/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/app/visualizations/treemap/index.js b/client/app/visualizations/treemap/index.js index f68949c0a9..e81afdd7cf 100644 --- a/client/app/visualizations/treemap/index.js +++ b/client/app/visualizations/treemap/index.js @@ -64,7 +64,7 @@ function createTreemap(element, data, scope, $sanitize) { if (!item.parent) { name = ''; } else { - name = item.children ? item.name : item.parent.name; + name = item.children ? item[scope.options.childColumn] : item.parent[scope.options.childColumn]; } return defaultColor(name); } From 1bc980f9eb223590fed77ea5d46b1410b6928771 Mon Sep 17 00:00:00 2001 From: deecay Date: Sun, 2 Dec 2018 12:40:02 +0900 Subject: [PATCH 3/4] Fix css --- .../treemap/treemap-editor.html | 2 +- .../app/visualizations/treemap/treemap.less | 75 ++++++++----------- 2 files changed, 31 insertions(+), 46 deletions(-) diff --git a/client/app/visualizations/treemap/treemap-editor.html b/client/app/visualizations/treemap/treemap-editor.html index 751775234e..bbcdc28311 100644 --- a/client/app/visualizations/treemap/treemap-editor.html +++ b/client/app/visualizations/treemap/treemap-editor.html @@ -64,7 +64,7 @@ -
+
diff --git a/client/app/visualizations/treemap/treemap.less b/client/app/visualizations/treemap/treemap.less index b5a03bd884..dc12fc7397 100644 --- a/client/app/visualizations/treemap/treemap.less +++ b/client/app/visualizations/treemap/treemap.less @@ -1,48 +1,33 @@ -.node { - cursor: pointer; +.treemap-visualization-container { + rect { + stroke: #fff; + } + + text { + font: 10px sans-serif; + } } -.node circle { - fill: #fff; - stroke: steelblue; - stroke-width: 3px; +visualization-renderer { + .treemap-tooltip { + display: none; + position: absolute; + padding: 6px; + background-color: #fff; + border: 1px solid #fff; + border-radius: 3px; + color: #222; + white-space: nowrap; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + pointer-events: none; + box-shadow: 0 1px 3px rgba(0,0,0,0.4); + opacity: 0.9; + } + + .treemap-text { + pointer-events: none; + } } - -.node text { font: 12px sans-serif; } - -.link { - fill: none; - stroke: #ccc; - stroke-width: 2px; -} - -rect { - stroke: #fff; -} - -text { - font: 10px sans-serif; -} - -.treemap-tooltip { - display: none; - position: absolute; - padding: 6px; - background-color: #fff; - border: 1px solid #fff; - border-radius: 3px; - color: #222; - white-space: nowrap; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - pointer-events: none; - box-shadow: 0 1px 3px rgba(0,0,0,0.4); - opacity: 0.9; -} - -.treemap-text { - pointer-events: none; -} - From 59575d2d731ad2b1b6b14da5fc8d8089f09b3845 Mon Sep 17 00:00:00 2001 From: deecay Date: Tue, 7 May 2019 20:53:53 +0900 Subject: [PATCH 4/4] Comply to React migration --- client/app/visualizations/treemap/index.js | 395 +++++++++--------- .../treemap/treemap-editor.html | 34 +- .../app/visualizations/treemap/treemap.less | 4 +- 3 files changed, 214 insertions(+), 219 deletions(-) diff --git a/client/app/visualizations/treemap/index.js b/client/app/visualizations/treemap/index.js index e81afdd7cf..cbac7e3329 100644 --- a/client/app/visualizations/treemap/index.js +++ b/client/app/visualizations/treemap/index.js @@ -1,14 +1,36 @@ +import { each, debounce, min, max, merge, reduce, cloneDeep } from 'lodash'; import d3 from 'd3'; import angular from 'angular'; +import { angular2react } from 'angular2react'; +import { registerVisualization } from '@/visualizations'; + import chroma from 'chroma-js'; -import { each, debounce, min, max } from 'lodash'; -import { ColorPalette } from '@/visualizations/chart/plotly/utils'; +import ColorPalette from '@/visualizations/ColorPalette'; import { formatSimpleTemplate } from '@/lib/value-format'; import editorTemplate from './treemap-editor.html'; import './treemap.less'; +const DEFAULT_OPTIONS = { + cellPadding: 1, + tooltip: { + enabled: true, + template: '{{ @@child }} : {{ @@size }}', + }, + datalabel: { + enabled: true, + template: '{{ @@child }}', + }, + customColor: { + enabled: false, + encoding: 'value', + min: '#356AFF', + max: '#50F5ED', + }, +}; + function createTreemap(element, data, scope, $sanitize) { + const options = scope.$ctrl.options; const margin = { top: 20, right: 50, bottom: 20, left: 50, }; @@ -26,12 +48,10 @@ function createTreemap(element, data, scope, $sanitize) { .append('g') .attr('transform', 'translate(-.5,-.5)'); - const tooltip = d3.select('visualization-renderer').append('div').attr('class', 'treemap-tooltip'); - - const rootTree = data; + const tooltip = d3.select(element).append('div').attr('class', 'treemap-tooltip'); function datalabelText(item) { - return $sanitize(formatSimpleTemplate(scope.options.datalabel.template, item)); + return $sanitize(formatSimpleTemplate(options.datalabel.template, item)); } function getTreeAttr(array, attr) { @@ -44,225 +64,200 @@ function createTreemap(element, data, scope, $sanitize) { }, []); } - function update(source) { - const zMin = min(getTreeAttr(source, scope.options.colorColumn)); - const zMax = max(getTreeAttr(source, scope.options.colorColumn)); + const zMin = min(getTreeAttr(data, options.colorColumn)); + const zMax = max(getTreeAttr(data, options.colorColumn)); - const chromaColor = chroma.scale([ - scope.options.customColor.min, - scope.options.customColor.max, - ]).domain([zMin, zMax]); + const chromaColor = chroma.scale([ + options.customColor.min, + options.customColor.max, + ]).domain([zMin, zMax]); - function getColor(item) { - if (scope.options.customColor.enabled) { - if (scope.options.customColor.encoding === 'category') { - return defaultColor(item[scope.options.colorColumn]); - } - return chromaColor(item[scope.options.colorColumn]); + function getColor(item) { + if (options.customColor.enabled) { + if (options.customColor.encoding === 'category') { + return defaultColor(item[options.colorColumn]); } - let name = ''; - if (!item.parent) { - name = ''; - } else { - name = item.children ? item[scope.options.childColumn] : item.parent[scope.options.childColumn]; - } - return defaultColor(name); + return chromaColor(item[options.colorColumn]); } - - function ascSorter(a, b) { - return a.value - b.value; - } - - function descSorter(a, b) { - return b.value - a.value; + let name = ''; + if (!item.parent) { + name = ''; + } else { + name = item.children ? item[options.childColumn] : item.parent[options.childColumn]; } + return defaultColor(name); + } - d3.select('svg') - .attr('height', height + margin.top + margin.bottom) - .attr('width', width + margin.left + margin.right); - - const treemap = d3.layout.treemap() - .size([width, height]) - .padding(scope.options.cellPadding) - .sort((a, b) => (scope.options.sortReverse ? descSorter(a, b) : ascSorter(a, b))) - .value(d => d[scope.options.sizeColumn]); - - const cell = svg.data(source).selectAll('g') - .data(treemap.nodes) - .enter() - .append('g') - .attr('class', 'cell') - .attr('transform', () => 'translate(' + margin.left + ',' + margin.top + ')'); - - const rects = cell.append('rect') - .attr('id', d => 'rect-' + d['@@rownum']) - .attr('x', d => d.x) - .attr('y', d => d.y) - .attr('width', d => d.dx) - .attr('height', d => d.dy) - .attr('stroke', 'white') - .attr('stroke-width', 0.5) - .attr('fill', d => getColor(d)); - - if (scope.options.tooltip.enabled) { - rects - .on('mousemove', (d) => { - tooltip.html($sanitize(formatSimpleTemplate(scope.options.tooltip.template, d))); - tooltip.style('display', 'block'); - - const tooltipWidth = Number(tooltip.style('width').slice(0, -2)); - const tooltipHeight = Number(tooltip.style('height').slice(0, -2)); - const posTop = margin.top + d.y + d.dy / 2 - tooltipHeight / 2; - const posLeft = margin.left + d.x + d.dx / 2; - - tooltip.style('top', posTop + 'px'); - - const rightEnd = posLeft + tooltipWidth; - if (rightEnd > element.clientWidth) { - tooltip.style('left', element.clientWidth - tooltipWidth + 'px'); - } else { - tooltip.style('left', posLeft + 'px'); - } - }) - .on('mouseout', () => { - tooltip.style('display', 'none'); - }); - } + function ascSorter(a, b) { + return a.value - b.value; + } - if (scope.options.datalabel.enabled) { - cell.append('clipPath') - .attr('id', d => 'clip-' + d['@@rownum']) - .append('use') - .attr('transform', 'translate(-2,0)') - .attr('xlink:href', d => '#rect-' + d['@@rownum'] + ''); - - cell.append('text') - .attr('class', 'treemap-text') - .attr('clip-path', d => 'url(#clip-' + d['@@rownum'] + ')') - .attr('x', d => d.x + 3) - .attr('y', d => d.y + 10) - .html(d => (d.children ? null : datalabelText(d))); - } + function descSorter(a, b) { + return b.value - a.value; } - update(rootTree); -} -function treemapRenderer($sanitize) { - return { - restrict: 'E', - scope: { - queryResult: '=', - options: '=', - }, - template: '
', - replace: false, - link($scope, element) { - function refreshData() { - const queryData = angular.copy($scope.queryResult.getData()); - - if (!queryData) { return; } - - // eslint-disable-next-line prefer-arrow-callback - const dataMap = queryData.reduce(function makeDatamap(map, node) { - map[node[$scope.options.childColumn]] = node; - return map; - }, {}); - - each(queryData, (item, index) => { - item['@@child'] = item[$scope.options.childColumn]; - item['@@parent'] = item[$scope.options.parentColumn]; - item['@@size'] = item[$scope.options.sizeColumn]; - item['@@rownum'] = index; - }); - - const treeData = []; - // eslint-disable-next-line prefer-arrow-callback - queryData.forEach(function makeTree(node) { - // add to parent - const parent = dataMap[node[$scope.options.parentColumn]]; - if (parent) { - // create child array if it doesn't exist - (parent.children || (parent.children = [])) - // add node to child array - .push(node); - } else { - // parent is null or missing - treeData.push(node); - } - }); - if (treeData) { - const container = element[0].querySelector('.treemap-visualization-container'); - angular.element(container).empty(); - createTreemap(container, treeData, $scope, $sanitize); + const treemap = d3.layout.treemap() + .size([width, height]) + .padding(options.cellPadding) + .sort((a, b) => (options.sortReverse ? descSorter(a, b) : ascSorter(a, b))) + .value(d => d[options.sizeColumn]); + + const cell = svg.data(data).selectAll('g') + .data(treemap.nodes) + .enter() + .append('g') + .attr('class', 'cell') + .attr('transform', () => 'translate(' + margin.left + ',' + margin.top + ')'); + + const rects = cell.append('rect') + .attr('id', d => 'rect-' + d['@@rownum']) + .attr('x', d => d.x) + .attr('y', d => d.y) + .attr('width', d => d.dx) + .attr('height', d => d.dy) + .attr('stroke', 'white') + .attr('stroke-width', 0.5) + .attr('fill', d => getColor(d)); + + if (options.tooltip.enabled) { + rects + .on('mousemove', (d) => { + tooltip.html($sanitize(formatSimpleTemplate(options.tooltip.template, d))); + tooltip.style('display', 'block'); + + const tooltipWidth = Number(tooltip.style('width').slice(0, -2)); + const tooltipHeight = Number(tooltip.style('height').slice(0, -2)); + const posTop = margin.top + d.y + d.dy / 2 - tooltipHeight / 2; + const posLeft = margin.left + d.x + d.dx / 2; + + tooltip.style('top', posTop + 'px'); + + const rightEnd = posLeft + tooltipWidth; + if (rightEnd > element.clientWidth) { + tooltip.style('left', element.clientWidth - tooltipWidth + 'px'); + } else { + tooltip.style('left', posLeft + 'px'); } - } + }) + .on('mouseout', () => { + tooltip.style('display', 'none'); + }); + } - $scope.$watch('queryResult && queryResult.getData()', refreshData); - $scope.$watch('options', refreshData, true); - $scope.handleResize = debounce(refreshData, 50); - }, - }; + if (options.datalabel.enabled) { + cell.append('text') + .attr('class', 'treemap-text') + .attr('clip-path', (d) => { + const clipWidth = d.dx - 4; + const clipHeight = d.dy; + return 'polygon(0px ' + clipHeight + 'px, ' + + clipWidth + 'px ' + clipHeight + 'px, ' + + clipWidth + 'px 0px, 0px 0px )'; + }) + .attr('x', d => d.x + 3) + .attr('y', d => d.y + 12) + .html(d => (d.children ? null : datalabelText(d))); + } } -function treemapEditor() { - return { - restrict: 'E', - template: editorTemplate, - scope: { - queryResult: '=', - options: '=?', - }, - link($scope) { - $scope.colors = ColorPalette; - $scope.currentTab = 'general'; - - $scope.colorEncodings = { - value: 'value', - category: 'category', - }; - - $scope.templateHint = ` +const TreemapRenderer = { + restrict: 'E', + template: '
', + bindings: { + data: '<', + options: '<', + }, + controller($scope, $element, $sanitize) { + const update = () => { + const queryData = cloneDeep(this.data.rows); + const options = $scope.$ctrl.options; + + if (!queryData) { return; } + + // eslint-disable-next-line prefer-arrow-callback + const dataMap = reduce(queryData, function makeDatamap(map, node) { + map[node[options.childColumn]] = node; + return map; + }, {}); + + each(queryData, (item, index) => { + item['@@child'] = item[options.childColumn]; + item['@@parent'] = item[options.parentColumn]; + item['@@size'] = item[options.sizeColumn]; + item['@@rownum'] = index; + }); + + const treeData = []; + // eslint-disable-next-line prefer-arrow-callback + queryData.forEach(function makeTree(node) { + // add to parent + const parent = dataMap[node[options.parentColumn]]; + if (parent) { + // create child array if it doesn't exist + (parent.children || (parent.children = [])) + // add node to child array + .push(node); + } else { + // parent is null or missing + treeData.push(node); + } + }); + if (treeData) { + const container = $element[0].querySelector('.treemap-visualization-container'); + angular.element(container).empty(); + createTreemap(container, treeData, $scope, $sanitize); + } + }; + + $scope.handleResize = debounce(update, 50); + + $scope.$watch('$ctrl.data', update); + $scope.$watch('$ctrl.options', update, true); + }, +}; + +const TreemapEditor = { + template: editorTemplate, + bindings: { + data: '<', + options: '<', + onOptionsChange: '<', + }, + controller($scope) { + $scope.colors = ColorPalette; + $scope.currentTab = 'general'; + + $scope.colorEncodings = { + value: 'value', + category: 'category', + }; + + $scope.templateHint = `
All query result columns can be referenced using {{ column_name }} syntax.
Use special names to access additional properties:
{{ @@parent }} parent node;
{{ @@child }} child node;
{{ @@size }} size;
This syntax is applicable to tooltip and popup templates.
- `; - }, - }; -} + `; + }, +}; export default function init(ngModule) { - ngModule.directive('treemapRenderer', treemapRenderer); - ngModule.directive('treemapEditor', treemapEditor); - - ngModule.config((VisualizationProvider) => { - const editTemplate = ''; + ngModule.component('treemapRenderer', TreemapRenderer); + ngModule.component('treemapEditor', TreemapEditor); - VisualizationProvider.registerVisualization({ + ngModule.run(($injector) => { + registerVisualization({ type: 'TREEMAP', name: 'Treemap', - renderTemplate: '', - editorTemplate: editTemplate, - defaultOptions: { - cellPadding: 1, - tooltip: { - enabled: true, - template: '{{ @@child }} : {{ @@size }}', - }, - datalabel: { - enabled: true, - template: '{{ @@child }}', - }, - customColor: { - enabled: false, - encoding: 'value', - min: '#356AFF', - max: '#50F5ED', - }, - }, + getOptions: options => merge({}, DEFAULT_OPTIONS, options), + Renderer: angular2react('treemapRenderer', TreemapRenderer, $injector), + Editor: angular2react('treemapEditor', TreemapEditor, $injector), + + defaultRows: 10, }); }); } +init.init = true; diff --git a/client/app/visualizations/treemap/treemap-editor.html b/client/app/visualizations/treemap/treemap-editor.html index bbcdc28311..1a7dad0491 100644 --- a/client/app/visualizations/treemap/treemap-editor.html +++ b/client/app/visualizations/treemap/treemap-editor.html @@ -8,51 +8,51 @@
- +
- +
- +
- +
- +
+ ng-model="$ctrl.options.tooltip.template" ng-model-options="{ allowInvalid: true, debounce: 200 }" + ng-disabled="!$ctrl.options.tooltip.enabled">
- +
+ ng-model="$ctrl.options.datalabel.template" ng-model-options="{ allowInvalid: true, debounce: 200 }" + ng-disabled="!$ctrl.options.datalabel.enabled">
@@ -66,12 +66,12 @@
- +
- +
@@ -79,8 +79,8 @@
@@ -89,7 +89,7 @@
- + @@ -104,7 +104,7 @@
- + diff --git a/client/app/visualizations/treemap/treemap.less b/client/app/visualizations/treemap/treemap.less index dc12fc7397..6b42317c42 100644 --- a/client/app/visualizations/treemap/treemap.less +++ b/client/app/visualizations/treemap/treemap.less @@ -4,11 +4,11 @@ } text { - font: 10px sans-serif; + font: 12px sans-serif; } } -visualization-renderer { +treemap-renderer { .treemap-tooltip { display: none; position: absolute;