UNPKG

@awsui/components-react

Version:

On July 19th, 2022, we launched [Cloudscape Design System](https://cloudscape.design). Cloudscape is an evolution of AWS-UI. It consists of user interface guidelines, front-end components, design resources, and development tools for building intuitive, en

121 lines 8.35 kB
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 'use client'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import clsx from 'clsx'; import { pie } from 'd3-shape'; import { useMergeRefs } from '@awsui/component-toolkit/internal'; import { getBaseProps } from '../internal/base-component'; import Filter from '../internal/components/chart-filter'; import Legend from '../internal/components/chart-legend'; import ChartSeriesMarker from '../internal/components/chart-series-marker'; import ChartStatusContainer, { getChartStatus } from '../internal/components/chart-status-container'; import { ChartWrapper } from '../internal/components/chart-wrapper'; import { fireNonCancelableEvent } from '../internal/events'; import useBaseComponent from '../internal/hooks/use-base-component'; import { useControllable } from '../internal/hooks/use-controllable'; import { useVisualRefresh } from '../internal/hooks/use-visual-mode'; import { applyDisplayName } from '../internal/utils/apply-display-name'; import createCategoryColorScale from '../internal/utils/create-category-color-scale'; import { nodeBelongs } from '../internal/utils/node-belongs'; import useContainerWidth from '../internal/utils/use-container-width'; import InternalPieChart from './pie-chart'; import { getDimensionsBySize } from './utils'; import styles from './styles.css.js'; const PieChart = function PieChart({ fitHeight, variant = 'pie', size = 'medium', hideTitles = false, hideDescriptions = false, hideLegend = false, hideFilter = false, statusType = 'finished', data: externalData = [], i18nStrings = {}, highlightedSegment: controlledHighlightedSegment, visibleSegments: controlledVisibleSegments, onHighlightChange: controlledOnHighlightChange, onFilterChange, additionalFilters, legendTitle, detailPopoverSize = 'medium', ...props }) { const { __internalRootRef = null } = useBaseComponent('PieChart', { props: { fitHeight, variant, size, hideTitles, hideDescriptions, hideLegend, hideFilter, detailPopoverSize, }, }); const baseProps = getBaseProps(props); const containerRef = useRef(null); const [containerWidth, measureRef] = useContainerWidth(); const data = useMemo(() => { const colors = createCategoryColorScale(externalData, undefined, it => it.color || null); return externalData.map((datum, i) => ({ index: i, color: colors[i], datum, })); }, [externalData]); const [highlightedSegment = null, setHighlightedSegment] = useControllable(controlledHighlightedSegment, controlledOnHighlightChange, null, { componentName: 'PieChart', controlledProp: 'highlightedSegment', changeHandler: 'onHighlightChange', }); const [legendSegment, setLegendSegment] = useState(highlightedSegment); useEffect(() => { setLegendSegment(controlledHighlightedSegment || null); }, [controlledHighlightedSegment]); const [visibleSegments, setVisibleSegments] = useControllable(controlledVisibleSegments, onFilterChange, externalData, { componentName: 'PieChart', controlledProp: 'visibleSegments', changeHandler: 'onFilterChange', }); const visibleData = useMemo(() => data.filter(d => (visibleSegments === null || visibleSegments === void 0 ? void 0 : visibleSegments.indexOf(d.datum)) !== -1), [data, visibleSegments]); const filterItems = data === null || data === void 0 ? void 0 : data.map(data => ({ label: data.datum.title, marker: React.createElement(ChartSeriesMarker, { color: data.color, type: "rectangle" }), datum: data.datum, })); const legendItems = filterItems.filter(d => (visibleSegments === null || visibleSegments === void 0 ? void 0 : visibleSegments.indexOf(d.datum)) !== -1); const filterChange = useCallback((selectedSeries) => { setVisibleSegments(selectedSeries); fireNonCancelableEvent(onFilterChange, { visibleSegments: selectedSeries, }); }, [setVisibleSegments, onFilterChange]); const onHighlightChange = useCallback((segment) => { setLegendSegment(segment); setHighlightedSegment(segment); fireNonCancelableEvent(controlledOnHighlightChange, { highlightedSegment: segment }); }, [controlledOnHighlightChange, setHighlightedSegment]); const onBlur = (event) => { if (event.relatedTarget && !nodeBelongs(containerRef.current, event.relatedTarget)) { if (highlightedSegment) { onHighlightChange(null); } setLegendSegment(null); } }; const mergedRef = useMergeRefs(containerRef, measureRef, __internalRootRef); const { pieData, dataSum } = useMemo(() => { const dataSum = visibleData.reduce((sum, d) => sum + d.datum.value, 0); const pieFactory = pie() // Minimum 1% segment size .value(d => (d.datum.value < dataSum / 100 ? dataSum / 100 : d.datum.value)) .sort(null); // Filter out segments with value of zero or below const pieData = pieFactory(visibleData.filter(d => d.datum.value > 0)); return { pieData, dataSum }; }, [visibleData]); const hasNoData = !externalData || externalData.length === 0; const { isEmpty, showChart } = getChartStatus({ externalData: data, visibleData: pieData, statusType }); // Pie charts have a special condition for empty/noMatch due to how zero-value segments are handled. const isNoMatch = isEmpty && visibleData.length !== data.length; const showFilters = statusType === 'finished' && !hasNoData && (additionalFilters || !hideFilter); const reserveLegendSpace = !showChart && !hideLegend; const reserveFilterSpace = statusType !== 'finished' && !isNoMatch && (!hideFilter || additionalFilters); const hasLabels = !(hideTitles && hideDescriptions); const isRefresh = useVisualRefresh(); const defaultDimensions = getDimensionsBySize({ size, hasLabels, visualRefresh: isRefresh }); const radius = defaultDimensions.outerRadius; const height = 2 * (radius + defaultDimensions.padding + (hasLabels ? defaultDimensions.paddingLabels : 0)); return (React.createElement(ChartWrapper, { ref: mergedRef, fitHeight: !!fitHeight, ...baseProps, className: clsx(baseProps.className, styles.root), contentClassName: clsx(styles.content, styles[`content--${defaultDimensions.size}`], { [styles['content--without-labels']]: !hasLabels, [styles['content--fit-height']]: fitHeight, }), defaultFilter: showFilters && !hideFilter ? (React.createElement(Filter, { series: filterItems, onChange: filterChange, selectedSeries: visibleSegments, i18nStrings: i18nStrings })) : null, additionalFilters: showFilters ? additionalFilters : null, reserveFilterSpace: !!reserveFilterSpace, reserveLegendSpace: !!reserveLegendSpace, chartStatus: React.createElement(ChartStatusContainer, { isEmpty: isEmpty, isNoMatch: isNoMatch, showChart: showChart, statusType: statusType, empty: props.empty, noMatch: props.noMatch, loadingText: props.loadingText, errorText: props.errorText, recoveryText: props.recoveryText, onRecoveryClick: props.onRecoveryClick }), chart: showChart ? (React.createElement(InternalPieChart, { ...props, variant: variant, size: size, height: height, fitHeight: fitHeight, data: externalData, width: containerWidth, hideTitles: hideTitles, hideDescriptions: hideDescriptions, i18nStrings: i18nStrings, onHighlightChange: onHighlightChange, highlightedSegment: highlightedSegment, legendSegment: legendSegment, detailPopoverSize: detailPopoverSize, pieData: pieData, dataSum: dataSum })) : null, legend: !hideLegend && !hasNoData && statusType === 'finished' && (React.createElement(Legend, { series: legendItems, highlightedSeries: legendSegment, legendTitle: legendTitle, ariaLabel: i18nStrings === null || i18nStrings === void 0 ? void 0 : i18nStrings.legendAriaLabel, onHighlightChange: onHighlightChange, plotContainerRef: containerRef })), onBlur: onBlur })); }; applyDisplayName(PieChart, 'PieChart'); export default PieChart; //# sourceMappingURL=index.js.map