Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add sparkline component (#4923)
* feat: add sparkline * fix: sparkline porps * feat(sparkline): add show-label prop * refactor(VSparkline): rename file to .ts * refactor(VSparkline): add types * test: update a-la-carte snapshots * style: combine imports * docs: add props as wrapperFor * docs: add sparklines page and example * fix: use non-boolean smooth values in rect * docs: add meta info, "new" tag in drawer * docs: update sparkline playground and prop descriptions * docs: reduce width slider max value * docs: add type for "type" prop * docs: add labels to playground button groups * docs: fix "type" prop definition * enh: reduce default auto-draw duration for type="bar" * docs: add AppDrawer.sparklines string Co-authored-by: qingwei.li <cinwell.li@gmail.com>
- Loading branch information
1 parent
98917af
commit 2729a0e
Showing
25 changed files
with
670 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import mixins from '../../util/mixins' | ||
import { VNode } from 'vue' | ||
|
||
import props from './mixins/props' | ||
import Rect from './components/rect' | ||
import Text from './components/text' | ||
import Gradient from './components/gradient' | ||
import { genPoints } from './helpers/core' | ||
|
||
export default mixins(props).extend({ | ||
name: 'bar', | ||
|
||
props: { | ||
autoDrawDuration: { | ||
type: Number, | ||
default: 500 | ||
} | ||
}, | ||
|
||
render (h): VNode { | ||
if (!this.data || this.data.length < 2) return undefined as never | ||
const { width, height, padding, lineWidth } = this | ||
const viewWidth = width || 300 | ||
const viewHeight = height || 75 | ||
const boundary = { | ||
minX: padding, | ||
minY: padding, | ||
maxX: viewWidth - padding, | ||
maxY: viewHeight - padding | ||
} | ||
const props = this.$props | ||
|
||
props.points = genPoints(this.data, boundary) | ||
|
||
const totalWidth = boundary.maxX / (props.points.length - 1) | ||
|
||
props.boundary = boundary | ||
props.id = 'sparkline-bar-' + this._uid | ||
props.lineWidth = lineWidth || (totalWidth - (padding || 5)) | ||
props.offsetX = (totalWidth - props.lineWidth) / 2 | ||
|
||
return h('svg', { | ||
attrs: { | ||
width: width || '100%', | ||
height: height || '25%', | ||
viewBox: `0 0 ${viewWidth} ${viewHeight}` | ||
} | ||
}, [ | ||
h(Gradient, { props }), | ||
h(Rect, { props }), | ||
this.showLabel ? h(Text, { props }) : undefined as never, | ||
h('g', { | ||
attrs: { | ||
transform: `scale(1,-1) translate(0,-${boundary.maxY})`, | ||
'clip-path': `url(#${props.id}-clip)`, | ||
fill: `url(#${props.id})` | ||
} | ||
}, [ | ||
h('rect', { | ||
attrs: { | ||
x: 0, | ||
y: 0, | ||
width: viewWidth, | ||
height: viewHeight | ||
} | ||
}) | ||
]) | ||
]) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { VNode } from 'vue' | ||
import mixins, { ExtractVue } from '../../util/mixins' | ||
|
||
import props from './mixins/props' | ||
import Path from './components/path' | ||
import Text from './components/text' | ||
import Gradient from './components/gradient' | ||
import { genPoints } from './helpers/core' | ||
|
||
interface options { | ||
$refs: { | ||
path: InstanceType<typeof Path> | ||
} | ||
} | ||
|
||
export default mixins<options & ExtractVue<typeof props>>(props).extend({ | ||
name: 'trend', | ||
|
||
data: () => ({ | ||
lastLength: 0 | ||
}), | ||
|
||
watch: { | ||
data: { | ||
immediate: true, | ||
handler () { | ||
this.$nextTick(() => { | ||
if (!this.autoDraw) { | ||
return | ||
} | ||
|
||
const path = this.$refs.path.$el | ||
const length = path.getTotalLength() | ||
|
||
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' | ||
this.lastLength = length | ||
}) | ||
} | ||
} | ||
}, | ||
|
||
render (h): VNode { | ||
if (!this.data || this.data.length < 2) return undefined as never | ||
const { width, height, padding } = this | ||
const viewWidth = width || 300 | ||
const viewHeight = height || 75 | ||
const boundary = { | ||
minX: padding, | ||
minY: padding, | ||
maxX: viewWidth - padding, | ||
maxY: viewHeight - padding | ||
} | ||
const props = this.$props | ||
|
||
props.boundary = boundary | ||
props.id = 'sparkline-trend-' + this._uid | ||
props.points = genPoints(this.data, boundary) | ||
|
||
return h('svg', { | ||
attrs: { | ||
'stroke-width': this.lineWidth || 1, | ||
width: width || '100%', | ||
height: height || '25%', | ||
viewBox: `0 0 ${viewWidth} ${viewHeight}` | ||
} | ||
}, [ | ||
h(Gradient, { props }), | ||
this.showLabel ? h(Text, { props }) : undefined as never, | ||
h(Path, { | ||
props, | ||
ref: 'path' | ||
}) | ||
]) | ||
} | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import Vue from 'vue' | ||
import { PropValidator } from 'vue/types/options' | ||
|
||
import bar from './Bar' | ||
import trend from './Trend' | ||
import props from './mixins/props' | ||
|
||
const COMPONENTS = { | ||
bar, | ||
trend | ||
} | ||
|
||
export type SparklineItem = number | { value: number } | ||
|
||
export interface Boundary { | ||
minX: number | ||
minY: number | ||
maxX: number | ||
maxY: number | ||
} | ||
|
||
export interface Point { | ||
x: number | ||
y: number | ||
value: number | ||
} | ||
|
||
export default Vue.extend({ | ||
name: 'v-sparkline', | ||
|
||
functional: true, | ||
|
||
$_wrapperFor: props, | ||
|
||
props: { | ||
type: { | ||
type: String, | ||
default: 'trend', | ||
validator: (val: string) => ['trend', 'bar'].includes(val) | ||
} as PropValidator<'trend' | 'bar'> | ||
}, | ||
|
||
render (h, { props, data }) { | ||
return h('div', { | ||
staticClass: 'sparkline' | ||
}, [ | ||
h(COMPONENTS[props.type], data) | ||
]) | ||
} | ||
}) |
35 changes: 35 additions & 0 deletions
35
packages/vuetify/src/components/VSparkline/components/gradient.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* eslint-disable no-multi-spaces, object-property-newline */ | ||
|
||
import Vue, { VNode } from 'vue' | ||
import { Prop } from 'vue/types/options' | ||
|
||
export default Vue.extend({ | ||
props: ['gradient', 'id', 'gradientDirection'] as any as { | ||
gradient: Prop<string[]> | ||
id: Prop<string> | ||
gradientDirection: Prop<string> | ||
}, | ||
|
||
render (h): VNode { | ||
const { gradient, id, gradientDirection } = this | ||
const len = gradient.length - 1 | ||
const stops = gradient.slice().reverse().map((color, index) => | ||
h('stop', { | ||
attrs: { | ||
offset: index / len, | ||
'stop-color': color | ||
} | ||
}) | ||
) | ||
|
||
return h('defs', [ | ||
h('linearGradient', { | ||
attrs: { | ||
id, | ||
x1: +(gradientDirection === 'left'), y1: +(gradientDirection === 'top'), | ||
x2: +(gradientDirection === 'right'), y2: +(gradientDirection === 'bottom') | ||
} | ||
}, stops) | ||
]) | ||
} | ||
}) |
24 changes: 24 additions & 0 deletions
24
packages/vuetify/src/components/VSparkline/components/path.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import Vue, { VNode } from 'vue' | ||
import { Prop } from 'vue/types/options' | ||
|
||
import { Point } from '../VSparkline' | ||
import { genPath } from '../helpers/path' | ||
|
||
export default Vue.extend<Vue & { $el: SVGPathElement }>().extend({ | ||
props: ['smooth', 'radius', 'id', 'points'] as any as { | ||
smooth: Prop<boolean> | ||
radius: Prop<number> | ||
id: Prop<string> | ||
points: Prop<Point[]> | ||
}, | ||
|
||
render (h): VNode { | ||
const { smooth, id } = this | ||
const radius = smooth === true ? 8 : Number(smooth) | ||
const d = genPath(this.points, radius) | ||
|
||
return h('path', { | ||
attrs: { d, fill: 'none', stroke: `url(#${id})` } | ||
}) | ||
} | ||
}) |
62 changes: 62 additions & 0 deletions
62
packages/vuetify/src/components/VSparkline/components/rect.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import Vue, { VNode } from 'vue' | ||
import { Prop } from 'vue/types/options' | ||
|
||
import { Boundary, Point } from '../VSparkline' | ||
|
||
export default Vue.extend({ | ||
props: [ | ||
'id', | ||
'smooth', | ||
'boundary', | ||
'lineWidth', | ||
'gradient', | ||
'autoDrawDuration', | ||
'autoDraw', | ||
'points', | ||
'offsetX' | ||
] as any as { | ||
id: Prop<string> | ||
smooth: Prop<boolean | number> | ||
boundary: Prop<Boundary> | ||
lineWidth: Prop<number> | ||
gradient: Prop<string[]> | ||
autoDrawDuration: Prop<number> | ||
autoDraw: Prop<boolean> | ||
points: Prop<Point[]> | ||
offsetX: Prop<number> | ||
}, | ||
|
||
render (h): VNode { | ||
const { maxY } = this.boundary | ||
const rounding = typeof this.smooth === 'number' | ||
? this.smooth | ||
: this.smooth ? 2 : 0 | ||
|
||
return h('clipPath', { | ||
attrs: { | ||
id: `${this.id}-clip` | ||
} | ||
}, this.points.map((item, index) => | ||
h('rect', { | ||
attrs: { | ||
x: item.x - this.offsetX, | ||
y: 0, | ||
width: this.lineWidth, | ||
height: maxY - item.y, | ||
rx: rounding, | ||
ry: rounding | ||
} | ||
}, [ | ||
this.autoDraw ? h('animate', { | ||
attrs: { | ||
attributeName: 'height', | ||
from: 0, | ||
to: maxY - item.y, | ||
dur: `${this.autoDrawDuration}ms`, | ||
fill: 'freeze' | ||
} | ||
}) : undefined as never | ||
]) | ||
)) | ||
} | ||
}) |
32 changes: 32 additions & 0 deletions
32
packages/vuetify/src/components/VSparkline/components/text.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import Vue, { VNode } from 'vue' | ||
import { Prop } from 'vue/types/options' | ||
|
||
import { Boundary, Point } from '../VSparkline' | ||
|
||
export default Vue.extend({ | ||
props: ['points', 'boundary', 'offsetX'] as any as { | ||
points: Prop<Point[]> | ||
boundary: Prop<Boundary> | ||
offsetX: Prop<number> | ||
}, | ||
|
||
render (h): VNode { | ||
const offsetX = (this.offsetX || 0) / 2 | ||
|
||
return h('g', { | ||
style: { | ||
fontSize: '8', | ||
textAnchor: 'middle', | ||
dominantBaseline: 'mathematical', | ||
fill: this.$vuetify.theme.secondary | ||
} | ||
}, this.points.map(item => ( | ||
h('text', { | ||
attrs: { | ||
x: item.x - offsetX, | ||
y: this.boundary.maxY + 2 | ||
} | ||
}, item.value.toString()) | ||
))) | ||
} | ||
}) |
21 changes: 21 additions & 0 deletions
21
packages/vuetify/src/components/VSparkline/helpers/core.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import { SparklineItem, Boundary, Point } from '../VSparkline' | ||
|
||
export function genPoints (points: SparklineItem[], boundary: Boundary): Point[] { | ||
const { minX, minY, maxX, maxY } = boundary | ||
const normalisedPoints = points.map(item => (typeof item === 'number' ? item : item.value)) | ||
const minValue = Math.min(...normalisedPoints) - 0.001 | ||
const gridX = (maxX - minX) / (normalisedPoints.length - 1) | ||
const gridY = (maxY - minY) / (Math.max(...normalisedPoints) + 0.001 - minValue) | ||
|
||
return normalisedPoints.map((value, index) => { | ||
return { | ||
x: index * gridX + minX, | ||
y: | ||
maxY - | ||
(value - minValue) * gridY + | ||
+(index === normalisedPoints.length - 1) * 0.00001 - | ||
+(index === 0) * 0.00001, | ||
value | ||
} | ||
}) | ||
} |
Oops, something went wrong.