routie dev init since i didn't adhere to any proper guidance up until now
This commit is contained in:
+333
@@ -0,0 +1,333 @@
|
||||
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
|
||||
Reference in New Issue
Block a user