UNPKG

@vuetify/nightly

Version:

Vue Material Component Framework

338 lines (336 loc) • 12.1 kB
import { createElementVNode as _createElementVNode, createVNode as _createVNode, normalizeClass as _normalizeClass, normalizeStyle as _normalizeStyle, mergeProps as _mergeProps, withDirectives as _withDirectives } from "vue"; // Styles import "./VPie.css"; // Components import { makeVPieSegmentProps, VPieSegment } from "./VPieSegment.js"; import { VPieTooltip } from "./VPieTooltip.js"; import { VAvatar } from "../../components/VAvatar/index.js"; import { VChip } from "../../components/VChip/index.js"; import { VChipGroup } from "../../components/VChipGroup/index.js"; import { VDefaultsProvider } from "../../components/VDefaultsProvider/index.js"; // Composables import { useColor } from "../../composables/color.js"; import { makeDensityProps } from "../../composables/density.js"; // Directives import vClickOutside from "../../directives/click-outside/index.js"; // Utilities import { computed, shallowRef, toRef, watch } from 'vue'; import { formatTextTemplate } from "./utils.js"; import { convertToUnit, genericComponent, pick, propsFactory } from "../../util/index.js"; // Types export const makeVPieProps = propsFactory({ title: String, bgColor: String, items: { type: Array, default: () => [] }, palette: { type: Array, default: () => [] }, itemKey: { type: String, default: 'key' }, itemValue: { type: String, default: 'value' }, itemTitle: { type: String, default: 'title' }, size: { type: [Number, String], default: 250 }, rotate: [Number, String], gaugeCut: [Number, String], legend: { type: [Boolean, Object], default: false }, tooltip: { type: [Boolean, Object], default: false }, ...makeDensityProps(), ...pick(makeVPieSegmentProps(), ['animation', 'gap', 'rounded', 'innerCut', 'hoverScale', 'hideSlice', 'reveal']) }, 'VPie'); export const VPie = genericComponent()({ name: 'VPie', directives: { vClickOutside }, props: makeVPieProps(), setup(props, _ref) { let { slots } = _ref; const legendConfig = computed(() => ({ visible: !!props.legend, position: 'bottom', textFormat: '[title]', ...(typeof props.legend === 'object' ? props.legend : {}) })); const { colorClasses, colorStyles } = useColor(() => ({ background: props.bgColor })); const textColorStyles = toRef(() => pick(colorStyles.value, ['color', 'caretColor'])); const legendAvatarSize = toRef(() => ({ default: 20, comfortable: 18, compact: 16 })[props.density ?? 'default']); const legendDirection = toRef(() => ['left', 'right'].includes(legendConfig.value.position) ? 'vertical' : 'horizontal'); const legendMode = toRef(() => !legendConfig.value.visible ? 'hidden' : legendConfig.value.position); const legendTextFormatFunction = toRef(() => item => { return typeof legendConfig.value.textFormat === 'function' ? legendConfig.value.textFormat(item) : formatTextTemplate(legendConfig.value.textFormat, item); }); const arcs = computed(() => { // hidden items get (value: 0) to trigger disappearing animation return props.items.filter(Boolean).map((item, index) => { return { key: item[props.itemKey], color: item.color ?? colorFromPalette(index), value: item[props.itemValue], title: String(item[props.itemTitle]), pattern: item.pattern ?? patternFromPalette(index), raw: item }; }); }); const visibleItemsKeys = shallowRef([]); watch(() => arcs.value.length, () => { // reset when number of items changes visibleItemsKeys.value = arcs.value.map(a => a.key); }, { immediate: true }); const visibleItems = computed(() => { // hidden items get (value: 0) to trigger disappearing animation return arcs.value.map(item => { return isVisible(item) ? item : { ...item, value: 0 }; }); }); const total = computed(() => visibleItems.value.reduce((sum, item) => sum + item.value, 0)); const gaugeCut = toRef(() => Number(props.gaugeCut ?? 0)); const gaugeOffset = computed(() => (1 - Math.cos(Math.PI * Math.min(90, gaugeCut.value / 2) / 180)) / 2); const rotateDeg = computed(() => `${gaugeCut.value ? 180 + gaugeCut.value / 2 : props.rotate ?? 0}deg`); function arcOffset(index) { return visibleItems.value.slice(0, index).reduce((acc, s) => acc + (total.value > 0 ? s.value / total.value : 0) * (360 - gaugeCut.value), 0); } function arcSize(v) { return v / total.value * (100 - gaugeCut.value / 3.6); } function colorFromPalette(index) { if (props.palette.length === 0) return undefined; const paletteItem = props.palette[index % props.palette.length]; return typeof paletteItem === 'object' ? paletteItem.color : paletteItem; } function patternFromPalette(index) { if (props.palette.length === 0) return undefined; const paletteItem = props.palette[index % props.palette.length]; return typeof paletteItem === 'object' ? paletteItem.pattern : undefined; } function isVisible(item) { return visibleItemsKeys.value.includes(item.key); } function toggle(item) { if (isVisible(item)) { visibleItemsKeys.value = visibleItemsKeys.value.filter(x => x !== item.key); } else { visibleItemsKeys.value = [...visibleItemsKeys.value, item.key]; } } const tooltipItem = shallowRef(null); const tooltipVisible = shallowRef(false); const tooltipTarget = shallowRef([0, 0]); let mouseLeaveTimeout = null; function setItemActive(item, active) { arcs.value.forEach(a => a.isActive = a.key === item.key && active); if (props.tooltip) { setTooltip(item, active); } } function setTooltip(item, active) { clearTimeout(mouseLeaveTimeout); if (active) { tooltipVisible.value = true; tooltipItem.value = item; } else { mouseLeaveTimeout = setTimeout(() => { tooltipVisible.value = false; // intentionally reusing timeout here mouseLeaveTimeout = setTimeout(() => { tooltipItem.value = null; }, 500); }, 100); } } let frame = -1; function onSvgMousemove(_ref2) { let { clientX, clientY } = _ref2; cancelAnimationFrame(frame); frame = requestAnimationFrame(() => { tooltipTarget.value = [clientX, clientY]; }); } function onSvgTouchstart(_ref3) { let { touches } = _ref3; if (!touches) return; const { clientX, clientY } = touches[0]; tooltipTarget.value = [clientX, clientY]; } function onSvgClickOutside() { arcs.value.forEach(a => a.isActive = false); tooltipVisible.value = false; } return () => { const segmentProps = pick(props, ['animation', 'gap', 'rounded', 'hideSlice', 'reveal', 'innerCut', 'hoverScale']); const defaultTooltipTransition = { name: 'fade-transition', duration: 150 }; const tooltipProps = { item: tooltipItem.value, modelValue: tooltipVisible.value, titleFormat: typeof props.tooltip === 'object' ? props.tooltip.titleFormat : '[title]', subtitleFormat: typeof props.tooltip === 'object' ? props.tooltip.subtitleFormat : '[value]', transition: typeof props.tooltip === 'object' ? props.tooltip.transition : defaultTooltipTransition, offset: typeof props.tooltip === 'object' ? props.tooltip.offset : 16, target: tooltipTarget.value }; const legendDefaults = { VChipGroup: { direction: legendDirection.value }, VChip: { density: props.density }, VAvatar: { size: legendAvatarSize.value } }; const tooltipDefaults = { VAvatar: { size: typeof props.tooltip === 'object' ? props.tooltip.avatarSize ?? 28 : 28 } }; const avatarSlot = _ref4 => { let { item } = _ref4; return _createVNode(VAvatar, { "color": item.color, "start": true }, { default: () => [item.pattern && _createElementVNode("svg", { "height": "40", "width": "40" }, [_createElementVNode("rect", { "width": "40", "height": "40", "fill": item.pattern }, null)])] }); }; return _createElementVNode("div", { "class": _normalizeClass(['v-pie', `v-pie--legend-${legendMode.value}`]), "style": { '--v-pie-size': convertToUnit(props.size) } }, [slots.title?.() ?? (props.title && _createElementVNode("div", { "class": "v-pie__title" }, [props.title])), _createElementVNode("div", { "class": _normalizeClass(['v-pie__content', colorClasses.value]), "style": _normalizeStyle([{ transform: `rotate(${rotateDeg.value})`, marginBottom: `calc(-1 * ${convertToUnit(props.size)} * ${gaugeOffset.value})` }, textColorStyles.value]) }, [_createElementVNode("div", { "class": _normalizeClass(['v-pie__content-underlay', colorClasses.value]), "style": _normalizeStyle(colorStyles.value) }, null), _withDirectives(_createElementVNode("svg", { "xmlns": "http://www.w3.org/2000/svg", "viewBox": "0 0 100 100", "class": "v-pie__segments", "onMousemove": onSvgMousemove, "onTouchstart": onSvgTouchstart }, [arcs.value.map((item, index) => _createVNode(VPieSegment, _mergeProps(segmentProps, { "key": item.key, "active": item.isActive, "color": item.color, "value": isVisible(item) ? arcSize(item.value) : 0, "rotate": arcOffset(index), "pattern": item.pattern, "onUpdate:active": val => setItemActive(item, val), "onTouchend": () => setItemActive(item, true) }), null))]), [[vClickOutside, { handler: onSvgClickOutside }]]), _createElementVNode("div", { "class": "v-pie__center-content", "style": { transform: `translate(-50%, -50%) rotate(-${rotateDeg.value}) translateY(calc(-100% * ${gaugeOffset.value}))` } }, [_createElementVNode("div", null, [slots.center?.({ total: total.value })])])]), legendConfig.value.visible && _createVNode(VDefaultsProvider, { "key": "legend", "defaults": legendDefaults }, { default: () => [_createElementVNode("div", { "class": "v-pie__legend" }, [slots.legend?.({ isActive: isVisible, toggle, items: arcs.value, total: total.value }) ?? _createVNode(VChipGroup, { "column": true, "multiple": true, "modelValue": visibleItemsKeys.value, "onUpdate:modelValue": $event => visibleItemsKeys.value = $event }, { default: () => [arcs.value.map(item => _createVNode(VChip, { "value": item.key }, { prepend: () => avatarSlot({ item }), default: () => _createElementVNode("div", { "class": "v-pie__legend__text" }, [slots['legend-text']?.({ item, total: total.value }) ?? legendTextFormatFunction.value(item)]) }))] })])] }), !!props.tooltip && _createVNode(VDefaultsProvider, { "defaults": tooltipDefaults }, { default: () => [_createVNode(VPieTooltip, tooltipProps, { default: slots.tooltip ? slotProps => slots.tooltip?.({ ...slotProps, total: total.value }) : undefined, prepend: avatarSlot })] })]); }; } }); //# sourceMappingURL=VPie.js.map