UNPKG

vuetify

Version:

Vue Material Component Framework

422 lines (385 loc) 11 kB
// Mixins import Colorable from '../../mixins/colorable' // Utilities import mixins, { ExtractVue } from '../../util/mixins' import { genPoints, genBars } from './helpers/core' import { genPath } from './helpers/path' // Types import Vue, { VNode } from 'vue' import { Prop, PropValidator } from 'vue/types/options' export type SparklineItem = number | { value: number } export type SparklineText = { x: number value: string } export interface Boundary { minX: number minY: number maxX: number maxY: number } export interface Point { x: number y: number value: number } export interface Bar { x: number y: number height: number value: number } interface options extends Vue { $refs: { path: SVGPathElement } } export default mixins<options & /* eslint-disable indent */ ExtractVue<[ typeof Colorable ]> /* eslint-enable indent */ >( Colorable ).extend({ name: 'VSparkline', inheritAttrs: false, props: { autoDraw: Boolean, autoDrawDuration: { type: Number, default: 2000, }, autoDrawEasing: { type: String, default: 'ease', }, autoLineWidth: { type: Boolean, default: false, }, color: { type: String, default: 'primary', }, fill: { type: Boolean, default: false, }, gradient: { type: Array, default: () => ([]), } as PropValidator<string[]>, gradientDirection: { type: String as Prop<'top' | 'bottom' | 'left' | 'right'>, validator: (val: string) => ['top', 'bottom', 'left', 'right'].includes(val), default: 'top', }, height: { type: [String, Number], default: 75, }, labels: { type: Array, default: () => ([]), } as PropValidator<SparklineItem[]>, labelSize: { type: [Number, String], default: 7, }, lineWidth: { type: [String, Number], default: 4, }, padding: { type: [String, Number], default: 8, }, showLabels: Boolean, smooth: { type: [Boolean, Number, String], default: false, }, type: { type: String as Prop<'trend' | 'bar'>, default: 'trend', validator: (val: string) => ['trend', 'bar'].includes(val), }, value: { type: Array, default: () => ([]), } as PropValidator<SparklineItem[]>, width: { type: [Number, String], default: 300, }, }, data: () => ({ lastLength: 0, }), computed: { parsedPadding (): number { return Number(this.padding) }, parsedWidth (): number { return Number(this.width) }, parsedHeight (): number { return parseInt(this.height, 10) }, parsedLabelSize (): number { return parseInt(this.labelSize, 10) || 7 }, totalHeight (): number { let height = this.parsedHeight if (this.hasLabels) height += parseInt(this.labelSize, 10) * 1.5 return height }, totalWidth (): number { let width = this.parsedWidth if (this.type === 'bar') width = Math.max(this.value.length * this._lineWidth, width) return width }, totalValues (): number { return this.value.length }, _lineWidth (): number { if (this.autoLineWidth && this.type !== 'trend') { const totalPadding = this.parsedPadding * (this.totalValues + 1) return (this.parsedWidth - totalPadding) / this.totalValues } else { return parseFloat(this.lineWidth) || 4 } }, boundary (): Boundary { if (this.type === 'bar') return { minX: 0, maxX: this.totalWidth, minY: 0, maxY: this.parsedHeight } const padding = this.parsedPadding return { minX: padding, maxX: this.totalWidth - padding, minY: padding, maxY: this.parsedHeight - padding, } }, hasLabels (): boolean { return Boolean( this.showLabels || this.labels.length > 0 || this.$scopedSlots.label ) }, parsedLabels (): SparklineText[] { const labels = [] const points = this._values const len = points.length for (let i = 0; labels.length < len; i++) { const item = points[i] let value = this.labels[i] if (!value) { value = typeof item === 'object' ? item.value : item } labels.push({ x: item.x, value: String(value), }) } return labels }, normalizedValues (): number[] { return this.value.map(item => (typeof item === 'number' ? item : item.value)) }, _values (): Point[] | Bar[] { return this.type === 'trend' ? genPoints(this.normalizedValues, this.boundary) : genBars(this.normalizedValues, this.boundary) }, textY (): number { let y = this.parsedHeight if (this.type === 'trend') y -= 4 return y }, _radius (): number { return this.smooth === true ? 8 : Number(this.smooth) }, }, watch: { value: { immediate: true, handler () { this.$nextTick(() => { if ( !this.autoDraw || this.type === 'bar' || !this.$refs.path ) return const path = this.$refs.path const length = path.getTotalLength() if (!this.fill) { path.style.transition = 'none' path.style.strokeDasharray = length + ' ' + length path.style.strokeDashoffset = Math.abs(length - (this.lastLength || 0)).toString() path.getBoundingClientRect() path.style.transition = `stroke-dashoffset ${this.autoDrawDuration}ms ${this.autoDrawEasing}` path.style.strokeDashoffset = '0' } else { path.style.transformOrigin = 'bottom center' path.style.transition = 'none' path.style.transform = `scaleY(0)` path.getBoundingClientRect() path.style.transition = `transform ${this.autoDrawDuration}ms ${this.autoDrawEasing}` path.style.transform = `scaleY(1)` } this.lastLength = length }) }, }, }, methods: { genGradient () { const gradientDirection = this.gradientDirection const gradient = this.gradient.slice() // Pushes empty string to force // a fallback to currentColor if (!gradient.length) gradient.push('') const len = Math.max(gradient.length - 1, 1) const stops = gradient.reverse().map((color, index) => this.$createElement('stop', { attrs: { offset: index / len, 'stop-color': color || 'currentColor', }, }) ) return this.$createElement('defs', [ this.$createElement('linearGradient', { attrs: { id: this._uid, gradientUnits: 'userSpaceOnUse', x1: gradientDirection === 'left' ? '100%' : '0', y1: gradientDirection === 'top' ? '100%' : '0', x2: gradientDirection === 'right' ? '100%' : '0', y2: gradientDirection === 'bottom' ? '100%' : '0', }, }, stops), ]) }, genG (children: VNode[]) { return this.$createElement('g', { style: { fontSize: '8', textAnchor: 'middle', dominantBaseline: 'mathematical', fill: 'currentColor', } as object, // TODO: TS 3.5 is too eager with the array type here }, children) }, genPath () { const points = genPoints(this.normalizedValues, this.boundary) return this.$createElement('path', { attrs: { d: genPath(points, this._radius, this.fill, this.parsedHeight), fill: this.fill ? `url(#${this._uid})` : 'none', stroke: this.fill ? 'none' : `url(#${this._uid})`, }, ref: 'path', }) }, genLabels (offsetX: number) { const children = this.parsedLabels.map((item, i) => ( this.$createElement('text', { attrs: { x: item.x + offsetX + this._lineWidth / 2, y: this.textY + (this.parsedLabelSize * 0.75), 'font-size': Number(this.labelSize) || 7, }, }, [this.genLabel(item, i)]) )) return this.genG(children) }, genLabel (item: SparklineText, index: number) { return this.$scopedSlots.label ? this.$scopedSlots.label({ index, value: item.value }) : item.value }, genBars () { if (!this.value || this.totalValues < 2) return undefined as never const bars = genBars(this.normalizedValues, this.boundary) const offsetX = (Math.abs(bars[0].x - bars[1].x) - this._lineWidth) / 2 return this.$createElement('svg', { attrs: { display: 'block', viewBox: `0 0 ${this.totalWidth} ${this.totalHeight}`, }, }, [ this.genGradient(), this.genClipPath(bars, offsetX, this._lineWidth, 'sparkline-bar-' + this._uid), this.hasLabels ? this.genLabels(offsetX) : undefined as never, this.$createElement('g', { attrs: { 'clip-path': `url(#sparkline-bar-${this._uid}-clip)`, fill: `url(#${this._uid})`, }, }, [ this.$createElement('rect', { attrs: { x: 0, y: 0, width: this.totalWidth, height: this.height, }, }), ]), ]) }, genClipPath (bars: Bar[], offsetX: number, lineWidth: number, id: string) { const rounding = typeof this.smooth === 'number' ? this.smooth : this.smooth ? 2 : 0 return this.$createElement('clipPath', { attrs: { id: `${id}-clip`, }, }, bars.map(item => { return this.$createElement('rect', { attrs: { x: item.x + offsetX, y: item.y, width: lineWidth, height: item.height, rx: rounding, ry: rounding, }, }, [ this.autoDraw ? this.$createElement('animate', { attrs: { attributeName: 'height', from: 0, to: item.height, dur: `${this.autoDrawDuration}ms`, fill: 'freeze', }, }) : undefined as never, ]) })) }, genTrend () { return this.$createElement('svg', this.setTextColor(this.color, { attrs: { ...this.$attrs, display: 'block', 'stroke-width': this._lineWidth || 1, viewBox: `0 0 ${this.width} ${this.totalHeight}`, }, }), [ this.genGradient(), this.hasLabels && this.genLabels(-(this._lineWidth / 2)), this.genPath(), ]) }, }, render (h): VNode { if (this.totalValues < 2) return undefined as never return this.type === 'trend' ? this.genTrend() : this.genBars() }, })