Skip to content

Commit

Permalink
✨ (entity selector) render into panel, drawer or modal (#3373)
Browse files Browse the repository at this point in the history
[Cycle 2024.2: Entity selector](#3349) | [Designs](https://www.figma.com/file/X5mOEX8zULS6qyHocUYdmh/Grapher-UI?type=design&node-id=2523%3A6266&mode=design&t=7edFp79OOjz6RENz-1)

## Summary

Renders the entity selector either into...
- a side panel next to the chart
- a drawer that slides in from the right
- or a modal

The entity selector itself hasn't been touched but will be redesigned in subsequent PRs.

<details><summary><b>Screenshots</b></summary>
<p>

![Screenshot 2024-03-21 at 10 10 14](https://github.com/owid/owid-grapher/assets/12461810/de7bc544-15c8-4d61-a92b-718b4d3431f6)

![Screenshot 2024-03-21 at 10 10 42](https://github.com/owid/owid-grapher/assets/12461810/59829930-401c-47e3-b5fc-4b7ede6fbc58)

![Screenshot 2024-03-21 at 10 11 25](https://github.com/owid/owid-grapher/assets/12461810/75a810e8-7e5d-4e95-b03f-858759d4394d)

</p>
</details> 

## Details

- Bounds (see screenshot below)
    - `frameBounds` are the bounds of the whole Grapher frame (including the side panel if it exists)
    - `captionedChartBounds` are the bounds of the `<CaptionedChart />` component that renders the chart itself and its header and footer
    - `sidePanelBounds` are the bounds of the side panel (if present)
- New components
    - `EntitySelector` has been broken out of `EntitySelectorModal` so that we can render it into different spaces
    - `SidePanel` and `SlideInDrawer` are both utility components that don't know anything about the content they render
        - `SlideInDrawer` renders a drawer outside of Grapher that slides in on request (it works exactly like the slide-in drawer for the settings menu used to work)
        - `SidePanel` renders a panel to the right of the chart

<details><summary><b>Screenshot</b></summary>
<p>

![Screenshot 2024-03-21 at 10 56 40](https://github.com/owid/owid-grapher/assets/12461810/f2dff3c6-2e9b-4bb9-9ee2-0954826fb9b5)

</p>
</details>     


## Caveats

- Ideal bounds:
    - Grapher uses ideal bounds on Grapher pages
    - If the side panel is visible, then the ideal bounds should apply to the captioned chart, not the whole frame, since we care about the aspect ratio of the chart
    - Making it so that the `captionedChartBounds` are ideal (rather than the `frameBounds`) makes it more difficult to mirror that behaviour in CSS
    - Since making this work is not trivial, and in theory the design suggests that Grapher should be rendered into a 12-column grid anyway, I decided to look into sizing on Grapher pages at the end of the project

## Notes for the reviewer

- It looks like a big PR, but most of it comes down to moving code around
- I didn't spend any time making the current (old) design work when rendered into the side panel or drawer since it will be redesigned in subsequent PRs
- There is no need to thoroughly review the CSS in particular (it's mostly just copy-pasted from other places or will change in the near future)
- I removed the behaviour where clicking on an entity name opened the entity selector (it was difficult to discover for users and also inconsistent across charts (this only worked for line charts and stacked area charts))
- That's also the reason that the SVG tester fails for all line charts and all stacked area charts
  • Loading branch information
sophiamersmann authored May 3, 2024
1 parent 6a13031 commit df7e545
Show file tree
Hide file tree
Showing 38 changed files with 1,127 additions and 632 deletions.
2 changes: 1 addition & 1 deletion adminSiteClient/ChartEditorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ export class ChartEditorPage
@computed private get bounds(): Bounds {
return this.isMobilePreview
? new Bounds(0, 0, 380, 525)
: this.grapher.idealBounds
: this.grapher.defaultBounds
}

@computed private get staticFormat(): GrapherStaticFormat {
Expand Down
4 changes: 2 additions & 2 deletions baker/GrapherImageBaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export async function bakeGraphersToPngs(
.then(() => console.log(`${outPath}.svg`)),
sharp(Buffer.from(grapher.staticSVG), { density: 144 })
.png()
.resize(grapher.idealBounds.width, grapher.idealBounds.height)
.resize(grapher.defaultBounds.width, grapher.defaultBounds.height)
.flatten({ background: "#ffffff" })
.toFile(`${outPath}.png`),
])
Expand Down Expand Up @@ -102,7 +102,7 @@ export async function bakeGrapherToSvg(
verbose = true
) {
const grapher = initGrapherForSvgExport(jsonConfig, queryStr)
const { width, height } = grapher.idealBounds
const { width, height } = grapher.defaultBounds
const outPath = buildSvgOutFilepath(
outDir,
{
Expand Down
2 changes: 1 addition & 1 deletion devTools/svgTester/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ export async function renderSvg(
},
queryStr
)
const { width, height } = grapher.idealBounds
const { width, height } = grapher.defaultBounds
const outFilename = buildSvgOutFilename(
{
slug: configAndData.config.slug!,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rules:
"@typescript-eslint/explicit-function-return-type": "warn"
"@typescript-eslint/explicit-module-boundary-types": "warn"
32 changes: 32 additions & 0 deletions packages/@ourworldindata/grapher/src/bodyPortal/BodyPortal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from "react"
import ReactDOM from "react-dom"

interface BodyPortalProps {
id?: string
tagName?: string // default: "div"
children: React.ReactNode
}

// Render a component on the Body instead of inside the current Tree.
// https://reactjs.org/docs/portals.html
export class BodyPortal extends React.Component<BodyPortalProps> {
el: HTMLElement

constructor(props: BodyPortalProps) {
super(props)
this.el = document.createElement(props.tagName || "div")
if (props.id) this.el.id = props.id
}

componentDidMount(): void {
document.body.appendChild(this.el)
}

componentWillUnmount(): void {
document.body.removeChild(this.el)
}

render(): any {
return ReactDOM.createPortal(this.props.children, this.el)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// keep in sync with constant values in CaptionedChart.tsx
$controlRowHeight: 32px; // keep in sync with CONTROLS_ROW_HEIGHT

.CaptionedChart {
width: 100%;
}

.HeaderHTML,
.SourcesFooterHTML {
font-family: $sans-serif-font-stack;
Expand All @@ -22,7 +26,6 @@ $controlRowHeight: 32px; // keep in sync with CONTROLS_ROW_HEIGHT
align-items: center;
border-top: 1px solid $frame-color;
position: absolute;
width: 100%;
bottom: 0;
color: $dark-text;
font-weight: 700;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default {
const table = SynthesizeGDPTable({ entityCount: 5 })

const manager: CaptionedChartManager = {
tabBounds: DEFAULT_BOUNDS,
captionedChartBounds: DEFAULT_BOUNDS,
table,
selection: table.availableEntityNames,
currentTitle: "This is the Title",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ import {
GrapherTabOption,
RelatedQuestionsConfig,
} from "@ourworldindata/types"
import { AxisConfig } from "../axis/AxisConfig"
import { DataTable, DataTableManager } from "../dataTable/DataTable"
import {
ContentSwitchers,
Expand All @@ -68,33 +67,42 @@ export interface CaptionedChartManager
MapProjectionMenuManager,
SettingsMenuManager {
containerElement?: HTMLDivElement
tabBounds?: Bounds
bakedGrapherURL?: string
isReady?: boolean
whatAreWeWaitingFor?: string

// bounds
captionedChartBounds?: Bounds
sidePanelBounds?: Bounds
staticBounds?: Bounds
staticBoundsWithDetails?: Bounds

// layout
isSmall?: boolean
isMedium?: boolean
framePaddingHorizontal?: number
framePaddingVertical?: number
fontSize?: number
bakedGrapherURL?: string

// state
tab?: GrapherTabOption
type: ChartTypeName
yAxis: AxisConfig
xAxis: AxisConfig
typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart?: ChartTypeName
isReady?: boolean
whatAreWeWaitingFor?: string
entityType?: string
entityTypePlural?: string
shouldIncludeDetailsInStaticExport?: boolean
detailRenderers: MarkdownTextWrap[]
isOnMapTab?: boolean
isOnTableTab?: boolean
type: ChartTypeName
typeExceptWhenLineChartAndSingleTimeThenWillBeBarChart?: ChartTypeName
showEntitySelectionToggle?: boolean

// timeline
hasTimeline?: boolean
timelineController?: TimelineController
hasRelatedQuestion?: boolean
isRelatedQuestionTargetDifferentFromCurrentPage?: boolean

// details on demand
shouldIncludeDetailsInStaticExport?: boolean
detailRenderers: MarkdownTextWrap[]

// related question
relatedQuestions?: RelatedQuestionsConfig[]
isSmall?: boolean
isMedium?: boolean
framePaddingHorizontal?: number
framePaddingVertical?: number
showRelatedQuestion?: boolean
}

interface CaptionedChartProps {
Expand Down Expand Up @@ -144,10 +152,6 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
return this.manager.isMedium ? 8 : 16
}

@computed protected get relatedQuestionHeight(): number {
return this.manager.isMedium ? 24 : 28
}

@computed protected get header(): Header {
return new Header({
manager: this.manager,
Expand Down Expand Up @@ -181,7 +185,9 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {

@computed protected get bounds(): Bounds {
const bounds =
this.props.bounds ?? this.manager.tabBounds ?? DEFAULT_BOUNDS
this.props.bounds ??
this.manager.captionedChartBounds ??
DEFAULT_BOUNDS
// the padding ensures grapher's frame is not cut off
return bounds.padRight(2).padBottom(2)
}
Expand Down Expand Up @@ -258,28 +264,36 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
return this.manager.selection
}

@computed get showRelatedQuestion(): boolean {
return (
!!this.manager.relatedQuestions &&
!!this.manager.hasRelatedQuestion &&
!!this.manager.isRelatedQuestionTargetDifferentFromCurrentPage
)
@computed private get showRelatedQuestion(): boolean {
return !!this.manager.showRelatedQuestion
}

@computed get relatedQuestionHeight(): number {
if (!this.showRelatedQuestion) return 0
return this.manager.isMedium ? 24 : 28
}

@computed private get sidePanelWidth(): number {
return this.manager.sidePanelBounds?.width ?? 0
}

private renderControlsRow(): JSX.Element {
const { showContentSwitchers } = this
const { showEntitySelectionToggle } = this.manager
return (
<nav
className="controlsRow"
style={{ padding: `0 ${this.framePaddingHorizontal}px` }}
>
<div>
{showContentSwitchers && (
{this.showContentSwitchers && (
<ContentSwitchers manager={this.manager} />
)}
</div>
<div className="chart-controls">
<EntitySelectionToggle manager={this.manager} />
{showEntitySelectionToggle && (
<EntitySelectionToggle manager={this.manager} />
)}

<SettingsMenu
manager={this.manager}
top={
Expand All @@ -290,6 +304,9 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
4 // margin between button and menu
}
bottom={this.framePaddingVertical}
right={
this.sidePanelWidth + this.framePaddingHorizontal
}
/>
<MapProjectionMenu manager={this.manager} />
</div>
Expand All @@ -303,6 +320,7 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
<div
className="relatedQuestion"
style={{
width: this.bounds.width,
height: this.relatedQuestionHeight,
padding: `0 ${this.framePaddingHorizontal}px`,
}}
Expand Down Expand Up @@ -433,7 +451,7 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {
// #5 Footer
// #6 [Related question]
return (
<>
<div className="CaptionedChart">
{/* #1 Header */}
<Header manager={this.manager} maxWidth={this.maxWidth} />
<VerticalSpace height={this.verticalPadding} />
Expand Down Expand Up @@ -461,7 +479,7 @@ export class CaptionedChart extends React.Component<CaptionedChartProps> {

{/* #6 [Related question] */}
{this.showRelatedQuestion && this.renderRelatedQuestion()}
</>
</div>
)
}

Expand Down
2 changes: 0 additions & 2 deletions packages/@ourworldindata/grapher/src/chart/ChartManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,6 @@ export interface ChartManager {
table: OwidTable
transformedTable?: OwidTable

isSelectingData?: boolean
startSelectingWhenLineClicked?: boolean // used by lineLabels
isExportingToSvgOrPng?: boolean
isRelativeMode?: boolean
comparisonLines?: ComparisonLineConfig[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import {
import classnames from "classnames"

export interface EntitySelectionManager {
showSelectEntitiesButton?: boolean
showChangeEntityButton?: boolean
showAddEntityButton?: boolean
canHighlightEntities?: boolean
canChangeEntity?: boolean
canAddEntities?: boolean
entityType?: string
entityTypePlural?: string
isSelectingData?: boolean
isEntitySelectorModalOrDrawerOpen?: boolean
isOnChartTab?: boolean
}

Expand Down Expand Up @@ -43,42 +43,43 @@ export class EntitySelectionToggle extends React.Component<{
const {
entityType = "",
entityTypePlural = "",
showSelectEntitiesButton,
showChangeEntityButton,
showAddEntityButton,
canHighlightEntities,
canChangeEntity,
canAddEntities,
} = this.props.manager

return showSelectEntitiesButton
return canHighlightEntities
? {
action: "Select",
entity: entityTypePlural,
icon: <FontAwesomeIcon icon={faEye} />,
}
: showChangeEntityButton
? {
action: "Change",
entity: entityType,
icon: <FontAwesomeIcon icon={faRightLeft} />,
}
: showAddEntityButton
? {
action: "Edit",
entity: entityTypePlural,
icon: <FontAwesomeIcon icon={faPencilAlt} />,
}
: null
: canChangeEntity
? {
action: "Change",
entity: entityType,
icon: <FontAwesomeIcon icon={faRightLeft} />,
}
: canAddEntities
? {
action: "Edit",
entity: entityTypePlural,
icon: <FontAwesomeIcon icon={faPencilAlt} />,
}
: null
}

render(): JSX.Element | null {
const { showToggle, label } = this
const { isSelectingData: active } = this.props.manager
const { isEntitySelectorModalOrDrawerOpen: active } = this.props.manager

return showToggle && label ? (
<div className="entity-selection-menu">
<button
className={classnames("menu-toggle", { active })}
onClick={(e): void => {
this.props.manager.isSelectingData = !active
this.props.manager.isEntitySelectorModalOrDrawerOpen =
!active
e.stopPropagation()
}}
type="button"
Expand Down
Loading

0 comments on commit df7e545

Please sign in to comment.