import { cloneElement, Fragment } from 'react'; import hoistNonReactStatics from 'hoist-non-react-statics'; import { Helpers, NumberOrCallback, StringOrNumberOrCallback } from 'victory-core'; import { VictoryLegend } from 'victory-legend'; import { ChartLabel } from '../ChartLabel/ChartLabel'; import { ChartLegend } from '../ChartLegend/ChartLegend'; import { ChartLegendTooltipStyles } from '../ChartTheme/ChartStyles'; import { ChartLegendTooltipLabel } from './ChartLegendTooltipLabel'; import { ChartThemeDefinition } from '../ChartTheme/ChartTheme'; import { getLegendTooltipDataProps, getLegendTooltipSize, getLegendTooltipVisibleData, getLegendTooltipVisibleText } from '../ChartUtils/chart-tooltip'; import { getTheme } from '../ChartUtils/chart-theme'; /** * ChartLegendTooltipContent renders a legend tooltip component. * * See https://github.com/FormidableLabs/victory/blob/main/packages/victory-legend/src/index.d.ts */ export interface ChartLegendTooltipContentProps { /** * The activePoints prop specifies the active data * * Note: This prop should not be set manually. * * @private Not intended as public API and subject to change * @hide */ activePoints?: any[]; /** * The center prop determines the position of the center of the tooltip flyout. This prop should be given as an object * that describes the desired x and y svg coordinates of the center of the tooltip. This prop is useful for * positioning the flyout of a tooltip independent from the pointer. When ChartTooltip is used with * ChartVoronoiContainer, the center prop is what enables the mouseFollowTooltips option. When this prop is set, * non-zero pointerLength values will no longer be respected. */ center?: { x: number; y: number }; /** * Victory components can pass a datum prop to their label component. This can be used to calculate functional styles, * and determine child text */ datum?: any; /** * The dx prop defines a horizontal shift from the x coordinate. * * @propType number | Function */ dx?: NumberOrCallback; /** * The dy prop defines a horizontal shift from the y coordinate. * * @propType number | Function */ dy?: NumberOrCallback; /** * The flyoutHeight prop defines the height of the tooltip flyout. This prop may be given as a positive number or a function * of datum. If this prop is not set, height will be determined based on an approximate text size calculated from the * text and style props provided to ChartTooltip. * * @propType number | Function */ flyoutHeight?: NumberOrCallback; /** * The flyoutWidth prop defines the width of the tooltip flyout. This prop may be given as a positive number or a * function of datum. If this prop is not set, flyoutWidth will be determined based on an approximate text size * calculated from the text and style props provided to VictoryTooltip. * * @propType number | Function */ flyoutWidth?: NumberOrCallback; /** * Specifies the height the svg viewBox of the chart container. This value should be given as a * number of pixels. * * Because Victory renders responsive containers, the width and height props do not determine the width and * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. */ height?: number; /** * The labelComponent prop takes a component instance which will be used * to render each legend label. The new element created from the passed * labelComponent will be supplied with the following properties: x, y, * style, and text. Any of these props may be overridden by passing in * props to the supplied component, or modified or ignored within the * custom component itself. If labelComponent is omitted, a new * ChartLabel will be created with the props described above. */ labelComponent?: React.ReactElement; /** * The legend component to render with chart. * * Note: Use legendData so the legend width can be calculated and positioned properly. * Default legend properties may be applied */ legendComponent?: React.ReactElement; /** * Specify data via the data prop. ChartLegend expects data as an array of objects with name (required), symbol, and * labels properties. The childName is used to sync the data series associated with the given chart child name. * * The data prop must be given as an array. * * @example * * legendData={[{ name: `GBps capacity - 45%` }, { name: 'Unused' }]} * legendData={[{ childName: `cats`, name: `Total cats` }, { childName: `dogs`, name: 'Total dogs' }]} */ legendData?: { childName?: string; name?: string; labels?: { fill?: string; }; symbol?: { fill?: string; type?: string; }; }[]; /** * The patternScale prop is an optional prop that defines patterns to apply, where applicable. This prop should be * given as a string array of pattern URLs. Patterns will be assigned to children by index and will repeat when there * are more children than patterns in the provided patternScale. Use null to omit the pattern for a given index. * * Note: Not all components are supported; for example, ChartLine, ChartBullet, ChartThreshold, etc. * * @example patternScale={[ 'url("#pattern1")', 'url("#pattern2")', null ]} */ patternScale?: string[]; /** * The text prop defines the text ChartTooltip will render. The text prop may be given as a string, number, or * function of datum. When ChartLabel is used as the labelComponent, strings may include newline characters, which * ChartLabel will split in to separate elements. * * @propType number | string | Function | string[] | number[] */ text?: StringOrNumberOrCallback | string[] | number[]; /** * The theme prop takes a style object with nested data, labels, and parent objects. * You can create this object yourself, or you can use a theme provided by * When using ChartLegend as a solo component, implement the theme directly on * ChartLegend. If you are wrapping ChartLegend in ChartChart or * ChartGroup, please call the theme on the outermost wrapper component instead. * * @propType object */ theme?: ChartThemeDefinition; /** * Specifies the theme color. Valid values are 'blue', 'green', 'multi', etc. * * Note: Not compatible with theme prop * * @example themeColor={ChartThemeColor.blue} */ themeColor?: string; /** * The title prop specifies a title to render with the legend. * This prop should be given as a string, or an array of strings for multi-line titles. * * Valid types are string, string[], or Function * * Example: title={(datum) => datum.x} */ title?: string | string[] | Function; /** * The titleComponent prop takes a component instance which will be used to render * a title for the component. The new element created from the passed * labelComponent will be supplied with the following properties: x, y, index, data, * datum, verticalAnchor, textAnchor, style, text, and events. Any of these props * may be overridden by passing in props to the supplied component, or modified * or ignored within the custom component itself. If labelComponent is omitted, * a new ChartLabel will be created with the props described above. */ titleComponent?: React.ReactElement; /** * Specifies the width of the svg viewBox of the chart container. This value should be given as a * number of pixels. * * Because Victory renders responsive containers, the width and height props do not determine the width and * height of the chart in number of pixels, but instead define an aspect ratio for the chart. The exact number of * pixels will depend on the size of the container the chart is rendered into. */ width?: number; } export const ChartLegendTooltipContent: React.FunctionComponent = ({ activePoints, center, datum, dx = 0, dy = 0, flyoutHeight, flyoutWidth, height, labelComponent = , legendComponent = , legendData, patternScale, text, themeColor, title, titleComponent = , width, // destructure last theme = getTheme(themeColor), ...rest }: ChartLegendTooltipContentProps) => { const pointerLength = theme && theme.tooltip ? Helpers.evaluateProp(theme.tooltip.pointerLength, undefined) : 10; const legendProps = getLegendTooltipDataProps(legendComponent.props); const visibleLegendData = getLegendTooltipVisibleData({ activePoints, colorScale: legendProps.colorScale, legendData, text, theme }); // Component offsets const legendOffsetX = 0; const legendOffsetY = title ? 5 : -10; const titleOffsetX = 10; const titleOffsetY = 0; // Returns x position of flyout const getX = () => { if (!(center || flyoutWidth || width)) { const x = (rest as any).x; return x ? x : undefined; } const _flyoutWidth = Helpers.evaluateProp(flyoutWidth, undefined); if (width > center.x + _flyoutWidth + pointerLength) { return center.x + ChartLegendTooltipStyles.flyout.padding / 2; } else if (center.x < _flyoutWidth + pointerLength) { return ChartLegendTooltipStyles.flyout.padding / 2 - pointerLength; } else { return center.x - _flyoutWidth; } }; // Returns y position const getY = () => { if (!(center || flyoutHeight || height)) { const y = (rest as any).y; return y ? y : undefined; } const _flyoutHeight = Helpers.evaluateProp(flyoutHeight, undefined); if (center.y < _flyoutHeight / 2) { return ChartLegendTooltipStyles.flyout.padding / 2; } else if (center.y > height - _flyoutHeight / 2) { return height - _flyoutHeight + ChartLegendTooltipStyles.flyout.padding / 2; } else { return center.y - _flyoutHeight / 2 + ChartLegendTooltipStyles.flyout.padding / 2; } }; // Min & max dimensions do not include flyout padding const maxLegendDimensions = getLegendTooltipSize({ legendData: visibleLegendData, legendProps, text: getLegendTooltipVisibleText({ activePoints, legendData, text }), theme }); const minLegendDimensions = getLegendTooltipSize({ legendData: [{ name: '' }], legendProps, minSpacing: 0, theme }); // Returns the label component const getLabelComponent = () => cloneElement(labelComponent, { datum, dx: maxLegendDimensions.width - minLegendDimensions.width, legendData: visibleLegendData, ...labelComponent.props }); // Returns the title component const getTitleComponent = () => { const _title = title instanceof Function ? title(datum) : title; return cloneElement(titleComponent, { style: { fill: ChartLegendTooltipStyles.label.fill, fontWeight: ChartLegendTooltipStyles.label.fontWeight }, text: _title, textAnchor: 'start', x: getX() + titleOffsetX + Helpers.evaluateProp(dx, undefined), y: getY() + titleOffsetY + Helpers.evaluateProp(dy, undefined), ...titleComponent.props }); }; // Returns the legend component const getLegendComponent = () => cloneElement(legendComponent, { data: getLegendTooltipVisibleData({ activePoints, colorScale: legendProps.colorScale, legendData, patternScale, text, textAsLegendData: true, theme }), labelComponent: getLabelComponent(), patternScale, standalone: false, theme, themeColor, x: getX() + legendOffsetX + Helpers.evaluateProp(dx, undefined), y: getY() + legendOffsetY + Helpers.evaluateProp(dy, undefined), ...legendProps }); return ( {getTitleComponent()} {getLegendComponent()} ); }; ChartLegendTooltipContent.displayName = 'ChartLegendTooltipContent'; // Note: VictoryLegend.role must be hoisted, but getBaseProps causes error with ChartVoronoiContainer hoistNonReactStatics(ChartLegendTooltipContent, VictoryLegend, { getBaseProps: true });