├── .eslintignore ├── .eslintrc.js ├── .gitattributes ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.MD ├── babel.config.js ├── package.json ├── packages ├── Bar.ts ├── Curve.ts ├── Gradient.ts ├── Line.ts ├── Pie.ts ├── RefLine.ts ├── Sparklines.ts ├── Spot.ts ├── Text.ts ├── constant.ts ├── globals.d.ts ├── hooks │ ├── useSpotCheck.ts │ └── useStyles.ts ├── index.ts ├── interface.ts ├── props.ts ├── utils │ ├── dataToPoints.ts │ ├── debounce.ts │ ├── genColor.ts │ ├── index.ts │ ├── isEmpty.ts │ ├── max.ts │ ├── mean.ts │ ├── median.ts │ ├── midRange.ts │ ├── min.ts │ ├── stdev.ts │ ├── uid.ts │ └── variance.ts └── withInstall.ts ├── rollup.config.js └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | snapshot* 2 | node_modules 3 | temp* 4 | 5 | esm 6 | es 7 | lib 8 | dist 9 | cjs 10 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'vue-eslint-parser', 4 | parserOptions: { 5 | parser: '@typescript-eslint/parser', 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | // Allows for the parsing of JSX 9 | jsx: true 10 | } 11 | }, 12 | plugins: ['@typescript-eslint'], 13 | extends: [ 14 | 'plugin:vue/vue3-recommended', 15 | 'plugin:@typescript-eslint/recommended', 16 | 'plugin:import/typescript' 17 | ], 18 | rules: { 19 | '@typescript-eslint/no-non-null-assertion': 'off' 20 | }, 21 | globals: { 22 | PKG_VERSION: true 23 | } 24 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /es 5 | /lib 6 | /type 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | .env.development 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | 18 | yarn.lock 19 | package-lock.json 20 | 21 | # Editor directories and files 22 | .idea 23 | .vscode 24 | *.suo 25 | *.ntvs* 26 | *.njsproj 27 | *.sln 28 | *.sw? 29 | *.history 30 | # Git 31 | *.diff 32 | *.patch 33 | *.bak 34 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | build/ 4 | npm-debug.log 5 | .idea -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 更新日志 2 | *2022-05-24* 3 | 4 | ### 2.0.3升级内容 5 | - 更新spot 6 | 7 | *2022-05-04* 8 | 9 | ### 2.0.2升级内容 10 | - 修复tooltip event maximum call 11 | 12 | *2022-04-28* 13 | 14 | ### 2.0.1升级内容 15 | - 修复type 16 | 17 | ### 2.0.0升级内容 18 | - 重构,当前为Vue3 + Typescript 19 | 20 | *2017-xx-xx* 21 | 22 | ### 1.x.x 23 | - Vue2版本 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-present vue-sparklines 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Vue Sparklines 2 | 3 | Sparkline charts based on Vue 3 & Typescript without other dependencies. 4 | 5 | * Refactor 6 | * Line chart 7 | * Curve chart 8 | * Bar chart 9 | * Pie chart 10 | * Dynamic chart 11 | * Mixins 12 | * Gradient color chart 13 | * Tooltip & reference line 14 | ### How to install 15 | ``` 16 | npm install vue-sparklines -D 17 | 18 | ``` 19 | 20 | Please visit http://www.7te.net/vue-sparklines for live examples and documents. -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | browsers: [ 8 | 'last 3 Chrome versions', 9 | 'last 3 Firefox versions', 10 | 'Safari >= 10', 11 | 'Explorer >= 11', 12 | 'Edge >= 12' 13 | ], 14 | esmodules: true 15 | }, 16 | modules: false 17 | } 18 | ] 19 | ], 20 | plugins: ['@vue/babel-plugin-jsx'], 21 | env: { 22 | test: { 23 | presets: [ 24 | [ 25 | '@babel/preset-env', 26 | { 27 | targets: { 28 | node: 'current' 29 | }, 30 | modules: 'commonjs' 31 | } 32 | ] 33 | ], 34 | plugins: ['@vue/babel-plugin-jsx'] 35 | }, 36 | production: { 37 | presets: [ 38 | [ 39 | '@babel/preset-env', 40 | { 41 | modules: false 42 | } 43 | ] 44 | ], 45 | plugins: ['@vue/babel-plugin-jsx', '@babel/plugin-transform-runtime'] 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-sparklines", 3 | "version": "2.0.3", 4 | "title": "vue-sparklines", 5 | "description": "Sparkline chart based on Vue & Typescript.", 6 | "main": "lib/index.js", 7 | "module": "es/index.js", 8 | "typings": "type/index.d.ts", 9 | "files": [ 10 | "es", 11 | "lib", 12 | "type", 13 | "LICENSE", 14 | "README.md", 15 | "CHANGELOG.md" 16 | ], 17 | "scripts": { 18 | "build": "rollup -c --environment NODE_ENV:production" 19 | }, 20 | "keywords": [ 21 | "vue", 22 | "vue-next", 23 | "vue-sparklines", 24 | "chart", 25 | "sparkline", 26 | "typescript" 27 | ], 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/Zen33/vue-sparklines.git" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/Zen33/vue-sparklines/issues" 34 | }, 35 | "homepage": "http://7te.net", 36 | "author": "tangzhen@me.com", 37 | "license": "MIT", 38 | "dependencies": { 39 | "vue": "^3.2.31" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.17.9", 43 | "@babel/plugin-transform-runtime": "^7.17.0", 44 | "@babel/preset-env": "^7.16.11", 45 | "@babel/preset-typescript": "^7.16.7", 46 | "@rollup/plugin-babel": "^5.3.0", 47 | "@rollup/plugin-commonjs": "^21.0.1", 48 | "@rollup/plugin-json": "^4.1.0", 49 | "@rollup/plugin-node-resolve": "^13.0.4", 50 | "@rollup/plugin-replace": "^4.0.0", 51 | "@rollup/plugin-url": "^6.0.0", 52 | "@typescript-eslint/eslint-plugin": "^5.13.0", 53 | "@typescript-eslint/parser": "^5.13.0", 54 | "@vue/babel-plugin-jsx": "^1.1.1", 55 | "@vue/babel-plugin-transform-vue-jsx": "^1.2.1", 56 | "@vue/compiler-sfc": "^3.2.31", 57 | "core-js": "^3.21.1", 58 | "esbuild": "^0.14.38", 59 | "eslint-plugin-import": "^2.25.4", 60 | "rollup": "^2.70.2", 61 | "rollup-plugin-dts": "^4.2.1", 62 | "rollup-plugin-esbuild": "^4.5.0", 63 | "rollup-plugin-filesize": "^9.1.2", 64 | "rollup-plugin-multi-input": "^1.1.1", 65 | "rollup-plugin-vue": "^6.0.0", 66 | "typescript": "^4.6.3" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /packages/Bar.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, inject } from 'vue' 2 | import { barProps as props } from './props' 3 | import RefLine from './RefLine' 4 | import { Point, EventBus } from './interface' 5 | import * as utils from './utils' 6 | 7 | export default defineComponent({ 8 | name: 'SparklineBar', 9 | inheritAttrs: false, 10 | props, 11 | setup () { 12 | const eventBus = inject('eventBus')! 13 | const id = utils.uid() 14 | 15 | return { 16 | eventBus, 17 | id 18 | } 19 | }, 20 | render () { 21 | const { 22 | id, 23 | data = [], 24 | limit, 25 | width, 26 | height, 27 | margin, 28 | styles, 29 | max, 30 | min, 31 | refLineStyles, 32 | eventBus 33 | } = this 34 | 35 | if (!data.length) { 36 | return null 37 | } 38 | const props = this.$props 39 | const points: Point[] = utils.dataToPoints({ 40 | data, 41 | limit, 42 | width, 43 | height, 44 | margin, 45 | max, 46 | min 47 | }) 48 | const strokeWidth = styles && styles.strokeWidth || 0 49 | const marginWidth = margin || 0 50 | const nonLimit = data.length === limit 51 | const limitedWidth = nonLimit ? 52 | (width - limit * (marginWidth + strokeWidth )) / limit : (points && points.length >= 2 ? 53 | Math.max(0, points[1].x - points[0].x - strokeWidth - marginWidth) : 0) 54 | let minWidth: number 55 | 56 | if (!isNaN(limitedWidth) && limitedWidth < 0) { 57 | minWidth = 1 58 | } else { 59 | minWidth = limitedWidth 60 | } 61 | 62 | if (minWidth === 0) { 63 | return console.warn(`[vue-sparklines]: The limit value is too large.`) 64 | } 65 | 66 | const adjustPos: Partial = [] 67 | const barWidth = styles.barWidth || minWidth 68 | 69 | return h('g', (() => { 70 | const items = [] 71 | 72 | points.map((p, i) => { 73 | const curW = Math.ceil(barWidth) 74 | const curH = Math.ceil(Math.max(0, height - p.y)) 75 | 76 | return items.push(h('rect', { 77 | style: { 78 | ...styles, 79 | width: `${curW}px`, 80 | height: `${curH}px` 81 | }, 82 | // x: p.x - (barWidth + strokeWidth) / 2, 83 | x: (() => { 84 | adjustPos[i] = adjustPos[i] || { x: 0, y: 0} 85 | if (nonLimit) { 86 | const curX = Math.ceil((barWidth + strokeWidth + marginWidth) * i + margin) 87 | 88 | adjustPos[i].x = curX + barWidth 89 | return curX 90 | } 91 | const curX = Math.ceil(p.x - strokeWidth * i) 92 | 93 | adjustPos[i].x = curX + barWidth 94 | return curX 95 | })(), 96 | // y: -height, 97 | y: (() => (adjustPos[i].y = Math.ceil(p.y)))(), 98 | width: curW, 99 | height: curH, 100 | rel: data[i], 101 | key: i 102 | })) 103 | }) 104 | 105 | eventBus.updateValue({ 106 | id: `sparkline__${id}`, 107 | color: styles.stroke || styles.fill || '#fff', 108 | data, 109 | points: adjustPos, 110 | limit, 111 | type: 'bar' 112 | }) 113 | !utils.isEmpty(refLineStyles) && items.push(h(RefLine, { ...props, points })) 114 | 115 | return items 116 | })()) 117 | } 118 | }) 119 | -------------------------------------------------------------------------------- /packages/Curve.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, inject } from 'vue' 2 | import type { VNode } from 'vue' 3 | import Spot from './Spot' 4 | import RefLine from './RefLine' 5 | import { curveProps as props } from './props' 6 | import { Point, EventBus } from './interface' 7 | import useSpotCheck from './hooks/useSpotCheck' 8 | import useStyles from './hooks/useStyles' 9 | import * as utils from './utils' 10 | 11 | export default defineComponent({ 12 | name: 'SparklineCurve', 13 | inheritAttrs: false, 14 | props, 15 | setup (props) { 16 | const eventBus = inject('eventBus')! 17 | const id = utils.uid() 18 | const { checkSpotType } = useSpotCheck() 19 | const { lineStyle, fillStyle } = useStyles(props.styles) 20 | 21 | return { 22 | eventBus, 23 | id, 24 | checkSpotType, 25 | lineStyle, 26 | fillStyle 27 | } 28 | }, 29 | render () { 30 | const { 31 | id, 32 | data, 33 | hasSpot, 34 | spotProps, 35 | spotlight, 36 | spotStyles, 37 | limit, 38 | width, 39 | height, 40 | margin, 41 | max, 42 | min, 43 | styles, 44 | refLineStyles, 45 | divisor, 46 | eventBus, 47 | checkSpotType, 48 | lineStyle, 49 | fillStyle 50 | } = this 51 | 52 | if (!data.length) { 53 | return null 54 | } 55 | const props = this.$props 56 | const hasSpotlight = typeof spotlight === 'number' 57 | const leeway = 10 58 | const points = utils.dataToPoints({ 59 | data, 60 | limit, 61 | width, 62 | height, 63 | margin, 64 | max, 65 | min, 66 | textHeight: hasSpotlight ? leeway : 0 67 | }) 68 | let prev: Partial 69 | const curve = (p: Point) => { 70 | let result: unknown[] = [] 71 | 72 | if (!prev) { 73 | result = [p.x, p.y] 74 | } else { 75 | const len = (p.x - prev.x) * divisor 76 | 77 | result = [ 78 | 'C', 79 | prev.x + len, 80 | prev.y, 81 | p.x - len, 82 | p.y, 83 | p.x, 84 | p.y 85 | ] 86 | } 87 | prev = p 88 | 89 | return result 90 | } 91 | const linePoints = points.map((p: Point) => curve(p)).reduce((a: number[], b: number[]) => a.concat(b)) 92 | const closePolyPoints = [ 93 | `L${points[points.length - 1].x}`, 94 | height - margin, 95 | margin, 96 | height - margin, 97 | margin, 98 | points[0].y 99 | ] 100 | const fillPoints = linePoints.concat(closePolyPoints) 101 | 102 | eventBus.updateValue({ 103 | id: `sparkline__${id}`, 104 | color: styles.stroke || styles.fill || '#fff', 105 | data, 106 | points, 107 | limit, 108 | type: 'curve' 109 | }) 110 | 111 | return h('g', (() => { 112 | const items: VNode[] = [] 113 | 114 | items.push(h('path', { 115 | style: fillStyle, 116 | d: `M${fillPoints.join(' ')}` 117 | }), 118 | h('path', { 119 | style: lineStyle, 120 | d: `M${linePoints.join(' ')}` 121 | })) 122 | hasSpotlight && points.map((p: Point, key: number) => checkSpotType({ 123 | props, 124 | items, 125 | point: p, 126 | i: key, 127 | hasSpotlight 128 | }) && items.push(h('circle', { 129 | style: spotStyles, 130 | cx: p.x, 131 | cy: p.y, 132 | r: spotProps.size, 133 | rel: data[key], 134 | key 135 | }))) 136 | hasSpot && items.push(h(Spot, { ...props, points })) 137 | !utils.isEmpty(refLineStyles) && items.push(h(RefLine, { ...props, points })) 138 | 139 | return items 140 | })()) 141 | } 142 | }) 143 | -------------------------------------------------------------------------------- /packages/Gradient.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue' 2 | import { gradientProps as props } from './props' 3 | 4 | export default defineComponent({ 5 | name: 'SparklineGradient', 6 | inheritAttrs: false, 7 | props, 8 | render () { 9 | const { gradient, id } = this 10 | 11 | if (!gradient.length) { 12 | return null 13 | } 14 | const len = gradient.length - 1 || 1 15 | const stops = gradient.slice().reverse().map((c: string, i: number) => h('stop', { 16 | offset: i / len, 17 | 'stop-color': c, 18 | key: i 19 | })) 20 | 21 | return h('defs', [ 22 | h('linearGradient', { 23 | id, 24 | x1: 0, 25 | y1: 0, 26 | x2: 0, 27 | y2: 1 28 | }, stops) 29 | ]) 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /packages/Line.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h, inject } from 'vue' 2 | import type { VNode } from 'vue' 3 | import Spot from './Spot' 4 | import RefLine from './RefLine' 5 | import { lineProps as props } from './props' 6 | import { Point, EventBus } from './interface' 7 | import useSpotCheck from './hooks/useSpotCheck' 8 | import useStyles from './hooks/useStyles' 9 | import * as utils from './utils' 10 | 11 | export default defineComponent({ 12 | name: 'SparklineLine', 13 | inheritAttrs: false, 14 | props, 15 | setup (props) { 16 | const eventBus = inject('eventBus')! 17 | const id = utils.uid() 18 | const { checkSpotType } = useSpotCheck() 19 | const { lineStyle, fillStyle } = useStyles(props.styles) 20 | 21 | return { 22 | eventBus, 23 | id, 24 | checkSpotType, 25 | lineStyle, 26 | fillStyle 27 | } 28 | }, 29 | render () { 30 | const { 31 | id, 32 | data, 33 | hasSpot, 34 | spotProps, 35 | spotlight, 36 | spotStyles, 37 | limit, 38 | width, 39 | height, 40 | margin, 41 | max, 42 | min, 43 | styles, 44 | refLineStyles, 45 | eventBus, 46 | checkSpotType, 47 | lineStyle, 48 | fillStyle 49 | } = this 50 | 51 | if (!data.length) { 52 | return null 53 | } 54 | const props = this.$props 55 | const hasSpotlight = typeof spotlight === 'number' 56 | const leeway = 10 57 | const points: Point[] = utils.dataToPoints({ 58 | data, 59 | limit, 60 | width, 61 | height, 62 | margin, 63 | max, 64 | min, 65 | textHeight: hasSpotlight ? leeway : 0 66 | }) 67 | const linePoints = points.map(p => [p.x, p.y]).reduce((a: number[], b: number[]) => a.concat(b)) 68 | const closePolyPoints = [ 69 | points[points.length - 1].x, 70 | height - margin, 71 | margin, 72 | height - margin, 73 | margin, 74 | points[0].y 75 | ] 76 | const fillPoints = linePoints.concat(closePolyPoints) 77 | 78 | eventBus.updateValue({ 79 | id: `sparkline__${id}`, 80 | color: styles.stroke || styles.fill || '#fff', 81 | data, 82 | points, 83 | limit, 84 | type: 'line' 85 | }) 86 | 87 | return h('g', (() => { 88 | const items: VNode[] = [] 89 | 90 | items.push(h('polyline', { 91 | style: fillStyle, 92 | points: fillPoints.join(' ') 93 | }), 94 | h('polyline', { 95 | style: lineStyle, 96 | points: linePoints.join(' ') 97 | })) 98 | hasSpotlight && points.map((p: Point, key: number) => checkSpotType({ 99 | props, 100 | items, 101 | point: p, 102 | i: key, 103 | hasSpotlight 104 | }) && items.push(h('circle', { 105 | style: spotStyles, 106 | cx: p.x, 107 | cy: p.y, 108 | r: spotProps.size, 109 | rel: data[key], 110 | key 111 | }))) 112 | hasSpot && items.push(h(Spot, { ...props, points })) 113 | !utils.isEmpty(refLineStyles) && items.push(h(RefLine, { ...props, points })) 114 | 115 | return items 116 | })()) 117 | } 118 | }) 119 | -------------------------------------------------------------------------------- /packages/Pie.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | h, 4 | inject, 5 | watch 6 | } from 'vue' 7 | import type { CSSProperties } from 'vue' 8 | import { pieProps as props } from './props' 9 | import { Pie, EventBus } from './interface' 10 | import * as utils from './utils' 11 | 12 | const getValue = (ref: Pie | number) => (typeof ref === 'object' && ref !== null) ? ref.value : ref as number 13 | const getColor = (ref: Pie | number, styles: CSSProperties) => (typeof ref === 'object' && ref !== null) ? ref.color : (styles && styles.fill || utils.color()) 14 | 15 | export default defineComponent({ 16 | name: 'SparklinePie', 17 | inheritAttrs: false, 18 | props, 19 | setup (props) { 20 | const eventBus = inject('eventBus')! 21 | const updateData = (evt: MouseEvent, value: number, color: string) => { 22 | const tooltip = eventBus.tooltip.value 23 | 24 | if (!tooltip) { 25 | return 26 | } 27 | tooltip.style.display = '' 28 | 29 | const rect = tooltip?.getBoundingClientRect?.() 30 | const tooltipContent = ` ${value}
` 31 | 32 | if (rect) { 33 | tooltip.style.left = `${evt.clientX + rect.width / 2}px` 34 | tooltip.style.top = `${evt.clientY - props.height - rect.height}px` 35 | try { 36 | tooltip.innerHTML = props.tooltipProps?.formatter?.({ value, color}) || tooltipContent 37 | } catch (err) { 38 | tooltip.innerHTML = tooltipContent 39 | } 40 | } 41 | } 42 | const updateTooltip = utils.debounce(updateData, props.tooltipDelay) 43 | 44 | watch(() => props.data, () => eventBus.setStatus(false)) 45 | 46 | eventBus.svgMouseEvt.value = false 47 | return { 48 | eventBus, 49 | updateTooltip 50 | } 51 | }, 52 | render () { 53 | const { 54 | data = [], 55 | width, 56 | height, 57 | styles, 58 | align 59 | } = this 60 | 61 | if (!data.length) { 62 | return null 63 | } 64 | const center = Math.min(width / 2, height / 2) 65 | const strokeWidth = styles && styles.strokeWidth || 0 66 | const radius = center - strokeWidth / 2 67 | const total = Math.ceil(data.reduce((a: Pie | number, b: Pie | number) => getValue(b) + getValue(a), 0)) 68 | let angleStart = 0 69 | let angleEnd = 0 70 | let startX = center 71 | 72 | if (align === 'center') { 73 | startX = width / 2 74 | } else if (align === 'right') { 75 | startX = width - radius 76 | } 77 | 78 | return h('g', (() => { 79 | const items = [] 80 | 81 | if (data.length === 1) { 82 | items.push(h('ellipse', { 83 | style: styles, 84 | cx: startX, 85 | cy: center, 86 | rx: radius, 87 | ry: radius, 88 | fill: getColor(data[0], styles), 89 | onMousemove: (evt: MouseEvent) => { 90 | evt.stopPropagation() 91 | this.eventBus.isFocusing.value = true 92 | this.updateTooltip(evt, getValue(data[0]),getColor(data[0], styles)) 93 | }, 94 | onMouseleave: () => { 95 | this.eventBus.isFocusing.value = false 96 | this.eventBus.setStatus(false) 97 | } 98 | })) 99 | } else { 100 | data.map((d: Pie | number, i: number) => { 101 | const value = getValue(d) 102 | const isLarge = value / total > 0.5 103 | const angle = 360 * value / total 104 | 105 | angleStart = angleEnd 106 | angleEnd = angleStart + angle 107 | 108 | const x1 = startX + radius * Math.cos(Math.PI * angleStart / 180) 109 | const y1 = center + radius * Math.sin(Math.PI * angleStart / 180) 110 | const x2 = startX + radius * Math.cos(Math.PI * angleEnd / 180) 111 | const y2 = center + radius * Math.sin(Math.PI * angleEnd / 180) 112 | const path = `M${startX},${center} L${x1},${y1} A${radius},${radius} 0 ${isLarge ? 1 : 0},1 ${x2},${y2} Z` 113 | const color = getColor(d, styles) 114 | 115 | items.push(h('path', { 116 | style: styles, 117 | fill: color, 118 | d: path, 119 | key: i, 120 | onMousemove: (evt: MouseEvent) => { 121 | evt.stopPropagation() 122 | this.eventBus.isFocusing.value = true 123 | this.updateTooltip(evt, value, color) 124 | }, 125 | onMouseleave: (evt: MouseEvent) => { 126 | evt.stopPropagation() 127 | this.eventBus.isFocusing.value = false 128 | this.eventBus.setStatus(false) 129 | } 130 | })) 131 | }) 132 | } 133 | return items 134 | })()) 135 | } 136 | }) 137 | -------------------------------------------------------------------------------- /packages/RefLine.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue' 2 | import * as utils from './utils' 3 | import { refLineProps as props } from './props' 4 | import { refLineTypes } from './constant' 5 | 6 | export default defineComponent({ 7 | inheritAttrs: false, 8 | props, 9 | render () { 10 | const { 11 | points, 12 | refLineType, 13 | refLineProps, 14 | refLineStyles 15 | } = this 16 | const ypoints = points.map(p => p.y) 17 | let type = 'mean' 18 | 19 | if (typeof refLineType === 'string' && refLineTypes.includes(refLineType)) { 20 | if (refLineType === 'max') { 21 | type = 'min' 22 | } else if (refLineType === 'min') { 23 | type = 'max' 24 | } else { 25 | type = refLineType 26 | } 27 | } 28 | const y = type === 'custom' ? (refLineProps && refLineProps.value || utils['mean'](ypoints)) : utils[type](ypoints) 29 | 30 | refLineStyles['shape-rendering'] = 'crispEdges' 31 | 32 | return h('line', { 33 | style: refLineStyles, 34 | x1: points[0].x, 35 | y1: y, 36 | x2: points[points.length - 1].x, 37 | y2: y 38 | }) 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /packages/Sparklines.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | ref, 4 | unref, 5 | h, 6 | computed, 7 | provide, 8 | nextTick 9 | } from 'vue' 10 | import { sparklineProps as props } from './props' 11 | import { SparkEvent, SetValueArgs } from './interface' 12 | import * as utils from './utils' 13 | 14 | let curEvt: Partial = {} 15 | 16 | export default defineComponent({ 17 | name: 'Sparklines', 18 | props, 19 | setup (props) { 20 | const isFocusing = ref(false) 21 | const indicator = ref(null) 22 | const tooltip = ref(null) 23 | const pointDatum = ref({}) 24 | const svgMouseEvt = ref(true) 25 | const execMouseEvt = computed(() => { 26 | if (!props.indicatorStyles || !indicator.value || !svgMouseEvt.value) { 27 | return false 28 | } 29 | return true 30 | }) 31 | const styles = computed(() => { 32 | const styles = props.styles 33 | 34 | styles.width = props.width 35 | styles.height = props.height 36 | 37 | return styles 38 | }) 39 | const setStatus = (status = true) => { 40 | const display = status ? '' : 'none' 41 | 42 | tooltip.value && (tooltip.value.style.display = display) 43 | indicator.value && (indicator.value.style.display = display) 44 | } 45 | const updateData = () => { 46 | if (!isFocusing.value) { 47 | return false 48 | } 49 | 50 | let rect: Partial 51 | let curData = null 52 | let tooltipContent = '' 53 | const pd = unref(pointDatum.value) 54 | 55 | for (const datum in pd) { 56 | curData = null 57 | if (pd.hasOwnProperty(datum)) { 58 | setStatus(false) 59 | for (const [i, p] of pd[datum].points.entries()) { 60 | if (curEvt.ox < p.x && curData === null) { 61 | setStatus() 62 | rect = tooltip.value?.getBoundingClientRect?.() 63 | curData = { 64 | value: pd[datum].data[i], 65 | color: pd[datum].color, 66 | index: i 67 | } 68 | tooltipContent += ` ${curData.value}
` 69 | } 70 | } 71 | } 72 | } 73 | if (rect) { 74 | tooltip.value.style.left = `${curEvt.cx + rect.width / 2}px` 75 | tooltip.value.style.top = `${curEvt.cy - props.height - rect.height}px` 76 | try { 77 | tooltip.value.innerHTML = props.tooltipProps?.formatter?.(curData) || tooltipContent 78 | } catch (err) { 79 | tooltip.value.innerHTML = tooltipContent 80 | } 81 | } 82 | } 83 | const updateTooltip = utils.debounce(updateData, props.tooltipDelay) 84 | const updateValue = (value: SetValueArgs) => { 85 | const { id, data, points, color, limit } = value 86 | 87 | nextTick(() => { 88 | pointDatum.value[id] = { 89 | data: data.length >= limit ? data.slice(-limit) : data, 90 | points, 91 | color 92 | } 93 | 94 | Object.keys(curEvt).length && updateTooltip() 95 | }) 96 | } 97 | 98 | provide('eventBus', { 99 | tooltip, 100 | setStatus, 101 | updateValue, 102 | svgMouseEvt, 103 | isFocusing 104 | }) 105 | 106 | return { 107 | styles, 108 | updateData, 109 | isFocusing, 110 | setStatus, 111 | tooltip, 112 | indicator, 113 | updateTooltip, 114 | execMouseEvt 115 | } 116 | }, 117 | render () { 118 | const { 119 | width, 120 | height, 121 | preserveAspectRatio, 122 | styles, 123 | indicatorStyles, 124 | tooltipStyles, 125 | setStatus, 126 | updateTooltip 127 | } = this 128 | 129 | return h('div', { 130 | 'class': 'sparkline' 131 | }, [ 132 | h('svg', { 133 | style: styles, 134 | viewBox: `0 0 ${width} ${height}`, 135 | preserveAspectRatio, 136 | onMousemove: (evt: MouseEvent) => { 137 | if (!this.execMouseEvt) { 138 | return 139 | } 140 | const ox = evt.offsetX 141 | const oy = evt.offsetY 142 | const cx = evt.clientX 143 | const cy = evt.clientY 144 | 145 | curEvt = { 146 | ox, 147 | oy, 148 | cx, 149 | cy, 150 | target: evt.target 151 | } 152 | 153 | this.indicator.setAttribute('x1', `${ox}`) 154 | this.indicator.setAttribute('x2', `${ox}`) 155 | this.isFocusing = true 156 | updateTooltip() 157 | }, 158 | onMouseleave: () => { 159 | this.isFocusing = false 160 | setStatus(false) 161 | } 162 | }, 163 | (() => { 164 | const items = this.$slots?.default?.() || [] 165 | 166 | if (typeof indicatorStyles !== 'boolean') { 167 | indicatorStyles['shape-rendering'] = 'crispEdges' 168 | indicatorStyles['display'] = 'none' 169 | items.push(h('line', { 170 | style: indicatorStyles, 171 | x1: 0, 172 | y1: 0, 173 | x2: 0, 174 | y2: height, 175 | ref: 'indicator' 176 | })) 177 | } 178 | 179 | return items 180 | })()), 181 | h('div', { 182 | 'class': 'sparkline__tooltip', 183 | style: {...tooltipStyles, position: 'fixed'}, 184 | ref: 'tooltip' 185 | }) 186 | ]) 187 | } 188 | }) 189 | -------------------------------------------------------------------------------- /packages/Spot.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue' 2 | import { spotProps as props } from './props' 3 | import { Point } from './interface' 4 | 5 | export default defineComponent({ 6 | name: 'Spot', 7 | inheritAttrs: false, 8 | props, 9 | setup () { 10 | const lastPoint = (points: Point[]) => { 11 | const pl = points.length 12 | 13 | Math.sign = Math.sign || (x => { 14 | return x > 0 ? 1 : -1 15 | }) 16 | return pl < 2 ? 0 : Math.sign(points[pl - 2].y - points[pl - 1].y) 17 | } 18 | 19 | return { lastPoint } 20 | }, 21 | render () { 22 | const { points, spotStyles, spotProps } = this 23 | const pl = points.length 24 | 25 | return h('g', (() => { 26 | const items = [] 27 | 28 | props.spotStyles && items.push(h('circle', { 29 | ...spotStyles, 30 | cx: points[0].x, 31 | cy: points[0].y, 32 | r: spotProps.size 33 | })) 34 | const style = spotStyles || spotProps.spotColors[this.lastPoint(points)] 35 | 36 | items.push(h('circle', { 37 | style, 38 | cx: points[pl - 1].x, 39 | cy: points[pl - 1].y, 40 | r: spotProps.size 41 | })) 42 | return items 43 | })()) 44 | } 45 | }) 46 | -------------------------------------------------------------------------------- /packages/Text.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineComponent, 3 | h, 4 | ref, 5 | onMounted 6 | } from 'vue' 7 | import { textProps as props } from './props' 8 | 9 | export default defineComponent({ 10 | name: 'Text', 11 | inheritAttrs: false, 12 | props, 13 | setup (props) { 14 | const sparklineText = ref(null) 15 | 16 | onMounted(() => { 17 | const text = sparklineText.value 18 | const textBox = text && text.getBBox() 19 | 20 | if (textBox) { 21 | text.setAttribute('x', `${textBox.x - textBox.width / 2}`) 22 | text.setAttribute('y', `${props.margin + textBox.height / 2}`) 23 | } 24 | }) 25 | }, 26 | render () { 27 | const { point, text, textStyles } = this 28 | const { x, y } = point 29 | 30 | return h('g', [ 31 | h('text', { 32 | style: textStyles, 33 | x, 34 | y, 35 | ref: 'sparklineText' 36 | }, text) 37 | ]) 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /packages/constant.ts: -------------------------------------------------------------------------------- 1 | export const defaultLineStyle = { 2 | stroke: 'slategray', 3 | strokeWidth: 1, 4 | strokeLinejoin: 'round', 5 | strokeLinecap: 'round', 6 | fill: 'none' 7 | } 8 | export const defaultFillStyle = { 9 | stroke: 'none', 10 | strokeWidth: 0, 11 | fillOpacity: .1, 12 | fill: 'none' 13 | } 14 | export const defaultTooltipStyle = { 15 | display: 'none', 16 | background: 'rgba(0, 0, 0, 0.6)', 17 | borderRadius: '3px', 18 | minWidth: '30px', 19 | padding: '3px', 20 | color: '#fff', 21 | fontSize: '12px' 22 | } 23 | export const defaultSpotProps = { 24 | size: 3, 25 | spotColors: { 26 | '-1': '#dd2c00', 27 | '0': '#ffab00', 28 | '1': '#00c853' 29 | } 30 | } 31 | export const refLineTypes = ['max', 'min', 'mean', 'avg', 'median', 'custom'] 32 | -------------------------------------------------------------------------------- /packages/globals.d.ts: -------------------------------------------------------------------------------- 1 | declare const PKG_VERSION: string -------------------------------------------------------------------------------- /packages/hooks/useSpotCheck.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import type { SpotType } from '../interface' 3 | import Text from '../Text' 4 | 5 | const useSpotCheck = () => { 6 | const checkSpotType = ({ 7 | props, 8 | items, 9 | point, 10 | i, 11 | hasSpotlight 12 | }: SpotType) => { 13 | if (!props.hasSpot && !hasSpotlight) { 14 | return true 15 | } 16 | if (!props.hasSpot && props.spotlight === i) { 17 | items.push(h(Text, { 18 | ...props, 19 | text: `${props.data[props.spotlight]}`, 20 | point 21 | })) 22 | return true 23 | } 24 | return false 25 | } 26 | 27 | return { checkSpotType } 28 | } 29 | 30 | export default useSpotCheck 31 | -------------------------------------------------------------------------------- /packages/hooks/useStyles.ts: -------------------------------------------------------------------------------- 1 | import { defaultLineStyle, defaultFillStyle } from '../constant' 2 | import type { CSSProperties } from 'vue' 3 | 4 | const useStyles = (styles: CSSProperties) => { 5 | const { 6 | stroke, 7 | strokeWidth, 8 | strokeLinejoin, 9 | strokeLinecap, 10 | fillOpacity, 11 | fill 12 | } = styles 13 | 14 | return { 15 | lineStyle: { 16 | stroke: stroke ?? defaultLineStyle.stroke, 17 | strokeWidth: strokeWidth ?? defaultLineStyle.strokeWidth, 18 | strokeLinejoin: strokeLinejoin ?? defaultLineStyle.strokeLinejoin, 19 | strokeLinecap: strokeLinecap ?? defaultLineStyle.strokeLinecap, 20 | fill: 'none' 21 | }, 22 | fillStyle: { 23 | stroke: stroke ?? defaultFillStyle.stroke, 24 | strokeWidth: 0, 25 | fillOpacity : fillOpacity ?? defaultFillStyle.fillOpacity, 26 | fill: fill ?? defaultFillStyle.fill 27 | } 28 | } 29 | } 30 | 31 | export default useStyles 32 | -------------------------------------------------------------------------------- /packages/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import Base from './Sparklines' 3 | import Line from './Line' 4 | import Bar from './Bar' 5 | import Curve from './Curve' 6 | import Pie from './Pie' 7 | import Gradient from './Gradient' 8 | import { withInstall } from './withInstall' 9 | 10 | export const Sparklines = withInstall(Base) 11 | export const SparklineLine = withInstall(Line) 12 | export const SparklineBar = withInstall(Bar) 13 | export const SparklineCurve = withInstall(Curve) 14 | export const SparklinePie = withInstall(Pie) 15 | export const SparklineGradient = withInstall(Gradient) 16 | 17 | export default { 18 | install: (app: App) => { 19 | app.use(Sparklines) 20 | app.use(SparklineLine) 21 | app.use(SparklineBar) 22 | app.use(SparklineCurve) 23 | app.use(SparklinePie) 24 | app.use(SparklineGradient) 25 | }, 26 | version: typeof PKG_VERSION === 'undefined' ? '' : PKG_VERSION, // eslint-disable-line 27 | } 28 | -------------------------------------------------------------------------------- /packages/interface.ts: -------------------------------------------------------------------------------- 1 | import type { Ref, VNode } from 'vue' 2 | 3 | export interface SetValueArgs { 4 | id: string 5 | color: string 6 | data: number[] 7 | points: Point[] 8 | limit: number | string 9 | type: string 10 | } 11 | export interface Point { 12 | x: number 13 | y: number 14 | } 15 | export interface SparkEvent extends MouseEvent { 16 | ox: number 17 | oy: number 18 | cx: number 19 | cy: number 20 | } 21 | export interface EventBus { 22 | updateValue: (value: SetValueArgs) => void 23 | setStatus: (status?: boolean) => void 24 | tooltip: Ref 25 | isFocusing: Ref 26 | svgMouseEvt: Ref 27 | } 28 | export interface Pie { 29 | value: number 30 | color: string 31 | } 32 | export interface Props { 33 | [key: string]: unknown 34 | } 35 | export interface SpotType { 36 | props: Props 37 | items: VNode[] 38 | point: Point 39 | i: number 40 | hasSpotlight: boolean 41 | } 42 | export interface RefLine { 43 | value: number | null 44 | } 45 | export interface Tooltip { 46 | formatter: (args: unknown) => string 47 | } 48 | -------------------------------------------------------------------------------- /packages/props.ts: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | import { Point, RefLine, Tooltip } from './interface' 3 | import uid from './utils/uid' 4 | import { defaultSpotProps, defaultTooltipStyle } from './constant' 5 | 6 | const baseProps = { 7 | width: { 8 | type: Number, 9 | default: 200 10 | }, 11 | height: { 12 | type: Number, 13 | default: 60 14 | } 15 | } 16 | const childProps = { 17 | data: { 18 | type: Array as PropType 19 | }, 20 | limit: { 21 | type: Number, 22 | default: 3 23 | }, 24 | margin: { 25 | type: Number, 26 | default: 3 27 | }, 28 | styles: { 29 | type: Object, 30 | default: () => ({}) 31 | }, 32 | max: Number, 33 | min: Number 34 | } 35 | const spotPartProps = { 36 | points: Array as PropType, 37 | hasSpot: Boolean, 38 | spotlight: { 39 | type: [Number, Boolean], 40 | default: false 41 | }, 42 | spotStyles: { 43 | type: Object, 44 | default: () => ({ 45 | strokeOpacity: 0, 46 | fillOpacity: 0 47 | }) 48 | }, 49 | spotProps: { 50 | type: Object, 51 | default: () => (defaultSpotProps) 52 | } 53 | } 54 | const refLinePartProps = { 55 | points: Array as PropType, 56 | refLineType: { // 'max', 'min', 'mean', 'avg', 'median', 'custom' or false 57 | type: [String, Boolean], 58 | default: 'mean' 59 | }, 60 | refLineStyles: { 61 | type: Object, 62 | default: () => ({}) 63 | }, 64 | refLineProps: { 65 | type: Object as PropType 66 | } 67 | } 68 | 69 | export const textProps = { 70 | point: Object as PropType, 71 | margin: { 72 | type: Number, 73 | default: 2 74 | }, 75 | textStyles: { 76 | type: Object, 77 | default: () => ({ 78 | fontSize: 12 79 | }) 80 | }, 81 | text: { 82 | type: String, 83 | default: '' 84 | } 85 | } 86 | export const sparklineProps = { 87 | ...baseProps, 88 | preserveAspectRatio: { 89 | type: String, 90 | default: 'none' 91 | }, 92 | margin: { 93 | type: Number, 94 | default: 2 95 | }, 96 | styles: { 97 | type: Object, 98 | default: () => ({}) 99 | }, 100 | indicatorStyles: { 101 | type: [Object, Boolean], 102 | default: () => ({ 103 | stroke: '#dd2c00' 104 | }) 105 | }, 106 | tooltipProps: { 107 | type: Object as PropType 108 | }, 109 | tooltipStyles: { 110 | type: Object, 111 | default: () => (defaultTooltipStyle) 112 | }, 113 | tooltipDelay: { 114 | type: Number, 115 | default: 0 116 | } 117 | } 118 | export const pieProps = { 119 | ...sparklineProps, 120 | data: { 121 | type: Array as PropType 122 | }, 123 | align: { 124 | type: String, 125 | default: 'left' // 'left', 'center', 'right' 126 | } 127 | } 128 | export const refLineProps = { 129 | ...baseProps, 130 | ...childProps, 131 | ...spotPartProps, 132 | ...refLinePartProps, 133 | ...textProps, 134 | divisor: { 135 | type: Number, 136 | default: 0.5 137 | } 138 | } 139 | export const spotProps = { 140 | ...baseProps, 141 | ...childProps, 142 | ...spotPartProps, 143 | ...refLinePartProps, 144 | ...textProps, 145 | divisor: { 146 | type: Number, 147 | default: 0.5 148 | } 149 | } 150 | export const lineProps = { 151 | ...baseProps, 152 | ...childProps, 153 | ...spotPartProps, 154 | ...refLinePartProps, 155 | ...textProps 156 | } 157 | export const barProps = { 158 | ...baseProps, 159 | ...childProps, 160 | ...refLinePartProps 161 | } 162 | export const curveProps = { 163 | ...baseProps, 164 | ...childProps, 165 | ...spotPartProps, 166 | ...refLinePartProps, 167 | ...textProps, 168 | divisor: { 169 | type: Number, 170 | default: 0.5 171 | } 172 | } 173 | export const gradientProps = { 174 | gradient: { 175 | type: Array as PropType 176 | }, 177 | id: { 178 | type: String, 179 | default: uid() 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /packages/utils/dataToPoints.ts: -------------------------------------------------------------------------------- 1 | import arrayMin from './min' 2 | import arrayMax from './max' 3 | 4 | export default ({ 5 | data = [], 6 | limit = 0, 7 | width = 1, 8 | height = 1, 9 | margin = 0, 10 | max = arrayMax(data), 11 | min = arrayMin(data), 12 | textHeight = 0 13 | }) => { 14 | const len = data.length 15 | 16 | if (limit && limit < len) { 17 | data = data.slice(len - limit) 18 | } 19 | 20 | const vfactor = (height - margin * 2 - textHeight) / ((max - min) || 2) 21 | const hfactor = (width - margin * 2) / ((limit || len) - (len > 1 ? 1 : 0)) 22 | 23 | return data.map((d: number, i: number) => ({ 24 | x: i * hfactor + margin, 25 | y: (max === min ? 1 : (max - d)) * vfactor + margin + textHeight 26 | })) 27 | } 28 | -------------------------------------------------------------------------------- /packages/utils/debounce.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 2 | export default any>(fn: F, delay: number) => { 3 | let timeout: ReturnType | null = null 4 | 5 | const debounced = (...args: Parameters) => { 6 | if (timeout !== null) { 7 | clearTimeout(timeout) 8 | timeout = null 9 | } 10 | timeout = setTimeout(() => fn(...args), delay) 11 | } 12 | 13 | return debounced as (...args: Parameters) => ReturnType 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils/genColor.ts: -------------------------------------------------------------------------------- 1 | export default () => { 2 | const characters = '0123456789ABCDEF' 3 | let color = '#' 4 | let i = 0 5 | 6 | for (; i < 6; i++) { 7 | color += characters[Math.floor(Math.random() * 16)] 8 | } 9 | return color 10 | } 11 | -------------------------------------------------------------------------------- /packages/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { default as min } from './min' 2 | export { default as max } from './max' 3 | export { default as mean } from './mean' 4 | export { default as avg } from './mean' 5 | export { default as midRange } from './midRange' 6 | export { default as median } from './median' 7 | export { default as stdev } from './stdev' 8 | export { default as variance } from './variance' 9 | export { default as dataToPoints } from './dataToPoints' 10 | export { default as debounce } from './debounce' 11 | export { default as color } from './genColor' 12 | export { default as uid } from './uid' 13 | export { default as isEmpty } from './isEmpty' 14 | -------------------------------------------------------------------------------- /packages/utils/isEmpty.ts: -------------------------------------------------------------------------------- 1 | export default (ref: unknown) => Object.keys(ref).length === 0 && ref.constructor === Object 2 | -------------------------------------------------------------------------------- /packages/utils/max.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line prefer-spread 2 | export default (data: number[]) => Math.max.apply(Math, data) 3 | -------------------------------------------------------------------------------- /packages/utils/mean.ts: -------------------------------------------------------------------------------- 1 | export default (data: number[]) => data.reduce((a, b) => a + b) / data.length 2 | -------------------------------------------------------------------------------- /packages/utils/median.ts: -------------------------------------------------------------------------------- 1 | export default (data: number[]) => data.sort((a, b) => a - b)[Math.floor(data.length / 2)] 2 | -------------------------------------------------------------------------------- /packages/utils/midRange.ts: -------------------------------------------------------------------------------- 1 | import min from './min' 2 | import max from './max' 3 | 4 | export default (data: number[]) => max(data) - min(data) / 2 5 | -------------------------------------------------------------------------------- /packages/utils/min.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line prefer-spread 2 | export default (data: number[]) => Math.min.apply(Math, data) 3 | -------------------------------------------------------------------------------- /packages/utils/stdev.ts: -------------------------------------------------------------------------------- 1 | import mean from './mean' 2 | 3 | export default (data: number[]) => { 4 | const dataMean = mean(data) 5 | const sqDiff = data.map(n => Math.pow(n - dataMean, 2)) 6 | const avgSqDiff = mean(sqDiff) 7 | 8 | return Math.sqrt(avgSqDiff) 9 | } 10 | -------------------------------------------------------------------------------- /packages/utils/uid.ts: -------------------------------------------------------------------------------- 1 | const SIZE = 256 2 | let idx = SIZE 3 | const hex: string[] = [] 4 | let buffer: string 5 | 6 | while (idx--) hex[idx] = (idx + SIZE).toString(16).substring(1) 7 | 8 | export default (len?: number) => { 9 | const tmp = len || 11 10 | let i = 0 11 | 12 | if (!buffer || ((idx + tmp) > SIZE * 2)) { 13 | for (buffer = '', idx = 0; i < SIZE; i++) { 14 | buffer += hex[Math.random() * SIZE | 0] 15 | } 16 | } 17 | 18 | return buffer.substring(idx, idx++ + tmp) 19 | } 20 | -------------------------------------------------------------------------------- /packages/utils/variance.ts: -------------------------------------------------------------------------------- 1 | import mean from './mean' 2 | 3 | export default (data: number[]) => { 4 | const dataMean = mean(data) 5 | const sq = data.map(n => Math.pow(n - dataMean, 2)) 6 | 7 | return mean(sq) 8 | } 9 | -------------------------------------------------------------------------------- /packages/withInstall.ts: -------------------------------------------------------------------------------- 1 | import type { App, Plugin } from 'vue' 2 | 3 | export const withInstall = (comp: T) => { 4 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 5 | const c = comp as any 6 | 7 | c.install = (app: App, name?: string) => { 8 | app.component(name || c.componentName || c.name, comp) 9 | } 10 | 11 | return comp as T & Plugin 12 | } 13 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import url from '@rollup/plugin-url' 2 | import json from '@rollup/plugin-json' 3 | import babel from '@rollup/plugin-babel' 4 | import vue from 'rollup-plugin-vue' 5 | import esbuild from 'rollup-plugin-esbuild' 6 | import replace from '@rollup/plugin-replace' 7 | import commonjs from '@rollup/plugin-commonjs' 8 | import { DEFAULT_EXTENSIONS } from '@babel/core' 9 | import multiInput from 'rollup-plugin-multi-input' 10 | import resolve from '@rollup/plugin-node-resolve' 11 | import filesize from 'rollup-plugin-filesize' 12 | import dts from 'rollup-plugin-dts' 13 | import pkg from './package.json' 14 | 15 | const name = 'vue-sparklines' 16 | const externalDeps = Object.keys(pkg.dependencies || {}).concat([/@babel\/runtime/]) 17 | const externalPeerDeps = Object.keys(pkg.peerDependencies || {}) 18 | const banner = `/** 19 | * ${name} v${pkg.version} 20 | * (c) ${new Date().getFullYear()} ${pkg.author} 21 | * @license ${pkg.license} 22 | */ 23 | ` 24 | const input = ['packages/**/*.ts', '!packages/*.d.ts', '!packages/interface.ts'] 25 | const getPlugins = () => { 26 | const plugins = [ 27 | resolve(), 28 | vue(), 29 | commonjs(), 30 | esbuild({ 31 | target: 'esnext', 32 | minify: false, 33 | jsx: 'preserve', 34 | tsconfig: 'tsconfig.json' 35 | }), 36 | babel({ 37 | babelHelpers: 'runtime', 38 | extensions: [...DEFAULT_EXTENSIONS, '.vue', '.ts', '.tsx'] 39 | }), 40 | json(), 41 | url(), 42 | replace({ 43 | preventAssignment: true, 44 | values: { 45 | PKG_VERSION: JSON.stringify(pkg.version) 46 | } 47 | }), 48 | filesize() 49 | ] 50 | 51 | return plugins 52 | } 53 | const esConfig = { 54 | input, 55 | external: externalDeps.concat(externalPeerDeps), 56 | plugins: [multiInput({ 57 | relative: 'packages/' 58 | })].concat(getPlugins()), 59 | output: { 60 | banner, 61 | dir: 'es/', 62 | format: 'esm', 63 | sourcemap: true, 64 | chunkFileNames: '_chunks/dep-[hash].js' 65 | } 66 | } 67 | /** @type {import('rollup').RollupOptions} */ 68 | const cjsConfig = { 69 | input, 70 | external: externalDeps.concat(externalPeerDeps), 71 | plugins: [multiInput({ 72 | relative: 'packages/' 73 | })].concat(getPlugins()), 74 | output: { 75 | banner, 76 | dir: 'lib/', 77 | format: 'cjs', 78 | sourcemap: true, 79 | exports: 'named', 80 | chunkFileNames: '_chunks/dep-[hash].js' 81 | } 82 | } 83 | const dtsConfig = { 84 | input: 'packages/index.ts', 85 | output: { 86 | banner, 87 | dir: 'type/' 88 | }, 89 | plugins: [dts()] 90 | } 91 | 92 | export default [esConfig, cjsConfig, dtsConfig] 93 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "esnext", 5 | "dom", 6 | "dom.iterable", 7 | "scripthost" 8 | ], 9 | "target": "esnext", 10 | "outDir": "lib", 11 | "declaration": true, 12 | "declarationDir": "./type", 13 | "module": "esnext", 14 | "moduleResolution": "node", 15 | "isolatedModules": false, 16 | "experimentalDecorators": true, 17 | "esModuleInterop": true, 18 | "noImplicitAny": true, 19 | "noImplicitThis": true, 20 | "strictNullChecks": false, 21 | "removeComments": true, 22 | "suppressImplicitAnyIndexErrors": true, 23 | "allowSyntheticDefaultImports": true, 24 | "resolveJsonModule": true, 25 | "allowJs": true, 26 | "jsx": "preserve", 27 | "baseUrl": "./" 28 | }, 29 | "include": [ 30 | "./**/*.ts", 31 | "./**/*.tsx", 32 | "./rollup.config.js" 33 | ], 34 | 35 | "exclude": [ 36 | "node_modules", 37 | "dist", 38 | "lib", 39 | "esm", 40 | "es", 41 | ], 42 | "compileOnSave": false 43 | } --------------------------------------------------------------------------------