From cb68284b42180834b32411e2deb33d2790d39b55 Mon Sep 17 00:00:00 2001 From: Victor Malai Date: Mon, 8 Mar 2021 10:48:04 +0200 Subject: [PATCH] feat: responsive heatmap (#989) * feat: responsive heatmap * Rotate labels * Update story for heatmap chart * Add for withNullData chart * Resizable --- .../legacy-plugin-chart-heatmap/Stories.tsx | 48 ++++++- .../src/Heatmap.css | 2 +- .../src/Heatmap.js | 134 +++++++++++++----- 3 files changed, 144 insertions(+), 40 deletions(-) diff --git a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-heatmap/Stories.tsx b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-heatmap/Stories.tsx index 5e7787a8f6bf2..d04044857e9cb 100644 --- a/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-heatmap/Stories.tsx +++ b/superset-frontend/temporary_superset_ui/superset-ui/packages/superset-ui-demo/storybook/stories/plugins/legacy-plugin-chart-heatmap/Stories.tsx @@ -1,6 +1,7 @@ import React from 'react'; import { SuperChart } from '@superset-ui/core'; import HeatmapChartPlugin from '@superset-ui/legacy-plugin-chart-heatmap'; +import ResizableChartDemo from '../../../shared/components/ResizableChartDemo'; import data from './data'; new HeatmapChartPlugin().configure({ key: 'heatmap' }).register(); @@ -12,8 +13,8 @@ export default { export const basic = () => ( ( /> ); +export const resizable = () => ( + + {({ width, height }) => ( + + )} + +); + export const withNullData = () => ( b ? 1 : -1; } +const DEFAULT_PROPERTIES = { + minChartWidth: 150, + minChartHeight: 150, + marginLeft: 35, + marginBottom: 35, + marginTop: 10, + marginRight: 10, +}; + // Inspired from http://bl.ocks.org/mbostock/3074470 // https://jsfiddle.net/cyril123/h0reyumq/ function Heatmap(element, props) { @@ -97,12 +106,16 @@ function Heatmap(element, props) { bottom: 35, left: 35, }; + + let showY = true; + let showX = true; + const pixelsPerCharX = 4.5; // approx, depends on font size + const pixelsPerCharY = 6; // approx, depends on font size + const valueFormatter = getNumberFormatter(numberFormat); // Dynamically adjusts based on max x / y category lengths function adjustMargins() { - const pixelsPerCharX = 4.5; // approx, depends on font size - const pixelsPerCharY = 6; // approx, depends on font size let longestX = 1; let longestY = 1; @@ -127,6 +140,25 @@ function Heatmap(element, props) { : bottomMargin; } + // Check if x axis "x" position is outside of the container and rotate labels 90deg + function checkLabelPosition(container) { + const xAxisNode = container.select('.x.axis').node(); + + if (!xAxisNode) { + return; + } + + if (xAxisNode.getBoundingClientRect().x + 4 < container.node().getBoundingClientRect().x) { + container + .selectAll('.x.axis') + .selectAll('text') + .attr('transform', 'rotate(-90)') + .attr('x', -6) + .attr('y', 0) + .attr('dy', '0.3em'); + } + } + function ordScale(k, rangeBands, sortMethod) { let domain = {}; const actualKeys = {}; // hack to preserve type of keys when number @@ -163,8 +195,34 @@ function Heatmap(element, props) { adjustMargins(); - const hmWidth = width - (margin.left + margin.right); - const hmHeight = height - (margin.bottom + margin.top); + let hmWidth = width - (margin.left + margin.right); + let hmHeight = height - (margin.bottom + margin.top); + const hideYLabel = () => { + margin.left = leftMargin === 'auto' ? DEFAULT_PROPERTIES.marginLeft : leftMargin; + hmWidth = width - (margin.left + margin.right); + showY = false; + }; + + const hideXLabel = () => { + margin.bottom = bottomMargin === 'auto' ? DEFAULT_PROPERTIES.marginBottom : bottomMargin; + hmHeight = height - (margin.bottom + margin.top); + showX = false; + }; + + // Hide Y Labels + if (hmWidth < DEFAULT_PROPERTIES.minChartWidth) { + hideYLabel(); + } + + // Hide X Labels + if (hmHeight < DEFAULT_PROPERTIES.minChartHeight || hmWidth < DEFAULT_PROPERTIES.minChartWidth) { + hideXLabel(); + } + + if (showY && hmHeight < DEFAULT_PROPERTIES.minChartHeight) { + hideYLabel(); + } + const fp = getNumberFormatter(NumberFormats.PERCENT); const xScale = ordScale('x', null, sortXAxis); @@ -204,6 +262,7 @@ function Heatmap(element, props) { .append('svg') .attr('width', width) .attr('height', height) + .attr('class', 'heatmap-container') .style('position', 'relative'); if (showValues) { @@ -287,38 +346,43 @@ function Heatmap(element, props) { rect.call(tip); - const xAxis = d3.svg - .axis() - .scale(xRbScale) - .outerTickSize(0) - .tickValues(xRbScale.domain().filter((d, i) => !(i % xScaleInterval))) - .orient('bottom'); - - const yAxis = d3.svg - .axis() - .scale(yRbScale) - .outerTickSize(0) - .tickValues(yRbScale.domain().filter((d, i) => !(i % yScaleInterval))) - .orient('left'); - - svg - .append('g') - .attr('class', 'x axis') - .attr('transform', `translate(${margin.left},${margin.top + hmHeight})`) - .call(xAxis) - .selectAll('text') - .attr('x', -4) - .attr('y', 10) - .attr('dy', '0.3em') - .style('text-anchor', 'end') - .attr('transform', 'rotate(-45)'); - - svg - .append('g') - .attr('class', 'y axis') - .attr('transform', `translate(${margin.left},${margin.top})`) - .call(yAxis); + if (showX) { + const xAxis = d3.svg + .axis() + .scale(xRbScale) + .outerTickSize(0) + .tickValues(xRbScale.domain().filter((d, i) => !(i % xScaleInterval))) + .orient('bottom'); + + svg + .append('g') + .attr('class', 'x axis') + .attr('transform', `translate(${margin.left},${margin.top + hmHeight})`) + .call(xAxis) + .selectAll('text') + .attr('x', -4) + .attr('y', 10) + .attr('dy', '0.3em') + .style('text-anchor', 'end') + .attr('transform', 'rotate(-45)'); + } + + if (showY) { + const yAxis = d3.svg + .axis() + .scale(yRbScale) + .outerTickSize(0) + .tickValues(yRbScale.domain().filter((d, i) => !(i % yScaleInterval))) + .orient('left'); + + svg + .append('g') + .attr('class', 'y axis') + .attr('transform', `translate(${margin.left},${margin.top})`) + .call(yAxis); + } + checkLabelPosition(container); const context = canvas.node().getContext('2d'); context.imageSmoothingEnabled = false;