diff --git a/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss b/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss index 9981e76f0b5..5197ae66a36 100644 --- a/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss +++ b/packages/@ourworldindata/grapher/src/modal/DownloadModal.scss @@ -34,6 +34,48 @@ margin-top: 4px; } + .grouped-menu-list + .grouped-menu-list { + margin-top: 8px; + } + .csv-options-list { + display: flex; + flex-direction: column; + gap: 8px; + padding: 7px 0; + button { + width: 100%; + + .option-icon { + display: flex; + flex-wrap: wrap; + width: 34px; + height: 24px; + justify-content: space-between; + margin-right: 8px; + span { + // the round-rects that make up the grid + display: inline-block; + width: 100%; + height: 100%; + border-radius: 2px; + background: $light-stroke; + } + } + + &.active span { + background: #a4b6ca; + } + + &:hover:not(.active) span { + background: $light-fill; + } + + &:active:not(.active) span { + background: $light-text; + } + } + } + .grouped-menu-item { display: flex; flex-direction: row; @@ -51,9 +93,21 @@ background-color: $hover-fill; } - &:active { + &.active { background-color: $active-fill; } + + &.active span { + background: #a4b6ca; + } + + &:hover:not(.active) span { + background: $light-fill; + } + + &:active:not(.active) span { + background: $light-text; + } } .grouped-menu-icon img { diff --git a/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx b/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx index 21dd4e3822b..adab4445fd6 100644 --- a/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx +++ b/packages/@ourworldindata/grapher/src/modal/DownloadModal.tsx @@ -1,14 +1,20 @@ import React from "react" -import { observable, computed, action } from "mobx" -import { observer } from "mobx-react" import { + Url, Bounds, DEFAULT_BOUNDS, isEmpty, triggerDownloadFromBlob, triggerDownloadFromUrl, } from "@ourworldindata/utils" -import { Checkbox, OverlayHeader } from "@ourworldindata/components" +import { observable, computed, action } from "mobx" +import { observer } from "mobx-react" +import { + Checkbox, + CodeSnippet, + OverlayHeader, + MarkdownTextWrap, +} from "@ourworldindata/components" import { LoadingIndicator } from "../loadingIndicator/LoadingIndicator" import { FontAwesomeIcon } from "@fortawesome/react-fontawesome/index.js" import { faDownload, faInfoCircle } from "@fortawesome/free-solid-svg-icons" @@ -20,6 +26,7 @@ import { } from "@ourworldindata/core-table" import { Modal } from "./Modal" import { GrapherExport } from "../captionedChart/StaticChartRasterizer.js" +import classnames from "classnames" export interface DownloadModalManager { displaySlug: string @@ -39,12 +46,20 @@ export interface DownloadModalManager { isOnChartOrMapTab?: boolean framePaddingVertical?: number showAdminControls?: boolean + bakedGrapherURL?: string + sourcesLine?: string + isSourcesModalOpen?: boolean } interface DownloadModalProps { manager: DownloadModalManager } +enum CsvFilterMode { + full, + visible, +} + @observer export class DownloadModal extends React.Component { @computed private get frameBounds(): Bounds { @@ -97,6 +112,8 @@ export class DownloadModal extends React.Component { @observable private isReady: boolean = false + @observable private csvFilterMode: CsvFilterMode = CsvFilterMode.full + @action.bound private export(): void { // render the graphic then cache data-urls for display & blobs for downloads this.manager @@ -136,6 +153,15 @@ export class DownloadModal extends React.Component { return this.manager.displaySlug } + @action.bound private onToggleCsvFilterMode(): () => void { + return (): void => { + this.csvFilterMode = + this.csvFilterMode === CsvFilterMode.full + ? CsvFilterMode.visible + : CsvFilterMode.full + } + } + @computed private get inputTable(): OwidTable { return this.manager.table ?? BlankOwidTable() } @@ -194,7 +220,7 @@ export class DownloadModal extends React.Component { if (manager.externalCsvLink) { triggerDownloadFromUrl(filename, manager.externalCsvLink) } else { - triggerDownloadFromBlob(filename, this.csvBlob) + triggerDownloadFromUrl(filename, this.csvFileUrl) } } @@ -217,6 +243,49 @@ export class DownloadModal extends React.Component { return this.hasDetails || !!this.manager.showAdminControls } + @computed protected get sourcesLine(): string { + return this.manager.sourcesLine?.replace(/\r\n|\n|\r/g, "") ?? "" + } + + @computed protected get sourcesText(): string { + return `**Data source:** ${this.sourcesLine}` + } + + @computed protected get csvFileUrl(): string { + const baseUrl = `${this.manager.bakedGrapherURL || ""}/${this.manager.displaySlug}.csv` + const searchParams = new URLSearchParams([ + ...Object.entries({ csvType: "filtered" }), + ...Array.from(new URLSearchParams(this.manager.queryStr).entries()), + ]).toString() + return this.csvFilterMode === CsvFilterMode.visible + ? `${baseUrl}?${searchParams}` + : baseUrl + } + private renderSources(): JSX.Element | null { + const sources = new MarkdownTextWrap({ + text: `**Data source:** ${this.sourcesLine}`, + fontSize: 13, + }) + + return ( +

+ {sources.renderHTML()} + {" – "} + { + e.stopPropagation() + + this.manager.isDownloadModalOpen = false + this.manager.isSourcesModalOpen = true + })} + > + Learn more about this data and citations + +

+ ) + } private renderReady(): React.ReactElement { const { manager, @@ -255,6 +324,21 @@ export class DownloadModal extends React.Component { opacity: this.isReady ? 1 : 0, } + const csvUrl = this.csvFileUrl + const metadataUrl = csvUrl.replace(".csv", ".metadata.json") + + const googleDocsCode = `=IMPORTDATA("${csvUrl}")` + + const pandasCode = `import pandas as pd +import requests + +# Fetch the data +df = pd.read_csv("${csvUrl}") + +# Fetch the metadata +metadata = requests.get("${metadataUrl}").json()` + + const rCode = `df <- read.csv("${csvUrl}")` return (
{manager.isOnChartOrMapTab && ( @@ -338,14 +422,92 @@ export class DownloadModal extends React.Component {
) : ( -
- +

Source

+

+ Whenever you use this data in a public context, + please make sure to credit the original source + and to verify that your use is permitted as per + the source's license. +

+

{this.renderSources()}

+

+ Download options +

+ +
+ + +
+

Download

+
+ + +
+

Code examples

+

+ Below are examples of how to load this data into + different data analysis tools. +

+

Excel/Google Sheets

+ -
+

Python with Pandas

+ +

R

+ + )}