355 lines
No EOL
9.3 KiB
JavaScript
355 lines
No EOL
9.3 KiB
JavaScript
// Mixins
|
|
import Colorable from '../../mixins/colorable'; // Utilities
|
|
|
|
import mixins from '../../util/mixins';
|
|
import { genPoints, genBars } from './helpers/core';
|
|
import { genPath } from './helpers/path';
|
|
export default mixins(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: () => []
|
|
},
|
|
gradientDirection: {
|
|
type: String,
|
|
validator: val => ['top', 'bottom', 'left', 'right'].includes(val),
|
|
default: 'top'
|
|
},
|
|
height: {
|
|
type: [String, Number],
|
|
default: 75
|
|
},
|
|
labels: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
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,
|
|
default: 'trend',
|
|
validator: val => ['trend', 'bar'].includes(val)
|
|
},
|
|
value: {
|
|
type: Array,
|
|
default: () => []
|
|
},
|
|
width: {
|
|
type: [Number, String],
|
|
default: 300
|
|
}
|
|
},
|
|
data: () => ({
|
|
lastLength: 0
|
|
}),
|
|
computed: {
|
|
parsedPadding() {
|
|
return Number(this.padding);
|
|
},
|
|
|
|
parsedWidth() {
|
|
return Number(this.width);
|
|
},
|
|
|
|
parsedHeight() {
|
|
return parseInt(this.height, 10);
|
|
},
|
|
|
|
parsedLabelSize() {
|
|
return parseInt(this.labelSize, 10) || 7;
|
|
},
|
|
|
|
totalHeight() {
|
|
let height = this.parsedHeight;
|
|
if (this.hasLabels) height += parseInt(this.labelSize, 10) * 1.5;
|
|
return height;
|
|
},
|
|
|
|
totalWidth() {
|
|
let width = this.parsedWidth;
|
|
if (this.type === 'bar') width = Math.max(this.value.length * this._lineWidth, width);
|
|
return width;
|
|
},
|
|
|
|
totalValues() {
|
|
return this.value.length;
|
|
},
|
|
|
|
_lineWidth() {
|
|
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() {
|
|
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() {
|
|
return Boolean(this.showLabels || this.labels.length > 0 || this.$scopedSlots.label);
|
|
},
|
|
|
|
parsedLabels() {
|
|
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() {
|
|
return this.value.map(item => typeof item === 'number' ? item : item.value);
|
|
},
|
|
|
|
_values() {
|
|
return this.type === 'trend' ? genPoints(this.normalizedValues, this.boundary) : genBars(this.normalizedValues, this.boundary);
|
|
},
|
|
|
|
textY() {
|
|
let y = this.parsedHeight;
|
|
if (this.type === 'trend') y -= 4;
|
|
return y;
|
|
},
|
|
|
|
_radius() {
|
|
return this.smooth === true ? 8 : Number(this.smooth);
|
|
}
|
|
|
|
},
|
|
watch: {
|
|
value: {
|
|
immediate: true,
|
|
|
|
handler() {
|
|
this.$nextTick(() => {
|
|
if (!this.autoDraw || this.type === 'bar') 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 || this.color || 'currentColor'
|
|
}
|
|
}));
|
|
return this.$createElement('defs', [this.$createElement('linearGradient', {
|
|
attrs: {
|
|
id: this._uid,
|
|
x1: +(gradientDirection === 'left'),
|
|
y1: +(gradientDirection === 'top'),
|
|
x2: +(gradientDirection === 'right'),
|
|
y2: +(gradientDirection === 'bottom')
|
|
}
|
|
}, stops)]);
|
|
},
|
|
|
|
genG(children) {
|
|
return this.$createElement('g', {
|
|
style: {
|
|
fontSize: '8',
|
|
textAnchor: 'middle',
|
|
dominantBaseline: 'mathematical',
|
|
fill: this.color || 'currentColor'
|
|
}
|
|
}, children);
|
|
},
|
|
|
|
genPath() {
|
|
const points = genPoints(this.normalizedValues, this.boundary);
|
|
return this.$createElement('path', {
|
|
attrs: {
|
|
id: this._uid,
|
|
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) {
|
|
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, index) {
|
|
return this.$scopedSlots.label ? this.$scopedSlots.label({
|
|
index,
|
|
value: item.value
|
|
}) : item.value;
|
|
},
|
|
|
|
genBars() {
|
|
if (!this.value || this.totalValues < 2) return undefined;
|
|
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, 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, offsetX, lineWidth, id) {
|
|
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]);
|
|
}));
|
|
},
|
|
|
|
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) {
|
|
if (this.totalValues < 2) return undefined;
|
|
return this.type === 'trend' ? this.genTrend() : this.genBars();
|
|
}
|
|
|
|
});
|
|
//# sourceMappingURL=VSparkline.js.map
|