diff --git a/.loki/reference/chrome_laptop_Visualizations_shared_RowChart_Default.png b/.loki/reference/chrome_laptop_Visualizations_shared_RowChart_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..e72364925bb137208fc5cf84660e055e1fdff75c Binary files /dev/null and b/.loki/reference/chrome_laptop_Visualizations_shared_RowChart_Default.png differ diff --git a/.loki/reference/chrome_laptop_Visualizations_shared_RowChart_Huge_Font.png b/.loki/reference/chrome_laptop_Visualizations_shared_RowChart_Huge_Font.png new file mode 100644 index 0000000000000000000000000000000000000000..77d1f501970fdd7fcd7c4d1fb4f606e6666de397 Binary files /dev/null and b/.loki/reference/chrome_laptop_Visualizations_shared_RowChart_Huge_Font.png differ diff --git a/.loki/reference/chrome_laptop_viz_BarChart_Default.png b/.loki/reference/chrome_laptop_viz_BarChart_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..2310a1d2c8647c13b8caf3fb8080b50f0307b128 Binary files /dev/null and b/.loki/reference/chrome_laptop_viz_BarChart_Default.png differ diff --git a/.loki/reference/chrome_laptop_viz_BarChart_Embedding_Huge_Font.png b/.loki/reference/chrome_laptop_viz_BarChart_Embedding_Huge_Font.png new file mode 100644 index 0000000000000000000000000000000000000000..dbb039e45a4617176ebf5d12f204ff9e02ec35db Binary files /dev/null and b/.loki/reference/chrome_laptop_viz_BarChart_Embedding_Huge_Font.png differ diff --git a/.loki/reference/chrome_laptop_viz_LineChart_Default.png b/.loki/reference/chrome_laptop_viz_LineChart_Default.png new file mode 100644 index 0000000000000000000000000000000000000000..4d3030b04f80cd3f1fdd96d57c7287896f1cea1b Binary files /dev/null and b/.loki/reference/chrome_laptop_viz_LineChart_Default.png differ diff --git a/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png b/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png new file mode 100644 index 0000000000000000000000000000000000000000..14b197ab2b87b64bd1a0aaa417f41c5bd90814f0 Binary files /dev/null and b/.loki/reference/chrome_laptop_viz_LineChart_Embedding_Huge_Font.png differ diff --git a/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx b/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx index 809c574a869af9220ac18e79262e93989862e8e6..26b84241360081b3624d3cd4474c3144febd7b9b 100644 --- a/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx +++ b/enterprise/frontend/src/embedding-sdk/components/private/SdkContentWrapper.tsx @@ -52,7 +52,7 @@ const SdkContentWrapperInner = styled.div< --mb-color-text-medium: ${({ theme }) => theme.fn.themeColor("text-medium")}; --mb-color-text-light: ${({ theme }) => theme.fn.themeColor("text-light")}; - font-size: ${({ theme }) => theme.other.fontSize ?? "0.875em"}; + font-size: ${({ theme }) => theme.other.fontSize}; ${aceEditorStyles} ${saveDomImageStyles} diff --git a/enterprise/frontend/src/embedding-sdk/lib/theme/default-component-theme.ts b/enterprise/frontend/src/embedding-sdk/lib/theme/default-component-theme.ts index 722effc2efd35d329e0f41fa2a0a007cb80c774d..09a6b9f5d262af678d6bdb727eb3ec45288aa376 100644 --- a/enterprise/frontend/src/embedding-sdk/lib/theme/default-component-theme.ts +++ b/enterprise/frontend/src/embedding-sdk/lib/theme/default-component-theme.ts @@ -1,8 +1,23 @@ -import type { MantineThemeOverride } from "@mantine/core"; import { merge } from "icepick"; import type { MetabaseComponentTheme } from "embedding-sdk"; import { EMBEDDING_SDK_ROOT_ELEMENT_ID } from "embedding-sdk/config"; +import type { MantineThemeOverride } from "metabase/ui"; + +export const DEFAULT_SDK_FONT_SIZE = 14; + +// Use em units to scale font sizes relative to the base font size. +// The em unit is used by default in the embedding SDK. +const units = (px: number) => ({ + px: `${px}px`, + em: `${px / DEFAULT_SDK_FONT_SIZE}em`, +}); + +export const FONT_SIZES = { + tableCell: units(12.5), + label: units(12), + goalLabel: units(14), +}; /** * Default theme options for Metabase components. @@ -14,12 +29,13 @@ import { EMBEDDING_SDK_ROOT_ELEMENT_ID } from "embedding-sdk/config"; */ export const DEFAULT_METABASE_COMPONENT_THEME: MetabaseComponentTheme = { table: { - idColumn: { - textColor: "brand", - }, cell: { + fontSize: FONT_SIZES.tableCell.px, textColor: "text-dark", }, + idColumn: { + textColor: "brand", + }, }, pivotTable: { rowToggle: { @@ -27,6 +43,12 @@ export const DEFAULT_METABASE_COMPONENT_THEME: MetabaseComponentTheme = { backgroundColor: "text-light", }, }, + cartesian: { + label: { fontSize: FONT_SIZES.label.px }, + goalLine: { + label: { fontSize: FONT_SIZES.goalLabel.px }, + }, + }, }; /** @@ -38,9 +60,16 @@ export const DEFAULT_EMBEDDED_COMPONENT_THEME: MetabaseComponentTheme = merge( { table: { cell: { + fontSize: FONT_SIZES.tableCell.em, backgroundColor: "bg-white", }, }, + cartesian: { + label: { fontSize: FONT_SIZES.label.em }, + goalLine: { + label: { fontSize: FONT_SIZES.goalLabel.em }, + }, + }, }, ); diff --git a/enterprise/frontend/src/embedding-sdk/lib/theme/get-embedding-theme.ts b/enterprise/frontend/src/embedding-sdk/lib/theme/get-embedding-theme.ts index d17890ba7745ef164414811220e29de88cd4241f..03ddde8bce5fc678e42a4a2b7f7080c739e086b4 100644 --- a/enterprise/frontend/src/embedding-sdk/lib/theme/get-embedding-theme.ts +++ b/enterprise/frontend/src/embedding-sdk/lib/theme/get-embedding-theme.ts @@ -12,6 +12,7 @@ import type { import { colorTuple } from "./color-tuple"; import { DEFAULT_EMBEDDED_COMPONENT_THEME, + DEFAULT_SDK_FONT_SIZE, EMBEDDING_SDK_COMPONENTS_OVERRIDES, } from "./default-component-theme"; import type { MappableSdkColor } from "./embedding-color-palette"; @@ -20,6 +21,8 @@ import { SDK_TO_MAIN_APP_COLORS_MAPPING } from "./embedding-color-palette"; const getFontFamily = (theme: MetabaseTheme) => theme.fontFamily ?? DEFAULT_FONT; +const SDK_BASE_FONT_SIZE = `${DEFAULT_SDK_FONT_SIZE / 16}em`; + /** * Transforms a public-facing Metabase theme configuration * into a Mantine theme override for internal use. @@ -39,7 +42,7 @@ export function getEmbeddingThemeOverride( other: { ...components, - ...(theme.fontSize && { fontSize: theme.fontSize }), + fontSize: theme.fontSize ?? SDK_BASE_FONT_SIZE, }, components: EMBEDDING_SDK_COMPONENTS_OVERRIDES, diff --git a/enterprise/frontend/src/embedding-sdk/types/theme/index.ts b/enterprise/frontend/src/embedding-sdk/types/theme/index.ts index 90247245632578e4e5aba034de45bf821db5cb1c..1a8531316c1787eb945d76a63cae6ce8f1563b78 100644 --- a/enterprise/frontend/src/embedding-sdk/types/theme/index.ts +++ b/enterprise/frontend/src/embedding-sdk/types/theme/index.ts @@ -5,7 +5,11 @@ import type { DeepPartial } from "../utils"; * Theme configuration for embedded Metabase components. */ export interface MetabaseTheme { - /** Base font size */ + /** + * Base font size. + * Supported units are px, em and rem. + * Defaults to ~14px (0.875em) + **/ fontSize?: string; /** @@ -83,6 +87,9 @@ export interface MetabaseComponentTheme { /** Default background color of cells, defaults to `background` */ backgroundColor?: string; + + /** Font size of cell values, defaults to ~12.5px */ + fontSize: string; }; idColumn?: { @@ -111,6 +118,21 @@ export interface MetabaseComponentTheme { lineHeight?: string; }; }; + + /** Cartesian charts */ + cartesian: { + label: { + /** Labels used in cartesian charts, such as axis ticks and series. */ + fontSize: string; + }; + + goalLine: { + label: { + /** Font size of goal line labels */ + fontSize: string; + }; + }; + }; } export type ChartColor = diff --git a/enterprise/frontend/src/embedding-sdk/types/theme/private.ts b/enterprise/frontend/src/embedding-sdk/types/theme/private.ts index 06d2a077d5e2fba87511fa957a98b60bbc1219f9..a2cd0867f92d7e6f68561771642f0dadf1413f4c 100644 --- a/enterprise/frontend/src/embedding-sdk/types/theme/private.ts +++ b/enterprise/frontend/src/embedding-sdk/types/theme/private.ts @@ -1,9 +1,7 @@ -import type { MetabaseComponentTheme } from "."; +import type { MetabaseComponentTheme, MetabaseTheme } from "."; /** * Mantine theme options specific to React embedding. */ -export type EmbeddingThemeOptions = MetabaseComponentTheme & { - /** Base font size */ - fontSize?: string; -}; +export type EmbeddingThemeOptions = MetabaseComponentTheme & + Pick<MetabaseTheme, "fontSize">; diff --git a/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx b/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx index 001c734fb384ebdd2e54f58a3a8c60898e243b93..0c95be526ef2114d39a17e0ae3091240e3fc5adf 100644 --- a/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/ComboChart/ComboChart.stories.tsx @@ -3,6 +3,7 @@ import type { ComponentStory } from "@storybook/react"; import { color } from "metabase/lib/colors"; import { formatStaticValue } from "metabase/static-viz/lib/format"; import { measureTextWidth } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { RenderingContext } from "metabase/visualizations/types"; import { ComboChart } from "./ComboChart"; @@ -27,6 +28,7 @@ const renderingContext: RenderingContext = { measureText: (text, style) => measureTextWidth(text, Number(style.size), Number(style.weight)), fontFamily: "Lato", + theme: DEFAULT_VISUALIZATION_THEME, }; export const LineLinearXScale = Template.bind({}); diff --git a/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx b/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx index e1edf76e700258ec985e259dc3f0f6af6d8303a3..c2857a3669fdc60ee9edf84c82f750d6b0b4fe44 100644 --- a/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/FunnelBarChart/FunnelBarChart.stories.tsx @@ -3,6 +3,7 @@ import type { ComponentStory } from "@storybook/react"; import { color } from "metabase/lib/colors"; import { formatStaticValue } from "metabase/static-viz/lib/format"; import { measureTextWidth } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { RenderingContext } from "metabase/visualizations/types"; import { FunnelBarChart } from "./FunnelBarChart"; @@ -27,6 +28,7 @@ const renderingContext: RenderingContext = { measureText: (text, style) => measureTextWidth(text, Number(style.size), Number(style.weight)), fontFamily: "Lato", + theme: DEFAULT_VISUALIZATION_THEME, }; export const Default = Template.bind({}); diff --git a/frontend/src/metabase/static-viz/components/ScalarChart/ScalarChart.stories.tsx b/frontend/src/metabase/static-viz/components/ScalarChart/ScalarChart.stories.tsx index bb522f1c80aeb8ed48aab56db8c1dfdacf2f5066..59a9576f65e8d20b05487bb14bc6762d598b7b39 100644 --- a/frontend/src/metabase/static-viz/components/ScalarChart/ScalarChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/ScalarChart/ScalarChart.stories.tsx @@ -3,6 +3,7 @@ import type { ComponentStory } from "@storybook/react"; import { color } from "metabase/lib/colors"; import { formatStaticValue } from "metabase/static-viz/lib/format"; import { measureTextWidth } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { RenderingContext } from "metabase/visualizations/types"; import { ScalarChart } from "./ScalarChart"; @@ -27,6 +28,7 @@ const renderingContext: RenderingContext = { measureText: (text, style) => measureTextWidth(text, Number(style.size), Number(style.weight)), fontFamily: "Lato", + theme: DEFAULT_VISUALIZATION_THEME, }; export const Default = Template.bind({}); diff --git a/frontend/src/metabase/static-viz/components/ScatterPlot/ScatterPlot.stories.tsx b/frontend/src/metabase/static-viz/components/ScatterPlot/ScatterPlot.stories.tsx index 81e22799b1aa4cb4dc821bb7b81fb0ca76abd231..979acb3783c91edad4be8940162c306e69e4025b 100644 --- a/frontend/src/metabase/static-viz/components/ScatterPlot/ScatterPlot.stories.tsx +++ b/frontend/src/metabase/static-viz/components/ScatterPlot/ScatterPlot.stories.tsx @@ -3,6 +3,7 @@ import type { ComponentStory } from "@storybook/react"; import { color } from "metabase/lib/colors"; import { formatStaticValue } from "metabase/static-viz/lib/format"; import { measureTextWidth } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { RenderingContext } from "metabase/visualizations/types"; import { ScatterPlot } from "./ScatterPlot"; @@ -27,6 +28,7 @@ const renderingContext: RenderingContext = { measureText: (text, style) => measureTextWidth(text, Number(style.size), Number(style.weight)), fontFamily: "Lato", + theme: DEFAULT_VISUALIZATION_THEME, }; export const Default = Template.bind({}); diff --git a/frontend/src/metabase/static-viz/components/SmartScalar/SmartScalar.stories.tsx b/frontend/src/metabase/static-viz/components/SmartScalar/SmartScalar.stories.tsx index be53b2d24c685fcd465f557514d47b0da899df82..10d6edb6cb2078785d94ebb543fb62e1bf5eae96 100644 --- a/frontend/src/metabase/static-viz/components/SmartScalar/SmartScalar.stories.tsx +++ b/frontend/src/metabase/static-viz/components/SmartScalar/SmartScalar.stories.tsx @@ -2,6 +2,7 @@ import { colors } from "metabase/lib/colors"; import { createColorGetter } from "metabase/static-viz/lib/colors"; import { formatStaticValue } from "metabase/static-viz/lib/format"; import { measureTextWidth } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { RowValues, VisualizationSettings } from "metabase-types/api"; import { createMockColumn, @@ -136,6 +137,7 @@ const createTemplate = ({ rows, vizSettings }: SmartScalarSeriesOpts) => getColor: createColorGetter(colors), measureText: (text, style) => measureTextWidth(text, Number(style.size), Number(style.weight)), + theme: DEFAULT_VISUALIZATION_THEME, }} /> ); diff --git a/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.stories.tsx b/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.stories.tsx index daf26ee33d94afa380dc7f4bcbb8278a04a743d8..e7e5201340f2181644d2794daf50dca17554280d 100644 --- a/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.stories.tsx +++ b/frontend/src/metabase/static-viz/components/WaterfallChart/WaterfallChart.stories.tsx @@ -4,6 +4,7 @@ import { color } from "metabase/lib/colors"; import { data } from "metabase/static-viz/components/WaterfallChart/stories-data"; import { formatStaticValue } from "metabase/static-viz/lib/format"; import { measureTextWidth } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { RenderingContext } from "metabase/visualizations/types"; import { WaterfallChart } from "./WaterfallChart"; @@ -27,6 +28,7 @@ const renderingContext: RenderingContext = { measureText: (text, style) => measureTextWidth(text, Number(style.size), Number(style.weight)), fontFamily: "Lato", + theme: DEFAULT_VISUALIZATION_THEME, }; export const YAxisCompactWithoutDataLabels = Template.bind({}); diff --git a/frontend/src/metabase/static-viz/index.js b/frontend/src/metabase/static-viz/index.js index 6987169ab82fde3daf02cf2ce02b6be26f793183..9cfc4a8c2d688baeeea21d9886a596ee0ea6eeaf 100644 --- a/frontend/src/metabase/static-viz/index.js +++ b/frontend/src/metabase/static-viz/index.js @@ -11,6 +11,7 @@ import { measureTextWidth, measureTextEChartsAdapter, } from "metabase/static-viz/lib/text"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import { LegacyStaticChart } from "./containers/LegacyStaticChart"; @@ -38,6 +39,7 @@ export function RenderChart(rawSeries, dashcardSettings, colors) { measureText: (text, style) => measureTextWidth(text, style.size, style.weight), fontFamily: "Lato, 'Helvetica Neue', Helvetica, Arial, sans-serif", + theme: DEFAULT_VISUALIZATION_THEME, }; const props = { diff --git a/frontend/src/metabase/ui/index.ts b/frontend/src/metabase/ui/index.ts index b926912c52eab0afc736161fe4c737361414bf21..fd469e35ff887aafac0dc97b8262a4a5128d2f82 100644 --- a/frontend/src/metabase/ui/index.ts +++ b/frontend/src/metabase/ui/index.ts @@ -3,6 +3,7 @@ export type { TabsValue, MantineTheme, MantineThemeOverride, + MantineThemeOther, } from "@mantine/core"; export { useHover } from "@mantine/hooks"; export * from "./components"; diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.styled.tsx b/frontend/src/metabase/visualizations/components/FunnelNormal.styled.tsx index 24cc6227f0341a82bd290a2fd0ca35a14e07d9e7..7b5bcdc5985ea1b9362a21efbd8fd20614404b20 100644 --- a/frontend/src/metabase/visualizations/components/FunnelNormal.styled.tsx +++ b/frontend/src/metabase/visualizations/components/FunnelNormal.styled.tsx @@ -2,7 +2,6 @@ import { css } from "@emotion/react"; import styled from "@emotion/styled"; import { isDesktopSafari } from "metabase/lib/browser"; -import { color } from "metabase/lib/colors"; interface SharedProps { isNarrow: boolean; @@ -17,7 +16,7 @@ interface FunnelStepProps { export const FunnelStep = styled.div<FunnelStepProps>` width: 100%; min-width: 20px; - border-right: 1px solid ${color("border")}; + border-right: 1px solid var(--mb-color-border); display: flex; flex-direction: column; @@ -95,7 +94,7 @@ interface FunnelNormalRootProps { export const FunnelNormalRoot = styled.div<FunnelNormalRootProps>` display: flex; padding: ${props => (props.isSmall ? "0.5rem" : "1rem")}; - color: ${color("text-medium")}; + color: var(--mb-color-text-medium); ${isDesktopSafari() ? css` diff --git a/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx index cae0d890549671f366d796618d714a0aa861b2b6..25e75885c858243fac8a52b685989f21ea347aea 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx +++ b/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.jsx @@ -121,9 +121,19 @@ class TableInteractive extends Component { static defaultProps = { isPivoted: false, hasMetadataPopovers: true, - renderTableHeaderWrapper: children => { + + // NOTE: the column and index arguments are used in the DatasetEditor, + // which renders table header differently. + renderTableHeaderWrapper: (children, _column, _index, theme) => { + const cellTheme = theme.other?.table?.cell; + return ( - <Box className={TableS.cellData} data-testid="cell-data" c="brand"> + <Box + className={TableS.cellData} + data-testid="cell-data" + c="brand" + fz={cellTheme?.fontSize} + > {children} </Box> ); @@ -145,6 +155,7 @@ class TableInteractive extends Component { c={cellTheme.color} bg={cellTheme.background} style={{ border: cellTheme.border }} + fz={cellTheme.fontSize} > {children} </Box> @@ -771,6 +782,7 @@ class TableInteractive extends Component { renderTableHeaderWrapper, question, mode, + theme, } = this.props; const { dragColIndex, showDetailShortcut } = this.state; @@ -921,6 +933,7 @@ class TableInteractive extends Component { </Ellipsified>, column, columnIndex, + theme, )} </QueryColumnInfoPopover> <TableDraggable diff --git a/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.module.css b/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.module.css index cfd1d2e9be57f23e504e8fde99692f5b0ee655d6..1553aa7a4b16107dcf2b403f2fd843d4058d523f 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.module.css +++ b/frontend/src/metabase/visualizations/components/TableInteractive/TableInteractive.module.css @@ -5,7 +5,6 @@ .TableInteractiveHeaderCellData .cellData { font-weight: 900; - font-size: 10px; border: 1px solid transparent; padding: 0.25em 0.65em; border-radius: 6px; @@ -56,7 +55,6 @@ text-overflow: ellipsis; overflow-x: hidden; font-weight: 700; - font-size: 12.5px; } /* pivot */ diff --git a/frontend/src/metabase/visualizations/components/TableInteractive/table-theme-utils.ts b/frontend/src/metabase/visualizations/components/TableInteractive/table-theme-utils.ts index b7184566dd8785e5d4a947458e4313bf7f244575..019005c847d7d03e2c11f01ca80c9e056dc3b415 100644 --- a/frontend/src/metabase/visualizations/components/TableInteractive/table-theme-utils.ts +++ b/frontend/src/metabase/visualizations/components/TableInteractive/table-theme-utils.ts @@ -16,9 +16,12 @@ export function getCellDataTheme({ const cellTheme = theme.other?.table?.cell; const idTheme = theme.other?.table?.idColumn; + const fontSize = cellTheme?.fontSize; + if (isIDColumn) { return { color: idTheme?.textColor, + fontSize, background: idTheme?.backgroundColor || alpha(theme.fn.themeColor("brand"), 0.08), border: `1px solid ${alpha( @@ -28,7 +31,7 @@ export function getCellDataTheme({ }; } - return { color: cellTheme?.textColor }; + return { color: cellTheme?.textColor, fontSize }; } export const getCellHoverBackground = ({ diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/chart-measurements/index.ts b/frontend/src/metabase/visualizations/echarts/cartesian/chart-measurements/index.ts index 0405feeaf910618325616b4ed1914948b4fc7305..016e1e7c8291c48b010bece91e4474721efcefd7 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/chart-measurements/index.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/chart-measurements/index.ts @@ -49,7 +49,7 @@ const getYAxisTicksWidth = ( axisModel: YAxisModel, yAxisScaleTransforms: NumericAxisScaleTransforms, settings: ComputedVisualizationSettings, - { measureText, fontFamily }: RenderingContext, + { measureText, fontFamily, theme }: RenderingContext, ): number => { if (!settings["graph.y_axis.axis_enabled"]) { return 0; @@ -58,7 +58,9 @@ const getYAxisTicksWidth = ( const fontStyle = { ...CHART_STYLE.axisTicks, family: fontFamily, + size: theme.cartesian.label.fontSize, }; + // extents need to be untransformed to get the value of the tick label const [min, max] = axisModel.extent.map(extent => yAxisScaleTransforms.fromEChartsAxisValue(extent), @@ -117,20 +119,23 @@ const getXAxisTicksWidth = ( dataset: ChartDataset, axisEnabledSetting: ComputedVisualizationSettings["graph.x_axis.axis_enabled"], axisModel: XAxisModel, - { measureText, fontFamily }: RenderingContext, + { theme, measureText, fontFamily }: RenderingContext, ) => { + const { fontSize } = theme.cartesian.label; + if (!axisEnabledSetting) { return { firstXTickWidth: 0, lastXTickWidth: 0 }; } if (axisEnabledSetting === "rotate-90") { return { - firstXTickWidth: CHART_STYLE.axisTicks.size, - lastXTickWidth: CHART_STYLE.axisTicks.size, + firstXTickWidth: fontSize, + lastXTickWidth: fontSize, }; } const fontStyle = { ...CHART_STYLE.axisTicks, + size: fontSize, family: fontFamily, }; @@ -161,13 +166,16 @@ const getXAxisTicksWidth = ( const getXAxisTicksHeight = ( maxXTickWidth: number, axisEnabledSetting: ComputedVisualizationSettings["graph.x_axis.axis_enabled"], + { theme }: RenderingContext, ) => { + const { fontSize } = theme.cartesian.label; + if (!axisEnabledSetting) { return 0; } if (axisEnabledSetting === true || axisEnabledSetting === "compact") { - return CHART_STYLE.axisTicks.size; + return fontSize; } if (axisEnabledSetting === "rotate-90") { @@ -182,7 +190,7 @@ const getXAxisTicksHeight = ( `Unexpected "graph.x_axis.axis_enabled" value ${axisEnabledSetting}`, ); - return CHART_STYLE.axisTicks.size + CHART_STYLE.axisNameMargin; + return fontSize + CHART_STYLE.axisNameMargin; }; const X_LABEL_HEIGHT_RATIO_THRESHOLD = 0.7; // x-axis labels cannot be taller than 70% of chart height @@ -211,6 +219,8 @@ const getAutoAxisEnabledSetting = ( outerHeight: number, renderingContext: RenderingContext, ): ComputedVisualizationSettings["graph.x_axis.axis_enabled"] => { + const { fontSize } = renderingContext.theme.cartesian.label; + const shouldAutoSelectSetting = settings["graph.x_axis.axis_enabled"] === true && (settings["graph.x_axis.scale"] === "ordinal" || @@ -232,19 +242,13 @@ const getAutoAxisEnabledSetting = ( return true; } - if ( - dimensionWidth >= - CHART_STYLE.axisTicks.size * X_LABEL_ROTATE_45_THRESHOLD_FACTOR - ) { + if (dimensionWidth >= fontSize * X_LABEL_ROTATE_45_THRESHOLD_FACTOR) { return checkHeight(maxXTickWidth, outerHeight, "rotate-45") ? "rotate-45" : false; } - if ( - dimensionWidth >= - CHART_STYLE.axisTicks.size * X_LABEL_ROTATE_90_THRESHOLD_FACTOR - ) { + if (dimensionWidth >= fontSize * X_LABEL_ROTATE_90_THRESHOLD_FACTOR) { return checkHeight(maxXTickWidth, outerHeight, "rotate-90") ? "rotate-90" : false; @@ -261,6 +265,8 @@ const getTicksDimensions = ( hasTimelineEvents: boolean, renderingContext: RenderingContext, ) => { + const { fontSize } = renderingContext.theme.cartesian.label; + const ticksDimensions: TicksDimensions = { yTicksWidthLeft: 0, yTicksWidthRight: 0, @@ -302,6 +308,7 @@ const getTicksDimensions = ( if (hasBottomAxis) { const fontStyle = { ...CHART_STYLE.axisTicks, + size: fontSize, family: renderingContext.fontFamily, }; @@ -333,7 +340,7 @@ const getTicksDimensions = ( ticksDimensions.lastXTickWidth = lastXTickWidth; ticksDimensions.xTicksHeight = - getXAxisTicksHeight(maxXTickWidth, axisEnabledSetting) + + getXAxisTicksHeight(maxXTickWidth, axisEnabledSetting, renderingContext) + CHART_STYLE.axisTicksMarginX + (isTimeSeries && hasTimelineEvents ? CHART_STYLE.timelineEvents.height @@ -357,7 +364,13 @@ export const getChartPadding = ( ticksDimensions: TicksDimensions, axisEnabledSetting: ComputedVisualizationSettings["graph.x_axis.axis_enabled"], chartWidth: number, + { theme }: RenderingContext, ): Padding => { + const { fontSize } = theme.cartesian.label; + + const axisNameFontSize = fontSize; + const seriesLabelFontSize = fontSize; + const padding: Padding = { top: CHART_STYLE.padding.y, left: CHART_STYLE.padding.x, @@ -372,8 +385,7 @@ export const getChartPadding = ( settings["graph.show_values"] || (settings["graph.show_goal"] && settings["graph.goal_label"]) ) { - padding.top += - CHART_STYLE.seriesLabels.size + CHART_STYLE.seriesLabels.offset; + padding.top += seriesLabelFontSize + CHART_STYLE.seriesLabels.offset; } // 2. Bottom Padding @@ -382,14 +394,12 @@ export const getChartPadding = ( const hasXAxisName = settings["graph.x_axis.labels_enabled"]; if (hasXAxisName) { - padding.bottom += - CHART_STYLE.axisName.size / 2 + CHART_STYLE.axisNameMargin; + padding.bottom += axisNameFontSize / 2 + CHART_STYLE.axisNameMargin; } // 3. Side (Left and Right) Padding - const yAxisNameTotalWidth = - CHART_STYLE.axisName.size + CHART_STYLE.axisNameMargin; + const yAxisNameTotalWidth = axisNameFontSize + CHART_STYLE.axisNameMargin; padding.left += ticksDimensions.yTicksWidthLeft; if (chartModel.leftAxisModel?.label) { @@ -500,10 +510,13 @@ const areHorizontalXAxisTicksOverlapping = ( dataset: ChartDataset, dimensionWidth: number, formatter: AxisFormatter, - { measureText, fontFamily }: RenderingContext, + { theme, measureText, fontFamily }: RenderingContext, ) => { + const { fontSize } = theme.cartesian.label; + const fontStyle = { ...CHART_STYLE.axisTicks, + size: fontSize, family: fontFamily, }; @@ -648,6 +661,7 @@ export const getChartMeasurements = ( ticksDimensions, axisEnabledSetting, width, + renderingContext, ); const bounds = getChartBounds(width, height, padding, ticksDimensions); diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/constants/style.ts b/frontend/src/metabase/visualizations/echarts/cartesian/constants/style.ts index 9e5483e38040c193e58d71bd19f9aa79b80e510c..5db7f467590e5f17dd8cb0db4085a2a9e58832ae 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/constants/style.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/constants/style.ts @@ -24,7 +24,6 @@ export const CHART_STYLE = { axisTicksMarginX: 5, axisTicksMarginY: 10, axisTicks: { - size: 12, weight: 700, }, seriesLabels: { @@ -34,7 +33,6 @@ export const CHART_STYLE = { stackedPadding: 2, }, axisName: { - size: 12, weight: 700, }, axisNameMargin: 12, diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/model/series.unit.spec.ts b/frontend/src/metabase/visualizations/echarts/cartesian/model/series.unit.spec.ts index 8395c77e705c8b654069e42fc22b11176ef7e4f0..d2bc91e206021fc813e5368b2188b890ba2f355f 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/model/series.unit.spec.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/model/series.unit.spec.ts @@ -2,6 +2,7 @@ import type { BreakoutChartColumns, CartesianChartColumns, } from "metabase/visualizations/lib/graph/columns"; +import { DEFAULT_VISUALIZATION_THEME } from "metabase/visualizations/shared/utils/theme"; import type { ComputedVisualizationSettings, RenderingContext, @@ -30,6 +31,7 @@ const renderingContextMock: RenderingContext = { getColor: colorName => colorName, measureText: () => 0, fontFamily: "Lato", + theme: DEFAULT_VISUALIZATION_THEME, }; describe("series", () => { diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/option/axis.ts b/frontend/src/metabase/visualizations/echarts/cartesian/option/axis.ts index 6af66adec5ee0243de063a9a22d4ae899a4b284f..e9f6eceb6f55b13de34acb2f31c2630d1365a010 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/option/axis.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/option/axis.ts @@ -65,7 +65,7 @@ export const getYAxisRange = ( }; export const getAxisNameDefaultOption = ( - { getColor, fontFamily }: RenderingContext, + { getColor, fontFamily, theme }: RenderingContext, nameGap: number, name: string | undefined, rotate?: number, @@ -76,20 +76,21 @@ export const getAxisNameDefaultOption = ( nameRotate: rotate, nameTextStyle: { color: getColor("text-dark"), - fontSize: CHART_STYLE.axisName.size, + fontSize: theme.cartesian.label.fontSize, fontWeight: CHART_STYLE.axisName.weight, fontFamily, }, }); export const getTicksDefaultOption = ({ + theme, getColor, fontFamily, }: RenderingContext) => { return { hideOverlap: true, color: getColor("text-dark"), - fontSize: CHART_STYLE.axisTicks.size, + fontSize: theme.cartesian.label.fontSize, fontWeight: CHART_STYLE.axisTicks.weight, fontFamily, }; @@ -110,7 +111,10 @@ const getHistogramTicksOptions = ( chartModel: BaseCartesianChartModel, settings: ComputedVisualizationSettings, chartMeasurements: ChartMeasurements, + { theme }: RenderingContext, ) => { + const { fontSize } = theme.cartesian.label; + if (settings["graph.x_axis.scale"] !== "histogram") { return {}; } @@ -120,16 +124,14 @@ const getHistogramTicksOptions = ( const options = { showMinLabel: false, showMaxLabel: true }; if (settings["graph.x_axis.axis_enabled"] === "rotate-45") { - const topOffset = - (histogramDimensionWidth + CHART_STYLE.axisTicks.size / 2) * Math.SQRT1_2; + const topOffset = (histogramDimensionWidth + fontSize / 2) * Math.SQRT1_2; return { ...options, padding: [0, topOffset, 0, 0], margin: -histogramDimensionWidth / 2 + CHART_STYLE.axisTicksMarginX, }; } else if (settings["graph.x_axis.axis_enabled"] === "rotate-90") { - const rightOffset = - histogramDimensionWidth / 2 - CHART_STYLE.axisTicks.size / 2; + const rightOffset = histogramDimensionWidth / 2 - fontSize / 2; return { ...options, verticalAlign: "bottom", @@ -334,7 +336,12 @@ export const buildCategoricalDimensionAxis = ( axisLabel: { margin: CHART_STYLE.axisTicksMarginX, ...getDimensionTicksDefaultOption(settings, renderingContext), - ...getHistogramTicksOptions(chartModel, settings, chartMeasurements), + ...getHistogramTicksOptions( + chartModel, + settings, + chartMeasurements, + renderingContext, + ), interval: () => true, formatter: (value: string) => { const numberValue = parseNumberValue(value); diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/option/goal-line.ts b/frontend/src/metabase/visualizations/echarts/cartesian/option/goal-line.ts index bd3321a507c7cd9d4479daee2b4bd32a0113a9bd..0cfbfbefc8bb0a5b244a78d6a1ae718702f882b7 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/option/goal-line.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/option/goal-line.ts @@ -36,6 +36,7 @@ export function getGoalLineSeriesOption( } const goalValue = settings["graph.goal_value"]; + const { fontSize } = renderingContext.theme.cartesian.goalLine.label; return { id: GOAL_LINE_SERIES_ID, @@ -78,8 +79,7 @@ export function getGoalLineSeriesOption( const hasRightYAxis = chartModel.rightAxisModel == null; const align = hasRightYAxis ? ("right" as const) : ("left" as const); const labelX = hasRightYAxis ? xEnd : xStart; - const labelY = - y - CHART_STYLE.goalLine.label.size - CHART_STYLE.goalLine.label.margin; + const labelY = y - fontSize - CHART_STYLE.goalLine.label.margin; const label = { type: "text" as const, @@ -94,7 +94,7 @@ export function getGoalLineSeriesOption( align, text: settings["graph.goal_label"] ?? "", fontFamily: renderingContext.fontFamily, - fontSize: CHART_STYLE.goalLine.label.size, + fontSize, fontWeight: CHART_STYLE.goalLine.label.weight, fill: renderingContext.getColor("text-medium"), }, diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/option/series.ts b/frontend/src/metabase/visualizations/echarts/cartesian/option/series.ts index 65729e223c88a122dd74e082bd99b79c292b7628..9abbad6896f39ae98c808ed59b9ec73e32b2fb76 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/option/series.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/option/series.ts @@ -266,6 +266,8 @@ export const buildEChartsLabelOptions = ( chartDataDensity?: ChartDataDensity, position?: "top" | "bottom" | "inside", ): SeriesLabelOption => { + const { fontSize } = renderingContext.theme.cartesian.label; + return { show: !!formatter, silent: true, @@ -273,7 +275,7 @@ export const buildEChartsLabelOptions = ( opacity: 1, fontFamily: renderingContext.fontFamily, fontWeight: CHART_STYLE.seriesLabels.weight, - fontSize: CHART_STYLE.seriesLabels.size, + fontSize, color: renderingContext.getColor("text-dark"), textBorderColor: renderingContext.getColor("white"), textBorderWidth: 3, diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/model.ts b/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/model.ts index 80e9276fef9a8ed4016986db2bc625bb4a722687..ff0d59e6e8ea6676df94dd90a405a43db2af03c0 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/model.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/model.ts @@ -62,6 +62,7 @@ const getMinDistanceFromTimelineEventGroup = ( const countLabelWidth = renderingContext.measureText(eventsCount.toString(), { ...CHART_STYLE.axisTicks, + size: renderingContext.theme.cartesian.label.fontSize, family: renderingContext.fontFamily, }); diff --git a/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/option.ts b/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/option.ts index 7249d3d46d22487ae70405b3565326e5ab3321de..5f021317fee5ee11c79f485b7735933269e30c94 100644 --- a/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/option.ts +++ b/frontend/src/metabase/visualizations/echarts/cartesian/timeline-events/option.ts @@ -40,8 +40,10 @@ function svgToImageUri(svgString: string) { export const getTimelineEventsSeries = ( timelineEventsModel: TimelineEventsModel, selectedEventsIds: TimelineEventId[], - { fontFamily, getColor }: RenderingContext, + { fontFamily, getColor, theme }: RenderingContext, ): LineSeriesOption | null => { + const { fontSize } = theme?.cartesian?.label ?? {}; + if (timelineEventsModel.length === 0) { return null; } @@ -74,7 +76,7 @@ export const getTimelineEventsSeries = ( padding: [0, 0, 0, 24], hideOverlap: true, color, - fontSize: CHART_STYLE.axisTicks.size, + fontSize, fontWeight: CHART_STYLE.axisTicks.weight, fontFamily, }, diff --git a/frontend/src/metabase/visualizations/hooks/use-browser-rendering-context.ts b/frontend/src/metabase/visualizations/hooks/use-browser-rendering-context.ts index 4f8d6110695c66f69f9f97ba1dbcae2dcde22c0d..6de4af88068f136a91f10b871ee8f3dd1a4ee552 100644 --- a/frontend/src/metabase/visualizations/hooks/use-browser-rendering-context.ts +++ b/frontend/src/metabase/visualizations/hooks/use-browser-rendering-context.ts @@ -4,20 +4,31 @@ import { usePalette } from "metabase/hooks/use-palette"; import { color } from "metabase/lib/colors"; import { formatValue } from "metabase/lib/formatting/value"; import { measureTextWidth } from "metabase/lib/measure-text"; +import { useMantineTheme } from "metabase/ui"; +import { getVisualizationTheme } from "metabase/visualizations/shared/utils/theme"; import type { RenderingContext } from "metabase/visualizations/types"; +interface RenderingOptions { + fontFamily: string; +} + export const useBrowserRenderingContext = ( - fontFamily: string, + options: RenderingOptions, ): RenderingContext => { + const { fontFamily } = options; + const palette = usePalette(); + const theme = useMantineTheme(); + + return useMemo(() => { + const style = getVisualizationTheme(theme.other); - return useMemo( - () => ({ + return { getColor: name => color(name, palette), formatValue: (value, options) => String(formatValue(value, options)), measureText: measureTextWidth, fontFamily: `${fontFamily}, Arial, sans-serif`, - }), - [fontFamily, palette], - ); + theme: style, + }; + }, [fontFamily, palette, theme]); }; diff --git a/frontend/src/metabase/visualizations/shared/components/RowChart/RowChart.stories.tsx b/frontend/src/metabase/visualizations/shared/components/RowChart/RowChart.stories.tsx index 1d1837a7451426fabf1977e3336fdc5b4a4a642f..674d553bd4f25f56a41a17db25f2056f0909c207 100644 --- a/frontend/src/metabase/visualizations/shared/components/RowChart/RowChart.stories.tsx +++ b/frontend/src/metabase/visualizations/shared/components/RowChart/RowChart.stories.tsx @@ -1,8 +1,11 @@ import type { ComponentStory } from "@storybook/react"; +import { SdkVisualizationWrapper } from "__support__/storybook"; import { color } from "metabase/lib/colors"; import { measureTextWidth } from "metabase/lib/measure-text"; import { getStaticChartTheme } from "metabase/static-viz/components/RowChart/theme"; +import { Box } from "metabase/ui"; +import { useRowChartTheme } from "metabase/visualizations/visualizations/RowChart/utils/theme"; import { RowChart } from "./RowChart"; @@ -13,14 +16,13 @@ export default { const Template: ComponentStory<typeof RowChart> = args => { return ( - <div style={{ padding: 8, height: 600, backgroundColor: "white" }}> + <Box h={600} bg="white" p="8px"> <RowChart {...args} /> - </div> + </Box> ); }; -export const Default = Template.bind({}); -Default.args = { +const DEFAULT_ROW_CHART_ARGS = { width: 800, height: 400, data: [ @@ -78,3 +80,22 @@ Default.args = { style: { fontFamily: "Lato" }, }; + +export const Default = Template.bind({}); +Default.args = DEFAULT_ROW_CHART_ARGS; + +const ThemedRowChart = () => { + const theme = useRowChartTheme({ fontFamily: "Lato" }); + + return ( + <Box h={600} bg="white" p="8px"> + <RowChart {...DEFAULT_ROW_CHART_ARGS} theme={theme} stackOffset={null} /> + </Box> + ); +}; + +export const HugeFont = () => ( + <SdkVisualizationWrapper theme={{ fontSize: "20px" }}> + <ThemedRowChart /> + </SdkVisualizationWrapper> +); diff --git a/frontend/src/metabase/visualizations/shared/utils/size-in-px.ts b/frontend/src/metabase/visualizations/shared/utils/size-in-px.ts new file mode 100644 index 0000000000000000000000000000000000000000..47b4be1ae379a368545e764b6f83d3a9abf0ed9a --- /dev/null +++ b/frontend/src/metabase/visualizations/shared/utils/size-in-px.ts @@ -0,0 +1,33 @@ +/** + * Convert string font sizes (e.g. 12px, 25em) to number in pixels. + * + * This is useful for visualizations that require font size in pixels, + * mainly for calculating padding and offsets. + **/ +export function getSizeInPx( + value?: string | number, + parentFontSize: number = 16, +): number | undefined { + if (typeof value !== "string") { + return value; + } + + if (value.endsWith("px")) { + return stripNaN(parseFloat(value)); + } + + if (value.endsWith("em")) { + const em = stripNaN(parseFloat(value.replace(/em|rem/, ""))); + + if (em === undefined) { + return undefined; + } + + const emValue = em * parentFontSize; + + return Math.round(emValue * 100) / 100; + } +} + +const stripNaN = (value: number): number | undefined => + isNaN(value) ? undefined : value; diff --git a/frontend/src/metabase/visualizations/shared/utils/size-in-px.unit.spec.ts b/frontend/src/metabase/visualizations/shared/utils/size-in-px.unit.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c6c1e48a1b53234930aad6de525d99937cab444c --- /dev/null +++ b/frontend/src/metabase/visualizations/shared/utils/size-in-px.unit.spec.ts @@ -0,0 +1,24 @@ +import { getSizeInPx } from "./size-in-px"; + +describe("getSizeInPx", () => { + it("returns the number value if it's not a string", () => { + expect(getSizeInPx(12)).toBe(12); + }); + + it("returns the px units as a number", () => { + expect(getSizeInPx("14px")).toBe(14); + expect(getSizeInPx("8.256px")).toBe(8.256); + }); + + it("converts em/rem units based on parent font size", () => { + expect(getSizeInPx("0.75em")).toBe(12); + expect(getSizeInPx("0.75rem", 14)).toBe(10.5); + expect(getSizeInPx("1.56em", 18)).toBe(28.08); + }); + + it("returns undefined if the value cannot be parsed", () => { + expect(getSizeInPx("15%")).toBe(undefined); + expect(getSizeInPx("22")).toBe(undefined); + expect(getSizeInPx("foobar")).toBe(undefined); + }); +}); diff --git a/frontend/src/metabase/visualizations/shared/utils/theme.ts b/frontend/src/metabase/visualizations/shared/utils/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..c0d3193f89521ec8e8563e2529206ff01bf8c888 --- /dev/null +++ b/frontend/src/metabase/visualizations/shared/utils/theme.ts @@ -0,0 +1,34 @@ +import { DEFAULT_METABASE_COMPONENT_THEME } from "embedding-sdk/lib/theme"; +import type { MantineThemeOther } from "metabase/ui"; +import { getSizeInPx } from "metabase/visualizations/shared/utils/size-in-px"; +import type { VisualizationTheme } from "metabase/visualizations/types"; + +/** + * Computes the visualization style from the Mantine theme. + */ +export function getVisualizationTheme( + options: MantineThemeOther, +): VisualizationTheme { + const { cartesian } = options; + + // This allows sdk users to set the base font size, + // which scales the visualization's font sizes. + const baseFontSize = getSizeInPx(options.fontSize); + + // ECharts requires font sizes in px for offset calculations. + const px = (value: string) => + getSizeInPx(value, baseFontSize) ?? baseFontSize ?? 14; + + return { + cartesian: { + label: { fontSize: px(cartesian.label.fontSize) }, + goalLine: { + label: { fontSize: px(cartesian.goalLine.label.fontSize) }, + }, + }, + }; +} + +export const DEFAULT_VISUALIZATION_THEME = getVisualizationTheme( + DEFAULT_METABASE_COMPONENT_THEME, +); diff --git a/frontend/src/metabase/visualizations/types/visualization.ts b/frontend/src/metabase/visualizations/types/visualization.ts index dd0ec98f2ddd9afdf0ec6c2fdc5064c9152add59..5fa19f2293695135cab8b74dab8174c075b4dbb7 100644 --- a/frontend/src/metabase/visualizations/types/visualization.ts +++ b/frontend/src/metabase/visualizations/types/visualization.ts @@ -31,6 +31,25 @@ export interface RenderingContext { measureText: TextWidthMeasurer; fontFamily: string; + + theme: VisualizationTheme; +} + +/** + * Visualization theming overrides. + * Refer to DEFAULT_METABASE_COMPONENT_THEME for the default values. + **/ +export interface VisualizationTheme { + cartesian: { + label: { + fontSize: number; + }; + goalLine: { + label: { + fontSize: number; + }; + }; + }; } export type OnChangeCardAndRunOpts = { diff --git a/frontend/src/metabase/visualizations/visualizations/BarChart/BarChart.stories.tsx b/frontend/src/metabase/visualizations/visualizations/BarChart/BarChart.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..53755d658a972810ed9a8e10fd57cbf46c4d73dc --- /dev/null +++ b/frontend/src/metabase/visualizations/visualizations/BarChart/BarChart.stories.tsx @@ -0,0 +1,59 @@ +import type { Story } from "@storybook/react"; + +import { + SdkVisualizationWrapper, + VisualizationWrapper, +} from "__support__/storybook"; +import { NumberColumn, StringColumn } from "__support__/visualizations"; +import type { MetabaseTheme } from "embedding-sdk"; +import { Box } from "metabase/ui"; +import { registerVisualization } from "metabase/visualizations"; +import Visualization from "metabase/visualizations/components/Visualization"; +import { createMockCard } from "metabase-types/api/mocks"; + +import { BarChart } from "./BarChart"; + +export default { + title: "viz/BarChart", + component: BarChart, +}; + +// @ts-expect-error: incompatible prop types with registerVisualization +registerVisualization(BarChart); + +const MOCK_SERIES = [ + { + card: createMockCard({ name: "Card", display: "bar" }), + data: { + cols: [ + StringColumn({ name: "Dimension" }), + NumberColumn({ name: "Count" }), + ], + rows: [ + ["foo", 1], + ["bar", 2], + ], + }, + }, +]; + +export const Default: Story = () => ( + <VisualizationWrapper> + <Box h={500}> + <Visualization rawSeries={MOCK_SERIES} width={500} /> + </Box> + </VisualizationWrapper> +); + +// Example of how themes can be applied in the SDK. +export const EmbeddingHugeFont: Story = () => { + const theme: MetabaseTheme = { fontSize: "20px" }; + + return ( + <SdkVisualizationWrapper theme={theme}> + <Box h={500}> + <Visualization rawSeries={MOCK_SERIES} width={500} /> + </Box> + </SdkVisualizationWrapper> + ); +}; diff --git a/frontend/src/metabase/visualizations/visualizations/CartesianChart/use-models-and-option.ts b/frontend/src/metabase/visualizations/visualizations/CartesianChart/use-models-and-option.ts index 3320b4a2abca8e234f27171268329562a5591669..a83514dc49d53822ee81c494579b2e24c7f784a3 100644 --- a/frontend/src/metabase/visualizations/visualizations/CartesianChart/use-models-and-option.ts +++ b/frontend/src/metabase/visualizations/visualizations/CartesianChart/use-models-and-option.ts @@ -33,6 +33,8 @@ export function useModelsAndOption({ onRender, hovered, }: VisualizationProps) { + const renderingContext = useBrowserRenderingContext({ fontFamily }); + const rawSeriesWithRemappings = useMemo( () => extractRemappings(rawSeries), [rawSeries], @@ -48,8 +50,6 @@ export function useModelsAndOption({ [onRender], ); - const renderingContext = useBrowserRenderingContext(fontFamily); - const hasTimelineEvents = timelineEvents ? timelineEvents.length !== 0 : false; diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.tsx b/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.tsx index 17455f87e88c28ad843a8d7311355c880b8f94a6..63504b5113b5f0554d4b50c8fe50c3939a9da10d 100644 --- a/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.tsx +++ b/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.tsx @@ -204,7 +204,7 @@ export function Funnel(props: VisualizationProps) { } = props; const hasTitle = showTitle && settings["card.title"]; - const renderingContext = useBrowserRenderingContext(fontFamily); + const renderingContext = useBrowserRenderingContext({ fontFamily }); if (settings["funnel.type"] === "bar") { return ( diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.unit.spec.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.unit.spec.jsx index ed32f6bb970d809a92055a9853cdbdbd4e7b0081..a8b6a397a2df788338026796deeb9f712d0f152e 100644 --- a/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.unit.spec.jsx +++ b/frontend/src/metabase/visualizations/visualizations/Funnel/Funnel.unit.spec.jsx @@ -1,4 +1,5 @@ import { render, screen } from "__support__/ui"; +import { ThemeProvider } from "metabase/ui"; import registerVisualizations from "metabase/visualizations/register"; import { createMockCard, @@ -45,14 +46,17 @@ const setup = (funnelProps, visualizationSettings = {}) => { }); render( - <Funnel - series={[series]} - rawSeries={[series]} - settings={settings} - visualizationIsClickable={jest.fn()} - card={card} - {...funnelProps} - />, + <ThemeProvider> + <Funnel + series={[series]} + rawSeries={[series]} + settings={settings} + visualizationIsClickable={jest.fn()} + card={card} + {...funnelProps} + /> + , + </ThemeProvider>, ); }; diff --git a/frontend/src/metabase/visualizations/visualizations/LineChart/LineChart.stories.tsx b/frontend/src/metabase/visualizations/visualizations/LineChart/LineChart.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a2988784d011a87a0966518002ce5eeb76a0032c --- /dev/null +++ b/frontend/src/metabase/visualizations/visualizations/LineChart/LineChart.stories.tsx @@ -0,0 +1,66 @@ +import type { Story } from "@storybook/react"; + +import { + SdkVisualizationWrapper, + VisualizationWrapper, +} from "__support__/storybook"; +import { NumberColumn, StringColumn } from "__support__/visualizations"; +import type { MetabaseTheme } from "embedding-sdk"; +import { Box } from "metabase/ui"; +import { registerVisualization } from "metabase/visualizations"; +import Visualization from "metabase/visualizations/components/Visualization"; +import { + createMockCard, + createMockStructuredDatasetQuery, +} from "metabase-types/api/mocks"; + +import { LineChart } from "./LineChart"; + +export default { + title: "viz/LineChart", + component: LineChart, +}; + +// @ts-expect-error: incompatible prop types with registerVisualization +registerVisualization(LineChart); + +const dataset_query = createMockStructuredDatasetQuery({ + query: { "source-table": 1 }, +}); + +const MOCK_SERIES = [ + { + card: createMockCard({ id: 1, display: "line", dataset_query }), + data: { + cols: [ + StringColumn({ name: "Dimension" }), + NumberColumn({ name: "Count" }), + ], + rows: [ + ["foo", 4], + ["bar", 20], + ["baz", 12], + ], + }, + }, +]; + +export const Default: Story = () => ( + <VisualizationWrapper> + <Box h={500}> + <Visualization rawSeries={MOCK_SERIES} width={500} /> + </Box> + </VisualizationWrapper> +); + +export const EmbeddingHugeFont: Story = () => { + const theme: MetabaseTheme = { fontSize: "20px" }; + + return ( + <SdkVisualizationWrapper theme={theme}> + <Box h={500}> + <Visualization rawSeries={MOCK_SERIES} width={500} /> + </Box> + </SdkVisualizationWrapper> + ); +}; diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx b/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx index 3322728ff57e223df3527349b4410db6d9f3a63f..ba1666de8850ec059f2f162e6686df120ed7f9ac 100644 --- a/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx +++ b/frontend/src/metabase/visualizations/visualizations/RowChart/RowChart.tsx @@ -48,7 +48,7 @@ import { getHoverData, getLegendClickData, } from "metabase/visualizations/visualizations/RowChart/utils/events"; -import { getChartTheme } from "metabase/visualizations/visualizations/RowChart/utils/theme"; +import { useRowChartTheme } from "metabase/visualizations/visualizations/RowChart/utils/theme"; import { isDimension, isMetric } from "metabase-lib/v1/types/utils/isa"; import type { DatasetData, VisualizationSettings } from "metabase-types/api"; @@ -134,8 +134,8 @@ const RowChartVisualization = ({ [chartColumns, data, formatColumnValue], ); const goal = useMemo(() => getChartGoal(settings), [settings]); - const theme = useMemo(getChartTheme, []); const stackOffset = getStackOffset(settings); + const theme = useRowChartTheme({ fontFamily }); const chartWarnings = useMemo( () => getChartWarnings(chartColumns, data.rows), diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart/utils/theme.ts b/frontend/src/metabase/visualizations/visualizations/RowChart/utils/theme.ts index 835caa2b1cd49d03750c02350ed3381287e7a6e3..36073fde259b666e1c7061d38266ac00b4aaa208 100644 --- a/frontend/src/metabase/visualizations/visualizations/RowChart/utils/theme.ts +++ b/frontend/src/metabase/visualizations/visualizations/RowChart/utils/theme.ts @@ -1,40 +1,57 @@ +import { useMemo } from "react"; + import { color } from "metabase/lib/colors"; +import { useMantineTheme } from "metabase/ui"; import type { RowChartTheme } from "metabase/visualizations/shared/components/RowChart/types"; +import { getVisualizationTheme } from "metabase/visualizations/shared/utils/theme"; -export const getChartTheme = (fontFamily: string = "Lato"): RowChartTheme => { - return { - axis: { - color: color("border"), - ticks: { - size: 12, - weight: 700, - color: color("text-medium"), - family: fontFamily, +interface RowChartThemeOptions { + fontFamily?: string; +} + +export const useRowChartTheme = ( + options: RowChartThemeOptions, +): RowChartTheme => { + const theme = useMantineTheme(); + + return useMemo(() => { + const { fontFamily = "Lato" } = options; + const { cartesian } = getVisualizationTheme(theme.other); + + return { + axis: { + color: color("border"), + ticks: { + size: cartesian.label.fontSize, + weight: 700, + color: color("text-medium"), + family: fontFamily, + }, + label: { + size: cartesian.label.fontSize, + weight: 700, + color: color("text-dark"), + family: fontFamily, + }, + }, + goal: { + lineStroke: color("text-medium"), + label: { + size: cartesian.goalLine.label.fontSize, + weight: 700, + color: color("text-medium"), + family: fontFamily, + }, }, - label: { - size: 12, + dataLabels: { weight: 700, color: color("text-dark"), + size: cartesian.label.fontSize, family: fontFamily, }, - }, - goal: { - lineStroke: color("text-medium"), - label: { - size: 14, - weight: 700, - color: color("text-medium"), - family: fontFamily, + grid: { + color: color("border"), }, - }, - dataLabels: { - weight: 700, - color: color("text-dark"), - size: 12, - family: fontFamily, - }, - grid: { - color: color("border"), - }, - }; + }; + }, [options, theme]); }; diff --git a/frontend/src/metabase/visualizations/visualizations/SmartScalar/SmartScalar.stories.tsx b/frontend/src/metabase/visualizations/visualizations/SmartScalar/SmartScalar.stories.tsx index 28ee83b47c4fbf05f351acd387a09ad70d66c00d..8b8e80c5ef091d88a029c5379f73f2e6ff858b90 100644 --- a/frontend/src/metabase/visualizations/visualizations/SmartScalar/SmartScalar.stories.tsx +++ b/frontend/src/metabase/visualizations/visualizations/SmartScalar/SmartScalar.stories.tsx @@ -4,6 +4,7 @@ import { SdkVisualizationWrapper, VisualizationWrapper, } from "__support__/storybook"; +import type { MetabaseTheme } from "embedding-sdk"; import { registerVisualization } from "metabase/visualizations"; import Visualization from "metabase/visualizations/components/Visualization"; @@ -36,7 +37,7 @@ export const Default: Story = () => ( // Example of how themes can be applied in the SDK. export const EmbeddingTemplate: Story = () => { - const theme = { + const theme: MetabaseTheme = { colors: { positive: "#4834d4", negative: "#e84118", diff --git a/loki.config.js b/loki.config.js index e684dd31599a13d4436d91f291b14f02656a5874..35fd56a7256192c82c2424496676f708d0b7a39e 100644 --- a/loki.config.js +++ b/loki.config.js @@ -1,6 +1,6 @@ module.exports = { diffingEngine: "looks-same", - storiesFilter: "static-viz|viz", + storiesFilter: "static-viz|viz|visualizations/shared", configurations: { "chrome.laptop": { target: "chrome.docker", diff --git a/webpack.static-viz.config.js b/webpack.static-viz.config.js index c69db2e5dc7613b26e4dfd05b68b8b33b2369a12..8fac3ca092d844ceb40b839520e692070b887542 100644 --- a/webpack.static-viz.config.js +++ b/webpack.static-viz.config.js @@ -9,6 +9,7 @@ const CLJS_SRC_PATH = __dirname + "/target/cljs_release"; const CLJS_SRC_PATH_DEV = __dirname + "/target/cljs_dev"; const LIB_SRC_PATH = __dirname + "/frontend/src/metabase-lib"; const TYPES_SRC_PATH = __dirname + "/frontend/src/metabase-types"; +const SDK_SRC_PATH = __dirname + "/enterprise/frontend/src/embedding-sdk"; const BABEL_CONFIG = { cacheDirectory: process.env.BABEL_DISABLE_CACHE ? null : ".babel_cache", @@ -89,6 +90,7 @@ module.exports = env => { cljs: devMode ? CLJS_SRC_PATH_DEV : CLJS_SRC_PATH, "metabase-lib": LIB_SRC_PATH, "metabase-types": TYPES_SRC_PATH, + "embedding-sdk": SDK_SRC_PATH, }, }, optimization: {