From a6faeeb656c82249f181ce9ec935f7b4aa46c770 Mon Sep 17 00:00:00 2001 From: Albert Backenhof Date: Mon, 27 May 2019 13:31:20 +0200 Subject: [PATCH] Add option to fit cells to chart width -To achieve pixel perfection the container of both header and data needs to calculate the size of the child tables' cells. Issue: QLIK-95962 --- src/data-table/data-cell.jsx | 15 +- src/data-table/index.jsx | 178 +++++++++-------- src/definition/table-format.js | 20 +- src/headers-table/column-header.jsx | 4 +- src/headers-table/index.jsx | 189 +++++++++--------- .../measurement-column-header.jsx | 36 ++-- src/initialize-transformed.js | 21 +- src/main.less | 4 +- src/root.jsx | 162 ++++++++++----- src/utilities.js | 4 +- 10 files changed, 367 insertions(+), 266 deletions(-) diff --git a/src/data-table/data-cell.jsx b/src/data-table/data-cell.jsx index 27f11f8..6e38a66 100644 --- a/src/data-table/data-cell.jsx +++ b/src/data-table/data-cell.jsx @@ -42,7 +42,7 @@ class DataCell extends React.PureComponent { render () { const { - general, + cellWidth, measurement, styleBuilder, styling @@ -55,17 +55,15 @@ class DataCell extends React.PureComponent { ...styleBuilder.getStyle(), paddingLeft: '5px', textAlign: textAlignment, - minWidth: general.cellWidth, - maxWidth: general.cellWidth + minWidth: cellWidth, + maxWidth: cellWidth }; const isEmptyCell = measurement.displayValue === ''; let formattedMeasurementValue; - if (isEmptyCell) { + if (isEmptyCell || styleBuilder.hasComments()) { formattedMeasurementValue = ''; cellStyle.cursor = 'default'; - } else if (styleBuilder.hasComments()) { - formattedMeasurementValue = '.'; } else { formattedMeasurementValue = formatMeasurementValue(measurement, styling); } @@ -104,6 +102,7 @@ class DataCell extends React.PureComponent { } DataCell.propTypes = { + cellWidth: PropTypes.string.isRequired, data: PropTypes.shape({ headers: PropTypes.shape({ dimension1: PropTypes.array.isRequired, @@ -114,9 +113,7 @@ DataCell.propTypes = { dimensionCount: PropTypes.number.isRequired }).isRequired }).isRequired, - general: PropTypes.shape({ - cellWidth: PropTypes.string.isRequired - }).isRequired, + general: PropTypes.shape({}).isRequired, measurement: PropTypes.shape({ format: PropTypes.string, name: PropTypes.string, diff --git a/src/data-table/index.jsx b/src/data-table/index.jsx index a798e53..b4d7116 100644 --- a/src/data-table/index.jsx +++ b/src/data-table/index.jsx @@ -5,102 +5,114 @@ import DataCell from './data-cell.jsx'; import RowHeader from './row-header.jsx'; import { injectSeparators } from '../utilities'; -const DataTable = ({ data, general, component, renderData, styling }) => { - const { - headers: { - dimension1, - measurements - }, - matrix - } = data; +class DataTable extends React.PureComponent { + render () { + const { + cellWidth, + columnSeparatorWidth, + component, + data, + general, + renderData, + styling + } = this.props; - return ( -
- - - {dimension1.map((dimensionEntry, dimensionIndex) => { - const rowHeaderText = dimensionEntry.displayValue || ''; - if (rowHeaderText === '-') { - return null; - } - const styleBuilder = new StyleBuilder(styling); - if (styling.hasCustomFileStyle) { - styleBuilder.parseCustomFileStyle(rowHeaderText); - } else { - styleBuilder.applyStandardAttributes(dimensionIndex); - styleBuilder.applyCustomStyle({ - fontSize: `${14 + styling.options.fontSizeAdjustment}px` - }); - } - const rowStyle = { - fontFamily: styling.options.fontFamily, - width: '230px', - ...styleBuilder.getStyle() - }; + const { + headers: { + dimension1, + measurements + }, + matrix + } = data; - return ( - - {!renderData ? - : null - } - {renderData && injectSeparators( - matrix[dimensionIndex], - styling.useSeparatorColumns, - { atEvery: measurements.length } - ).map((measurementData, index) => { - if (measurementData.isSeparator) { - const separatorStyle = { - color: 'white', - fontFamily: styling.options.fontFamily, - fontSize: `${12 + styling.options.fontSizeAdjustment}px` - }; + const separatorStyle = { + minWidth: columnSeparatorWidth, + maxWidth: columnSeparatorWidth + }; - return ( - - ); - } + return ( +
+
- * -
+ + {dimension1.map((dimensionEntry, dimensionIndex) => { + const rowHeaderText = dimensionEntry.displayValue || ''; + if (rowHeaderText === '-') { + return null; + } + const styleBuilder = new StyleBuilder(styling); + if (styling.hasCustomFileStyle) { + styleBuilder.parseCustomFileStyle(rowHeaderText); + } else { + styleBuilder.applyStandardAttributes(dimensionIndex); + styleBuilder.applyCustomStyle({ + fontSize: `${14 + styling.options.fontSizeAdjustment}px` + }); + } + const rowStyle = { + fontFamily: styling.options.fontFamily, + width: '230px', + ...styleBuilder.getStyle() + }; - const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents; - const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}-${measurement.index}`; - return ( - + {!renderData ? + - ); - })} - - ); - })} - -
-
- ); -}; + /> : null + } + {renderData && injectSeparators( + matrix[dimensionIndex], + columnSeparatorWidth, + { atEvery: measurements.length } + ).map((measurementData, index) => { + if (measurementData.isSeparator) { + return ( + + ); + } + + const { dimension1: dimension1Info, dimension2, measurement } = measurementData.parents; + const id = `${dimension1Info.elementNumber}-${dimension2 && dimension2.elementNumber}-${measurement.header}-${measurement.index}`; + return ( + + ); + })} + + ); + })} + + + + ); + } +} DataTable.defaultProps = { renderData: true }; DataTable.propTypes = { + cellWidth: PropTypes.string.isRequired, + columnSeparatorWidth: PropTypes.string.isRequired, data: PropTypes.shape({ headers: PropTypes.shape({ dimension1: PropTypes.array.isRequired diff --git a/src/definition/table-format.js b/src/definition/table-format.js index 726e09f..98f29f0 100644 --- a/src/definition/table-format.js +++ b/src/definition/table-format.js @@ -209,6 +209,23 @@ const tableFormat = { ], defaultValue: 'right' }, + FitChartWidth: { + ref: 'fitchartwidth', + type: 'boolean', + component: 'switch', + label: 'Fill chart width', + options: [ + { + value: true, + label: 'On' + }, + { + value: false, + label: 'Off' + } + ], + defaultValue: false + }, ColumnWidthSlider: { type: 'number', component: 'slider', @@ -217,7 +234,8 @@ const tableFormat = { min: 20, max: 250, step: 10, - defaultValue: 50 + defaultValue: 50, + show: data => !data.fitchartwidth }, SymbolForNulls: { ref: 'symbolfornulls', diff --git a/src/headers-table/column-header.jsx b/src/headers-table/column-header.jsx index b782034..cd9b8f7 100644 --- a/src/headers-table/column-header.jsx +++ b/src/headers-table/column-header.jsx @@ -17,7 +17,7 @@ class ColumnHeader extends React.PureComponent { } render () { - const { baseCSS, cellWidth, colSpan, entry, styling, component } = this.props; + const { baseCSS, cellWidth, colSpan, component, entry, styling } = this.props; const inEditState = component.inEditState(); const isMediumFontSize = styling.headerOptions.fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM; @@ -55,7 +55,7 @@ ColumnHeader.defaultProps = { ColumnHeader.propTypes = { baseCSS: PropTypes.shape({}).isRequired, - cellWidth: PropTypes.string, + cellWidth: PropTypes.string.isRequired, colSpan: PropTypes.number, entry: PropTypes.shape({ displayValue: PropTypes.string.isRequired, diff --git a/src/headers-table/index.jsx b/src/headers-table/index.jsx index c2ff861..0dabad3 100644 --- a/src/headers-table/index.jsx +++ b/src/headers-table/index.jsx @@ -5,121 +5,124 @@ import ColumnHeader from './column-header.jsx'; import MeasurementColumnHeader from './measurement-column-header.jsx'; import { injectSeparators } from '../utilities'; -const HeadersTable = ({ data, general, component, styling, isKpi }) => { - const baseCSS = { - backgroundColor: styling.headerOptions.colorSchema, - color: styling.headerOptions.textColor, - fontFamily: styling.options.fontFamily, - textAlign: styling.headerOptions.alignment - }; +class HeadersTable extends React.PureComponent { + render () { + const { + cellWidth, + columnSeparatorWidth, + component, + data, + general, + isKpi, + styling + } = this.props; - const { - dimension1, - dimension2, - measurements - } = data.headers; + const baseCSS = { + backgroundColor: styling.headerOptions.colorSchema, + color: styling.headerOptions.textColor, + fontFamily: styling.options.fontFamily, + textAlign: styling.headerOptions.alignment + }; - const hasSecondDimension = dimension2.length > 0; + const { + dimension1, + dimension2, + measurements + } = data.headers; - return ( -
- - - - {isKpi ? - : null - } - {!isKpi && !hasSecondDimension && measurements.map(measurementEntry => ( - - ))} - {!isKpi && hasSecondDimension && injectSeparators(dimension2, styling.useSeparatorColumns).map((entry, index) => { - if (entry.isSeparator) { - const separatorStyle = { - color: 'white', - fontFamily: styling.options.fontFamily, - fontSize: `${13 + styling.headerOptions.fontSizeAdjustment}px` - }; + const hasSecondDimension = dimension2.length > 0; - return ( - - ); - } - return ( - +
- * -
+ + + {isKpi ? + : null + } + {!isKpi && !hasSecondDimension && measurements.map(measurementEntry => ( + - ); - })} - - {!isKpi && hasSecondDimension && ( - - {injectSeparators(dimension2, styling.useSeparatorColumns).map((dimensionEntry, index) => { - if (dimensionEntry.isSeparator) { - const separatorStyle = { - color: 'white', - fontFamily: styling.options.fontFamily, - fontSize: `${12 + styling.headerOptions.fontSizeAdjustment}px` - }; - + ))} + {!isKpi && hasSecondDimension && injectSeparators(dimension2, columnSeparatorWidth).map((entry, index) => { + if (entry.isSeparator) { return ( + /> ); } - return measurements.map(measurementEntry => ( - - )); + ); })} - )} - -
- * -
-
- ); -}; + {!isKpi && hasSecondDimension && ( + + {injectSeparators(dimension2, columnSeparatorWidth).map((dimensionEntry, index) => { + if (dimensionEntry.isSeparator) { + return ( + + ); + } + return measurements.map(measurementEntry => ( + + )); + })} + + )} + + + + ); + } +} HeadersTable.propTypes = { + cellWidth: PropTypes.string.isRequired, + columnSeparatorWidth: PropTypes.string.isRequired, data: PropTypes.shape({ headers: PropTypes.shape({ dimension1: PropTypes.array, diff --git a/src/headers-table/measurement-column-header.jsx b/src/headers-table/measurement-column-header.jsx index 1f9d716..0b5ce5f 100644 --- a/src/headers-table/measurement-column-header.jsx +++ b/src/headers-table/measurement-column-header.jsx @@ -3,25 +3,27 @@ import PropTypes from 'prop-types'; import { HEADER_FONT_SIZE } from '../initialize-transformed'; import Tooltip from '../tooltip/index.jsx'; -const MeasurementColumnHeader = ({ baseCSS, general, hasSecondDimension, measurement, styling }) => { +const MeasurementColumnHeader = ({ baseCSS, cellWidth, hasSecondDimension, measurement, styling }) => { const title = `${measurement.name}`; const { fontSizeAdjustment } = styling.headerOptions; const isMediumFontSize = fontSizeAdjustment === HEADER_FONT_SIZE.MEDIUM; + const cellStyle = { + ...baseCSS, + verticalAlign: 'middle', + minWidth: cellWidth, + maxWidth: cellWidth + }; + if (hasSecondDimension) { const isPercentageFormat = measurement.format.substring(measurement.format.length - 1) === '%'; let baseFontSize = 14; if (isPercentageFormat) { baseFontSize = 13; } - const cellStyle = { - ...baseCSS, - fontSize: `${baseFontSize + fontSizeAdjustment}px`, - height: isMediumFontSize ? '45px' : '35px', - verticalAlign: 'middle', - minWidth: general.cellWidth, - maxWidth: general.cellWidth - }; + cellStyle.fontSize = `${baseFontSize + fontSizeAdjustment}px`; + cellStyle.height = isMediumFontSize ? '45px' : '35px'; + return ( 10 ? layout.columnwidthslider : 60}px`; + } + // top level properties could be reducers and then components connect to grab what they want, // possibly with reselect for some presentational transforms (moving some of the presentational logic like formatting and such) const transformedProperties = { @@ -245,13 +257,13 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay general: { allowExcelExport: layout.allowexportxls, allowFilteringByClick: layout.filteroncellclick, - // If using the previous solution just set 60px - cellWidth: `${layout.columnwidthslider > 10 ? layout.columnwidthslider : 60}px`, + cellWidth: cellWidth, errorMessage: layout.errormessage, footnote: layout.footnote, maxLoops, subtitle: layout.subtitle, - title: layout.title + title: layout.title, + useColumnSeparator: layout.separatorcols && dimensionCount > 1 }, selection: { dimensionSelectionCounts: dimensionsInformation.map(dimensionInfo => dimensionInfo.qStateCounts.qSelected) @@ -305,8 +317,7 @@ function initializeTransformed ({ $element, component, dataCube, designList, lay } }, symbolForNulls: layout.symbolfornulls, - usePadding: layout.indentbool, - useSeparatorColumns: dimensionCount === 1 ? false : layout.separatorcols + usePadding: layout.indentbool } }; diff --git a/src/main.less b/src/main.less index 569c3ff..384a250 100644 --- a/src/main.less +++ b/src/main.less @@ -57,10 +57,8 @@ } .empty { - width: 3%; background: #fff; - min-width: 4px !important; - max-width: 4px !important; + padding: 0 !important; } th.main-kpi { diff --git a/src/root.jsx b/src/root.jsx index c202ce7..b66be8f 100644 --- a/src/root.jsx +++ b/src/root.jsx @@ -4,61 +4,129 @@ import HeadersTable from './headers-table/index.jsx'; import DataTable from './data-table/index.jsx'; import { LinkedScrollWrapper, LinkedScrollSection } from './linked-scroll'; -const Root = ({ state, component, editmodeClass }) => ( -
- -
- - - - -
-
- - - - - - +class Root extends React.PureComponent { + constructor (props) { + super(props); + this.onDataTableRefSet = this.onDataTableRefSet.bind(this); + this.renderedTableWidth = 0; + } + + componentDidUpdate () { + const tableWidth = this.dataTableRef.getBoundingClientRect().width; + if (this.renderedTableWidth !== tableWidth) { + this.forceUpdate(); + } + } + + onDataTableRefSet (element) { + this.dataTableRef = element; + this.forceUpdate(); + } + + render () { + const { editmodeClass, component, state } = this.props; + const { data, general, styling } = state; + + // Determine cell- and column separator width + let cellWidth = '0px'; + let columnSeparatorWidth = ''; + if (this.dataTableRef) { + const tableWidth = this.dataTableRef.getBoundingClientRect().width; + this.renderedTableWidth = tableWidth; + + if (general.cellWidth) { + cellWidth = general.cellWidth; + if (general.useColumnSeparator) { + columnSeparatorWidth = '8px'; + } + } else { + const headerMarginRight = 8; + const borderWidth = 1; + const rowCellCount = data.matrix[0].length; + + let separatorCount = 0; + let separatorWidth = 0; + if (general.useColumnSeparator) { + separatorCount = data.headers.dimension2.length - 1; + separatorWidth = Math.min(Math.floor(tableWidth * 0.2 / separatorCount), 8); + columnSeparatorWidth = `${separatorWidth}px`; + } + + const separatorWidthSum = (separatorWidth + borderWidth) * separatorCount; + cellWidth = `${Math.floor((tableWidth - separatorWidthSum - headerMarginRight - borderWidth) + / rowCellCount) - borderWidth}px`; + } + } + + return ( +
+ +
+ + + + +
+
+ + + + + + +
+
- -
-); + ); + } +} Root.propTypes = { component: PropTypes.shape({}).isRequired, + editmodeClass: PropTypes.string.isRequired, state: PropTypes.shape({ data: PropTypes.object.isRequired, general: PropTypes.object.isRequired, styling: PropTypes.object.isRequired - }).isRequired, - editmodeClass: PropTypes.string.isRequired + }).isRequired }; export default Root; diff --git a/src/utilities.js b/src/utilities.js index 407c511..928269c 100644 --- a/src/utilities.js +++ b/src/utilities.js @@ -16,7 +16,7 @@ export function Deferred () { }); } -export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) { +export function injectSeparators (array, columnSeparatorWidth, suppliedOptions) { const defaultOptions = { atEvery: 1, separator: { isSeparator: true } @@ -26,7 +26,7 @@ export function injectSeparators (array, shouldHaveSeparator, suppliedOptions) { ...suppliedOptions }; - if (!shouldHaveSeparator) { + if (!columnSeparatorWidth) { return array; } return array.reduce((result, entry, index) => {