Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Grapher redesign: Move single data column closer to the entity column #2623

Merged
merged 1 commit into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/@ourworldindata/grapher/src/dataTable/DataTable.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
122 changes: 104 additions & 18 deletions packages/@ourworldindata/grapher/src/dataTable/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ import {
Tippy,
isCountryName,
range,
maxBy,
minBy,
excludeUndefined,
} from "@ourworldindata/utils"
import { makeSelectionArray } from "../chart/ChartUtils"
import { SelectionArray } from "../selection/SelectionArray"
Expand Down Expand Up @@ -81,6 +84,7 @@ export interface DataTableManager {
showSelectionOnlyInDataTable?: boolean
isSmall?: boolean
isMedium?: boolean
isNarrow?: boolean
}

@observer
Expand Down Expand Up @@ -238,6 +242,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) => {
Expand Down Expand Up @@ -279,13 +340,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 (
<ColumnHeader
key={column.key}
Expand All @@ -302,6 +370,10 @@ export class DataTable extends React.Component<{
colType="subdimension"
subdimensionType={column.key}
lastSubdimension={i === dim.columns.length - 1}
minWidth={this.singleDataColumnStyle?.minWidth}
contentMaxWidth={
this.singleDataColumnStyle?.contentMaxWidth
}
/>
)
})
Expand Down Expand Up @@ -355,6 +427,8 @@ export class DataTable extends React.Component<{
column.targetTime !== undefined &&
column.targetTime !== value.time

const { contentMaxWidth } = this.singleDataColumnStyle ?? {}

return (
<td
key={key}
Expand All @@ -366,12 +440,14 @@ export class DataTable extends React.Component<{
},
])}
>
{shouldShowClosestTimeNotice &&
makeClosestTimeNotice(
actualColumn.formatTime(column.targetTime!),
actualColumn.formatTime(value.time!) // todo: add back format: "MMM D",
)}
<span>{value.displayValue}</span>
<CellContent maxWidth={contentMaxWidth}>
{shouldShowClosestTimeNotice &&
makeClosestTimeNotice(
actualColumn.formatTime(column.targetTime!),
actualColumn.formatTime(value.time!) // todo: add back format: "MMM D",
)}
<span>{value.displayValue}</span>
</CellContent>
</td>
)
}
Expand Down Expand Up @@ -844,6 +920,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
Expand All @@ -869,24 +947,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 }}
>
<div
className={classnames({
deltaColumn: isDeltaColumn(subdimensionType),
})}
>
{!isEntityColumn && sortIcon}
<span>{props.headerText}</span>
{isEntityColumn && sortIcon}
</div>
<CellContent maxWidth={props.contentMaxWidth}>
<div className="content">
{!isEntityColumn && sortIcon}
<span>{props.headerText}</span>
{isEntityColumn && sortIcon}
</div>
</CellContent>
</th>
)
}

function CellContent(props: {
maxWidth?: number
children?: React.ReactNode
}): JSX.Element {
if (!props.maxWidth) return <>{props.children}</>
return <div style={{ maxWidth: props.maxWidth }}>{props.children}</div>
}

function SortIcon(props: {
isActiveIcon?: boolean
order: SortOrder
Expand Down