UNPKG

@amaui/ui-react

Version:
634 lines (607 loc) 23.4 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; const _excluded = ["tonal", "color", "size", "parts", "lineCap", "padding", "gap", "border", "background", "boundary", "boundaryWidth", "arcProgress", "arcsVisible", "marksVisible", "labelsVisible", "marks", "markSize", "markWidth", "labels", "renderLabel", "childrenPosition", "additional", "textProps", "pathProps", "SvgProps", "MarkProps", "LabelProps", "BackgroundProps", "BorderProps", "ArcProps", "ArcMainProps", "ArcsProgressProps", "ArcProgressProps", "Component", "className", "style", "children"], _excluded2 = ["size", "padding", "position"], _excluded3 = ["value", "padding", "position"], _excluded4 = ["x", "y", "value"]; function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } import React from 'react'; import { clamp, is, parse, valueFromPercentageWithinRange } from '@amaui/utils'; import { classNames, style as styleMethod, useAmauiTheme } from '@amaui/style-react'; import SurfaceElement from '../Surface'; import { angleToCoordinates, staticClassName, toNumber } from '../utils'; const useStyle = styleMethod(theme => ({ root: { width: '100vw' }, size_small: { maxWidth: '180px' }, size_regular: { maxWidth: '240px' }, size_large: { maxWidth: '300px' }, boundary_1: { aspectRatio: '1' }, boundary_075: { aspectRatio: '1' }, boundary_05: { aspectRatio: '1' }, boundary_025: { aspectRatio: '1' }, label: _objectSpread(_objectSpread({}, theme.typography.values.b2), {}, { textAnchor: 'middle', alignmentBaseline: 'central', dominantBaseline: 'central' }), svg: { position: 'relative', width: '100%', height: 'auto' } }), { name: 'amaui-RoundMeter' }); const RoundMeter = /*#__PURE__*/React.forwardRef((props_, ref) => { const theme = useAmauiTheme(); const props = React.useMemo(() => _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.amauiRoundMeter?.props?.default), props_), [props_]); const Surface = React.useMemo(() => theme?.elements?.Surface || SurfaceElement, [theme]); const { tonal = true, color = 'primary', size = 'regular', parts: parts_ = 1, lineCap, padding: outsidePadding = 0, gap: gap_ = 0, border = false, background = false, boundary: boundary_ = 1, boundaryWidth = 1, arcProgress = false, arcsVisible = true, marksVisible = true, labelsVisible = true, marks: marks_ = [], markSize = 4, markWidth = 1, labels: labels_ = [], renderLabel, childrenPosition = 'post', additional, textProps, pathProps, SvgProps, MarkProps, LabelProps, BackgroundProps, BorderProps, ArcProps, ArcMainProps, ArcsProgressProps, ArcProgressProps, Component = 'div', className, style, children } = props, other = _objectWithoutProperties(props, _excluded); const { classes } = useStyle(); const refs = { root: React.useRef(undefined) }; const styles = { root: {} }; let radius; const boundary = parse(boundary_); const width = 240; const height = width; let gap = ['round', 'square'].includes(lineCap) ? gap_ + boundaryWidth / 2 : gap_; const parts = clamp(parse(parts_), 1, 180); let min = 0; let max = 360; let yViewBox = 0; // 1 if (boundary === 1) { // 0 is middle top // ie. 270 degreese min = 270; max = 270 + 360; } // 0.75 if (boundary === 0.75) { // 0 is angle bottom left // ie. 270 degreese min = 135; max = 135 + 270; yViewBox = -15; } // 0.5 if (boundary === 0.5) { // 0 is left // ie. 180 degreese min = 180; max = 180 + 180; yViewBox = -50; } // 0.25 if (boundary === 0.25) { // 0 is angle top left // ie. 225 degreese min = 225; max = 225 + 90; yViewBox = -60; } if (!['small', 'regular', 'large'].includes(size)) styles.root.maxWidth = size; const marks = React.useMemo(() => { const values = []; if (marks_.length) { const center = toNumber(width / 2); radius = toNumber(width / 2 - boundaryWidth - outsidePadding); let marksValues = marks_; if (!is('array', marksValues[0])) marksValues = [marksValues]; marksValues.forEach((marksValue, index) => { values[index] = []; marksValue.forEach(mark => { const { size: size_, padding: markPadding = 0, position } = mark, other_ = _objectWithoutProperties(mark, _excluded2); const itemPadding = toNumber(markPadding); const angle = valueFromPercentageWithinRange(position, min, max); const start = angleToCoordinates(angle, center, center, radius - itemPadding); const end = angleToCoordinates(angle, center, center, radius - (size_ !== undefined ? size_ : markSize) - itemPadding); values[index].push(_objectSpread({ d: ['M', start.x, start.y, 'L', end.x, end.y].join(' ') }, other_)); }); }); } return values; }, [width, height, parts, marks_, markSize, boundary, boundaryWidth, lineCap, outsidePadding, gap]); const labels = React.useMemo(() => { const values = []; if (labels_.length) { const center = toNumber(width / 2); const marksPadding = toNumber(marks_?.length ? (marks_ || []).sort((a, b) => b.size - a.size)[0]?.size || markSize : 0); radius = toNumber(width / 2 - boundaryWidth - marksPadding - outsidePadding); let labelsValues = labels_; if (!is('array', labelsValues[0])) labelsValues = [labelsValues]; labelsValues.forEach((labelsValue, index) => { values[index] = []; labelsValue.forEach(label => { const { value, padding: labelPadding = 0, position } = label, other_ = _objectWithoutProperties(label, _excluded3); const itemPadding = toNumber(labelPadding); const fontSize = toNumber(label.style?.fontSize !== undefined ? label.style.fontSize : 14); const angle = valueFromPercentageWithinRange(position, min, max); const start = angleToCoordinates(angle, center, center, radius - fontSize / 2 - itemPadding); values[index].push(_objectSpread({ x: start.x, y: start.y, value }, other_)); }); }); } return values; }, [width, height, parts, marks_, markSize, boundary, boundaryWidth, lineCap, outsidePadding, gap]); const arcs = React.useMemo(() => { const values = []; let value = []; const offset = outsidePadding; // 1 if (boundary === 1) { if (parts === 1) { radius = width / 2 - boundaryWidth / 2 - offset; values.push({ d: [ // Move 'M', offset + boundaryWidth / 2, width / 2 + 0.001, // Arc 'A', radius, radius, 0, 1, 0, offset + boundaryWidth / 2, width / 2].join(' ') }); } else { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const total = 360; const part = (total - parts * gap) / parts; const angles = { start: angleToCoordinates(0, center, center, radius) }; let anglePrevious = 0; for (let i = 0; i < parts; i++) { // Move to 0 deg if (i === 0) value.push( // Move to 0 deg 'M', angles.start.x, angles.start.y); const angleEnd = anglePrevious + part; angles.end = angleToCoordinates(angleEnd, center, center, radius); angles.move = angleToCoordinates(angleEnd + gap, center, center, radius); // Arc value.push('A', radius, radius, 0, 0, 1, angles.end.x, angles.end.y); // Move the gap if there's a gap if (gap > 0 && i < parts - 1) { value.push('M', angles.move.x, angles.move.y); anglePrevious = angleEnd + gap; } else anglePrevious = angleEnd; values.push({ d: value.join(' ') }); // Move for the next value if (i < parts - 1) { value = ['M', angles.move.x, angles.move.y]; } } } } // 0.75 if (boundary === 0.75) { value = []; const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const angles = { end: angleToCoordinates(45, center, center, radius), start: angleToCoordinates(135, center, center, radius) }; if (parts === 1) { values.push({ d: [ // Line middle bottom 'M', angles.start.x, angles.start.y, // Arc 'A', radius, radius, 0, 1, 1, angles.end.x, angles.end.y].join(' ') }); } else { const total = 270; const part = (total - (parts - 1) * gap) / parts; const angles_ = { 0: angleToCoordinates(135, center, center, radius) }; let anglePrevious = 135; for (let i = 0; i < parts; i++) { // Move to 135 deg if (i === 0) value.push( // Move to 0 deg 'M', angles_[0].x, angles_[0].y); const angleEnd = anglePrevious + part; angles_.end = angleToCoordinates(angleEnd, center, center, radius); angles_.move = angleToCoordinates(angleEnd + gap, center, center, radius); // Arc value.push('A', radius, radius, 0, 0, 1, angles_.end.x, angles_.end.y); // Move the gap if there's a gap if (gap > 0 && i < parts - 1) { value.push('M', angles_.move.x, angles_.move.y); anglePrevious = angleEnd + gap; } else anglePrevious = angleEnd; values.push({ d: value.join(' ') }); // Move for the next value if (i < parts - 1) { value = ['M', angles_.move.x, angles_.move.y]; } } } } // 0.5 if (boundary === 0.5) { value = []; const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const total = 180; const part = (total - (parts - 1) * gap) / parts; const angles = { start: angleToCoordinates(180, center, center, radius) }; let anglePrevious = 180; for (let i = 0; i < parts; i++) { // Move to 180 deg if (i === 0) value.push( // Move to 0 deg 'M', angles.start.x, angles.start.y); const angleEnd = anglePrevious + part; angles.end = angleToCoordinates(angleEnd, center, center, radius); angles.move = angleToCoordinates(angleEnd + gap, center, center, radius); // Arc value.push('A', radius, radius, 0, 0, 1, angles.end.x, angles.end.y); // Move the gap if there's a gap if (gap > 0 && i < parts - 1) { value.push('M', angles.move.x, angles.move.y); anglePrevious = angleEnd + gap; } else anglePrevious = angleEnd; values.push({ d: value.join(' ') }); // Move for the next value if (i < parts - 1) { value = ['M', angles.move.x, angles.move.y]; } } } // 0.25 if (boundary === 0.25) { value = []; const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const total = 90; const part = clamp((total - (parts - 1) * gap) / parts, 0.01); gap = clamp(gap, 0, (total - part * parts) / (parts - 1)); const angles = { start: angleToCoordinates(225, center, center, radius) }; let anglePrevious = 225; for (let i = 0; i < parts; i++) { // Move to 225 deg if (i === 0) value.push( // Move to 0 deg 'M', angles.start.x, angles.start.y); const angleEnd = anglePrevious + part; angles.end = angleToCoordinates(angleEnd, center, center, radius); angles.move = angleToCoordinates(angleEnd + gap, center, center, radius); // Arc value.push('A', radius, radius, 0, 0, 1, angles.end.x, angles.end.y); // Move the gap if there's a gap if (gap > 0 && i < parts - 1) { value.push('M', angles.move.x, angles.move.y); anglePrevious = angleEnd + gap; } else anglePrevious = angleEnd; values.push({ d: value.join(' ') }); // Move for the next value if (i < parts - 1) { value = ['M', angles.move.x, angles.move.y]; } } } return values; }, [width, height, parts, boundary, boundaryWidth, lineCap, outsidePadding, gap, gap_]); const pathBackground = React.useMemo(() => { const values = []; const offset = outsidePadding; // 1 if (boundary === 1) { radius = width / 2 - boundaryWidth / 2 - offset; values.push( // Move 'M', offset + boundaryWidth / 2, width / 2 + 0.001, // Arc 'A', radius, radius, 0, 1, 0, offset + boundaryWidth / 2, width / 2); } // 0.75 if (boundary === 0.75) { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const angles = { end: angleToCoordinates(45, center, center, radius), start: angleToCoordinates(135, center, center, radius) }; values.push( // Move 'M', center, center, // Line middle bottom 'L', angles.start.x, angles.start.y, // Arc 'A', radius, radius, 0, 1, 1, angles.end.x, angles.end.y, // Line bottom middle 'L', center, center, 'Z'); } // 0.5 if (boundary === 0.5) { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const total = 180; const part = (total - (parts - 1) * gap) / parts; const angles = { start: angleToCoordinates(180, center, center, radius) }; const anglePrevious = 180; const angleEnd = anglePrevious + part; angles.end = angleToCoordinates(angleEnd, center, center, radius); angles.move = angleToCoordinates(angleEnd + gap, center, center, radius); values.push( // Move 'M', angles.start.x, angles.start.y, // Arc 'A', radius, radius, 0, 0, 1, angles.end.x, angles.end.y, 'Z'); } // 0.25 if (boundary === 0.25) { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const total = 90; const part = clamp((total - (parts - 1) * gap) / parts, 0.01); gap = clamp(gap, 0, (total - part * parts) / (parts - 1)); const angles = { start: angleToCoordinates(225, center, center, radius) }; const anglePrevious = 225; const angleEnd = anglePrevious + part; angles.end = angleToCoordinates(angleEnd, center, center, radius); angles.move = angleToCoordinates(angleEnd + gap, center, center, radius); values.push( // Move 'M', center, width / 2 - boundaryWidth, // Line middle bottom, top quarter left 'L', angles.start.x, angles.start.y, // Arc 'A', radius, radius, 0, 0, 1, angles.end.x, angles.end.y, // Line top quarter right, middle bottom 'L', center, width / 2 - boundaryWidth, 'Z'); } return values.join(' '); }, [width, height, boundary, boundaryWidth, outsidePadding]); const pathBorder = React.useMemo(() => { const values = []; const offset = outsidePadding; // 0.75 if (boundary === 0.75) { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const angles = { end: angleToCoordinates(45, center, center, radius), start: angleToCoordinates(135, center, center, radius) }; values.push( // Move bottom angle left 'M', angles.start.x, angles.start.y, // Line middle 'L', center, center, // Line bottom angle right 'L', angles.end.x, angles.end.y); } // 0.5 if (boundary === 0.5) { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const angles = { start: angleToCoordinates(180, center, center, radius) }; values.push( // Move 'M', angles.start.x, angles.start.y, // Line 'L', width - boundaryWidth / 2 - offset, angles.start.y); } // 0.25 if (boundary === 0.25) { const center = width / 2; radius = width / 2 - boundaryWidth / 2 - offset; const total = 90; const part = clamp((total - (parts - 1) * gap) / parts, 0.01); gap = clamp(gap, 0, (total - part * parts) / (parts - 1)); const angles = { start: angleToCoordinates(225, center, center, radius), end: angleToCoordinates(315, center, center, radius) }; values.push( // Move middle bottom, top quarter left 'M', angles.start.x, angles.start.y, // Line middle bottom 'L', center, width / 2 - boundaryWidth, // Arc top quarter right, middle bottom 'L', angles.end.x, angles.end.y); } return values.join(' '); }, [width, height, boundary, boundaryWidth, outsidePadding]); const children_ = children && /*#__PURE__*/React.createElement("g", { className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-children'], classes.children]) }, React.Children.toArray(children).map((item, index) => { return /*#__PURE__*/React.cloneElement(item, { key: index, fill: item.props.fill !== undefined ? item.props.fill : color, stroke: item.props.stroke !== undefined ? item.props.stroke : color, // clean up value: undefined, style: _objectSpread(_objectSpread({}, item.props.value !== undefined ? { transform: `rotate(${valueFromPercentageWithinRange(item.props.value, min, max)}deg)` } : undefined), item.props.style) }); })); return /*#__PURE__*/React.createElement(Component, _extends({ ref: item => { if (ref) { if (is('function', ref)) ref(item);else ref.current = item; } refs.root.current = item; }, className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-root', `amaui-RoundMeter-size-${size}`], className, classes.root, classes[`size_${size}`], classes[`boundary_${String(boundary).replace('.', '')}`]]), style: _objectSpread(_objectSpread({}, styles.root), style) }, other), additional, /*#__PURE__*/React.createElement(Surface, { tonal: tonal, color: color }, _ref => { let { color: color_, backgroundColor } = _ref; return /*#__PURE__*/React.createElement("svg", _extends({ xmlns: "http://www.w3.org/2000/svg", viewBox: `0 ${yViewBox} ${width || 0} ${height || 0}` }, SvgProps, { className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-svg'], SvgProps?.className, classes.svg]) }), childrenPosition === 'pre' && children_, background && /*#__PURE__*/React.createElement("path", _extends({ d: pathBackground, fill: backgroundColor, stroke: "none" }, pathProps, BackgroundProps)), border && /*#__PURE__*/React.createElement("path", _extends({ d: pathBorder, fill: "none", stroke: color_, strokeWidth: boundaryWidth }, pathProps, BorderProps)), arcsVisible && /*#__PURE__*/React.createElement("g", { className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-arcs'], classes.arcs]) }, arcs.map((item, index) => /*#__PURE__*/React.createElement("path", _extends({ key: index, d: item.d, fill: "none", stroke: color_, strokeWidth: boundaryWidth, strokeLinecap: lineCap }, pathProps, ArcProps, ArcMainProps)))), arcsVisible && arcProgress && /*#__PURE__*/React.createElement("g", _extends({}, ArcsProgressProps, { className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-arcs-progress'], ArcsProgressProps?.className, classes.arcs_progress]) }), arcs.map((item, index) => /*#__PURE__*/React.createElement("path", _extends({ key: index, d: item.d, fill: "none", stroke: color_, strokeWidth: boundaryWidth, strokeLinecap: lineCap }, pathProps, ArcProps, ArcProgressProps)))), childrenPosition === 'pre-marks' && children_, marksVisible && !!marks_.length && marks.map((marksValue, index) => /*#__PURE__*/React.createElement("g", { key: index, className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-marks'], classes.marks]) }, marksValue.map((item, index_) => /*#__PURE__*/React.createElement("path", _extends({ key: index_, d: item.d, fill: "none", stroke: color_, strokeWidth: item.width !== undefined ? item.width : markWidth, strokeLinecap: lineCap }, pathProps, MarkProps))))), childrenPosition === 'pre-labels' && children_, labelsVisible && !!labels_.length && labels.map((labelsValue, index) => { return /*#__PURE__*/React.createElement("g", { key: index, className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-labels'], classes.labels]) }, labelsValue.map((item, index_) => { const { x, y, value } = item, other_ = _objectWithoutProperties(item, _excluded4); const propsLabel = _objectSpread(_objectSpread(_objectSpread(_objectSpread({}, other_), textProps), LabelProps), {}, { style: _objectSpread(_objectSpread(_objectSpread({ fill: color_ }, other_.style), textProps?.style), LabelProps?.style) }); if (is('function', renderLabel)) return renderLabel(x, y, value, propsLabel); return /*#__PURE__*/React.createElement("text", _extends({ key: index_, x: x, y: y }, propsLabel, { className: classNames([staticClassName('RoundMeter', theme) && ['amaui-RoundMeter-label'], other_?.className, textProps?.className, LabelProps?.className, classes.label]) }), value); })); }), childrenPosition === 'post' && children_); })); }); RoundMeter.displayName = 'amaui-RoundMeter'; export default RoundMeter;