├── .github └── workflows │ └── workflow.yml ├── .gitignore ├── .gitmodules ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .scripts ├── get_gh_pages_url.js └── publish_storybook.sh ├── .storybook ├── main.js ├── manager.js └── preview.js ├── CHANGELOG.md ├── LICENSE ├── README.md ├── codecov.yml ├── cypress.config.cjs ├── cypress ├── fixtures │ └── example.json └── support │ ├── commands.js │ ├── component-index.html │ ├── component.js │ └── e2e.js ├── jest.config.js ├── nyc.config.cjs ├── package.json ├── pnpm-lock.yaml ├── slim.config.js ├── speedo.gif ├── src ├── __tests__ │ ├── custom-segment-labels.js │ ├── custom-text.js │ ├── force-render.js │ ├── format-value.js │ ├── index.js │ └── update-values.js ├── core ├── index.d.ts ├── index.vue ├── props.js └── stories │ └── Speedometer.stories.js ├── tests ├── E2E.md ├── components │ ├── ValueUpdate.spec.js │ ├── ValueUpdate.vue │ ├── VueSpeedometer.spec.js │ └── force-render │ │ ├── ForceRender.spec.js │ │ └── ForceRender.vue ├── plugins │ ├── cypress-webpack-config.js │ └── index.js └── support │ ├── commands.js │ └── index.js ├── vite.config.js └── vitest.config.js /.github/workflows/workflow.yml: -------------------------------------------------------------------------------- 1 | name: Test Coverage 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [18.x] 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | with: 22 | submodules: recursive 23 | 24 | # - name: Init Submodule to base repo 25 | # run: git submodule update --init 26 | 27 | - name: Set up Node.js ${{ matrix.node-version }} 28 | uses: actions/setup-node@v1 29 | with: 30 | node-version: ${{ matrix.node-version }} 31 | 32 | - name: Install dependencies 33 | run: yarn 34 | 35 | # ref: https://github.com/cypress-io/github-action 36 | - name: Setup cypress 37 | uses: cypress-io/github-action@v4 38 | with: 39 | browser: chrome 40 | component: true 41 | # just perform install 42 | runTests: false 43 | 44 | - name: Run the tests and generate coverage report 45 | run: yarn full-test && yarn report:combined 46 | 47 | - name: Upload coverage to Codecov 48 | uses: codecov/codecov-action@v2 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.log 3 | .idea 4 | dist 5 | *.sublime-* 6 | .DS_Store 7 | *.code-workspace 8 | 9 | coverage 10 | *-coverage 11 | cypress/videos 12 | 13 | .nyc_output 14 | reports 15 | instrumented 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/d3-speedometer"] 2 | path = lib/d3-speedometer 3 | url = https://github.com/palerdot/react-d3-speedometer 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | lib 2 | src 3 | stories 4 | .storybook 5 | 6 | .scripts 7 | 8 | .git 9 | .gitmodules 10 | 11 | .babelrc 12 | *.gif 13 | *.code-workspace 14 | .DS_Store 15 | 16 | *.config.js 17 | 18 | .prettierignore 19 | .prettierrc 20 | 21 | .travis.yml 22 | 23 | yarn.lock 24 | yarn-error.log 25 | 26 | *-coverage 27 | coverage 28 | cypress* 29 | 30 | tests 31 | codecov.yml 32 | .github 33 | reports 34 | 35 | 36 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /package.json 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "tabWidth": 2, 4 | "arrowParens": "always", 5 | "printWidth": 80, 6 | "trailingComma": "es5" 7 | } 8 | -------------------------------------------------------------------------------- /.scripts/get_gh_pages_url.js: -------------------------------------------------------------------------------- 1 | // IMPORTANT 2 | // --------- 3 | // This is an auto generated file with React CDK. 4 | // Do not modify this file. 5 | 6 | // const parse = require('git-url-parse'); 7 | import parse from "git-url-parse" 8 | 9 | var ghUrl = process.argv[2] 10 | const parsedUrl = parse(ghUrl) 11 | 12 | const ghPagesUrl = "https://" + parsedUrl.owner + ".github.io/" + parsedUrl.name 13 | console.log(ghPagesUrl) 14 | -------------------------------------------------------------------------------- /.scripts/publish_storybook.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # IMPORTANT 4 | # --------- 5 | # This is an auto generated file with React CDK. 6 | # Do not modify this file. 7 | 8 | set -e # exit with nonzero exit code if anything fails 9 | 10 | # get GIT url 11 | 12 | GIT_URL=`git config --get remote.origin.url` 13 | if [[ $GIT_URL == "" ]]; then 14 | echo "This project is not configured with a remote git repo". 15 | exit 1 16 | fi 17 | 18 | # clear and re-create the out directory 19 | rm -rf .out || exit 0; 20 | mkdir .out; 21 | 22 | # run our compile script, discussed above 23 | # build-storybook -o .out 24 | npm run build-storybook 25 | 26 | # go to the out directory and create a *new* Git repo 27 | cd .out 28 | git init 29 | 30 | # inside this git repo we'll pretend to be a new user 31 | git config user.name "palerdot GH Pages Bot" 32 | git config user.email "ghpages-bot@palerdot.in" 33 | 34 | # The first and only commit to this new Git repo contains all the 35 | # files present with the commit message "Deploy to GitHub Pages". 36 | git add . 37 | git commit -m "Deploy Storybook to GitHub Pages" 38 | 39 | # Force push from the current repo's master branch to the remote 40 | # repo's gh-pages branch. (All previous history on the gh-pages branch 41 | # will be lost, since we are overwriting it.) We redirect any output to 42 | # /dev/null to hide any sensitive credential data that might otherwise be exposed. 43 | git push --force --quiet $GIT_URL master:gh-pages > /dev/null 2>&1 44 | cd .. 45 | rm -rf .out 46 | 47 | echo "" 48 | echo "=> Storybook deployed to: `node .scripts/get_gh_pages_url.js $GIT_URL`" 49 | -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | stories: [ 3 | "../src/stories/**/*.stories.mdx", 4 | "../src/stories/**/*.stories.@(js|jsx|ts|tsx)", 5 | ], 6 | 7 | addons: ["@storybook/addon-links", "@storybook/addon-essentials"], 8 | 9 | async viteFinal(config, { configType }) { 10 | // customize the Vite config here 11 | return { 12 | ...config, 13 | define: { 14 | ...config.define, 15 | global: "window", 16 | }, 17 | } 18 | }, 19 | 20 | framework: { 21 | name: "@storybook/vue3-vite", 22 | options: {} 23 | }, 24 | 25 | docs: { 26 | autodocs: true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.storybook/manager.js: -------------------------------------------------------------------------------- 1 | import { addons } from "@storybook/addons" 2 | import { themes, create } from "@storybook/theming/create" 3 | import theme from "../src/core/theme" 4 | 5 | const speedoTheme = create({ 6 | ...themes.dark, 7 | ...theme, 8 | 9 | brandTitle: "vue-speedometer", 10 | brandUrl: "https://github.com/palerdot/vue-speedometer", 11 | }) 12 | 13 | addons.setConfig({ 14 | showPanel: true, 15 | theme: speedoTheme, 16 | }) 17 | -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | // ref: https://www.npmjs.com/package/@storybook/vue3 2 | // ref: https://github.com/storybookjs/storybook/issues/19295 3 | import { setup } from "@storybook/vue3" 4 | 5 | // DEVELOPMENT: 'src/index.js' 6 | import VueSpeedometer from "../src/index.vue" 7 | // PRODUCTION build testing 8 | // import VueSpeedometer from "../dist/index" 9 | // 10 | setup(app => { 11 | app.component("vue-speedometer", VueSpeedometer) 12 | app.mixin({ 13 | /* My mixin */ 14 | }) 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## `v3.0.1` 4 | 5 | - `v3` General Availability - Happy Vue Coding! 6 | 7 | ## v3.0.0-rc0 8 | 9 | - `Vue 3 Support - SFC/composition api`: Rewrite with SFC and composition api 10 | - `segmentLabelFormatter` prop 11 | - Experimental slim build 12 | - Core update `v2.2.1` 13 | 14 | ------------------------- 15 | 16 | ## v2.0 17 | 18 | - Compatible with `vue v3.0` (still uses options api under the hood) 19 | 20 | ------------------------- 21 | 22 | ## 1.8.0 23 | - **Bugfix**: Always take new value for `value` prop. This fixes the bug when value will not go to `0`. 24 | Resolves https://github.com/palerdot/vue-speedometer/issues/22 25 | 26 | ## 1.7.0 27 | - [`CORE`][0.14.0] 28 | - `valueTextFontWeight` config/prop. ref: https://codesandbox.io/s/competent-grothendieck-73cz8?file=/src/App.vue 29 | - `cypress` tests for `valueTextFontWeight` 30 | 31 | ## 1.6.0 32 | - [`CORE`][0.13.1] 33 | - `CustomSegmentLabelPosition`, `Transition` types for both Typescript and JS. ref - https://codesandbox.io/s/modest-cookies-tnhqx?file=/src/App.vue 34 | - `100%` Test coverage 35 | - `codecov`, `github actions` integration 36 | - `cypress` e2e tests 37 | 38 | ## 1.5.0 39 | - removed `@babel/runtime-corejs2` as dependency 40 | 41 | ## 1.4.0 42 | - [`CORE`][0.11.0] 43 | - migrated to `lodash-es` from `lodash` for better tree shaking. Exporting `themes` from core for better reusablility. 44 | - removed `lodash` dependency. ref: https://codesandbox.io/s/fragrant-haze-f3cqz 45 | 46 | ## 1.3.1 47 | - link banner gif from github master tree 48 | 49 | ## 1.3.0 50 | - [`CORE`][0.10.0] 51 | 52 | ## 1.2.1 53 | - *Fix Typescript declaration*. Use proper Vue type declaration. Fixes typescript support problem for Vue/Typescript. 54 | 55 | ## 1.2.0 56 | - `Typescript` support. [CORE][0.9.0] 57 | 58 | ## 1.1.0 59 | - [bugfix] Fix `forceRender` prop not updating from `true => false`. Fixes - https://github.com/palerdot/vue-speedometer/issues/4 60 | 61 | ## 1.0.4 62 | - [CORE][0.8.0] 63 | 64 | ## 1.0.3 65 | - core sync `[v0.7.2, v0.7.3]` 66 | 67 | ## 1.0.2 68 | - core sync `[v0.7.1]` 69 | 70 | ## 1.0.1 beta 71 | - minor `README` fixes 72 | ## 1.0.0.beta 73 | - Initial `beta` release - https://codesandbox.io/s/vue-template-5yuw8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Arun Kumar 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vue-speedometer 2 | 3 | **vue-speedometer** is a Vue component library for showing speedometer like gauge using d3. 4 | 5 | [![Codecov](https://img.shields.io/codecov/c/gh/palerdot/vue-speedometer)](https://codecov.io/gh/palerdot/vue-speedometer) 6 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg?style=flat-square)](https://github.com/prettier/prettier) 7 | [![Github](https://github.com/palerdot/vue-speedometer/actions/workflows/workflow.yml/badge.svg)](https://github.com/palerdot/vue-speedometer/actions/workflows/workflow.yml/badge.svg) 8 | 9 | [![npm version](https://img.shields.io/npm/v/vue-speedometer/latest?style=flat-square)](https://img.shields.io/npm/v/vue-speedometer/latest?style=flat-square) 10 | 11 | ![vue-speedometer](https://raw.githubusercontent.com/palerdot/vue-speedometer/master/speedo.gif) 12 | 13 | **IMPORTANT** `v3.0` is released which is a complete rewrite with composition api/SFC (single file components). Minimum required vue version is `v3.3`. If you are using Vue 3 composition api, please upgrade to `v3` 14 | 15 | Note: v2.x is still compatible with `Vue 3`. Please use latest v1.x (v1.8.0 at the time of writing) if you are using `Vue 2`. 16 | 17 | ## Usage: 18 | 19 | **pnpm:** 20 | `pnpm add vue-speedometer` 21 | 22 | **Yarn:** 23 | `yarn add vue-speedometer` 24 | 25 | **NPM:** 26 | `npm install --save vue-speedometer` 27 | 28 | ```javascript 29 | // import the component 30 | 33 | // and use it in your component like 34 | 37 | ``` 38 | 39 | ### Slim Build (Experimental): 40 | 41 | There is a `Slim` build available without bundling `d3`. This project uses `d3` *micro bundles*. If your project also uses `d3` *microbundles*, you can opt for **slim build**. Necessary `d3` dependencies required for slim build to work are - `d3-array`, `d3-color`, `d3-ease`, `d3-format`, `d3-interpolate`, `d3-scale`, `d3-selection`, `d3-shape`, `d3-transition`. 42 | ```javascript 43 | // sample slim build usage 44 | import VueSpeedometer from "vue-speedometer/slim" 45 | // and use it 46 | 47 | ``` 48 | 49 | 50 | ## Examples: 51 | 52 | You can view [Live Examples here](https://palerdot.in/vue-speedometer/) 53 | 54 | 55 | ## Configuration Options: 56 | 57 | | prop | type | default | comments | 58 | | ------------|:--------------:| --------:| ---------| 59 | | value | Number | 0 | Make sure your value is between your `minValue` and `maxValue` | 60 | | minValue | Number | 0 | | 61 | | maxValue | Number | 1000 | | 62 | | segments | Number | 5 | Number of segments in the speedometer. Please note, `segments` is calculated with [d3-ticks]() which is an approximate count that is uniformly spaced between min and max. Please refer to [d3-ticks](https://github.com/d3/d3-scale/blob/master/README.md#continuous_ticks) and [d3-array ticks](https://github.com/d3/d3-array#ticks) for more detailed info. | 63 | | maxSegmentLabels | Number | value from 'segments' prop | Limit the number of segment labels to displayed. This is useful for acheiving a gradient effect by giving arbitrary large number of `segments` and limiting the labels with this prop. [See Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--gradient-effect-with-large-number-of-segments-and-maxsegmentlabels-config). Please note, `maxSegmentLabels` is calculated with [d3-ticks]() which is an approximate count that is uniformly spaced between min and max. Please refer to [d3-ticks](https://github.com/d3/d3-scale/blob/master/README.md#continuous_ticks) and [d3-array ticks](https://github.com/d3/d3-array#ticks) for more detailed info. | 64 | | forceRender | Boolean | false | After initial rendering/mounting, when props change, only the `value` is changed and animated to maintain smooth visualization. But, if you want to force rerender the whole component like change in segments, colors, dimensions etc, you can use this option to force rerender of the whole component on props change. | 65 | | width | Number | 300 | **diameter** of the speedometer and the **width** of the svg element | 66 | | height | Number | 300 | height of the svg element. Height of the speedometer is always half the width since it is a **semi-circle**. For fluid width, please refere to `fluidWidth` config | 67 | | dimensionUnit | String | px | Default to `px` for `width/height`. Possible values - `"em" , "ex" , "px" , "in" , "cm" , "mm" , "pt" , ,"pc"` ... Please refer to [specification](https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#Length) for more details | 68 | | fluidWidth | Boolean | false | If `true` takes the width of the parent component. See [Live Example](https://palerdot.in/vue-speedometer/?selectedStory=Fluid%20Width%20view&full=0&down=0&left=1&panelRight=0&downPanel=kadirahq%2Fstorybook-addon-actions%2Factions-panel) for more details | 69 | | needleColor | String | steelblue | Should be a valid color code - colorname, hexadecimal name or rgb value. Should be a valid input for [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl) | 70 | | startColor | String | #FF471A | Should be a valid color code - colorname, hexadecimal name or rgb value. Should be a valid input for [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl) | 71 | | endColor | String | #33CC33 | Should be a valid color code - colorname, hexadecimal name or rgb value. Should be a valid input for [d3.interpolateHsl](https://github.com/d3/d3-interpolate#interpolateHsl) | 72 | | segmentColors | Array (of colors) | [] | Custom segment colors can be given with this option. Should be an array of valid color codes. If this option is given **startColor** and **endColor** options will be ignored. | 73 | | needleTransition | String (JS) / Transition (TS) | easeQuadInOut | [d3-easing-identifiers](https://github.com/d3/d3-ease) - easeLinear, easeQuadIn, easeQuadOut, easeQuadInOut, easeCubicIn, easeCubicOut, easeCubicInOut, easePolyIn, easePolyOut, easePolyInOut, easeSinIn, easeSinOut, easeSinInOut, easeExpIn, easeExpOut, easeExpInOut, easeCircleIn, easeCircleOut, easeCircleInOut, easeBounceIn, easeBounceOut, easeBounceInOut, easeBackIn, easeBackOut, easeBackInOut, easeElasticIn, easeElasticOut, easeElasticInOut, easeElastic. There is a helper Object/Type 'Transtion', which you can import like `import { Transition } from 'vue-speedometer'` and use it like `Transition.easeElastic`. This works for both JS and Typescript. For `type(script)` definitions, please refer [here](./src/index.d.ts). | 74 | | needleTransitionDuration | number | 500 | Time in milliseconds. | 75 | | needleHeightRatio | Float (between 0 and 1) | 0.9 | Control the height of the needle by giving a number/float between `0` and `1`. Default height ratio is `0.9`. | 76 | | ringWidth | Number | 60 | Width of the speedometer ring. | 77 | | textColor | String | #666 | Should be a valid color code - colorname, hexadecimal name or rgb value. Used for both showing the current value and the segment values | 78 | | valueFormat | String | | should be a valid format for [d3-format](https://github.com/d3/d3-format#locale_format). By default, no formatter is used. You can use a valid d3 format identifier (for eg: `d` to convert float to integers), to format the values. **Note:** This formatter affects all the values (current value, segment values) displayed in the speedometer | 79 | | segmentValueFormatter | Function | value => value | Custom segment values formatter function. This function is applied after 'valueFormat' prop if present. | 80 | | currentValueText | String | ${value} | Should be provided a string which should have **${value}** placeholder which will be replaced with current value. By default, current value is shown (formatted with `valueFormat`). For example, if current Value is 333 if you would like to show `Current Value: 333`, you should provide a string **`Current Value: ${value}`**. See [Live Example](https://palerdot.in/vue-speedometer/?selectedKind=vue-speedometer&selectedStory=Custom%20Current%20Value%20Text&full=0&down=1&left=1&panelRight=0) | 81 | | currentValuePlaceholderStyle | String | ${value} | Should be provided a placeholder string which will be replaced with current value in `currentValueTextProp`. For example: you can use ruby like interpolation by giving following props - ``. This is also helpful if you face `no-template-curly-in-string` eslint warnings and would like to use different placeholder for current value | 82 | | customSegmentStops | Array | [] | Array of values **starting** at `min` value, and **ending** at `max` value. This configuration is useful if you would like to split the segments at custom points or have unequal segments at preferred values. If the values does not begin and end with `min` and `max` value respectively, an error will be thrown. This configuration will override `segments` prop, since total number of segments will be `length - 1` of `customSegmentProps`. For example, `[0, 50, 75, 100]` value will have three segments - `0-50`, `50-75`, `75-100`. See [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--custom-segment-stops) | 83 | | customSegmentLabels | Array`` | [] | Takes an array of `CustomSegmentLabel` objects. Each object has following keys for custom rendering of labels - `text`, `fontSize`, `color`, `position: OUTSIDE/INSIDE`. For `position`, there is a helper `CustomSegmentLabelPosition` Object/Type which you can import like `import { CustomSegmentLabelPosition } from 'vue-speedometer'`, and use it like `CustomSegmentLabelPosition.Inside / CustomSegmentLabelPosition.Outside`. This works for both JS and Typescript. For `type(script)` definitions, please refer [here](./src/index.d.ts). | 84 | | labelFontSize | String | 14px | Font size for segment labels/legends | 85 | | valueTextFontSize | String | 16px | Font size for current value text | 86 | | valueTextFontWeight | String | bold | Font weight for current value text. Any valid font weight identifier (500, bold etc) can be used. | 87 | | paddingHorizontal | Number | 0 | Provides right/left space for the label text. Takes a number (without explicit unit, unit will be taken from dimensionUnit config which defaults to px). Helpful when using a bigger font size for label texts. | 88 | | paddingVertical | Number | 0 | Provides top/bottom space for the current value label text below the needle. Takes a number (without explicit unit, unit will be taken from dimensionUnit config which defaults to px). Helpful when using a bigger font size for label texts. | 89 | | svgAriaLabel | String | Vue Speedometer | SVG aria-label property for Accessibility purposes | 90 | 91 | ## Examples 92 | 93 | You can view [Live Examples here](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--default-with-no-config) 94 | 95 | #### Default with no config - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--default-with-no-config) 96 | 97 | ```javascript 98 | export default { 99 | components: { VueSpeedometer }, 100 | template: ``, 101 | } 102 | ``` 103 | 104 | #### With configurations - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--configuring-values) 105 | 106 | ```javascript 107 | export default { 108 | components: { VueSpeedometer }, 109 | template: ``, 110 | } 111 | ``` 112 | 113 | #### Custom Segment Labels - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--custom-segment-labels) 114 | 115 | ```javascript 116 | // 'customSegmentLabels' prop takes an array of 'CustomSegmentLabel' Object 117 | /* 118 | type CustomSegmentLabel = { 119 | text?: string 120 | position?: OUTSIDE/INSIDE 121 | fontSize?: string 122 | color?: string 123 | } 124 | */ 125 | 126 | export default { 127 | components: { VueSpeedometer }, 128 | template: ` 129 |
130 | 169 |
170 | `, 171 | } 172 | 173 | /> 174 | ``` 175 | 176 | #### Custom Segment Colors - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--custom-segment-colors) 177 | 178 | ```javascript 179 | export default { 180 | components: { VueSpeedometer }, 181 | template: ` 182 |
183 | 190 |
191 | `, 192 | } 193 | // startColor will be ignored 194 | // endColor will be ignored 195 | /> 196 | ``` 197 | 198 | #### Custom Segment Stops - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--custom-segment-stops) 199 | 200 | ```javascript 201 | export default { 202 | components: { VueSpeedometer }, 203 | template: ` 204 |
205 | 213 |
214 | `, 215 | } 216 | // `segments` prop will be ignored since it will be calculated from `customSegmentStops` 217 | // In this case there will be `4` segments (0-500, 500-750, 750-900, 900-1000) 218 | /> 219 | ``` 220 | 221 | #### Fluid Width Example - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--fluid-width-view) 222 | 223 | ```javascript 224 | // Speedometer will take the width of the parent div (500) 225 | // any width passed will be ignored 226 | export default { 227 | components: { VueSpeedometer }, 228 | data() { 229 | return { 230 | styles: { 231 | width: "500px", 232 | height: "300px", 233 | background: "#EFEFEF", 234 | }, 235 | } 236 | }, 237 | template: ` 238 |
239 | 246 |
247 | Fluid width takes the width of the parent div (500px in this case) 248 |
249 |
250 | `, 251 | } 252 | ``` 253 | 254 | #### Needle Transition Example - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--needle-transition-duration) 255 | 256 | ```javascript 257 | export default { 258 | components: { VueSpeedometer }, 259 | template: ` 260 |
261 | 267 |
268 | `, 269 | } 270 | ``` 271 | 272 | #### Force Render component on props change - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--force-render-the-component) 273 | 274 | ```javascript 275 | // By default, when props change, only the value prop is updated and animated. 276 | // This is to maintain smooth visualization and to ignore breaking appearance changes like segments, colors etc. 277 | // You can override this behaviour by giving forceRender: true 278 | 279 | export default { 280 | components: { VueSpeedometer }, 281 | template: ` 282 |
283 | 290 |
291 | `, 292 | } 293 | ``` 294 | 295 | #### Needle Height Configuration Example - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--configure-needle-length-and-font-sizes) 296 | 297 | ```javascript 298 | export default { 299 | components: { VueSpeedometer }, 300 | template: ` 301 |
302 | 306 |
307 | `, 308 | } 309 | ``` 310 | 311 | You can give a value between `0` and `1` to control the needle height. 312 | 313 | 314 | #### Gradient Like Effect - [Live Example](https://palerdot.in/vue-speedometer/?path=/story/vue-speedometer--gradient-effect-with-large-number-of-segments-and-max-segment-labels-config) 315 | 316 | ```javascript 317 | export default { 318 | components: { VueSpeedometer }, 319 | template: ` 320 |
321 | 327 |
328 | `, 329 | } 330 | ``` 331 | 332 | --- 333 | 334 | ### Todos: 335 | 336 | - [x] Test coverage (with [vue-test-utils](https://vue-test-utils.vuejs.org/)) 337 | - [x] Convert entire code base to ES6 338 | - [x] Split core from lifecycles 339 | - [x] Typescript support 340 | 341 | --- 342 | 343 | ### Tests: 344 | 345 | `vue-speedometer` comes with a test suite using [vue-test-utils](https://vue-test-utils.vuejs.org/). 346 | 347 | ```javascript 348 | // navigate to root folder and run 349 | npm test 350 | // or 'yarn test' if you are using yarn 351 | ``` 352 | 353 | --- 354 | 355 | ### FAQ 356 | 357 | - Please refer this [comment](https://github.com/vuejs/vue-cli/issues/1875#issuecomment-408739414) if you run into `vue cli you are using the runtime only build of vue where the template compiler is not available` message when running from your local setup bootstrapped with `vue-cli`. Basically create a `vue.config.js` 358 | ```javascript 359 | // vue.config.js 360 | module.exports = { 361 | runtimeCompiler: true 362 | } 363 | ``` 364 | 365 | --- 366 | 367 | #### Changelog: 368 | 369 | [View Changelog](CHANGELOG.md) 370 | 371 | --- 372 | 373 | ## Ports: 374 | - React: [react-d3-speedometer](https://github.com/palerdot/react-d3-speedometer) 375 | - Svelte: [svelte-speedometer](https://github.com/palerdot/svelte-speedometer) 376 | 377 | --- 378 | 379 | ## About 380 | 381 | `vue-speedometer` shares its core with [react-d3-speedometer](https://github.com/palerdot/react-d3-speedometer). For more info and context, please visit [react-d3-speedometer](https://github.com/palerdot/react-d3-speedometer) 382 | 383 | --- 384 | 385 | #### License: 386 | 387 | [MIT](LICENSE) 388 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | disable_default_path_fixes: false 3 | -------------------------------------------------------------------------------- /cypress.config.cjs: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress") 2 | const vitePreprocessor = require("cypress-vite") 3 | 4 | module.exports = defineConfig({ 5 | video: false, 6 | 7 | component: { 8 | setupNodeEvents(on, config) { 9 | require("@cypress/code-coverage/task")(on, config) 10 | 11 | return config 12 | }, 13 | specPattern: "tests/components/**/*.spec.js*", 14 | devServer: { 15 | framework: "vue", 16 | bundler: "vite", 17 | // bundler: "webpack", 18 | }, 19 | }, 20 | 21 | // ref: https://www.npmjs.com/package/cypress-vite 22 | e2e: { 23 | setupNodeEvents(on, config) { 24 | // implement node event listeners here 25 | on("file:preprocessor", vitePreprocessor()) 26 | }, 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /cypress/support/component.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // ref: https://docs.cypress.io/guides/tooling/code-coverage#Install-the-plugin 17 | import "@cypress/code-coverage/support" 18 | // Import commands.js using ES2015 syntax: 19 | import "./commands" 20 | 21 | // Alternatively you can use CommonJS syntax: 22 | // require('./commands') 23 | 24 | import { mount } from "cypress/vue" 25 | 26 | Cypress.Commands.add("mount", mount) 27 | -------------------------------------------------------------------------------- /cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/kb/8yfwdf950sx_nwk3_bndmfjc0000gp/T/jest_dy", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: "jest-coverage", 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // An array of directory names to be searched recursively up from the requiring module's location 64 | // moduleDirectories: ["node_modules"], 65 | 66 | // An array of file extensions your modules use 67 | // moduleFileExtensions: [ 68 | // "js", 69 | // "json", 70 | // "jsx", 71 | // "ts", 72 | // "tsx", 73 | // "node" 74 | // ], 75 | 76 | // A map from regular expressions to module names that allow to stub out resources with a single module 77 | // moduleNameMapper: {}, 78 | 79 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 80 | // modulePathIgnorePatterns: [], 81 | 82 | // Activates notifications for test results 83 | // notify: false, 84 | 85 | // An enum that specifies notification mode. Requires { notify: true } 86 | // notifyMode: "failure-change", 87 | 88 | // A preset that is used as a base for Jest's configuration 89 | // preset: null, 90 | 91 | // Run tests from one or more projects 92 | // projects: null, 93 | 94 | // Use this configuration option to add custom reporters to Jest 95 | // reporters: undefined, 96 | 97 | // Automatically reset mock state between every test 98 | // resetMocks: false, 99 | 100 | // Reset the module registry before running each individual test 101 | // resetModules: false, 102 | 103 | // A path to a custom resolver 104 | // resolver: null, 105 | 106 | // Automatically restore mock state between every test 107 | // restoreMocks: false, 108 | 109 | // The root directory that Jest should scan for tests and modules within 110 | // rootDir: null, 111 | 112 | // A list of paths to directories that Jest should use to search for files in 113 | // roots: [ 114 | // "" 115 | // ], 116 | 117 | // Allows you to use a custom runner instead of Jest's default test runner 118 | // runner: "jest-runner", 119 | 120 | // The paths to modules that run some code to configure or set up the testing environment before each test 121 | // setupFiles: [], 122 | 123 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 124 | // setupFilesAfterEnv: [], 125 | 126 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 127 | // snapshotSerializers: [], 128 | 129 | // The test environment that will be used for testing 130 | // testEnvironment: "jest-environment-jsdom", 131 | 132 | // Options that will be passed to the testEnvironment 133 | // testEnvironmentOptions: {}, 134 | 135 | // Adds a location field to test results 136 | // testLocationInResults: false, 137 | 138 | // The glob patterns Jest uses to detect test files 139 | // testMatch: [ 140 | // "**/__tests__/**/*.[jt]s?(x)", 141 | // "**/?(*.)+(spec|test).[tj]s?(x)" 142 | // ], 143 | 144 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 145 | testPathIgnorePatterns: [ 146 | "/node_modules/", 147 | "lib/d3-speedometer/src/__tests__/index.js", 148 | "tests/", 149 | ], 150 | 151 | // The regexp pattern or array of patterns that Jest uses to detect test files 152 | // testRegex: [], 153 | 154 | // This option allows the use of a custom results processor 155 | // testResultsProcessor: null, 156 | 157 | // This option allows use of a custom test runner 158 | // testRunner: "jasmine2", 159 | 160 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 161 | // testURL: "http://localhost", 162 | 163 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 164 | // timers: "real", 165 | 166 | // A map from regular expressions to paths to transformers 167 | // transform: null, 168 | 169 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 170 | transformIgnorePatterns: ["/node_modules/(?!lodash-es).+\\.js$"], 171 | 172 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 173 | // unmockedModulePathPatterns: undefined, 174 | 175 | // Indicates whether each individual test should be reported during the run 176 | verbose: true, 177 | 178 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 179 | // watchPathIgnorePatterns: [], 180 | 181 | // Whether to use watchman for file crawling 182 | // watchman: true, 183 | } 184 | -------------------------------------------------------------------------------- /nyc.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // extends: "@istanbuljs/nyc-config-babel", 3 | "report-dir": "cypress-coverage", 4 | "check-coverage": true, 5 | include: ["src"], 6 | extension: [".js", ".vue"], 7 | exclude: ["src/stories", "src/core", "src/__tests__", "src/index.d.ts"], 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-speedometer", 3 | "version": "3.0.1", 4 | "description": "Vue component for showing speedomenter like gauge with d3", 5 | "author": { 6 | "name": "palerdot", 7 | "email": "palerdot@gmail.com" 8 | }, 9 | "type": "module", 10 | "files": [ 11 | "dist" 12 | ], 13 | "types": "./dist/index.d.ts", 14 | "main": "./dist/vue-speedometer.es.js", 15 | "module": "./dist/vue-speedometer.es.js", 16 | "exports": { 17 | ".": { 18 | "types": "./dist/index.d.ts", 19 | "import": "./dist/vue-speedometer.es.js", 20 | "require": "./dist/vue-speedometer.umd.js" 21 | }, 22 | "./slim": { 23 | "import": "./dist/slim/index.js" 24 | } 25 | }, 26 | "scripts": { 27 | "lint": "prettier -l src/index.js src/props.js src/__tests__/**/**.js", 28 | "build-storybook": "storybook build -o .out", 29 | "init-submodule": "git submodule update --init", 30 | "lintfix": "prettier --write src/index.js src/props.js src/__tests__/**/**.js", 31 | "publish-storybook": "bash .scripts/publish_storybook.sh", 32 | "pull-submodules": "git submodule foreach git pull origin master", 33 | "storybook": "storybook dev -p 6006", 34 | "test": "vitest", 35 | "pretest": "rm -rf .nyc_output || true", 36 | "full-test": "vitest run && yarn instrument-code && yarn cy:run && yarn clean:instrumented-code", 37 | "clean:instrumented-code": "rm -rf instrumented", 38 | "instrument-code": "npm run clean:instrumented-code && npx nyc instrument --compact=false src instrumented && cp -R src/core instrumented/", 39 | "mkdir:reports": "mkdir reports || true", 40 | "precopy:reports": "npm run mkdir:reports", 41 | "copy:reports": "cp cypress-coverage/coverage-final.json reports/from-cypress.json || true", 42 | "precombine:reports": "npm run copy:reports && mkdir .nyc_output || true", 43 | "combine:reports": "npx nyc merge reports && mv coverage.json .nyc_output/out.json || true", 44 | "prereport:combined": "npm run combine:reports", 45 | "report:combined": "npx nyc report --reporter lcov --reporter text --report-dir coverage", 46 | "view-cypress-coverage": "npx nyc report", 47 | "cy:run": "cypress run --component", 48 | "cy:open": "cypress open", 49 | "build": "npm run build:main && npm run build:slim && npm run copy:types", 50 | "build:main": "NODE_ENV=production vite build", 51 | "build:slim": "NODE_ENV=production vite build -c slim.config.js", 52 | "copy:types": "cp ./src/index.d.ts ./dist/", 53 | "prepublishOnly": "npm run build", 54 | "preview": "vite preview" 55 | }, 56 | "dependencies": { 57 | "d3-array": "^3.1.4", 58 | "d3-color": "^3.1.0", 59 | "d3-ease": "^3.0.1", 60 | "d3-format": "^3.1.0", 61 | "d3-interpolate": "^3.0.1", 62 | "d3-scale": "^4.0.2", 63 | "d3-selection": "^3.0.0", 64 | "d3-shape": "^3.1.0", 65 | "d3-transition": "^3.0.1", 66 | "lodash-es": "^4.17.15", 67 | "memoize-one": "^6.0.0" 68 | }, 69 | "devDependencies": { 70 | "@cypress/code-coverage": "^3.12.26", 71 | "@cypress/vue": "^6.0.0", 72 | "@istanbuljs/nyc-config-babel": "^3.0.0", 73 | "@rollup/plugin-node-resolve": "^13.3.0", 74 | "@rollup/plugin-terser": "^0.4.4", 75 | "@storybook/addon-essentials": "^7.6.17", 76 | "@storybook/addon-links": "^7.6.17", 77 | "@storybook/manager-api": "^7.6.17", 78 | "@storybook/theming": "^7.6.17", 79 | "@storybook/vue3": "^7.6.17", 80 | "@storybook/vue3-vite": "^7.6.17", 81 | "@vitejs/plugin-vue": "^5.0.4", 82 | "@vue/compiler-sfc": "^3.2.37", 83 | "@vue/test-utils": "^2.4.4", 84 | "cypress": "^13.6.6", 85 | "cypress-vite": "^1.5.0", 86 | "git-url-parse": "^11.1.2", 87 | "happy-dom": "^13.6.2", 88 | "nyc": "^15.1.0", 89 | "prettier": "^2.1.2", 90 | "react": "^18.2.0", 91 | "react-dom": "^18.2.0", 92 | "rollup-plugin-analyzer": "^4.0.0", 93 | "sprintf-js": "^1.1.2", 94 | "storybook": "^7.6.17", 95 | "vite": "^5.1.4", 96 | "vitest": "^1.3.1", 97 | "vue": "^3.4.20" 98 | }, 99 | "peerDependencies": { 100 | "vue": "^3.3.0" 101 | }, 102 | "bugs": { 103 | "url": "https://github.com/palerdot/vue-speedometer/issues" 104 | }, 105 | "engines": { 106 | "node": ">=18.0", 107 | "npm": ">=10.0.0" 108 | }, 109 | "homepage": "https://github.com/palerdot/vue-speedometer#readme", 110 | "keywords": [ 111 | "vue", 112 | "speedometer", 113 | "gauge", 114 | "d3" 115 | ], 116 | "license": "MIT", 117 | "repository": { 118 | "type": "git", 119 | "url": "git+https://github.com/palerdot/vue-speedometer.git" 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /slim.config.js: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { defineConfig } from "vite" 3 | import vue from "@vitejs/plugin-vue" 4 | import terser from "@rollup/plugin-terser" 5 | import { nodeResolve } from "@rollup/plugin-node-resolve" 6 | // import analyze from "rollup-plugin-analyzer" 7 | 8 | const devMode = process.env.NODE_ENV === "development" 9 | 10 | // ref: https://blog.openreplay.com/the-ultimate-guide-to-getting-started-with-the-rollup-js-javascript-bundler 11 | function terserConfig() { 12 | return terser({ 13 | ecma: 2020, 14 | 15 | mangle: { toplevel: true }, 16 | 17 | compress: { 18 | module: true, 19 | toplevel: true, 20 | unsafe_arrows: true, 21 | drop_console: !devMode, 22 | drop_debugger: !devMode, 23 | }, 24 | 25 | output: { quote_style: 1 }, 26 | }) 27 | } 28 | 29 | // https://vitejs.dev/config/ 30 | export default defineConfig({ 31 | build: { 32 | outDir: path.resolve(__dirname, "dist/slim"), 33 | lib: { 34 | name: "VueSpeedometer", 35 | entry: path.resolve(__dirname, "src/index.vue"), 36 | // fileName: (format) => `vue-speedometer.${format}.js`, 37 | formats: ["es"], 38 | fileName: () => "index.js", 39 | }, 40 | rollupOptions: { 41 | external: [ 42 | "vue", 43 | "d3-array", 44 | "d3-color", 45 | "d3-ease", 46 | "d3-format", 47 | "d3-interpolate", 48 | "d3-scale", 49 | "d3-selection", 50 | "d3-shape", 51 | "d3-transition", 52 | ], 53 | output: { 54 | globals: { 55 | vue: "Vue", 56 | }, 57 | sourcemap: devMode ? "inline" : false, 58 | plugins: [terserConfig()], 59 | }, 60 | treeshake: { 61 | moduleSideEffects: false, 62 | }, 63 | // IMPORTANT: This plugins is different from output plugins 64 | plugins: [ 65 | nodeResolve(), 66 | // analyze({ 67 | // summaryOnly: true, 68 | // filterSummary: true, 69 | // }), 70 | ], 71 | }, 72 | }, 73 | resolve: { 74 | alias: { 75 | // ref: https://github.com/vitejs/vite/discussions/4158 76 | // needed to enable template compiling for cypress 77 | // vue: "vue/dist/vue.esm-bundler.js", 78 | }, 79 | }, 80 | plugins: [vue()], 81 | }) 82 | -------------------------------------------------------------------------------- /speedo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/palerdot/vue-speedometer/503f588ddd4d156ab9234fef92cf87fa7825dcca/speedo.gif -------------------------------------------------------------------------------- /src/__tests__/custom-segment-labels.js: -------------------------------------------------------------------------------- 1 | // NOTE: use 'test.only' to prevent jsdom svg breaking 2 | 3 | import { mount } from "@vue/test-utils" 4 | import VueSpeedometer from "../index" 5 | 6 | const div = document.createElement("div") 7 | div.id = "root" 8 | document.body.appendChild(div) 9 | 10 | const _mount = (options) => 11 | mount(VueSpeedometer, { 12 | attachTo: div, 13 | ...options, 14 | }) 15 | 16 | describe("custom segment labels entering", () => { 17 | // check the custom value text 18 | test.only("display custom segment labels", async () => { 19 | const currentValueText = "Happiness Level" 20 | 21 | const customSegmentLabels = [ 22 | { 23 | text: "Very Bad", 24 | position: "INSIDE", 25 | color: "#555", 26 | }, 27 | { 28 | text: "Bad", 29 | position: "INSIDE", 30 | color: "#555", 31 | }, 32 | { 33 | text: "Ok", 34 | position: "INSIDE", 35 | color: "#555", 36 | fontSize: "19px", 37 | }, 38 | { 39 | text: "Good", 40 | position: "INSIDE", 41 | color: "#555", 42 | }, 43 | { 44 | text: "Very Good", 45 | position: "INSIDE", 46 | color: "#555", 47 | }, 48 | ] 49 | 50 | const full_dom_wrapper = _mount({ 51 | propsData: { 52 | value: 777, 53 | customSegmentLabels, 54 | currentValueText, 55 | }, 56 | }) 57 | 58 | await full_dom_wrapper.vm.$nextTick() 59 | await new Promise((resolve) => setTimeout(resolve, 0)) 60 | 61 | customSegmentLabels.forEach((label, index) => { 62 | const textNode = full_dom_wrapper.findAll("text.segment-value").at(index) 63 | 64 | expect(textNode.text()).toEqual(label.text) 65 | 66 | const raw_styles = textNode.attributes().style.split(";") 67 | let styles = {} 68 | // construct the styles 69 | raw_styles.forEach((style) => { 70 | if (style === "") { 71 | return 72 | } 73 | const [key, value] = style.split(":") 74 | styles[key.trim()] = value.trim() 75 | }) 76 | 77 | expect(styles["fill"]).toEqual(label.color) 78 | 79 | if (label.fontSize) { 80 | expect(styles["font-size"]).toEqual(label.fontSize) 81 | } 82 | 83 | // destroy wrapper as per docs 84 | full_dom_wrapper.destroy() 85 | }) 86 | }) 87 | }) 88 | -------------------------------------------------------------------------------- /src/__tests__/custom-text.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NOTE: 3 | * IMPORTANT: 4 | * 5 | * This test limitation is fixed with e2e cypress tests (inside tests/) 6 | * 7 | * 8 | * IMPORTANT: Test limitation 9 | * @vue/test-utils uses jsdom which has a limitation of mimicking svg functionality 10 | * 11 | * ref: https://github.com/jsdom/jsdom/issues/2531 12 | * 13 | * We cannot test updating of svg in the dom and verify if the change is reflected in the vue component 14 | * Till the JSDOM issue is fixed or if @vue/test-utils uses a better adapter like 'Enzyme (React)' 15 | * we have this limitation of testing the actual update of svg elements for vue 16 | */ 17 | 18 | import { mount } from "@vue/test-utils" 19 | import VueSpeedometer from "../index" 20 | 21 | const div = document.createElement("div") 22 | div.id = "root" 23 | document.body.appendChild(div) 24 | 25 | const _mount = (options) => 26 | mount(VueSpeedometer, { 27 | attachTo: div, 28 | ...options, 29 | }) 30 | 31 | describe("custom current text value", () => { 32 | // check the custom value text 33 | test.only("should display custom current text value", async () => { 34 | // checking the default value 35 | const full_dom_wrapper = _mount({ 36 | propsData: { 37 | value: 333, 38 | currentValueText: "Porumai: ${value}", 39 | }, 40 | }) 41 | expect(full_dom_wrapper.find("text.current-value").text()).toBe( 42 | "Porumai: 333" 43 | ) 44 | // change props to another text 45 | full_dom_wrapper.setProps({ 46 | value: 555, 47 | currentValueText: "Current Value: ${value}", 48 | }) 49 | 50 | /* 51 | * Please note the vue svg test limitation added at the start 52 | */ 53 | expect(full_dom_wrapper.vm.$props.value).toEqual(555) 54 | 55 | full_dom_wrapper.destroy() 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/__tests__/force-render.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NOTE: 3 | * IMPORTANT: 4 | * 5 | * This test limitation is fixed with e2e cypress tests (inside tests/) 6 | * 7 | * IMPORTANT: Test limitation 8 | * @vue/test-utils uses jsdom which has a limitation of mimicking svg functionality 9 | * 10 | * ref: https://github.com/jsdom/jsdom/issues/2531 11 | * 12 | * We cannot test updating of svg in the dom and verify if the change is reflected in the vue component 13 | * Till the JSDOM issue is fixed or if @vue/test-utils uses a better adapter like 'Enzyme (React)' 14 | * we have this limitation of testing the actual update of svg elements for vue 15 | */ 16 | 17 | import { mount } from "@vue/test-utils" 18 | import VueSpeedometer from "../index" 19 | 20 | const div = document.createElement("div") 21 | div.id = "root" 22 | document.body.appendChild(div) 23 | 24 | const _mount = (options) => 25 | mount(VueSpeedometer, { 26 | attachTo: div, 27 | ...options, 28 | }) 29 | 30 | describe("forceRender testing", () => { 31 | // mount with default 'propsData' which you want to watch on 'props' change 32 | const wrapper = _mount({ 33 | propsData: { 34 | segments: 5, 35 | }, 36 | }) 37 | 38 | test.only("'forceRender' => true => false", async () => { 39 | expect(wrapper.findAll("path.speedo-segment").length).toBe(5) 40 | // change the props and give 'rerender' true 41 | wrapper.setProps({ 42 | segments: 10, 43 | // set force render to true so that we should get 10 segments 44 | forceRender: true, 45 | }) 46 | 47 | /* // should update segments to 10 48 | await new Promise((resolve) => setTimeout(resolve, 0)) 49 | await wrapper.vm.$nextTick() 50 | 51 | expect(wrapper.findAll("path.speedo-segment").length).toBe(10) 52 | 53 | // setting to force render as false 54 | // change the props and give 'rerender' true 55 | wrapper.setProps({ 56 | segments: 15, 57 | // set force render to true so that we should get 10 segments 58 | forceRender: false, 59 | }) */ 60 | 61 | /* 62 | * Please note the vue svg test limitation added at the start 63 | */ 64 | expect(wrapper.vm.$props.segments).toEqual(10) 65 | 66 | /* await wrapper.vm.$nextTick() 67 | await new Promise((resolve) => setTimeout(resolve, 0)) 68 | 69 | expect(wrapper.findAll("path.speedo-segment").length).toBe(10) */ 70 | 71 | wrapper.destroy() 72 | }) 73 | }) 74 | -------------------------------------------------------------------------------- /src/__tests__/format-value.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NOTE: 3 | * IMPORTANT: 4 | * 5 | * This test limitation is fixed with e2e cypress tests (inside tests/)* 6 | * 7 | * IMPORTANT: Test limitation 8 | * @vue/test-utils uses jsdom which has a limitation of mimicking svg functionality 9 | * 10 | * ref: https://github.com/jsdom/jsdom/issues/2531 11 | * 12 | * We cannot test updating of svg in the dom and verify if the change is reflected in the vue component 13 | * Till the JSDOM issue is fixed or if @vue/test-utils uses a better adapter like 'Enzyme (React)' 14 | * we have this limitation of testing the actual update of svg elements for vue 15 | */ 16 | 17 | import { mount } from "@vue/test-utils" 18 | import VueSpeedometer from "../index" 19 | 20 | const div = document.createElement("div") 21 | div.id = "root" 22 | document.body.appendChild(div) 23 | 24 | const _mount = (options) => 25 | mount(VueSpeedometer, { 26 | attachTo: div, 27 | ...options, 28 | }) 29 | 30 | window.SVGPathElement = () => {} 31 | 32 | describe("value format", () => { 33 | // check the format of the values 34 | test.only("display the format of the values correctly", async () => { 35 | // checking the default value 36 | const full_dom_wrapper = _mount({ 37 | propsData: { 38 | value: 0, 39 | valueFormat: "d", 40 | }, 41 | }) 42 | expect(full_dom_wrapper.find("text.current-value").text()).toBe("0") 43 | // setting label format to "d" and verifying the resulting value 44 | let passed_value = 477.7, 45 | transformed_value = "478" 46 | // change the props 47 | full_dom_wrapper.setProps({ 48 | value: passed_value, 49 | }) 50 | 51 | /* 52 | * Please note the vue svg test limitation added at the start 53 | */ 54 | expect(full_dom_wrapper.vm.$props.value).toEqual(passed_value) 55 | 56 | full_dom_wrapper.destroy() 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import { shallowMount, mount } from "@vue/test-utils" 2 | import VueSpeedometer from "../index" 3 | import debounce from "lodash-es/debounce" 4 | 5 | // import validators 6 | import { 7 | calculateNeedleHeight, 8 | calculateScale, 9 | calculateTicks, 10 | calculateSegmentLabelCount, 11 | } from "../core/util" 12 | 13 | // helper function to mount with default options to attach to dom 14 | export const _mount = (options) => { 15 | const div = document.createElement("div") 16 | div.id = "root" 17 | document.body.appendChild(div) 18 | 19 | return mount(VueSpeedometer, { 20 | attachTo: div, 21 | ...options, 22 | }) 23 | } 24 | 25 | export const debouncedWait = async (wrapper, fn) => { 26 | await new Promise((resolve) => setTimeout(resolve, 0)) 27 | 28 | await wrapper.vm.$nextTick(fn) 29 | } 30 | 31 | describe("", () => { 32 | // test if it has the parent div component for the "svg" 33 | test("should render one parent div component", () => { 34 | const wrapper = _mount() 35 | expect(wrapper.find("div")).toBeTruthy() 36 | 37 | wrapper.destroy() 38 | }) 39 | 40 | // test if we have the 'svg.speedometer' 41 | test("svg.speedometer is present", () => { 42 | const wrapper = _mount() 43 | expect(wrapper.find("svg.speedometer").exists()).toBe(true) 44 | 45 | wrapper.destroy() 46 | }) 47 | 48 | // check if the default segments is 5 by counting 'speedo-segment' 49 | test("by default we should have 5 segments", () => { 50 | const wrapper = _mount() 51 | expect(wrapper.findAll("path.speedo-segment").length).toBe(5) 52 | 53 | wrapper.destroy() 54 | }) 55 | 56 | // check the text color of the current value is the default (#666) 57 | test("should have the default text color for current value", () => { 58 | const wrapper = _mount() 59 | expect(wrapper.find("text.current-value").element.style.fill).toBe("#666") 60 | 61 | wrapper.destroy() 62 | }) 63 | 64 | // should take the color given by us in 'textColor' 65 | test("should have the text color given by us => steelblue ", () => { 66 | const wrapper = _mount({ 67 | propsData: { 68 | textColor: "steelblue", 69 | }, 70 | }) 71 | expect(wrapper.find("text.current-value").element.style.fill).toBe( 72 | "steelblue" 73 | ) 74 | 75 | wrapper.destroy() 76 | }) 77 | 78 | // it should not break on invalid needle transition 79 | test("should not break on invalid needle transition", () => { 80 | const wrapper = _mount({ 81 | propsData: { 82 | needleTransition: "porumai-transition", 83 | }, 84 | }) 85 | expect(wrapper.findAll("path.speedo-segment").length).toBe(5) 86 | 87 | wrapper.destroy() 88 | }) 89 | 90 | test("should throw error on invalid needle height", () => { 91 | expect(() => 92 | calculateNeedleHeight({ heightRatio: 1.1, radius: 2 }) 93 | ).toThrowError() 94 | // this one should not throw and should return some value 95 | expect(() => 96 | calculateNeedleHeight({ heightRatio: 0.9, radius: 2 }) 97 | ).not.toThrowError() 98 | expect(typeof calculateNeedleHeight({ heightRatio: 0.9, radius: 2 })).toBe( 99 | "number" 100 | ) 101 | }) 102 | 103 | test("should correctly take current Value placeholder from passed props", () => { 104 | const current_value = 333 105 | const wrapper = _mount({ 106 | propsData: { 107 | value: current_value, 108 | currentValuePlaceholderStyle: "#{value}", 109 | currentValueText: "#{value}", 110 | }, 111 | }) 112 | expect(wrapper.find("text.current-value").text()).toEqual( 113 | current_value.toString() 114 | ) 115 | 116 | wrapper.destroy() 117 | }) 118 | 119 | test("scale and ticks works properly", () => { 120 | const min = 0 121 | const max = 1000 122 | const segments = 1000 123 | const max_segment_labels = 10 124 | 125 | const full_dom_wrapper = _mount({ 126 | propsData: { 127 | segments, 128 | maxSegmentLabels: max_segment_labels, 129 | }, 130 | }) 131 | 132 | const scale1 = calculateScale({ min, max, segments }) 133 | const ticks1 = calculateTicks(scale1, { min, max, segments }) 134 | 135 | const scale2 = calculateScale({ min, max, segments: max_segment_labels }) 136 | const ticks2 = calculateTicks(scale2, { 137 | min, 138 | max, 139 | segments: max_segment_labels, 140 | }) 141 | 142 | const scale3 = calculateScale({ min, max, segments: 1 }) 143 | const ticks3 = calculateTicks(scale3, { min, max, segments: 1 }) 144 | 145 | expect(ticks2.length).toBeLessThan(ticks1.length) 146 | expect(ticks3.length).toBe(2) 147 | 148 | expect(full_dom_wrapper.findAll("text.segment-value").length).toBe( 149 | ticks2.length 150 | ) 151 | 152 | full_dom_wrapper.destroy() 153 | }) 154 | 155 | test("'maxSegmentLabels' config with no labels ", () => { 156 | const min = 0 157 | const max = 1000 158 | let segments = 1000 159 | let max_segment_labels = 0 160 | let label_count = calculateSegmentLabelCount({ 161 | maxSegmentLabelCount: max_segment_labels, 162 | segmentCount: segments, 163 | }) 164 | 165 | const full_dom_wrapper = _mount({ 166 | propsData: { 167 | segments, 168 | maxSegmentLabels: max_segment_labels, 169 | }, 170 | }) 171 | 172 | const scale1 = calculateScale({ min, max, segments }) 173 | const ticks1 = calculateTicks(scale1, { min, max, segments: label_count }) 174 | expect(full_dom_wrapper.findAll("text.segment-value").length).toBe( 175 | max_segment_labels 176 | ) 177 | 178 | full_dom_wrapper.destroy() 179 | }) 180 | 181 | test("custom segment colors", () => { 182 | const segmentColors = ["red", "blue", "green"] 183 | const full_dom_wrapper = _mount({ 184 | propsData: { 185 | segments: 3, 186 | segmentColors, 187 | }, 188 | }) 189 | 190 | segmentColors.forEach((color, index) => { 191 | expect( 192 | full_dom_wrapper 193 | .findAll("path.speedo-segment") 194 | .at(index) 195 | .attributes("fill") 196 | ).toEqual(color) 197 | }) 198 | 199 | full_dom_wrapper.destroy() 200 | }) 201 | }) 202 | -------------------------------------------------------------------------------- /src/__tests__/update-values.js: -------------------------------------------------------------------------------- 1 | /* 2 | * NOTE: 3 | * IMPORTANT: 4 | * 5 | * This test limitation is fixed with e2e cypress tests (inside tests/) 6 | * 7 | * IMPORTANT: Test limitation 8 | * @vue/test-utils uses jsdom which has a limitation of mimicking svg functionality 9 | * 10 | * ref: https://github.com/jsdom/jsdom/issues/2531 11 | * 12 | * We cannot test updating of svg in the dom and verify if the change is reflected in the vue component 13 | * Till the JSDOM issue is fixed or if @vue/test-utils uses a better adapter like 'Enzyme (React)' 14 | * we have this limitation of testing the actual update of svg elements for vue 15 | */ 16 | 17 | import { mount } from "@vue/test-utils" 18 | import VueSpeedometer from "../index" 19 | 20 | class SVGPathElement extends HTMLElement {} 21 | class SVGElement extends HTMLElement {} 22 | 23 | window.SVGPathElement = SVGPathElement 24 | window.SVGElement = SVGElement 25 | 26 | const _mount = (options) => { 27 | const div = document.createElement("div") 28 | div.id = "root" 29 | document.body.appendChild(div) 30 | 31 | return mount(VueSpeedometer, { 32 | attachTo: div, 33 | ...options, 34 | }) 35 | } 36 | 37 | describe("smooth update of values", () => { 38 | // should smoothly animate only the current value; not other breaking changes 39 | test.only("update values correctly", async () => { 40 | const value = 333 41 | const updatedValue = 470 42 | const full_dom_wrapper = _mount({ 43 | propsData: { 44 | value, 45 | }, 46 | }) 47 | expect(full_dom_wrapper.find("text.current-value").text()).toBe( 48 | value.toString() 49 | ) 50 | 51 | expect( 52 | full_dom_wrapper.findAll("path.speedo-segment").at(0).attributes("fill") 53 | ).toBe(`rgb(255, 71, 26)`) // rgb value of our default 'startColor' 54 | 55 | // set updated props 56 | full_dom_wrapper.setProps({ 57 | value: updatedValue, 58 | startColor: "red", 59 | }) 60 | 61 | /* 62 | * Please note the vue svg test limitation added at the start 63 | */ 64 | expect(full_dom_wrapper.vm.$props.value).toEqual(updatedValue) 65 | 66 | // confirm our start color is intact 67 | /* expect( 68 | full_dom_wrapper 69 | .findAll("path.speedo-segment") 70 | .at(0) 71 | .attributes("fill") 72 | ).toBe(`rgb(255, 71, 26)`) // rgb value of our default 'startColor' 73 | 74 | expect(full_dom_wrapper.find("text.current-value").text()).toBe( 75 | updatedValue.toString() 76 | ) */ 77 | 78 | full_dom_wrapper.destroy() 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /src/core: -------------------------------------------------------------------------------- 1 | ../lib/d3-speedometer/src/core -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "vue-speedometer" { 2 | import Vue from "vue" 3 | 4 | enum Transition { 5 | easeLinear = "easeLinear", 6 | easeQuadIn = "easeQuadIn", 7 | easeQuadOut = "easeQuadOut", 8 | easeQuadInOut = "easeQuadInOut", 9 | easeCubicIn = "easeCubicIn", 10 | easeCubicOut = "easeCubicOut", 11 | easeCubicInOut = "easeCubicInOut", 12 | easePolyIn = "easePolyIn", 13 | easePolyOut = "easePolyOut", 14 | easePolyInOut = "easePolyInOut", 15 | easeSinIn = "easeSinIn", 16 | easeSinOut = "easeSinOut", 17 | easeSinInOut = "easeSinInOut", 18 | easeExpIn = "easeExpIn", 19 | easeExpOut = "easeExpOut", 20 | easeExpInOut = "easeExpInOut", 21 | easeCircleIn = "easeCircleIn", 22 | easeCircleOut = "easeCircleOut", 23 | easeCircleInOut = "easeCircleInOut", 24 | easeBounceIn = "easeBounceIn", 25 | easeBounceOut = "easeBounceOut", 26 | easeBounceInOut = "easeBounceInOut", 27 | easeBackIn = "easeBackIn", 28 | easeBackOut = "easeBackOut", 29 | easeBackInOut = "easeBackInOut", 30 | easeElasticIn = "easeElasticIn", 31 | easeElasticOut = "easeElasticOut", 32 | easeElasticInOut = "easeElasticInOut", 33 | easeElastic = "easeElastic", 34 | } 35 | 36 | enum CustomSegmentLabelPosition { 37 | Outside = "OUTSIDE", 38 | Inside = "INSIDE", 39 | } 40 | 41 | type CustomSegmentLabel = { 42 | text?: string 43 | position?: CustomSegmentLabelPosition 44 | fontSize?: string 45 | color?: string 46 | } 47 | 48 | interface Props { 49 | value?: number 50 | 51 | minValue?: number 52 | maxValue?: number 53 | 54 | segments?: number 55 | maxSegmentLabels?: number 56 | 57 | forceRender?: boolean 58 | 59 | width?: number 60 | height?: number 61 | 62 | dimensionUnit?: string 63 | fluidWidth?: boolean 64 | 65 | needleColor?: string 66 | startColor?: string 67 | endColor?: string 68 | segmentColors?: string[] 69 | 70 | needleTransition?: Transition 71 | needleTransitionDuration?: number 72 | needleHeightRatio?: number 73 | 74 | ringWidth?: number 75 | textColor?: string 76 | 77 | valueFormat?: string 78 | currentValueText?: string 79 | currentValuePlaceholderStyle?: string 80 | 81 | customSegmentStops?: number[] 82 | 83 | customSegmentLabels?: CustomSegmentLabel[] 84 | 85 | labelFontSize?: string 86 | valueTextFontSize?: string 87 | valueTextFontWeight?: string 88 | 89 | paddingHorizontal?: number 90 | paddingVertical?: number 91 | 92 | svgAriaLabel?: string 93 | } 94 | 95 | class VueSpeedometer extends Vue {} 96 | 97 | // named exports of all the types 98 | export { Props, CustomSegmentLabel, CustomSegmentLabelPosition, Transition } 99 | 100 | export default VueSpeedometer 101 | } 102 | -------------------------------------------------------------------------------- /src/index.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 91 | 92 | 95 | -------------------------------------------------------------------------------- /src/props.js: -------------------------------------------------------------------------------- 1 | // export prop types for 'Vue' and use the core 'default' values to match with the format Vue expects 2 | import mapValues from "lodash-es/mapValues" 3 | import _keys from "lodash-es/keys" 4 | import _each from "lodash-es/each" 5 | 6 | import { DEFAULT_PROPS } from "./core/config" 7 | 8 | const prop_types = { 9 | value: { 10 | type: Number, 11 | required: true, 12 | }, 13 | minValue: { 14 | type: Number, 15 | required: true, 16 | }, 17 | maxValue: { 18 | type: Number, 19 | required: true, 20 | }, 21 | 22 | // tracks if the component should update as the whole or just animate the value 23 | // default will just animate the value after initialization/mounting 24 | forceRender: { 25 | type: Boolean, 26 | required: true, 27 | }, 28 | 29 | width: { 30 | type: Number, 31 | required: true, 32 | }, 33 | height: { 34 | type: Number, 35 | required: true, 36 | }, 37 | paddingHorizontal: { 38 | type: Number, 39 | required: true, 40 | }, 41 | paddingVertical: { 42 | type: Number, 43 | required: true, 44 | }, 45 | dimensionUnit: { 46 | type: String, 47 | required: true, 48 | }, 49 | fluidWidth: { 50 | type: Boolean, 51 | required: true, 52 | }, 53 | 54 | // segments to show in the speedometer 55 | segments: { 56 | type: Number, 57 | required: true, 58 | }, 59 | // maximum number of labels to be shown 60 | maxSegmentLabels: { 61 | type: Number, 62 | }, 63 | // custom segment points to create unequal segments 64 | customSegmentStops: { 65 | type: Array, 66 | }, 67 | 68 | // custom segment labels 69 | customSegmentLabels: { 70 | type: Array, 71 | }, 72 | 73 | // color strings 74 | needleColor: { 75 | type: String, 76 | required: true, 77 | }, 78 | startColor: { 79 | type: String, 80 | required: true, 81 | }, 82 | endColor: { 83 | type: String, 84 | required: true, 85 | }, 86 | // custom segment colors 87 | segmentColors: { 88 | type: Array, 89 | required: true, 90 | }, 91 | 92 | // needle transition type and duration 93 | needleTransition: { 94 | type: String, 95 | required: true, 96 | }, 97 | needleTransitionDuration: { 98 | type: Number, 99 | required: true, 100 | }, 101 | needleHeightRatio: { 102 | type: Number, 103 | required: true, 104 | }, 105 | 106 | ringWidth: { 107 | type: Number, 108 | required: true, 109 | }, 110 | textColor: { 111 | type: String, 112 | required: true, 113 | }, 114 | 115 | // d3 format identifier is generally a string; default "" (empty string) 116 | valueFormat: { 117 | type: String, 118 | required: true, 119 | }, 120 | // value text format 121 | currentValueText: { 122 | type: String, 123 | required: true, 124 | }, 125 | // placeholder style for current value 126 | currentValuePlaceholderStyle: { 127 | type: String, 128 | required: true, 129 | }, 130 | 131 | // font sizes 132 | labelFontSize: { 133 | type: String, 134 | required: true, 135 | }, 136 | valueTextFontSize: { 137 | type: String, 138 | required: true, 139 | }, 140 | valueTextFontWeight: { 141 | type: String, 142 | required: true, 143 | }, 144 | svgAriaLabel: { 145 | type: String, 146 | required: true, 147 | }, 148 | // ref: https://vuejs.org/guide/components/props.html 149 | segmentValueFormatter: { 150 | type: Function, 151 | required: false, 152 | }, 153 | } 154 | 155 | // update the props with default values 156 | const props = mapValues(prop_types, (value, key) => { 157 | const CUSTOM_DEFAULTS = { 158 | customSegmentStops: function () { 159 | return [] 160 | }, 161 | customSegmentLabels: function () { 162 | return [] 163 | }, 164 | segmentColors: function () { 165 | return [] 166 | }, 167 | svgAriaLabel: function () { 168 | return "Vue Speedometer" 169 | }, 170 | } 171 | 172 | return { 173 | ...value, 174 | default: CUSTOM_DEFAULTS[key] || DEFAULT_PROPS[key], 175 | // so since we are giving default let us turn of 'required' (In React it will be taken care by default Props) 176 | required: false, 177 | } 178 | }) 179 | 180 | export default props 181 | -------------------------------------------------------------------------------- /src/stories/Speedometer.stories.js: -------------------------------------------------------------------------------- 1 | // IMPORTANT: checkout `.storybook/preview.js` to switch between DEV and DIST/PROD builds 2 | 3 | export default { title: "vue-speedometer" } 4 | 5 | const textColor = "#AAA" 6 | 7 | export const DefaultWithNoConfig = () => ({ 8 | template: ``, 9 | }) 10 | 11 | export const ConfiguringValues = () => ({ 12 | template: ``, 13 | }) 14 | 15 | export const CustomSegmentLabels = () => ({ 16 | template: ` 17 |
18 | 57 | 87 |
88 | `, 89 | }) 90 | 91 | export const CustomSegmentColors = () => ({ 92 | template: ` 93 |
94 | 102 | 110 |
111 | `, 112 | }) 113 | 114 | export const CustomSegmentStops = () => ({ 115 | template: ` 116 |
117 | 126 | 136 | 147 |
148 | `, 149 | }) 150 | 151 | export const FluidWidthView = () => ({ 152 | data() { 153 | return { 154 | styles: { 155 | width: "500px", 156 | height: "300px", 157 | background: "#2a2744", 158 | }, 159 | } 160 | }, 161 | template: ` 162 |
163 | 171 |
172 | Fluid width takes the width of the parent div (500px in this case) 173 |
174 |
175 | `, 176 | }) 177 | 178 | export const NeedleTransitionDuration = () => ({ 179 | template: ` 180 |
181 | 188 | 195 |
196 | `, 197 | }) 198 | 199 | export const ForceRenderTheComponent = () => ({ 200 | data() { 201 | return { 202 | buttonStyles: { 203 | padding: "7px", 204 | border: "thin solid steelblue", 205 | background: "white", 206 | cursor: "pointer", 207 | marginBottom: "17px", 208 | }, 209 | value1: { 210 | value: 111, 211 | startColor: "blue", 212 | segments: 5, 213 | width: 300, 214 | height: 300, 215 | currentValueText: "${value}", 216 | }, 217 | value2: { 218 | value: 222, 219 | startColor: "orange", 220 | segments: 10, 221 | width: 400, 222 | height: 400, 223 | currentValueText: "Current Value: ${value}", 224 | }, 225 | toggleStatus: false, 226 | } 227 | }, 228 | methods: { 229 | onClick: function () { 230 | this.toggleStatus = !this.toggleStatus 231 | }, 232 | }, 233 | template: ` 234 |
235 |
236 | By default, on props change only the current value and needle transition is updated. 237 | Force render completly re-renders the whole component on update. 238 | This is helpful for features like dynmaic width/height on resize 239 |
240 |
241 | 244 | 245 | 255 |
256 | `, 257 | }) 258 | 259 | export const ConfiguringTheFormatForValuesDisplayed = () => ({ 260 | template: ` 261 |
262 | 270 | 277 |
278 | `, 279 | }) 280 | 281 | function segmentValueFormatter(value) { 282 | if (value < 200) { 283 | return `${value} 😒` 284 | } 285 | if (value < 400) { 286 | return `${value} 😐` 287 | } 288 | if (value < 600) { 289 | return `${value} 😌` 290 | } 291 | if (value < 800) { 292 | return `${value} 😊` 293 | } 294 | if (value < 900) { 295 | return `${value} 😉` 296 | } 297 | 298 | return `${value} 😇` 299 | } 300 | 301 | // ref: https://github.com/vuejs/vue/issues/2436 302 | export const CustomSegmentValueFormatter = () => ({ 303 | methods: { 304 | segmentValueFormatter, 305 | }, 306 | 307 | template: ` 308 |
309 | 315 |
316 | `, 317 | }) 318 | 319 | export const CustomCurrentValueText = () => ({ 320 | template: ` 321 |
322 | 330 |
331 | `, 332 | }) 333 | 334 | export const CustomCurrentValuePlaceholderStyleForEgValue = () => ({ 335 | template: ` 336 |
337 | 346 |
347 | `, 348 | }) 349 | 350 | export const ConfigureNeedleLengthAndFontSizes = () => ({ 351 | template: ` 352 |
353 | 360 |
361 | `, 362 | }) 363 | 364 | export const GradientEffectWithLargeNumberOfSegmentsAndMaxSegmentLabelsConfig = 365 | () => ({ 366 | template: ` 367 |
368 | 375 |
376 | `, 377 | }) 378 | 379 | export const NoSegmentLabels = () => ({ 380 | template: ` 381 |
382 | 387 | 396 |
397 | `, 398 | }) 399 | 400 | export const NormalUpdationOfValues = () => ({ 401 | created() { 402 | window.setInterval(() => { 403 | this.toggle = !this.toggle 404 | }, 3000) 405 | }, 406 | data() { 407 | return { 408 | toggle: false, 409 | } 410 | }, 411 | template: ` 412 |
413 | 421 | 430 |
431 | `, 432 | }) 433 | 434 | export const CustomizeFontSizesAndSpacing = () => ({ 435 | template: ` 436 |
437 | 448 |
449 | `, 450 | }) 451 | -------------------------------------------------------------------------------- /tests/E2E.md: -------------------------------------------------------------------------------- 1 | ### E2E (cypress) 2 | 3 | This is a document with pointers how the `e2e cypress` tests is setup with code coverage with istanbul/nyc. 4 | 5 | Uses `cypress-vue-unit-test`, a.k.a `@cypress/vue` - https://www.npmjs.com/package/cypress-vue-unit-test 6 | Uses `@cypress/code-coverage/`. 7 | 8 | All `e2e` tests are within `tests` folder. 9 | 10 | There were some confusions ons correct package name to include in the config 11 | files. This setup was done right at the time when the package is in the process of moving to `@cypress/vue` (it is not completely 12 | moved at the time of this writing). As a result of this confusion, the setup was sourcing empty files from `@cypress/vue` (which did 13 | not have files published in this namespace) 14 | 15 | 16 | It is important to note that `@cypress/vue` works only with webpack setup. So we had to configure/provision `@cypress/webpack-preprocessor`, 17 | just to make the `e2e` tests run for vue files. More on this setup in next section. 18 | 19 | Following files are configured to use `cypress-vue-unit-test`, a.k.a `@cypress/vue`. 20 | 21 | - Support file: `tests/support/index.js` 22 | - Config file: `tests/plugins/index.js` (main config file) 23 | 24 | #### Configuration 25 | 26 | All the configuration related logic is inside `tests/plugins/index.js`. 27 | Custom webpack configuration which uses `babel-plugin-istanbul` for code coverage is inside `/tests/plugins/cypress-webpack-config.js` 28 | 29 | Roughly this setup works in the following way 30 | ```javascript 31 | @cypress/vue 32 | | 33 | | 34 | | 35 | reads `tests/support/index.js` and `tests/plugins/index.js` 36 | | 37 | | 38 | | 39 | reads webpack config from `tests/plugins/cypress-webpack-config` with `babel-plugin-istanbul` 40 | added in `plugins: [istanbul]` 41 | | 42 | | 43 | | 44 | Generates code coverage and runs tests 45 | ``` 46 | 47 | ### Configuring `nyc` 48 | 49 | `nyc` is the command line tool for `istanbul`. It is currently configured in `nyc.config.js`. 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /tests/components/ValueUpdate.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "cypress/vue" 2 | // import VueSpeedometer from "../../src/index.vue" 3 | // NOTE: we are manually instrumenting using NYC and using it for running cypress tests 4 | // import VueSpeedometer from "../../instrumented/index.vue" 5 | import ValueUpdate from "./ValueUpdate.vue" 6 | 7 | describe("VueSpeedometer", () => { 8 | it("Updates component correctly", () => { 9 | mount(ValueUpdate, { 10 | props: {}, 11 | }) 12 | // now we can use any Cypress command to interact with the component 13 | // https://on.cypress.io/api 14 | cy.get(".current-value").contains("333") 15 | 16 | // click the button 17 | cy.get("button#reset-value").click() 18 | 19 | // now we should have the updated value 20 | cy.get(".current-value").contains("0") 21 | 22 | // we will update the value again 23 | cy.get("button#update-value").click() 24 | 25 | // we should get the updated value again 26 | cy.get(".current-value").contains("333") 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /tests/components/ValueUpdate.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /tests/components/VueSpeedometer.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "cypress/vue" 2 | // import VueSpeedometer from "../../src/index.vue" 3 | import VueSpeedometer from "../../instrumented/index.vue" 4 | 5 | describe("VueSpeedometer", () => { 6 | it("renders the component with correct value", () => { 7 | mount(VueSpeedometer, { 8 | propsData: { 9 | value: 333, 10 | }, 11 | }) 12 | 13 | // now we can use any Cypress command to interact with the component 14 | // https://on.cypress.io/api 15 | cy.contains("333") 16 | }) 17 | 18 | it("displays default font weight correctly", () => { 19 | mount(VueSpeedometer, { 20 | propsData: { 21 | value: 333, 22 | }, 23 | }) 24 | 25 | // now we can use any Cypress command to interact with the component 26 | // https://on.cypress.io/api 27 | cy.get("svg.speedometer") 28 | .find(".current-value") 29 | // bold => 700 30 | .should("have.css", "font-weight", "700") 31 | }) 32 | 33 | it("displays 'valueTextFontWeight' correctly", () => { 34 | mount(VueSpeedometer, { 35 | propsData: { 36 | valueTextFontWeight: "500", 37 | }, 38 | }) 39 | 40 | // now we can use any Cypress command to interact with the component 41 | // https://on.cypress.io/api 42 | cy.get("svg.speedometer") 43 | .find(".current-value") 44 | // bold => 700 45 | .should("have.css", "font-weight", "500") 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /tests/components/force-render/ForceRender.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "cypress/vue" 2 | // import VueSpeedometer from "../../../src/index.vue" 3 | // import VueSpeedometer from "../../../instrumented/index.vue" 4 | import ForceRender from "./ForceRender.vue" 5 | 6 | describe("VueSpeedometer", () => { 7 | it("updates component normally", () => { 8 | mount(ForceRender, { 9 | props: {}, 10 | }) 11 | 12 | // now we can use any Cypress command to interact with the component 13 | // https://on.cypress.io/api 14 | cy.contains("333") 15 | cy.get(".speedo-segment").should("have.length", 5) 16 | 17 | // click the button 18 | cy.get("button#normal-update").click() 19 | 20 | // now we should have the updated value 21 | cy.contains("777") 22 | 23 | // we did not force rendered; our segments should be the same(5) 24 | cy.get(".speedo-segment").should("have.length", 5) 25 | }) 26 | 27 | it("force renders the component with correct value", () => { 28 | mount(ForceRender, { 29 | props: {}, 30 | }) 31 | 32 | // now we can use any Cypress command to interact with the component 33 | // https://on.cypress.io/api 34 | cy.contains("333") 35 | cy.get(".speedo-segment").should("have.length", 5) 36 | 37 | // click the button 38 | cy.get("button#force-render").click() 39 | 40 | // now we should have the updated value 41 | cy.contains("417") 42 | 43 | // we force rendered; our segments should be 10 (from 5) 44 | cy.get(".speedo-segment").should("have.length", 10) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /tests/components/force-render/ForceRender.vue: -------------------------------------------------------------------------------- 1 | 20 | 36 | -------------------------------------------------------------------------------- /tests/plugins/cypress-webpack-config.js: -------------------------------------------------------------------------------- 1 | // ref: https://vue-loader.vuejs.org/guide/#manual-setup 2 | const VueLoaderPlugin = require("vue-loader/lib/plugin") 3 | 4 | module.exports = { 5 | module: { 6 | rules: [ 7 | { 8 | test: /\.m?js$/, 9 | exclude: /(node_modules|bower_components)/, 10 | use: { 11 | loader: "babel-loader", 12 | options: { 13 | presets: ["@babel/preset-env"], 14 | plugins: ["istanbul"], 15 | }, 16 | }, 17 | }, 18 | { 19 | test: /\.vue$/, 20 | loader: "vue-loader", 21 | }, 22 | ], 23 | }, 24 | plugins: [ 25 | // make sure to include the plugin! 26 | new VueLoaderPlugin(), 27 | ], 28 | } 29 | -------------------------------------------------------------------------------- /tests/plugins/index.js: -------------------------------------------------------------------------------- 1 | // const preprocessor = require('@cypress/vue/dist/plugins/webpack'); 2 | const webpackPreprocessor = require("@cypress/webpack-preprocessor") 3 | const coverage = require("@cypress/code-coverage/task") 4 | 5 | module.exports = (on, config) => { 6 | coverage(on, config) 7 | 8 | // webpackPreprocessor(on, config) 9 | const webpackOptions = { 10 | // send in the options from your webpack.config.js, so it works the same 11 | // as your app's code 12 | webpackOptions: require("./cypress-webpack-config"), 13 | watchOptions: {}, 14 | } 15 | on("file:preprocessor", webpackPreprocessor(webpackOptions)) 16 | 17 | // IMPORTANT return the config object 18 | return config 19 | } 20 | -------------------------------------------------------------------------------- /tests/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /tests/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Component Testing resets 17 | // NOTE: enable this after migration to '@cypress/vue' is complete 18 | // require('@cypress/vue/dist/support') 19 | require("cypress-vue-unit-test/dist/support") 20 | require("@cypress/code-coverage/support") 21 | 22 | // Import commands.js using ES2015 syntax: 23 | // import './commands' 24 | 25 | // Alternatively you can use CommonJS syntax: 26 | require("./commands") 27 | 28 | // Import any global stylesheets here 29 | // require('../../src/styles/index.scss') // 💅 30 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { defineConfig } from "vite" 3 | import vue from "@vitejs/plugin-vue" 4 | import terser from "@rollup/plugin-terser" 5 | import { nodeResolve } from "@rollup/plugin-node-resolve" 6 | // import analyze from "rollup-plugin-analyzer" 7 | 8 | const devMode = process.env.NODE_ENV === "development" 9 | 10 | // ref: https://blog.openreplay.com/the-ultimate-guide-to-getting-started-with-the-rollup-js-javascript-bundler 11 | function terserConfig() { 12 | return terser({ 13 | ecma: 2020, 14 | 15 | mangle: { toplevel: true }, 16 | 17 | compress: { 18 | module: true, 19 | toplevel: true, 20 | unsafe_arrows: true, 21 | drop_console: !devMode, 22 | drop_debugger: !devMode, 23 | }, 24 | 25 | output: { quote_style: 1 }, 26 | }) 27 | } 28 | 29 | // https://vitejs.dev/config/ 30 | export default defineConfig({ 31 | build: { 32 | lib: { 33 | name: "VueSpeedometer", 34 | entry: path.resolve(__dirname, "src/index.vue"), 35 | fileName: (format) => `vue-speedometer.${format}.js`, 36 | }, 37 | rollupOptions: { 38 | external: ["vue"], 39 | output: { 40 | globals: { 41 | vue: "Vue", 42 | }, 43 | sourcemap: devMode ? "inline" : false, 44 | plugins: [terserConfig()], 45 | }, 46 | // IMPORTANT: This plugins is different from output plugins 47 | plugins: [ 48 | nodeResolve(), 49 | // analyze({ 50 | // summaryOnly: true, 51 | // filterSummary: true, 52 | // }), 53 | ], 54 | }, 55 | }, 56 | resolve: { 57 | alias: { 58 | // ref: https://github.com/vitejs/vite/discussions/4158 59 | // needed to enable template compiling for cypress 60 | // vue: "vue/dist/vue.esm-bundler.js", 61 | }, 62 | }, 63 | plugins: [vue()], 64 | }) 65 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite" 2 | import Vue from "@vitejs/plugin-vue" 3 | 4 | export default defineConfig({ 5 | plugins: [Vue()], 6 | test: { 7 | globals: true, 8 | environment: "happy-dom", 9 | // spec.js belongs to cypress 10 | include: ["src/**/*.test.js"], 11 | }, 12 | }) 13 | --------------------------------------------------------------------------------