UNPKG

vuetify

Version:

Vue Material Component Framework

334 lines (327 loc) 13.1 kB
import { createElementVNode as _createElementVNode, mergeProps as _mergeProps, createVNode as _createVNode, Fragment as _Fragment } from "vue"; // Components import { VSparklineTooltip } from "./VSparklineTooltip.js"; // Utilities import { computed, Fragment, nextTick, ref, shallowRef, useId, watch } from 'vue'; import { makeLineProps } from "./util/line.js"; import { genericComponent, getPropertyFromItem, PREFERS_REDUCED_MOTION, propsFactory, useRender } from "../../util/index.js"; import { easingPatterns, useTransition } from "../../util/easing.js"; // Types export const makeVBarlineProps = propsFactory({ autoLineWidth: Boolean, ...makeLineProps() }, 'VBarline'); export const VBarline = genericComponent()({ name: 'VBarline', inheritAttrs: false, props: makeVBarlineProps(), emits: { 'update:currentIndex': _index => true }, setup(props, { slots, attrs, emit }) { const uid = useId(); const id = computed(() => props.id || `barline-${uid}`); const autoDrawDuration = computed(() => Number(props.autoDrawDuration) || 500); const hasDrawn = ref(false); const clipRects = shallowRef([]); const animationDuration = computed(() => typeof props.animation === 'object' ? props.animation.duration ?? 300 : 300); const animationEasing = computed(() => typeof props.animation === 'object' ? props.animation.easing ?? 'ease' : 'ease'); const hasLabels = computed(() => { return Boolean(props.showLabels || props.labels.length > 0 || !!slots?.label); }); const lineWidth = computed(() => parseFloat(props.lineWidth) || 4); const items = computed(() => props.modelValue.map(item => getPropertyFromItem(item, props.itemValue, item))); const totalWidth = computed(() => Math.max(items.value.length * lineWidth.value, Number(props.width))); const boundary = computed(() => { return { minX: 0, maxX: totalWidth.value, minY: 0, maxY: parseInt(props.height, 10) }; }); function genBars(values, boundary) { const { minX, maxX, minY, maxY } = boundary; const totalValues = values.length; let maxValue = props.max != null ? Number(props.max) : Math.max(...values); let minValue = props.min != null ? Number(props.min) : Math.min(...values); if (minValue > 0 && props.min == null) minValue = 0; if (maxValue < 0 && props.max == null) maxValue = 0; const gridX = maxX / (totalValues === 1 ? 2 : totalValues); const gridY = (maxY - minY) / (maxValue - minValue || 1); const horizonY = maxY - Math.abs(minValue * gridY); return values.map((value, index) => { const height = Math.abs(gridY * value); return { x: minX + index * gridX, y: horizonY - height + Number(value < 0) * height, height, value }; }); } const bars = computed(() => genBars(items.value, boundary.value)); const prevBarCount = ref(0); function applyTransition(el, duration, easing) { el.style.transition = `y ${duration}ms ${easing}, height ${duration}ms ${easing}`; } function collapseNewBars(bars, fromIndex, duration, easing) { for (let i = fromIndex; i < bars.length; i++) { const el = clipRects.value[i]; if (!el) continue; // Snap to collapsed at bar's baseline el.style.transition = 'none'; el.setAttribute('y', String(bars[i].y + bars[i].height)); el.setAttribute('height', '0'); el.getBoundingClientRect(); // Animate to final state applyTransition(el, duration, easing); el.setAttribute('y', String(bars[i].y)); el.setAttribute('height', String(bars[i].height)); } } function applyBarsTransition(oldCount) { if (!props.animation) return; clipRects.value.forEach(el => { if (el) applyTransition(el, animationDuration.value, animationEasing.value); }); if (bars.value.length > oldCount && oldCount > 0) { collapseNewBars(bars.value, oldCount, animationDuration.value, animationEasing.value); } } watch(() => props.modelValue, async () => { await nextTick(); if (PREFERS_REDUCED_MOTION() || !clipRects.value.length) return; const oldCount = prevBarCount.value; prevBarCount.value = bars.value.length; // Animation-only mode (no auto-draw), or draw-once after first draw: just ensure transition is set if (!props.autoDraw || props.autoDraw === 'once' && hasDrawn.value) { applyBarsTransition(oldCount); return; } hasDrawn.value = true; const shouldDrawOnce = props.autoDraw === 'once'; clipRects.value.forEach((el, i) => { const bar = bars.value[i]; if (!el || !bar) return; // Snap to collapsed state el.style.transition = 'none'; el.setAttribute('y', String(bar.y + bar.height)); el.setAttribute('height', '0'); el.getBoundingClientRect(); // Animate to final state applyTransition(el, autoDrawDuration.value, props.autoDrawEasing); el.setAttribute('y', String(bar.y)); el.setAttribute('height', String(bar.height)); // After initial draw, switch to animation timing for subsequent changes if (shouldDrawOnce && props.animation) { el.addEventListener('transitionend', () => { applyTransition(el, animationDuration.value, animationEasing.value); }, { once: true }); } }); }, { immediate: true }); const parsedLabels = computed(() => { return bars.value.map((bar, i) => ({ x: bar.x, value: String(props.labels[i] ?? bar.value) })); }); const offsetX = computed(() => bars.value.length === 1 ? (boundary.value.maxX - lineWidth.value) / 2 : (Math.abs(bars.value[0].x - bars.value[1].x) - lineWidth.value) / 2); const smooth = computed(() => typeof props.smooth === 'boolean' ? props.smooth ? 2 : 0 : Number(props.smooth)); const columnWidth = computed(() => { const len = bars.value.length; return totalWidth.value / (len === 1 ? 2 : len); }); // Hover / tooltip state const svgRef = shallowRef(null); const currentIndex = shallowRef(null); const tooltipVisible = shallowRef(false); const targetX = shallowRef(0); const targetY = shallowRef(0); const targetHeight = shallowRef(0); watch(currentIndex, index => { if (index === null) return; const bar = bars.value[index]; if (!bar) return; targetX.value = bar.x + offsetX.value; targetY.value = bar.y; targetHeight.value = bar.height; }); const transitionOptions = { duration: 150, transition: easingPatterns.easeOutQuad }; const animatedX = useTransition(targetX, transitionOptions); const animatedY = useTransition(targetY, transitionOptions); const tooltipTarget = computed(() => { if (currentIndex.value === null || !svgRef.value) return undefined; const ctm = svgRef.value.getScreenCTM(); if (!ctm) return undefined; const svgPoint = svgRef.value.createSVGPoint(); svgPoint.x = animatedX.value + lineWidth.value / 2; svgPoint.y = animatedY.value; const { x, y } = svgPoint.matrixTransform(ctm); return [x, y]; }); const tooltipConfig = computed(() => ({ showCrosshair: false, titleFormat: item => String(item.value), ...(typeof props.tooltip === 'object' ? props.tooltip : {}) })); let frame = -1; function onSvgMousemove(e) { const target = e.currentTarget; cancelAnimationFrame(frame); frame = requestAnimationFrame(() => { const rect = target.getBoundingClientRect(); const svgX = (e.clientX - rect.left) / rect.width * totalWidth.value; let nearest = 0; let minDist = Infinity; bars.value.forEach((bar, i) => { const barCenter = bar.x + offsetX.value + lineWidth.value / 2; const dist = Math.abs(barCenter - svgX); if (dist < minDist) { minDist = dist; nearest = i; } }); currentIndex.value = nearest; emit('update:currentIndex', nearest); tooltipVisible.value = true; }); } function onSvgMouseleave() { cancelAnimationFrame(frame); tooltipVisible.value = false; if (!props.tooltip) { currentIndex.value = null; emit('update:currentIndex', null); } } function onTooltipAfterLeave() { currentIndex.value = null; emit('update:currentIndex', null); } function setIndex(index) { currentIndex.value = index; emit('update:currentIndex', index); tooltipVisible.value = index !== null; } function onSvgFocus() { if (!bars.value.length) return; setIndex(bars.value.length - 1); } function onSvgBlur() { tooltipVisible.value = false; if (!props.tooltip) { setIndex(null); } } function onSvgKeydown(e) { if (!bars.value.length) return; const len = bars.value.length; if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') { e.preventDefault(); const direction = e.key === 'ArrowLeft' ? -1 : 1; const current = currentIndex.value ?? (direction === 1 ? -1 : len); const next = Math.max(0, Math.min(len - 1, current + direction)); setIndex(next); } } useRender(() => { const gradientData = !props.gradient.slice().length ? [''] : props.gradient.slice().reverse(); return _createElementVNode(_Fragment, null, [_createElementVNode("svg", _mergeProps({ "ref": svgRef, "display": "block", "tabindex": props.interactive ? 0 : undefined, "onMousemove": props.interactive ? onSvgMousemove : undefined, "onMouseleave": props.interactive ? onSvgMouseleave : undefined, "onFocus": props.interactive ? onSvgFocus : undefined, "onBlur": props.interactive ? onSvgBlur : undefined, "onKeydown": props.interactive ? onSvgKeydown : undefined }, attrs), [_createElementVNode("defs", null, [_createElementVNode("linearGradient", { "id": id.value, "gradientUnits": "userSpaceOnUse", "x1": props.gradientDirection === 'left' ? '100%' : '0', "y1": props.gradientDirection === 'top' ? '100%' : '0', "x2": props.gradientDirection === 'right' ? '100%' : '0', "y2": props.gradientDirection === 'bottom' ? '100%' : '0' }, [gradientData.map((color, index) => _createElementVNode("stop", { "offset": index / Math.max(gradientData.length - 1, 1), "stop-color": color || 'currentColor' }, null))])]), _createElementVNode("clipPath", { "id": `${id.value}-clip` }, [bars.value.map((item, i) => _createElementVNode("rect", { "ref": el => { if (el) clipRects.value[i] = el; }, "x": item.x + offsetX.value, "y": item.y, "width": lineWidth.value, "height": item.height, "rx": smooth.value, "ry": smooth.value }, null))]), hasLabels.value && _createElementVNode("g", { "key": "labels", "style": { textAnchor: 'middle', dominantBaseline: 'mathematical', fill: 'currentColor' } }, [parsedLabels.value.map((item, i) => _createElementVNode("text", { "x": item.x + offsetX.value + lineWidth.value / 2, "y": parseInt(props.height, 10) - 2 + (parseInt(props.labelSize, 10) || 7 * 0.75), "font-size": Number(props.labelSize) || 7 }, [slots.label?.({ index: i, value: item.value }) ?? item.value]))]), props.interactive && currentIndex.value !== null && _createElementVNode("rect", { "key": "highlight", "x": animatedX.value - offsetX.value, "y": 0, "width": columnWidth.value, "height": props.height, "fill": "currentColor", "opacity": 0.1, "pointer-events": "none" }, null), _createElementVNode("g", { "clip-path": `url(#${id.value}-clip)`, "fill": `url(#${id.value})` }, [_createElementVNode("rect", { "x": 0, "y": 0, "width": totalWidth.value, "height": props.height }, null)])]), !!props.tooltip && _createVNode(VSparklineTooltip, { "key": "tooltip", "modelValue": tooltipVisible.value, "target": tooltipTarget.value, "index": currentIndex.value, "value": currentIndex.value !== null ? bars.value[currentIndex.value].value : 0, "offset": tooltipConfig.value.offset, "contentClass": tooltipConfig.value.class, "titleFormat": tooltipConfig.value.titleFormat, "location": "top center", "onAfterLeave": onTooltipAfterLeave }, { default: slots.tooltip })]); }); } }); //# sourceMappingURL=VBarline.js.map