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

Add "embed this chart" modal to map tiles #4782

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
5 changes: 5 additions & 0 deletions static/css/base/_color.scss
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,9 @@ $dc-primary-color: #467bd5;
--button-background-color: var(--gm-3-white);
--button-highlight-background-color: #ecf2fc;
--table-link-color: var(--link-color);
--button-emphasized-text-color: var(--gm-3-white);
--button-emphasized-background-color: var(--gm-3-sys-light-primary);
--button-emphasized-highlight-background-color: var(
--gm-3-ref-secondary-secondary-30
);
}
10 changes: 5 additions & 5 deletions static/css/shared/_metadata_modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
@use "./modal.scss";
@use "../base";


.metadata-modal {
// TODO(beets): Apply these styles to all modals.
.modal-content {
border: none;
border-radius: 28px;
}
.modal-header {
Expand Down Expand Up @@ -53,7 +53,7 @@
padding: 0 24px 12px;

/* body/medium */
font-size: .9rem;
font-size: 0.9rem;
font-weight: 400;
line-height: 1.4;
}
Expand All @@ -65,7 +65,7 @@
color: var(--gm-3-sys-light-primary);

/* label/large */
font-size: .9rem;
font-size: 0.9rem;
font-style: normal;
font-weight: 500;
line-height: 1.4;
Expand All @@ -91,7 +91,7 @@

a {
/* button/button */
font-size: .9rem;
font-size: 0.9rem;
font-style: normal;
font-weight: 500;
line-height: 1.4;
Expand All @@ -100,4 +100,4 @@
}
}
}
}
}
14 changes: 9 additions & 5 deletions static/css/shared/modal.scss
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@
@import "node_modules/bootstrap/scss/variables";
@import "node_modules/bootstrap/scss/mixins";

$border: 0.5px solid #dee2e6;
$border: 1px solid #dadce0;

.modal textarea {
background: #efefef;
border-radius: 3px;
background: #fff;
border-radius: 8px;
border: $border;
display: flex;
font-family: monospace;
font-family: "Google Sans Text", sans-serif;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use css variable here - might need to verify which one, but maybe font-family: var(--dc-font-family, base.$font-family-sans-serif);

font-size: 14px;
font-weight: 400;
line-height: 20px;
margin: auto;
padding: 0.5rem;
padding: 8px;
width: 100%;
}

.modal-dialog {
Expand Down
52 changes: 38 additions & 14 deletions static/js/components/form_components/icon_buttons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,44 +30,65 @@ const ICON_SELECTED_TIMEOUT = 2000;

/* Base Button Component*/

const StyledButton = styled.button`
export const StyledButton = styled.button`
align-items: center;
background: var(--button-background-color, transparent);
border: 1px solid #747775;
background: ${(props) => props.theme.background};
border: ${(props) => props.theme.border};
border-radius: 100px;
color: var(--button-text-color, black);
color: ${(props) => props.theme.color};
cursor: pointer;
display: flex;
font-size: 14px;
font-weight: 500;
gap: 8px;
justify-content: center;
line-height: 20px;
padding: 10px 24px 10px 16px;
padding: 10px 24px;
text-align: center;
width: fit-content;

&:hover {
background-color: var(--button-highlight-background-color, transparent);
background-color: ${(props) => props.theme.hoverBackgroundColor};
box-shadow: ${(props) => props.theme.hoverBoxShadow};
}
.icon {
font-size: 18px;
}
`;

const defaultColorTheme = {
background: "var(--button-background-color, transparent)",
border: "1px solid #747775",
color: "var(--button-text-color, black)",
hoverBackgroundColor: "var(--button-highlight-background-color, transparent)",
hoverBoxShadow: "none",
};

const emphasizedColorTheme = {
background: "var(--button-emphasized-background-color, transparent)",
border: "1px solid var(--button-emphasized-background-color, transparent)",
color: "var(--button-emphasized-text-color, black)",
hoverBackgroundColor:
"var(--button-emphasized-background-color, transparent)",
hoverBoxShadow:
"0px 1px 2px 0px rgba(0, 0, 0, 0.30), 0px 1px 3px 1px rgba(0, 0, 0, 0.15)",
};

interface ButtonProps {
children?: React.ReactNode;
// Class name to add to button
class?: string;
// Whether to use emphasized styling
emphasized?: boolean;
// Icon to show on button to the left of text
icon?: string;
// Icon to show temporarily when button is clicked
iconWhenClicked?: string;
// Text to show on button
label: string;
// Handler for what happens when button is clicked
onClick: () => void;
}

function IconButton(props: ButtonProps): JSX.Element {
export function IconButton(props: ButtonProps): JSX.Element {
const [isClicked, setIsClicked] = useState<boolean>(false);
const timerRef = useRef<NodeJS.Timeout>(null);

Expand All @@ -89,13 +110,14 @@ function IconButton(props: ButtonProps): JSX.Element {
<StyledButton
onClick={onClickHandler}
className={`button ${props.class || ""}`}
theme={props.emphasized ? emphasizedColorTheme : defaultColorTheme}
>
{props.icon && (
<span className="material-symbols-outlined icon">
{(isClicked && props.iconWhenClicked) || props.icon}
</span>
)}
{props.label}
{props.children}
</StyledButton>
);
}
Expand All @@ -113,8 +135,9 @@ export function CopyButton(props: CopyButtonProps): JSX.Element {
icon="file_copy"
iconWhenClicked="done"
onClick={() => navigator.clipboard.writeText(props.textToCopy)}
label="Copy"
></IconButton>
>
Copy
</IconButton>
);
}

Expand All @@ -133,7 +156,8 @@ export function DownloadButton(props: DownloadButtonProps): JSX.Element {
icon="download"
iconWhenClicked="download_done"
onClick={() => saveToFile(props.filename, props.content)}
label="Download"
></IconButton>
>
Download
</IconButton>
);
}
15 changes: 15 additions & 0 deletions static/js/components/tiles/chart_footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,23 @@ import {
GA_PARAM_TILE_TYPE,
triggerGAEvent,
} from "../../shared/ga_events";
import { TileCodeModal } from "../../tools/shared/tile_code_modal";

// Number of characters in footnote to show before "show more"
const FOOTNOTE_CHAR_LIMIT = 150;

interface ChartFooterPropType {
// Reference to containing chart tile element
containerRef?: React.RefObject<HTMLElement>;
// Callback to download chart
handleEmbed?: () => void;
// Link to explore more. Only show explore button if this object is non-empty.
exploreLink?: { displayText: string; url: string };
children?: React.ReactNode;
// Text to show above buttons
footnote?: string;
// Code to show API users for embedding the chart to their websites
sourceCode?: string;
}

export function ChartFooter(props: ChartFooterPropType): JSX.Element {
Expand Down Expand Up @@ -83,6 +89,15 @@ export function ChartFooter(props: ChartFooterPropType): JSX.Element {
</a>
</div>
)}
{props.sourceCode && (
<div className="outlink-item">
<span className="material-icons-outlined">code</span>
<TileCodeModal
sourceCode={props.sourceCode}
containerRef={props.containerRef}
/>
</div>
)}
</div>
{props.children}
</div>
Expand Down
7 changes: 6 additions & 1 deletion static/js/components/tiles/chart_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
import { NlChartFeedback } from "../nl_feedback";
import { ChartFooter } from "./chart_footer";
import { LoadingHeader } from "./loading_header";

interface ChartTileContainerProp {
id: string;
isLoading?: boolean;
Expand Down Expand Up @@ -68,6 +69,8 @@ interface ChartTileContainerProp {
forwardRef?: MutableRefObject<HTMLDivElement | null>;
// Optional: Chart height
chartHeight?: number;
// Optional: Code to show when clicking "Embed this chart"
sourceCode?: string;
}

export function ChartTileContainer(props: ChartTileContainerProp): JSX.Element {
Expand Down Expand Up @@ -117,9 +120,11 @@ export function ChartTileContainer(props: ChartTileContainerProp): JSX.Element {
{props.children}
</div>
<ChartFooter
containerRef={containerRef}
handleEmbed={showEmbed ? handleEmbed : null}
exploreLink={props.exploreLink}
footnote={props.footnote}
sourceCode={props.sourceCode}
>
<NlChartFeedback id={props.id} />
</ChartFooter>
Expand All @@ -129,7 +134,7 @@ export function ChartTileContainer(props: ChartTileContainerProp): JSX.Element {
</div>
);

// Handle when chart embed is clicked .
// Handle when chart download is clicked .
function handleEmbed(): void {
const chartTitle = props.title
? formatString(props.title, props.replacementStrings)
Expand Down
93 changes: 93 additions & 0 deletions static/js/components/tiles/map_tile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { getPointWithin, getSeriesWithin } from "../../utils/data_fetch_utils";
import { getDateRange } from "../../utils/string_utils";
import {
clearContainer,
getChartTitle,
getDenomInfo,
getNoDataErrorMsg,
getStatFormat,
Expand Down Expand Up @@ -353,6 +354,7 @@ export function MapTile(props: MapTilePropType): JSX.Element {
? [props.dataSpecs[0].variable]
: [props.statVarSpec]
}
sourceCode={getWebComponentSourceCode(mapChartData)}
>
{showZoomButtons && !mapChartData.errorMsg && (
<div className="map-zoom-button-section">
Expand Down Expand Up @@ -749,3 +751,94 @@ function getExploreLink(props: MapTilePropType): {
url: `${props.apiRoot || ""}${URL_PATH}#${hash}`,
};
}

/**
* Get the HTML that can be used to embed this tile as a web component
* @param props this tile's props
* @returns HTML used to embed this tile as a web component
*/
export function getWebComponentSourceCode(mapChartData: MapChartData): string {
if (!mapChartData) {
return "";
}

const props = mapChartData.props;

// Get all places and types in the chart
const places = mapChartData.layerData
.map((layer) => layer.place.dcid)
.join(" ");
const enclosedPlaceTypes = mapChartData.layerData
.map((layer) => layer.enclosedPlaceType)
.join(" ");

// Get unique variables
const variables = _.uniq(
mapChartData.layerData.map((layer) => {
return layer.variable.statVar;
})
).join(" ");

// Get list of variables that are per capita
const perCapitaVariables = mapChartData.layerData
.map((layer) => {
return layer.variable;
})
.filter((variable) => {
return variable?.denom == "Count_Person";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use DEFAULT_PER_CAPITA_DENOM here

})
.map((variable) => {
return variable?.statVar;
})
.join(" ");

// Check if a specific date is being used for all variables
let date = "";
if (
mapChartData?.layerData.length > 0 &&
mapChartData.layerData.every((layer) => layer.variable.date)
) {
date = mapChartData.layerData[0]?.variable?.date || "";
}

// Generate title with replacement strings filled in
const replacementStrings =
mapChartData && getReplacementStrings(props, mapChartData);
const header = getChartTitle(props.title, replacementStrings);

let sourceCode = `<script src="https://datacommons.org/datacommons.js"></script>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use the apiRoot value here for the hostname if set. This way the functionality will work on https://unstats.un.org/UNSDWebsite/undatacommons/sdgs

<datacommons-map
\theader="${header}"
\tchildPlaceTypes="${enclosedPlaceTypes}"
\tparentPlaces="${places}"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use childPlaceType & parentPlace (singular) if there's only a single parent place? Right now i dont think we actually use multiple parent places anywhere, and using multiple places isn't documented in the web component map api

\tvariables="${variables}"`;
if (date) {
sourceCode += `\n\tdate="${date}"`;
}
if (props.allowZoom) {
sourceCode += "\n\tallowZoom";
}
if (props.apiRoot) {
sourceCode += `\n\tapiRoot="${props.apiRoot}"`;
}
if (props.colors) {
sourceCode += `\n\tcolors="${props.colors.join(" ")}"`;
}
if (props.footnote) {
sourceCode += `\n\tfootnote="${props.footnote}"`;
}
if (props.geoJsonProp) {
sourceCode += `\n\tgeoJsonProp="${props.geoJsonProp}"`;
}
if (perCapitaVariables) {
sourceCode += `\n\tperCapita="${perCapitaVariables}"`;
}
if (props.placeNameProp) {
sourceCode += `\n\tplaceNameProp="${props.placeNameProp}"`;
}
if (props.sources) {
sourceCode += `\n\tplaceNameProp="${props.sources.join(" ")}"`;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be \n\tsources=.. ?

}
sourceCode += `\n></datacommons-map>`;
return sourceCode;
}
Loading
Loading