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

feat: Image download #1931

Merged
merged 33 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
615c1dc
chore: Add html-to-image
bprusinowski Dec 3, 2024
85e46a4
feat: Add screenshot-making logic
bprusinowski Dec 3, 2024
2ef7911
fix: Making screenshots of canvas elements
bprusinowski Dec 3, 2024
90c905f
feat: Allow the MenuActionItem to be disabled
bprusinowski Dec 3, 2024
ebdd3c6
feat: Disable certain elements from being screenshotted
bprusinowski Dec 3, 2024
53a9efa
feat: Add a way to screenshot charts
bprusinowski Dec 3, 2024
5864301
docs: Update CHANGELOG
bprusinowski Dec 3, 2024
dd3c3d0
feat: Disable interactive filters panel from screenshots
bprusinowski Dec 3, 2024
7668511
chore: Add meta-png
bprusinowski Dec 3, 2024
98b957c
feat: Add a way to attach file metadata to PNG screenshots
bprusinowski Dec 3, 2024
1723945
refactor: Extract
bprusinowski Dec 3, 2024
3506fac
fix: Enable screenshots in published mode
bprusinowski Dec 3, 2024
f484bf7
feat: Disable more interactive elements from screenshots
bprusinowski Dec 4, 2024
9810604
feat: Add better labels
bprusinowski Dec 4, 2024
1aacf85
feat: Improve the Accent sync command
bprusinowski Dec 4, 2024
246bf0c
feat: Remove SVG image export for now
bprusinowski Dec 4, 2024
59e2be3
feat: Make all texts dark-grey in PNGs
bprusinowski Dec 4, 2024
af03396
refactor: Pass createdWith to VisualizeLink
bprusinowski Dec 4, 2024
d6ad38a
feat: Add Created with Visualize link to screenshot footnotes
bprusinowski Dec 4, 2024
7571dc9
chore: Update react-table types
bprusinowski Dec 5, 2024
cd3eb7c
fix: Use React 18's createRoot to render additional screenshot elements
bprusinowski Dec 5, 2024
e65ec30
refactor: Move modifyNode closer to useScreenshot
bprusinowski Dec 5, 2024
d38201c
feat: Use better names for downloaded images
bprusinowski Dec 5, 2024
506de01
feat: Add file metadata to downloaded PNG images
bprusinowski Dec 5, 2024
75fd1fb
fix: Remove diacritics
bprusinowski Dec 5, 2024
b1cc7fc
feat: Improve publisher definition
bprusinowski Dec 5, 2024
d6e9c49
refactor: Extract TablePreviewWrapper
bprusinowski Dec 5, 2024
1a8aa2c
refactor: Make it possible to access original node when making screen…
bprusinowski Dec 5, 2024
0c4c21c
fix: Screenshotting of free canvas layout charts
bprusinowski Dec 5, 2024
ae9492d
feat: Disable map control elements
bprusinowski Dec 5, 2024
e9dba7b
feat: Remove attribution from map screenshots
bprusinowski Dec 5, 2024
d66cec2
fix: Make modifyNode async and await animation frame where needed
bprusinowski Dec 5, 2024
c8df4a7
fix: Typos
bprusinowski Dec 5, 2024
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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ You can also check the

# Unreleased

Nothing yet.
- Features
- It's now possible to export charts as images

# [5.0.2] - 2024-11-28

Expand Down
5 changes: 4 additions & 1 deletion app/charts/map/map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { GeoFeature, GeoPoint } from "@/domain/data";
import { Icon, IconName } from "@/icons";
import { useLocale } from "@/src";
import useEvent from "@/utils/use-event";
import { DISABLE_SCREENSHOT_ATTR } from "@/utils/use-screenshot";

// supported was removed as of maplibre-gl v3.0.0, so we need to add it back
const maplibregl = { ...maplibreglraw, supported };
Expand Down Expand Up @@ -391,7 +392,7 @@ export const MapComponent = () => {
return (
<>
{locked ? null : (
<div className={classes.controlButtons}>
<div className={classes.controlButtons} {...DISABLE_SCREENSHOT_ATTR}>
<ControlButton iconName="refresh" onClick={reset} />
<ControlButton iconName="add" onClick={zoomIn} />
<ControlButton iconName="minus" onClick={zoomOut} />
Expand All @@ -403,6 +404,8 @@ export const MapComponent = () => {
initialViewState={defaultViewState}
mapLib={maplibregl}
mapStyle={mapStyle}
// Important so we can take a screenshot of the map
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need this comment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I would say yes, as preserveDrawingBuffer is false by default and can decrease the performance of the map, the comment should make sure that we don't remove it and unknowingly break screenshotting of the maps :)

preserveDrawingBuffer
style={{
position: "absolute",
left: 0,
Expand Down
2 changes: 2 additions & 0 deletions app/charts/shared/brush/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
} from "@/stores/interactive-filters";
import { useTransitionStore } from "@/stores/transition";
import { getTextWidth } from "@/utils/get-text-width";
import { DISABLE_SCREENSHOT_ATTR } from "@/utils/use-screenshot";

// Brush constants
const HANDLE_HEIGHT = 14;
Expand Down Expand Up @@ -371,6 +372,7 @@ export const BrushTime = () => {

return fullData.length ? (
<g
{...DISABLE_SCREENSHOT_ATTR}
transform={`translate(0, ${
chartHeight + margins.top + margins.bottom - HEIGHT * 1.5
})`}
Expand Down
14 changes: 14 additions & 0 deletions app/charts/shared/chart-helpers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,20 @@ export const extractChartConfigComponentIds = ({
);
};

export const extractChartConfigUsedComponents = (
chartConfig: ChartConfig,
{ components }: { components: Component[] }
) => {
const componentIds = extractChartConfigComponentIds({
chartConfig,
includeFilters: false,
});

return componentIds
.map((id) => components.find((component) => component.id === id))
.filter(truthy); // exclude potential joinBy components
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we still need this comment?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here I would also say yes, as I wouldn't know why we need to use truthy for – in this case, we have a connection between using it and joinBy dimensions. Maybe it would be better to have something like

const excludeJoinByComponent = (c: Component) => truthy(c);

but I think a comment should suffice here 👍 It was a bit of a pain to catch all the edge-cases of merging of cubes, I'd rather keep more context in places related to them 🥹

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I think comments that describe a "clear" logic should be generally removed, like this:

// Find a component
const component = components.find(c => c.id === id);

but when they are related to other, connected parts of the code or generally are not "obvious", it's okay to use them 👍

};

/** Use to remove missing values from chart data. */
export const usePlottableData = (
data: Observation[],
Expand Down
2 changes: 2 additions & 0 deletions app/charts/shared/containers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useConfiguratorState,
} from "@/configurator";
import { useTransitionStore } from "@/stores/transition";
import { DISABLE_SCREENSHOT_ATTR } from "@/utils/use-screenshot";

export const useStyles = makeStyles<{}, {}, "chartContainer">(() => ({
chartContainer: {
Expand Down Expand Up @@ -70,6 +71,7 @@ export const ChartSvg = ({ children }: { children: ReactNode }) => {
>
{interactiveFiltersConfig?.calculation.active && (
<foreignObject
{...DISABLE_SCREENSHOT_ATTR}
width={width - margins.right}
y={20}
height="26"
Expand Down
6 changes: 5 additions & 1 deletion app/charts/table/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { TableChartState } from "@/charts/table/table-state";
import Flex from "@/components/flex";
import { Input, Switch } from "@/components/form";
import { Observation } from "@/domain/data";
import { DISABLE_SCREENSHOT_ATTR } from "@/utils/use-screenshot";

const MOBILE_VIEW_THRESHOLD = 384;

Expand Down Expand Up @@ -299,7 +300,10 @@ export const Table = () => {
return (
<>
{showSearch && (
<Box sx={{ mb: 4, width: "min(100%, 300px)" }}>
<Box
sx={{ mb: 4, width: "min(100%, 300px)" }}
{...DISABLE_SCREENSHOT_ATTR}
>
<Input
type="text"
name="search-input"
Expand Down
32 changes: 16 additions & 16 deletions app/components/chart-footnotes.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Trans } from "@lingui/macro";
import { t, Trans } from "@lingui/macro";
import { Box, Link, Theme, Typography } from "@mui/material";
import { makeStyles } from "@mui/styles";
import uniqBy from "lodash/uniqBy";
import { useMemo } from "react";
import { ReactNode, useMemo } from "react";

import { extractChartConfigComponentIds } from "@/charts/shared/chart-helpers";
import { extractChartConfigUsedComponents } from "@/charts/shared/chart-helpers";
import { LegendItem } from "@/charts/shared/legend-color";
import { ChartFiltersList } from "@/components/chart-filters-list";
import { OpenMetadataPanelWrapper } from "@/components/metadata-panel";
Expand All @@ -17,7 +17,6 @@ import {
DataSource,
} from "@/configurator";
import { Component, Measure } from "@/domain/data";
import { truthy } from "@/domain/types";
import { useTimeFormatLocale } from "@/formatters";
import { useDataCubesMetadataQuery } from "@/graphql/hooks";
import { useLocale } from "@/locales/use-locale";
Expand All @@ -42,6 +41,8 @@ export const useFootnotesStyles = makeStyles<Theme, { useMarginTop: boolean }>(
})
);

export const CHART_FOOTNOTES_CLASS_NAME = "chart-footnotes";

export const ChartFootnotes = ({
dataSource,
chartConfig,
Expand All @@ -57,14 +58,7 @@ export const ChartFootnotes = ({
}) => {
const locale = useLocale();
const usedComponents = useMemo(() => {
const componentIds = extractChartConfigComponentIds({
chartConfig,
includeFilters: false,
});

return componentIds
.map((id) => components.find((component) => component.id === id))
.filter(truthy); // exclude potential joinBy components
return extractChartConfigUsedComponents(chartConfig, { components });
}, [chartConfig, components]);
const [{ data }] = useDataCubesMetadataQuery({
variables: {
Expand All @@ -81,7 +75,10 @@ export const ChartFootnotes = ({
const formatLocale = useTimeFormatLocale();

return (
<Box sx={{ mt: 1, "& > :not(:last-child)": { mb: 3 } }}>
<Box
className={CHART_FOOTNOTES_CLASS_NAME}
sx={{ mt: 1, "& > :not(:last-child)": { mb: 3 } }}
>
{data?.dataCubesMetadata.map((metadata) => (
<div key={metadata.iri}>
<ChartFootnotesLegend
Expand Down Expand Up @@ -115,7 +112,9 @@ export const ChartFootnotes = ({
) : null}
</div>
))}
{showVisualizeLink ? <VisualizeLink /> : null}
{showVisualizeLink ? (
<VisualizeLink createdWith={t({ id: "metadata.link.created.with" })} />
) : null}
</Box>
);
};
Expand Down Expand Up @@ -289,11 +288,12 @@ const ChartFootnotesComboLineSingle = ({
) : null;
};

export const VisualizeLink = () => {
export const VisualizeLink = ({ createdWith }: { createdWith: ReactNode }) => {
const locale = useLocale();

return (
<Typography variant="caption" color="grey.600">
<Trans id="metadata.link.created.with">Created with</Trans>
{createdWith}
<Link
href={`https://visualize.admin.ch/${locale}/`}
target="_blank"
Expand Down
42 changes: 28 additions & 14 deletions app/components/chart-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ import { Trans } from "@lingui/macro";
import { Box } from "@mui/material";
import { makeStyles } from "@mui/styles";
import Head from "next/head";
import { forwardRef, ReactNode, useCallback, useMemo, useState } from "react";
import {
forwardRef,
ReactNode,
useCallback,
useMemo,
useRef,
useState,
} from "react";

import { DataSetTable } from "@/browse/datatable";
import { LoadingStateProvider } from "@/charts/shared/chart-loading-state";
Expand All @@ -29,6 +36,7 @@ import {
} from "@/components/chart-shared";
import {
ChartTablePreviewProvider,
TablePreviewWrapper,
useChartTablePreview,
} from "@/components/chart-table-preview";
import { ChartWithFilters } from "@/components/chart-with-filters";
Expand Down Expand Up @@ -63,6 +71,7 @@ import { InteractiveFiltersChartProvider } from "@/stores/interactive-filters";
import { useTransitionStore } from "@/stores/transition";
import { useTheme } from "@/themes";
import { createSnapCornerToCursor } from "@/utils/dnd";
import { DISABLE_SCREENSHOT_ATTR_KEY } from "@/utils/use-screenshot";

export const ChartPreview = ({ dataSource }: { dataSource: DataSource }) => {
const [state] = useConfiguratorState(hasChartConfigs);
Expand Down Expand Up @@ -393,6 +402,7 @@ const ChartPreviewInner = ({
chartKey?: string | null;
actionElementSlot?: ReactNode;
}) => {
const ref = useRef<HTMLDivElement>(null);
const [state, dispatch] = useConfiguratorState();
const configuring = isConfiguring(state);
const chartConfig = getChartConfig(state, chartKey);
Expand Down Expand Up @@ -421,7 +431,7 @@ const ChartPreviewInner = ({
})),
},
});
const { isTable, containerRef, containerHeight } = useChartTablePreview();
const { isTable } = useChartTablePreview();
const dimensions = components?.dataCubesComponents.dimensions;
const measures = components?.dataCubesComponents.measures;
const allComponents = useMemo(() => {
Expand All @@ -433,7 +443,7 @@ const ChartPreviewInner = ({
}, [dimensions, measures]);

return (
<Box className={chartClasses.root}>
<Box ref={ref} className={chartClasses.root}>
{children}
<ChartErrorBoundary resetKeys={[state]}>
{hasChartConfigs(state) && (
Expand Down Expand Up @@ -474,6 +484,10 @@ const ChartPreviewInner = ({
})
: undefined
}
{...{
[DISABLE_SCREENSHOT_ATTR_KEY]:
!chartConfig.meta.title[locale],
}}
/>
) : (
// We need to have a span here to keep the space between the
Expand All @@ -488,7 +502,11 @@ const ChartPreviewInner = ({
mt: "-0.33rem",
}}
>
<ChartMoreButton chartKey={chartConfig.key} />
<ChartMoreButton
chartKey={chartConfig.key}
chartWrapperNode={ref.current}
components={allComponents}
/>
{actionElementSlot}
</Box>
</Flex>
Expand All @@ -507,6 +525,10 @@ const ChartPreviewInner = ({
}
: undefined
}
{...{
[DISABLE_SCREENSHOT_ATTR_KEY]:
!chartConfig.meta.description[locale],
}}
/>
) : (
// We need to have a span here to keep the space between the
Expand Down Expand Up @@ -538,15 +560,7 @@ const ChartPreviewInner = ({
top: BANNER_MARGIN_TOP,
}}
/>
<div
ref={containerRef}
style={{
minWidth: 0,
height: containerHeight,
paddingTop: 16,
flexGrow: 1,
}}
>
<TablePreviewWrapper>
{isTable ? (
<DataSetTable
dataSource={dataSource}
Expand All @@ -562,7 +576,7 @@ const ChartPreviewInner = ({
dashboardFilters={state.dashboardFilters}
/>
)}
</div>
</TablePreviewWrapper>
<ChartFootnotes
dataSource={dataSource}
chartConfig={chartConfig}
Expand Down
Loading
Loading