@mui/x-charts
Version: 
The community edition of the charts components (MUI X).
408 lines (406 loc) • 13.4 kB
JavaScript
import _objectWithoutPropertiesLoose from "@babel/runtime/helpers/esm/objectWithoutPropertiesLoose";
import _extends from "@babel/runtime/helpers/esm/extends";
const _excluded = ["rotate", "dominantBaseline"],
  _excluded2 = ["label"];
import * as React from 'react';
import PropTypes from 'prop-types';
import { useSlotProps } from '@mui/base/utils';
import { NoSsr } from '@mui/base/NoSsr';
import { unstable_composeClasses as composeClasses } from '@mui/utils';
import { useThemeProps, useTheme, styled } from '@mui/material/styles';
import { DrawingContext } from '../context/DrawingProvider';
import { getSeriesToDisplay } from './utils';
import { SeriesContext } from '../context/SeriesContextProvider';
import { getLegendUtilityClass } from './chartsLegendClasses';
import { ChartsText } from '../ChartsText';
import { getWordsByLines } from '../internals/getWordsByLines';
import { jsx as _jsx } from "react/jsx-runtime";
import { jsxs as _jsxs } from "react/jsx-runtime";
const useUtilityClasses = ownerState => {
  const {
    classes,
    direction
  } = ownerState;
  const slots = {
    root: ['root', direction],
    mark: ['mark'],
    label: ['label'],
    series: ['series']
  };
  return composeClasses(slots, getLegendUtilityClass, classes);
};
export const ChartsLegendRoot = styled('g', {
  name: 'MuiChartsLegend',
  slot: 'Root',
  overridesResolver: (props, styles) => styles.root
})({});
const defaultProps = {
  position: {
    horizontal: 'middle',
    vertical: 'top'
  },
  direction: 'row'
};
/**
 * Transforms number or partial padding object to a defaultized padding object.
 */
const getStandardizedPadding = padding => {
  if (typeof padding === 'number') {
    return {
      left: padding,
      right: padding,
      top: padding,
      bottom: padding
    };
  }
  return _extends({
    left: 0,
    right: 0,
    top: 0,
    bottom: 0
  }, padding);
};
function DefaultChartsLegend(props) {
  const {
    hidden,
    position,
    direction,
    seriesToDisplay,
    drawingArea,
    classes,
    itemMarkWidth = 20,
    itemMarkHeight = 20,
    markGap = 5,
    itemGap = 10,
    padding: paddingProps = 10,
    labelStyle: inLabelStyle
  } = props;
  const theme = useTheme();
  const labelStyle = React.useMemo(() => _extends({}, theme.typography.subtitle1, {
    color: 'inherit',
    dominantBaseline: 'central',
    textAnchor: 'start',
    fill: (theme.vars || theme).palette.text.primary,
    lineHeight: 1
  }, inLabelStyle),
  // To say to TS that the dominantBaseline and textAnchor are correct
  [inLabelStyle, theme]);
  const padding = React.useMemo(() => getStandardizedPadding(paddingProps), [paddingProps]);
  const getItemSpace = React.useCallback((label, inStyle = {}) => {
    const style = _objectWithoutPropertiesLoose(inStyle, _excluded);
    const linesSize = getWordsByLines({
      style,
      needsComputation: true,
      text: label
    });
    const innerSize = {
      innerWidth: itemMarkWidth + markGap + Math.max(...linesSize.map(size => size.width)),
      innerHeight: Math.max(itemMarkHeight, linesSize.length * linesSize[0].height)
    };
    return _extends({}, innerSize, {
      outerWidth: innerSize.innerWidth + itemGap,
      outerHeight: innerSize.innerHeight + itemGap
    });
  }, [itemGap, itemMarkHeight, itemMarkWidth, markGap]);
  const totalWidth = drawingArea.left + drawingArea.width + drawingArea.right;
  const totalHeight = drawingArea.top + drawingArea.height + drawingArea.bottom;
  const availableWidth = totalWidth - padding.left - padding.right;
  const availableHeight = totalHeight - padding.top - padding.bottom;
  const seriesWithPosition = React.useMemo(() => {
    // Start at 0, 0. Will be modified later by padding and position.
    let x = 0;
    let y = 0;
    // total values used to align legend later.
    let totalWidthUsed = 0;
    let totalHeightUsed = 0;
    let rowIndex = 0;
    const rowMaxHeight = [0];
    const seriesWithRawPosition = seriesToDisplay.map(_ref => {
      let {
          label
        } = _ref,
        other = _objectWithoutPropertiesLoose(_ref, _excluded2);
      const itemSpace = getItemSpace(label, labelStyle);
      const rep = _extends({}, other, {
        label,
        positionX: x,
        positionY: y,
        innerHeight: itemSpace.innerHeight,
        innerWidth: itemSpace.innerWidth,
        outerHeight: itemSpace.outerHeight,
        outerWidth: itemSpace.outerWidth,
        rowIndex
      });
      if (direction === 'row') {
        if (x + itemSpace.innerWidth > availableWidth) {
          // This legend item would create overflow along the x-axis, so we start a new row.
          x = 0;
          y += rowMaxHeight[rowIndex];
          rowIndex += 1;
          if (rowMaxHeight.length <= rowIndex) {
            rowMaxHeight.push(0);
          }
          rep.positionX = x;
          rep.positionY = y;
          rep.rowIndex = rowIndex;
        }
        totalWidthUsed = Math.max(totalWidthUsed, x + itemSpace.outerWidth);
        totalHeightUsed = Math.max(totalHeightUsed, y + itemSpace.outerHeight);
        rowMaxHeight[rowIndex] = Math.max(rowMaxHeight[rowIndex], itemSpace.outerHeight);
        x += itemSpace.outerWidth;
      }
      if (direction === 'column') {
        if (y + itemSpace.innerHeight > availableHeight) {
          // This legend item would create overflow along the y-axis, so we start a new column.
          x = totalWidthUsed + itemGap;
          y = 0;
          rowIndex = 0;
          rep.positionX = x;
          rep.positionY = y;
          rep.rowIndex = rowIndex;
        }
        if (rowMaxHeight.length <= rowIndex) {
          rowMaxHeight.push(0);
        }
        totalWidthUsed = Math.max(totalWidthUsed, x + itemSpace.outerWidth);
        totalHeightUsed = Math.max(totalHeightUsed, y + itemSpace.outerHeight);
        rowIndex += 1;
        y += itemSpace.outerHeight;
      }
      return rep;
    });
    // Move the legend according to padding and position
    let gapX = 0;
    let gapY = 0;
    switch (position.horizontal) {
      case 'left':
        gapX = padding.left;
        break;
      case 'right':
        gapX = totalWidth - padding.right - totalWidthUsed;
        break;
      default:
        gapX = (totalWidth - totalWidthUsed) / 2;
        break;
    }
    switch (position.vertical) {
      case 'top':
        gapY = padding.top;
        break;
      case 'bottom':
        gapY = totalHeight - padding.bottom - totalHeightUsed;
        break;
      default:
        gapY = (totalHeight - totalHeightUsed) / 2;
        break;
    }
    return seriesWithRawPosition.map(item => _extends({}, item, {
      // Add the gap due to the position
      positionX: item.positionX + gapX,
      // Add the gap due to the position
      positionY: item.positionY + gapY + (direction === 'row' ? rowMaxHeight[item.rowIndex] / 2 // Get the center of the entire row
      : item.outerHeight / 2) // Get the center of the item
    }));
  }, [seriesToDisplay, position.horizontal, position.vertical, getItemSpace, labelStyle, direction, availableWidth, availableHeight, itemGap, padding.left, padding.right, padding.top, padding.bottom, totalWidth, totalHeight]);
  if (hidden) {
    return null;
  }
  return /*#__PURE__*/_jsx(NoSsr, {
    children: /*#__PURE__*/_jsx(ChartsLegendRoot, {
      className: classes.root,
      children: seriesWithPosition.map(({
        id,
        label,
        color,
        positionX,
        positionY
      }) => /*#__PURE__*/_jsxs("g", {
        className: classes.series,
        transform: `translate(${positionX} ${positionY})`,
        children: [/*#__PURE__*/_jsx("rect", {
          className: classes.mark,
          y: -itemMarkHeight / 2,
          width: itemMarkWidth,
          height: itemMarkHeight,
          fill: color
        }), /*#__PURE__*/_jsx(ChartsText, {
          style: labelStyle,
          text: label,
          x: itemMarkWidth + markGap,
          y: 0
        })]
      }, id))
    })
  });
}
process.env.NODE_ENV !== "production" ? DefaultChartsLegend.propTypes = {
  // ----------------------------- Warning --------------------------------
  // | These PropTypes are generated from the TypeScript type definitions |
  // | To update them edit the TypeScript types and run "yarn proptypes"  |
  // ----------------------------------------------------------------------
  /**
   * Override or extend the styles applied to the component.
   */
  classes: PropTypes.object.isRequired,
  /**
   * The direction of the legend layout.
   * The default depends on the chart.
   */
  direction: PropTypes.oneOf(['column', 'row']).isRequired,
  drawingArea: PropTypes.shape({
    bottom: PropTypes.number.isRequired,
    height: PropTypes.number.isRequired,
    left: PropTypes.number.isRequired,
    right: PropTypes.number.isRequired,
    top: PropTypes.number.isRequired,
    width: PropTypes.number.isRequired
  }).isRequired,
  /**
   * Set to true to hide the legend.
   */
  hidden: PropTypes.bool,
  /**
   * Space between two legend items (in px).
   * @default 10
   */
  itemGap: PropTypes.number,
  /**
   * Height of the item mark (in px).
   * @default 20
   */
  itemMarkHeight: PropTypes.number,
  /**
   * Width of the item mark (in px).
   * @default 20
   */
  itemMarkWidth: PropTypes.number,
  /**
   * Style applied to legend labels.
   * @default theme.typography.subtitle1
   */
  labelStyle: PropTypes.object,
  /**
   * Space between the mark and the label (in px).
   * @default 5
   */
  markGap: PropTypes.number,
  /**
   * Legend padding (in px).
   * Can either be a single number, or an object with top, left, bottom, right properties.
   * @default 0
   */
  padding: PropTypes.oneOfType([PropTypes.number, PropTypes.shape({
    bottom: PropTypes.number,
    left: PropTypes.number,
    right: PropTypes.number,
    top: PropTypes.number
  })]),
  position: PropTypes.shape({
    horizontal: PropTypes.oneOf(['left', 'middle', 'right']).isRequired,
    vertical: PropTypes.oneOf(['bottom', 'middle', 'top']).isRequired
  }).isRequired,
  series: PropTypes.shape({
    bar: PropTypes.shape({
      series: PropTypes.object.isRequired,
      seriesOrder: PropTypes.arrayOf(PropTypes.string).isRequired,
      stackingGroups: PropTypes.arrayOf(PropTypes.shape({
        ids: PropTypes.arrayOf(PropTypes.string).isRequired,
        stackingOffset: PropTypes.func.isRequired,
        stackingOrder: PropTypes.func.isRequired
      })).isRequired
    }),
    line: PropTypes.shape({
      series: PropTypes.object.isRequired,
      seriesOrder: PropTypes.arrayOf(PropTypes.string).isRequired,
      stackingGroups: PropTypes.arrayOf(PropTypes.shape({
        ids: PropTypes.arrayOf(PropTypes.string).isRequired,
        stackingOffset: PropTypes.func.isRequired,
        stackingOrder: PropTypes.func.isRequired
      })).isRequired
    }),
    pie: PropTypes.shape({
      series: PropTypes.object.isRequired,
      seriesOrder: PropTypes.arrayOf(PropTypes.string).isRequired
    }),
    scatter: PropTypes.shape({
      series: PropTypes.object.isRequired,
      seriesOrder: PropTypes.arrayOf(PropTypes.string).isRequired
    })
  }).isRequired,
  seriesToDisplay: PropTypes.arrayOf(PropTypes.shape({
    color: PropTypes.string.isRequired,
    id: PropTypes.string.isRequired,
    label: PropTypes.string.isRequired
  })).isRequired
} : void 0;
function ChartsLegend(inProps) {
  const props = useThemeProps({
    props: _extends({}, defaultProps, inProps),
    name: 'MuiChartsLegend'
  });
  const {
    position,
    direction,
    hidden,
    slots,
    slotProps
  } = props;
  const theme = useTheme();
  const classes = useUtilityClasses(_extends({}, props, {
    theme
  }));
  const drawingArea = React.useContext(DrawingContext);
  const series = React.useContext(SeriesContext);
  const seriesToDisplay = getSeriesToDisplay(series);
  const ChartLegendRender = slots?.legend ?? DefaultChartsLegend;
  const chartLegendRenderProps = useSlotProps({
    elementType: ChartLegendRender,
    externalSlotProps: slotProps?.legend,
    additionalProps: {
      position,
      direction,
      classes,
      drawingArea,
      series,
      hidden,
      seriesToDisplay
    },
    ownerState: {}
  });
  return /*#__PURE__*/_jsx(ChartLegendRender, _extends({}, chartLegendRenderProps));
}
process.env.NODE_ENV !== "production" ? ChartsLegend.propTypes = {
  // ----------------------------- Warning --------------------------------
  // | These PropTypes are generated from the TypeScript type definitions |
  // | To update them edit the TypeScript types and run "yarn proptypes"  |
  // ----------------------------------------------------------------------
  /**
   * Override or extend the styles applied to the component.
   */
  classes: PropTypes.object,
  /**
   * The direction of the legend layout.
   * The default depends on the chart.
   */
  direction: PropTypes.oneOf(['column', 'row']),
  /**
   * Set to true to hide the legend.
   * @default false
   */
  hidden: PropTypes.bool,
  position: PropTypes.shape({
    horizontal: PropTypes.oneOf(['left', 'middle', 'right']).isRequired,
    vertical: PropTypes.oneOf(['bottom', 'middle', 'top']).isRequired
  }),
  /**
   * The props used for each component slot.
   * @default {}
   */
  slotProps: PropTypes.object,
  /**
   * Overridable component slots.
   * @default {}
   */
  slots: PropTypes.object
} : void 0;
export { ChartsLegend };