Skip to content

Commit

Permalink
Merge pull request #244 from adobe/feature/auto-height
Browse files Browse the repository at this point in the history
feat: adds support for percentage based height
  • Loading branch information
marshallpete committed Apr 16, 2024
2 parents 2af33a3 + f06c6dc commit c3ce5f5
Show file tree
Hide file tree
Showing 10 changed files with 120 additions and 56 deletions.
2 changes: 1 addition & 1 deletion .storybook/preview.tsx
Expand Up @@ -14,7 +14,7 @@ const decorators: Decorator[] = [
const darkMode = useDarkMode();
return (
<Provider theme={defaultTheme} colorScheme={darkMode ? 'dark' : 'light'} locale="en-US" height="100vh">
<View padding="size-300">
<View padding={24} height="calc(100% - 48px)">
<Story />
</View>
</Provider>
Expand Down
2 changes: 1 addition & 1 deletion src/Chart.css
Expand Up @@ -56,5 +56,5 @@ this removes transitions in the vega tooltip
.rsc-container {
position: relative;
width: auto;
height: auto;
height: 100%;
}
98 changes: 53 additions & 45 deletions src/Chart.tsx
Expand Up @@ -14,6 +14,7 @@ import { FC, forwardRef, useEffect, useMemo, useRef, useState } from 'react';
import { EmptyState } from '@components/EmptyState';
import { LoadingState } from '@components/LoadingState';
import { DEFAULT_BACKGROUND_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_LINE_TYPES, DEFAULT_LOCALE } from '@constants';
import useChartHeight from '@hooks/useChartHeight';
import useChartImperativeHandle from '@hooks/useChartImperativeHandle';
import useChartWidth from '@hooks/useChartWidth';
import { useResizeObserver } from '@hooks/useResizeObserver';
Expand Down Expand Up @@ -54,6 +55,8 @@ export const Chart = forwardRef<ChartHandle, ChartProps>(
lineWidths = ['M'],
loading,
locale = DEFAULT_LOCALE,
minHeight = 100,
maxHeight = Infinity,
minWidth = 100,
maxWidth = Infinity,
opacities,
Expand All @@ -74,14 +77,22 @@ export const Chart = forwardRef<ChartHandle, ChartProps>(
// The view returned by vega. This is above RscChart so it can be used for downloading and copying to clipboard.
const chartView = useRef<View>();
const [containerWidth, setContainerWidth] = useState<number>(0);
const [containerHeight, setContainerHeight] = useState<number>(0);

useChartImperativeHandle(forwardedRef, { chartView, title });

const containerRef = useResizeObserver<HTMLDivElement>((_target, entry) => {
if (typeof width === 'number') return;
setContainerWidth(entry.contentRect.width);

if (typeof width !== 'number') {
setContainerWidth(entry.contentRect.width);
}

if (typeof height !== 'number') {
setContainerHeight(entry.contentRect.height);
}
});
const chartWidth = useChartWidth(containerWidth, maxWidth, minWidth, width); // calculates the width the vega chart should be
const chartHeight = useChartHeight(containerHeight, maxHeight, minHeight, height); // calculates the height the vega chart should be

const showPlaceholderContent = useMemo(() => Boolean(loading ?? !data.length), [loading, data]);
useEffect(() => {
Expand Down Expand Up @@ -109,50 +120,47 @@ export const Chart = forwardRef<ChartHandle, ChartProps>(
colorScheme={colorScheme}
theme={isValidTheme(theme) ? theme : defaultTheme}
UNSAFE_style={{ backgroundColor: 'transparent' }}
height="100%"
>
<div
ref={containerRef}
id={chartId.current}
data-testid={dataTestId}
className="rsc-container"
style={{ backgroundColor: getColorValue(backgroundColor, colorScheme) }}
>
{showPlaceholderContent ? (
<PlaceholderContent
loading={loading}
data={data}
height={height}
emptyStateText={emptyStateText}
/>
) : (
<RscChart
chartView={chartView}
chartId={chartId}
data={data}
backgroundColor={backgroundColor}
colors={colors}
colorScheme={colorScheme}
config={config}
description={description}
debug={debug}
height={height}
hiddenSeries={hiddenSeries}
highlightedSeries={highlightedSeries}
lineTypes={lineTypes}
lineWidths={lineWidths}
locale={locale}
opacities={opacities}
padding={padding}
renderer={renderer}
symbolShapes={symbolShapes}
symbolSizes={symbolSizes}
title={title}
chartWidth={chartWidth}
UNSAFE_vegaSpec={UNSAFE_vegaSpec}
>
{props.children}
</RscChart>
)}
<div ref={containerRef} id={chartId.current} data-testid={dataTestId} className="rsc-container">
<div style={{ backgroundColor: getColorValue(backgroundColor, colorScheme) }}>
{showPlaceholderContent ? (
<PlaceholderContent
loading={loading}
data={data}
height={chartHeight}
emptyStateText={emptyStateText}
/>
) : (
<RscChart
chartView={chartView}
chartId={chartId}
data={data}
backgroundColor={backgroundColor}
colors={colors}
colorScheme={colorScheme}
config={config}
description={description}
debug={debug}
hiddenSeries={hiddenSeries}
highlightedSeries={highlightedSeries}
lineTypes={lineTypes}
lineWidths={lineWidths}
locale={locale}
opacities={opacities}
padding={padding}
renderer={renderer}
symbolShapes={symbolShapes}
symbolSizes={symbolSizes}
title={title}
chartHeight={chartHeight}
chartWidth={chartWidth}
UNSAFE_vegaSpec={UNSAFE_vegaSpec}
>
{props.children}
</RscChart>
)}
</div>
</div>
</Provider>
);
Expand Down
4 changes: 2 additions & 2 deletions src/RscChart.tsx
Expand Up @@ -73,7 +73,6 @@ export const RscChart = forwardRef<ChartHandle, RscChartProps>(
config,
description,
debug = false,
height = 300,
hiddenSeries = [],
highlightedSeries,
lineTypes = DEFAULT_LINE_TYPES,
Expand All @@ -85,6 +84,7 @@ export const RscChart = forwardRef<ChartHandle, RscChartProps>(
symbolShapes,
symbolSizes,
title,
chartHeight = 300,
chartWidth,
UNSAFE_vegaSpec,
chartId,
Expand Down Expand Up @@ -222,7 +222,7 @@ export const RscChart = forwardRef<ChartHandle, RscChartProps>(
debug={debug}
renderer={renderer}
width={chartWidth}
height={height}
height={chartHeight}
locale={locale}
padding={padding}
signals={signals}
Expand Down
30 changes: 30 additions & 0 deletions src/hooks/useChartHeight.tsx
@@ -0,0 +1,30 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
import { useMemo } from 'react';

import { Height } from 'types';

export default function useChartHeight(containerHeight: number, maxHeight: number, minHeight: number, height: Height) {
return useMemo(() => {
let targetHeight = minHeight;
if (typeof height === 'number') {
targetHeight = height;
} else if (/^\d+%$/.exec(height)) {
targetHeight = (containerHeight * Number(height.slice(0, -1))) / 100;
} else {
console.error(
`height of ${height} is not a valid height. Please provide a valid number or percentage ex. 75%`
);
}
return targetHeight === 0 ? 0 : Math.min(maxHeight, Math.max(minHeight, targetHeight));
}, [containerHeight, maxHeight, minHeight, height]);
}
2 changes: 1 addition & 1 deletion src/hooks/useResizeObserver.tsx
Expand Up @@ -23,7 +23,7 @@ export const useResizeObserver = <T extends HTMLElement>(callback: (target: T, e

// ResizeObserver is not supported in jest
if (typeof ResizeObserver === 'undefined') {
callback(element, { contentRect: { width: 500 } } as ResizeObserverEntry);
callback(element, { contentRect: { width: 500, height: 500 } } as ResizeObserverEntry);
return;
}

Expand Down
10 changes: 9 additions & 1 deletion src/stories/Chart.story.tsx
Expand Up @@ -83,4 +83,12 @@ Width.args = {
data,
};

export { Basic, BackgroundColor, Config, Locale, Width };
const Height = bindWithProps(ChartBarStory);
Height.args = {
height: '50%',
minHeight: 300,
maxHeight: 600,
data,
};

export { Basic, BackgroundColor, Config, Locale, Width, Height };
16 changes: 14 additions & 2 deletions src/stories/Chart.test.tsx
Expand Up @@ -16,7 +16,7 @@ import { Axis, Bar, Chart, ChartHandle, ChartTooltip, Line } from '@rsc';
import { findChart, getAllMarksByGroupName, render, screen } from '@test-utils';
import { getElement } from '@utils';

import { BackgroundColor, Basic, Config, Locale, Width } from './Chart.story';
import { BackgroundColor, Basic, Config, Height, Locale, Width } from './Chart.story';
import {
CssColors,
SpectrumColorNames,
Expand Down Expand Up @@ -88,6 +88,18 @@ describe('Chart', () => {
expect(chart).toBeInTheDocument();
});

test('Height renders properly', async () => {
render(<Height {...Height.args} />);
const chart = await findChart();
expect(chart).toBeInTheDocument();
});

test('Height renders properly with invalid Height', async () => {
render(<Height {...Height.args} height="50.2%" />);
const chart = await findChart();
expect(chart).toBeInTheDocument();
});

test('Chart does not render if the width or height are 0', () => {
render(<Width {...Width.args} width={0} />);
expect(screen.queryByRole('graphics-document')).not.toBeInTheDocument();
Expand Down Expand Up @@ -176,7 +188,7 @@ describe('Chart', () => {
const svg = chart.querySelector('svg');
expect(svg).toHaveStyle('background-color: rgb(255, 255, 255);');

const container = document.querySelector('.rsc-container');
const container = document.querySelector('.rsc-container > div');
expect(container).toHaveStyle('background-color: rgb(255, 255, 255);');
});
});
Expand Down
10 changes: 8 additions & 2 deletions src/types/Chart.ts
Expand Up @@ -98,8 +98,6 @@ export interface SharedChartProps extends SpecProps {
data: ChartData[];
/** Enables debug mode which will console log things like the generated vega spec and the datums for tooltips. */
debug?: boolean;
/** Chart height */
height?: number;
/** Number and time locales to use */
locale?: Locale | LocaleCode | { number?: NumberLocaleCode | NumberLocale; time?: TimeLocaleCode | TimeLocale };
/** Chart padding */
Expand All @@ -112,6 +110,7 @@ export interface RscChartProps extends SharedChartProps {
chartId: MutableRefObject<string>;
chartView: MutableRefObject<View | undefined>;
chartWidth: number;
chartHeight: number;
popoverIsOpen?: boolean;
}

Expand All @@ -126,6 +125,12 @@ export interface ChartProps extends SharedChartProps {
maxWidth?: number;
/** Minimum chart width. */
minWidth?: number;
/** Chart height */
height?: Height;
/** Maximum height of the chart */
maxHeight?: number;
/** Minimum height of the chart */
minHeight?: number;
/** react-spectrum theme. This sets the react-spectrum theming on tooltips and popovers. */
theme?: Theme;
/** Chart width */
Expand All @@ -138,6 +143,7 @@ export interface BaseProps {
}

export type Width = number | string | 'auto';
export type Height = number | `${number}%`;

export interface ChartHandle {
copy: () => Promise<string>;
Expand Down
2 changes: 1 addition & 1 deletion src/utils/markClickUtils.ts
Expand Up @@ -65,7 +65,7 @@ export const getOnMarkClickCallback = (
// we need to anchor the popover to a div that we move to the same location as the selected mark
selectedDataBounds.current = getItemBounds(item);
selectedDataName.current = getItemName(item);
(document.querySelector(`#${chartId.current} > button`) as HTMLButtonElement)?.click();
(document.querySelector(`#${chartId.current} > div > button`) as HTMLButtonElement)?.click();
}
};
};
Expand Down

0 comments on commit c3ce5f5

Please sign in to comment.