@amaui/ui-react
Version:
UI for React
634 lines (607 loc) • 23.4 kB
JavaScript
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;