diff --git a/packages/@ourworldindata/grapher/src/dataTable/DataTable.scss b/packages/@ourworldindata/grapher/src/dataTable/DataTable.scss index 849fbe49273..29998f2a91a 100644 --- a/packages/@ourworldindata/grapher/src/dataTable/DataTable.scss +++ b/packages/@ourworldindata/grapher/src/dataTable/DataTable.scss @@ -142,21 +142,21 @@ border-color: $header-hover; } - > div { + .content { display: flex; align-items: start; justify-content: flex-end; gap: 6px; } - &.entity > div { + &.entity .content { justify-content: flex-start; } .sort-icon { color: $sort-icon; font-size: 13px; - width: 19px; + width: 19px; // keeps column widths fixed when sorting text-align: right; &.active { diff --git a/packages/@ourworldindata/grapher/src/dataTable/DataTable.tsx b/packages/@ourworldindata/grapher/src/dataTable/DataTable.tsx index 94b7faaac25..36b0ca5a571 100644 --- a/packages/@ourworldindata/grapher/src/dataTable/DataTable.tsx +++ b/packages/@ourworldindata/grapher/src/dataTable/DataTable.tsx @@ -37,6 +37,9 @@ import { Tippy, isCountryName, range, + maxBy, + minBy, + excludeUndefined, } from "@ourworldindata/utils" import { makeSelectionArray } from "../chart/ChartUtils" import { SelectionArray } from "../selection/SelectionArray" @@ -78,6 +81,7 @@ export interface DataTableManager { endTime?: Time startTime?: Time dataTableSlugs?: ColumnSlug[] + isNarrow?: boolean } @observer @@ -222,6 +226,63 @@ export class DataTable extends React.Component<{ return this.displayDimensions.length > 1 } + // If the table has a single data column, we move the data column + // closer to the entity column to make it easier to read the table + @computed private get singleDataColumnStyle(): + | { + minWidth: number + contentMaxWidth: number + } + | undefined { + // no need to do this on mobile + if (this.manager.isNarrow) return + + const hasSingleDataColumn = + this.displayDimensions.length === 1 && + this.displayDimensions[0].columns.length === 1 + + if (!hasSingleDataColumn) return + + // header text + const dimension = this.displayDimensions[0] + const column = this.displayDimensions[0].columns[0] + const headerText = this.subheaderText(column, dimension) + + // display values + const values = excludeUndefined( + this.displayRows.map( + (row) => (row?.dimensionValues[0] as SingleValue).single + ) + ) + + const accessor = (v: Value): number | undefined => + typeof v.value === "string" ? v.value.length : v.value + const maxValue = maxBy(values, accessor) + const minValue = minBy(values, accessor) + + const measureWidth = (text: string): number => + Bounds.forText(text, { fontSize: 14 }).width + + // in theory, we should be measuring the length of all values + // but we might have a lot of values, so we just measure the length + // of the min and max values as a proxy + const contentMaxWidth = Math.ceil( + Math.max( + measureWidth(maxValue?.displayValue ?? "") + 20, // 20px accounts for a possible info icon + measureWidth(minValue?.displayValue ?? "") + 20, // 20px accounts for a possible info icon + measureWidth(headerText) + 26 // 26px accounts for the sort icon + ) + ) + + // minimum width of the column + const minWidth = 0.66 * this.bounds.width + + // only do this if there is an actual need + if (minWidth - contentMaxWidth < 320) return + + return { minWidth, contentMaxWidth } + } + private get dimensionHeaders(): JSX.Element[] | null { const { sort } = this.tableState return this.displayDimensions.map((dim, dimIndex) => { @@ -263,13 +324,20 @@ export class DataTable extends React.Component<{ }) } + private subheaderText( + column: DataTableColumn, + dimension: DataTableDimension + ): string { + return isDeltaColumn(column.key) + ? columnNameByType[column.key] + : dimension.coreTableColumn.formatTime(column.targetTime!) + } + private get dimensionSubheaders(): JSX.Element[][] { const { sort } = this.tableState return this.displayDimensions.map((dim, dimIndex) => dim.columns.map((column, i) => { - const headerText = isDeltaColumn(column.key) - ? columnNameByType[column.key] - : dim.coreTableColumn.formatTime(column.targetTime!) + const headerText = this.subheaderText(column, dim) return ( ) }) @@ -339,6 +411,8 @@ export class DataTable extends React.Component<{ column.targetTime !== undefined && column.targetTime !== value.time + const { contentMaxWidth } = this.singleDataColumnStyle ?? {} + return ( - {shouldShowClosestTimeNotice && - makeClosestTimeNotice( - actualColumn.formatTime(column.targetTime!), - actualColumn.formatTime(value.time!) // todo: add back format: "MMM D", - )} - {value.displayValue} + + {shouldShowClosestTimeNotice && + makeClosestTimeNotice( + actualColumn.formatTime(column.targetTime!), + actualColumn.formatTime(value.time!) // todo: add back format: "MMM D", + )} + {value.displayValue} + ) } @@ -828,6 +904,8 @@ function ColumnHeader(props: { colType: "entity" | "dimension" | "subdimension" subdimensionType?: ColumnKey lastSubdimension?: boolean + minWidth?: number + contentMaxWidth?: number }): JSX.Element { const { sortable, sortedCol, colType, subdimensionType, lastSubdimension } = props @@ -853,24 +931,32 @@ function ColumnHeader(props: { firstSubdimension: subdimensionType === "start", endSubdimension: subdimensionType === "end", lastSubdimension, + deltaColumn: isDeltaColumn(subdimensionType), })} rowSpan={props.rowSpan ?? 1} colSpan={props.colSpan ?? 1} onClick={props.onClick} + style={{ minWidth: props.minWidth }} > -
- {!isEntityColumn && sortIcon} - {props.headerText} - {isEntityColumn && sortIcon} -
+ +
+ {!isEntityColumn && sortIcon} + {props.headerText} + {isEntityColumn && sortIcon} +
+
) } +function CellContent(props: { + maxWidth?: number + children?: React.ReactNode +}): JSX.Element { + if (!props.maxWidth) return <>{props.children} + return
{props.children}
+} + function SortIcon(props: { isActiveIcon?: boolean order: SortOrder