333 lines
12 KiB
JavaScript
333 lines
12 KiB
JavaScript
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, {
|
|
slots
|
|
}) {
|
|
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 activeItemKey = shallowRef(null);
|
|
const tooltipItem = shallowRef(null);
|
|
const tooltipVisible = shallowRef(false);
|
|
const tooltipTarget = shallowRef([0, 0]);
|
|
let mouseLeaveTimeout = null;
|
|
function setItemActive(item, active) {
|
|
activeItemKey.value = active ? item.key : null;
|
|
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({
|
|
clientX,
|
|
clientY
|
|
}) {
|
|
cancelAnimationFrame(frame);
|
|
frame = requestAnimationFrame(() => {
|
|
tooltipTarget.value = [clientX, clientY];
|
|
});
|
|
}
|
|
function onSvgTouchstart({
|
|
touches
|
|
}) {
|
|
if (!touches) return;
|
|
const {
|
|
clientX,
|
|
clientY
|
|
} = touches[0];
|
|
tooltipTarget.value = [clientX, clientY];
|
|
}
|
|
function onSvgClickOutside() {
|
|
activeItemKey.value = null;
|
|
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 = ({
|
|
item
|
|
}) => _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": activeItemKey.value === item.key,
|
|
"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
|