├── .github └── workflows │ ├── deploy.yml │ ├── lint.yml │ └── node.js.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── LICENSE ├── README.md ├── eslint.config.js ├── jsconfig.json ├── package-lock.json ├── package.json ├── src ├── app.d.ts ├── app.html ├── lib │ ├── LinkedChart.svelte │ ├── LinkedChart.test.js │ ├── LinkedLabel.svelte │ ├── LinkedLabel.test.js │ ├── LinkedValue.svelte │ ├── LinkedValue.test.js │ ├── index.js │ └── stores │ │ └── tinyLinkedCharts.js ├── routes │ ├── +layout.js │ └── +page.svelte └── test.setup.js ├── static ├── .nojekyll └── favicon.png ├── svelte.config.js └── vite.config.js /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Build and Deploy to Pages 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | 7 | workflow_dispatch: 8 | 9 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 10 | permissions: 11 | contents: read 12 | pages: write 13 | id-token: write 14 | 15 | # Allow one concurrent deployment 16 | concurrency: 17 | group: "pages" 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - uses: actions/configure-pages@v1 27 | id: pages 28 | - uses: actions/setup-node@v3 29 | with: 30 | node-version: 20 31 | cache: 'npm' 32 | - run: npm ci --legacy-peer-deps 33 | - run: npm run build 34 | - name: Upload artifact 35 | uses: actions/upload-pages-artifact@v1 36 | with: 37 | path: ./build 38 | 39 | deploy: 40 | runs-on: ubuntu-latest 41 | needs: build 42 | environment: 43 | name: github-pages 44 | url: ${{ steps.deployment.outputs.page_url }} 45 | steps: 46 | - uses: actions/deploy-pages@v1 47 | id: deployment 48 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: push 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Install modules 13 | run: npm install 14 | 15 | - name: Run ESLint 16 | run: npx eslint 17 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: push 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - uses: actions/checkout@v2 11 | 12 | - name: Install modules 13 | run: npm install 14 | 15 | - name: Run tests 16 | run: npm run test 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /dist 5 | /.svelte-kit 6 | /package 7 | .env 8 | .env.* 9 | !.env.example 10 | vite.config.js.timestamp-* 11 | vite.config.ts.timestamp-* 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | 10 | # Ignore files for PNPM, NPM and YARN 11 | pnpm-lock.yaml 12 | package-lock.json 13 | yarn.lock 14 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "printWidth": 100, 6 | "plugins": ["prettier-plugin-svelte"], 7 | "pluginSearchDirs": ["."], 8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] 9 | } 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tiny Linked Charts for Svelte 2 | 3 | [![tests passing](https://github.com/MitchelJager/svelte-tiny-linked-charts/actions/workflows/node.js.yml/badge.svg)](https://github.com/Mitcheljager/svelte-tiny-linked-charts/actions/workflows/node.js.yml) 4 | [![npm version](https://badgen.net/npm/v/svelte-tiny-linked-charts)](https://www.npmjs.com/package/svelte-tiny-linked-charts) 5 | [![npm downloads](https://badgen.net/npm/dt/svelte-tiny-linked-charts)](https://www.npmjs.com/package/svelte-tiny-linked-charts) 6 | [![MadeWithSvelte.com shield](https://madewithsvelte.com/storage/repo-shields/3278-shield.svg)](https://madewithsvelte.com/p/tiny-linked-charts/shield-link) 7 | [![bundle size](https://img.shields.io/bundlephobia/minzip/svelte-tiny-linked-charts)](https://bundlephobia.com/package/svelte-tiny-linked-charts) 8 | 9 | This is a library to display tiny bar charts. These charts are more so meant for graphic aids, rather than scientific representations. There's no axis labels, no extensive data visualisation, just bars. 10 | 11 | **Demo and Docs**: https://mitcheljager.github.io/svelte-tiny-linked-charts/ 12 | 13 | ### Installation 14 | 15 | Install using Yarn or NPM. 16 | ```js 17 | yarn add svelte-tiny-linked-charts --dev 18 | ``` 19 | ```js 20 | npm install svelte-tiny-linked-charts --save-dev 21 | ``` 22 | 23 | If you are using Svelte 4, use version ^1.0.0. Version 2 is reserved for Svelte 5. 24 | 25 | Include the chart in your app. 26 | ```js 27 | import { LinkedChart, LinkedLabel, LinkedValue } from "svelte-tiny-linked-charts" 28 | ``` 29 | ```svelte 30 | 31 | 32 | 33 | ``` 34 | 35 | Supply your data in a simple key:value object: 36 | ```js 37 | let data = { 38 | "2005-01-01": 25, 39 | "2005-01-02": 20, 40 | "2005-01-03": 18, 41 | "2005-01-04": 17, 42 | "2005-01-05": 21 43 | } 44 | ``` 45 | ```svelte 46 | 47 | ``` 48 | 49 | Or if you prefer supply the labels and values separately: 50 | ```js 51 | let labels = [ 52 | "2005-01-01", 53 | "2005-01-02", 54 | "2005-01-03", 55 | "2005-01-04", 56 | "2005-01-05" 57 | ] 58 | let values = [ 59 | 25, 60 | 20, 61 | 18, 62 | 17, 63 | 21 64 | ] 65 | ``` 66 | ```svelte 67 | 68 | ``` 69 | 70 | ## Usage 71 | 72 | For detailed documentation on every property check out: [https://mitcheljager.github.io/svelte-tiny-linked-charts/](https://mitcheljager.github.io/svelte-tiny-linked-charts/) 73 | 74 | ### Configuration 75 | 76 | `` component. 77 | | Property | Default | Description | 78 | ---|---|--- 79 | data | {} | Data that will be displayed in the chart supplied in key:value object. 80 | labels | [] | Labels supplied separately, to be used together with "values" property. 81 | values | [] | Values supplied separately, to be used together with "labels" property. 82 | linked| | Key to link this chart to other charts with the same key. 83 | uid | | Unique ID to link this chart to a LinkedValue component with the same uid. 84 | height | 40 | Height of the chart in pixels. 85 | width | 150 | Width of the chart in pixels. 86 | barMinWidth | 4 | Width of the bars in the chart in pixels. 87 | barMinHeight | 0 | Minimum height of the bars in the chart in pixels. 88 | hideBarBelow | 0 | Bars below this value will be hidden, showing as 0 height. 89 | grow | false | Whether or not the bar should grow to fill out the full width of the chart. 90 | align | right | The side the bars should align to when they do not completely fill out the chart. 91 | gap | 1 | Gap between the bars in pixels. 92 | fill | #ff3e00 | Color of the bars, can be any valid CSS color. 93 | fillArray | [] | Array of colors for each individual bar. 94 | fadeOpacity | 0.5 | The opacity the faded out bars should display in. 95 | hover | true | Boolean whether or not this chart can be hovered at all. 96 | transition | 0 | Transition the chart between different stats. Value is time in milliseconds. 97 | showValue | false | Boolean whether or not a value will be shown. 98 | valueDefault | "\ " | Default value when not hovering. 99 | valueUndefined | 0 | For when the hovering value returns undefined. 100 | valuePrepend | | String to prepend the value. 101 | valueAppend | | String to append to the value. 102 | valuePosition | static | Can be set to "floating" to follow the position of the hover. 103 | scaleMax | 0 | Use this to overwrite the automatic scale set to the highest value in your array. 104 | scaleMax | 0 | Use this to overwrite the default value floor of 0. 105 | type | bar | Can be set to "line" to display a line chart instead. 106 | lineColor | fill | Color of the line if used with type="line". 107 | preserveAspectRatio | false | Sets whether or not the SVG will preserve it's aspect ratio. 108 | tabindex | -1 | Sets the tabindex of each bar. When a tabindex of 0 is given, each bar will contain a title that describes the bar's label and value. 109 | title | "" | Title that describes the chart for screen readers. 110 | description | "" | Description that describes the chart for screen readers. 111 | onclick | null | Function that executes on click and returns the key and index for the clicked data. 112 | onhover | null | Function that executes on hover of each bar. 113 | onblur | null | Function that executes when focus leaves the chart. 114 | 115 | `` component. 116 | Property | Default | Description 117 | --- | --- | --- 118 | linked | | Key to link this label to charts with the same key. 119 | empty | \  | String that will be displayed when no bar is being hovered. 120 | transform | (label) => label | Transform the given label to format it differently from how it was supplied. 121 | 122 | `` component. 123 | Property | Default | Description 124 | --- | --- | --- 125 | uid | | Unique ID to link this value to a chart with the same uid. 126 | empty | \  | String that will be displayed when no bar is being hovered. 127 | valueUndefined | 0 | For when the hovering value returns undefined. 128 | transform | (value) => value | Transform the given value to format it differently from how it was supplied. 129 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import prettier from "eslint-config-prettier" 2 | import js from "@eslint/js" 3 | import svelte from "eslint-plugin-svelte" 4 | import globals from "globals" 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | js.configs.recommended, 9 | ...svelte.configs["flat/recommended"], 10 | prettier, 11 | ...svelte.configs["flat/prettier"], 12 | { 13 | languageOptions: { 14 | globals: { 15 | ...globals.browser, 16 | ...globals.node 17 | } 18 | } 19 | }, 20 | { 21 | ignores: ["build/", ".svelte-kit/", "dist/"] 22 | }, 23 | { 24 | rules: { 25 | semi: ["error", "never"], 26 | quotes: ["error", "double"], 27 | "comma-dangle": ["error", "never"], 28 | "no-trailing-spaces": ["error"], 29 | "no-unused-vars": ["error", { 30 | "vars": "all", 31 | "args": "after-used", 32 | "argsIgnorePattern": "[\\w]", 33 | "caughtErrors": "all", 34 | "ignoreRestSiblings": false, 35 | "reportUsedIgnorePattern": false 36 | }] 37 | } 38 | } 39 | ] 40 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true, 12 | "moduleResolution": "bundler" 13 | } 14 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias 15 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files 16 | // 17 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes 18 | // from the referenced tsconfig.json - TypeScript does not merge them in 19 | } 20 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-tiny-linked-charts", 3 | "version": "2.2.0", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build && npm run package", 7 | "preview": "vite preview", 8 | "package": "svelte-kit sync && svelte-package && publint", 9 | "prepublishOnly": "npm run package", 10 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 11 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", 12 | "test": "vitest", 13 | "lint": "prettier --plugin-search-dir . --check . && eslint .", 14 | "format": "prettier --plugin-search-dir . --write .", 15 | "publish-pages": "npm run build && git subtree push --prefix build origin gh-pages" 16 | }, 17 | "exports": { 18 | ".": { 19 | "types": "./dist/index.d.ts", 20 | "svelte": "./dist/index.js" 21 | } 22 | }, 23 | "files": [ 24 | "dist", 25 | "!dist/**/*.test.*", 26 | "!dist/**/*.spec.*" 27 | ], 28 | "peerDependencies": { 29 | "svelte": ">=5.0.0" 30 | }, 31 | "devDependencies": { 32 | "@sveltejs/adapter-static": "^3.0.0", 33 | "@sveltejs/adapter-auto": "^3.0.0", 34 | "@sveltejs/kit": "^2.5.27", 35 | "@sveltejs/vite-plugin-svelte": "^4.0.0", 36 | "@sveltejs/package": "^2.3.7", 37 | "@testing-library/svelte": "^5.2.6", 38 | "eslint": "^9.18.0", 39 | "eslint-config-prettier": "^10.0.1", 40 | "eslint-plugin-svelte": "^2.46.1", 41 | "globals": "^15.14.0", 42 | "happy-dom": "^16.5.3", 43 | "prettier": "^3.1.0", 44 | "prettier-plugin-svelte": "^3.2.6", 45 | "publint": "^0.1.9", 46 | "svelte": "^5.0.0", 47 | "svelte-check": "^4.0.0", 48 | "tslib": "^2.4.1", 49 | "typescript": "^5.5.0", 50 | "vite": "^5.4.4", 51 | "vitest": "^2.1.8" 52 | }, 53 | "svelte": "./dist/index.js", 54 | "types": "./dist/index.d.ts", 55 | "main": "./dist/index.js", 56 | "type": "module", 57 | "description": "A library to display tiny bar charts using Svelte. These charts are more so meant for graphic aids, rather than scientific representations. There's no axis labels, no extensive data visualisation, just bars.", 58 | "keywords": [ 59 | "svelte", 60 | "tiny", 61 | "charts", 62 | "linked", 63 | "linked-charts", 64 | "tiny-linked-charts" 65 | ], 66 | "repository": { 67 | "type": "git", 68 | "url": "https://github.com/Mitcheljager/svelte-tiny-linked-charts" 69 | }, 70 | "homepage": "https://mitcheljager.github.io/svelte-tiny-linked-charts/" 71 | } 72 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | // interface Error {} 6 | // interface Locals {} 7 | // interface PageData {} 8 | // interface Platform {} 9 | } 10 | } 11 | 12 | export {}; 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Svelte Tiny Linked Charts 9 | 10 | %sveltekit.head% 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/lib/LinkedChart.svelte: -------------------------------------------------------------------------------- 1 | 219 | 220 | 221 | 230 | 231 | {#if title} 232 | {title} 233 | {/if} 234 | 235 | {#if description} 236 | {description} 237 | {/if} 238 | 239 | 240 | {#if type == "line"} 241 | 242 | {/if} 243 | 244 | {#each Object.entries(data) as [key, value], i} 245 | {#if type == "bar"} 246 | 254 | {:else if type == "line"} 255 | 260 | {/if} 261 | 262 | 263 | 264 | startHover(event, key, i)} 266 | onfocus={event => startHover(event, key, i)} 267 | ontouchstart={event => startHover(event, key, i)} 268 | onclick={() => onclick({ key, index: i })} 269 | onkeypress={() => onclick({ key, index: i })} 270 | width={barWidth} 271 | height={height} 272 | fill="transparent" 273 | x={(gap + barWidth) * i} 274 | {tabindex}> 275 | {#if tabindex !== -1} 276 | {key}: {value} 277 | {/if} 278 | 279 | {/each} 280 | 281 | 282 | 283 | {#if showValue && ($hoveringValue[uid] || valueDefault)} 284 |
285 | {#if $hoveringValue[uid] !== null} 286 | {valuePrepend} 287 | {$hoveringValue[uid] || valueUndefined} 288 | {valueAppend} 289 | {:else} 290 | 291 | {@html valueDefault} 292 | {/if} 293 |
294 | {/if} 295 | -------------------------------------------------------------------------------- /src/lib/LinkedChart.test.js: -------------------------------------------------------------------------------- 1 | import { fireEvent, render } from "@testing-library/svelte" 2 | import { describe, expect, it, vi } from "vitest" 3 | 4 | import LinkedChart from "$lib/LinkedChart.svelte" 5 | 6 | /** 7 | * @param {number} times 8 | * @param {number} [min] 9 | * @param {number} [max] 10 | */ 11 | function fakeData(times, min = 50, max = 100) { 12 | /** @type {Record} */ 13 | let data = {} 14 | 15 | for(let i = 0; i < times; i++) { 16 | data[i] = Math.floor(Math.random() * (max - min)) + min 17 | } 18 | 19 | return data 20 | } 21 | 22 | describe("LinkedChart.svelte", () => { 23 | it("Should render rect elements equal to length of data given", () => { 24 | const randomLength = Math.floor(Math.random() * 5) + 25 25 | const data = fakeData(randomLength) 26 | const { container } = render(LinkedChart, { data }) 27 | 28 | expect(container.querySelector("svg")).toBeTruthy() 29 | expect(container.querySelectorAll("rect[tabindex]").length).toBe(randomLength) 30 | }) 31 | 32 | it("Should still render if labels and values are given instead of data", () => { 33 | const data = fakeData(20) 34 | const { container } = render(LinkedChart, { labels: Object.keys(data), values: Object.values(data) }) 35 | 36 | expect(container.querySelector("svg")).toBeTruthy() 37 | expect(container.querySelectorAll("rect[tabindex]").length).toBe(20) 38 | }) 39 | 40 | it("Should show a value when showValue is enabled when a rect is hovered and no value when no longer hovered", async () => { 41 | const data = fakeData(20) 42 | const { getByText, container } = render(LinkedChart, { data, showValue: true }) 43 | 44 | const elements = container.querySelectorAll("rect[tabindex]") 45 | 46 | await fireEvent.focus(elements[0]) 47 | expect(getByText(data[0])).toBeTruthy() 48 | 49 | await fireEvent.focus(elements[10]) 50 | expect(getByText(data[10])).toBeTruthy() 51 | 52 | await fireEvent.blur(/** @type {SVGElement} */(container.querySelector("svg"))) 53 | expect(() => getByText(data[10])).toThrow() 54 | }) 55 | 56 | it("Should show valuePrepend before value when hovered", async () => { 57 | const data = fakeData(20) 58 | const { getByText, container } = render(LinkedChart, { data, showValue: true, valuePrepend: "Some prepend" }) 59 | 60 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect[tabindex]")) 61 | 62 | await fireEvent.mouseOver(rect) 63 | 64 | expect(getByText("Some prepend")).toBeTruthy() 65 | }) 66 | 67 | it("Should show valueAppend after value when hovered", async () => { 68 | const data = fakeData(20) 69 | const { getByText, container } = render(LinkedChart, { data, showValue: true, valueAppend: "Some append" }) 70 | 71 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect[tabindex]")) 72 | 73 | await fireEvent.mouseOver(rect) 74 | 75 | expect(getByText("Some append")).toBeTruthy() 76 | }) 77 | 78 | it("Should show default text for value if showValue is enabled and valueDefault is set", () => { 79 | const data = fakeData(20) 80 | const { getByText } = render(LinkedChart, { data, showValue: true, valueDefault: "Some Label" }) 81 | 82 | expect(getByText("Some Label")).toBeTruthy() 83 | }) 84 | 85 | it("Should show value as floating when valuePosition is given as floating", async () => { 86 | const data = fakeData(20) 87 | const { container } = render(LinkedChart, { data, showValue: true, valuePosition: "floating" }) 88 | 89 | const element = container.querySelector(".tiny-linked-charts-value") 90 | const rect = /** @type {SVGElement} */ (container.querySelector(".tiny-linked-charts-value")) 91 | 92 | await fireEvent.mouseOver(rect) 93 | 94 | expect(element?.getAttribute("style")).toContain("position: absolute; transform: translateX") 95 | }) 96 | 97 | it("Should display a line if type is set to line", () => { 98 | const data = fakeData(20) 99 | const { container } = render(LinkedChart, { data, type: "line" }) 100 | 101 | expect(container.querySelector("polyline")).toBeTruthy() 102 | }) 103 | 104 | it("Should use given fill color as color for bars", () => { 105 | const data = fakeData(20) 106 | const { container } = render(LinkedChart, { data, fill: "#ff00ff" }) 107 | 108 | expect(container.querySelector("rect")?.getAttribute("fill")).toBe("#ff00ff") 109 | }) 110 | 111 | it("Should use given fillArray color as color for bars", () => { 112 | const data = fakeData(20) 113 | const { container } = render(LinkedChart, { data, fillArray: ["red", "blue"] }) 114 | 115 | expect(container.querySelectorAll("rect[y]")[0]?.getAttribute("fill")).toBe("red") 116 | expect(container.querySelectorAll("rect[y]")[1]?.getAttribute("fill")).toBe("blue") 117 | }) 118 | 119 | it("Should fallback to fill color if fillArray does not contain value for bar index", () => { 120 | const data = fakeData(20) 121 | const { container } = render(LinkedChart, { data, fill: "green", fillArray: ["red", null, "blue"] }) 122 | 123 | expect(container.querySelectorAll("rect[y]")[0]?.getAttribute("fill")).toBe("red") 124 | expect(container.querySelectorAll("rect[y]")[1]?.getAttribute("fill")).toBe("green") 125 | expect(container.querySelectorAll("rect[y]")[2]?.getAttribute("fill")).toBe("blue") 126 | }) 127 | 128 | it("Should render bars with minimum given width when barMinWidth is given", () => { 129 | const data = fakeData(20) 130 | const { container } = render(LinkedChart, { data, barMinWidth: 5 }) 131 | 132 | expect(container.querySelector("rect")?.getAttribute("width")).toBe("5") 133 | }) 134 | 135 | it("Should render bars to fill out width of the container when grow prop is given", () => { 136 | const data = fakeData(5) 137 | const { container } = render(LinkedChart, { data, width: 100, grow: true, gap: 0 }) 138 | 139 | expect(container.querySelector("rect")?.getAttribute("width")).toBe("20") 140 | }) 141 | 142 | it("Should render bars to fill out width when grow is given and account for the gap size when given", () => { 143 | const data = fakeData(5) 144 | const { container } = render(LinkedChart, { data, width: 100, grow: true, gap: 5 }) 145 | 146 | expect(container.querySelector("rect")?.getAttribute("width")).toBe("15") 147 | }) 148 | 149 | it("Should change styling of bars on hover when hovered", async () => { 150 | const data = fakeData(5) 151 | const { container } = render(LinkedChart, { data }) 152 | 153 | const barRects = container.querySelectorAll("rect:not([tabindex])") 154 | const hoverableRects = container.querySelectorAll("rect[tabindex]") 155 | 156 | await fireEvent.mouseOver(hoverableRects[1]) 157 | 158 | expect(barRects[2]?.getAttribute("opacity")).not.toBe("1") 159 | }) 160 | 161 | it("Should change styling of bars to given fadeOpacity prop when hovered", async () => { 162 | const data = fakeData(5) 163 | const { container } = render(LinkedChart, { data, fadeOpacity: 0.1 }) 164 | 165 | const barRects = container.querySelectorAll("rect:not([tabindex])") 166 | const hoverableRects = container.querySelectorAll("rect[tabindex]") 167 | 168 | await fireEvent.mouseOver(hoverableRects[1]) 169 | 170 | expect(barRects[2]?.getAttribute("opacity")).toBe("0.1") 171 | }) 172 | 173 | it("Should not change styling of bars on hover when hover is false", async () => { 174 | const data = fakeData(5) 175 | const { container } = render(LinkedChart, { data, hover: false }) 176 | 177 | const barRects = container.querySelectorAll("rect:not([tabindex])") 178 | const hoverableRects = container.querySelectorAll("rect[tabindex]") 179 | 180 | await fireEvent.mouseOver(hoverableRects[1]) 181 | 182 | expect(barRects[2]?.getAttribute("opacity")).toBe("1") 183 | }) 184 | 185 | it("Should render with given height prop", () => { 186 | const data = fakeData(5) 187 | const { container } = render(LinkedChart, { data, height: 20 }) 188 | 189 | expect(container.querySelector("svg")?.getAttribute("height")).toBe("20") 190 | }) 191 | 192 | it("Should render with given width prop", () => { 193 | const data = fakeData(5) 194 | const { container } = render(LinkedChart, { data, width: 200 }) 195 | 196 | expect(container.querySelector("svg")?.getAttribute("width")).toBe("200") 197 | }) 198 | 199 | it("Should fire given onhover function when hovering bars", async () => { 200 | const data = fakeData(5) 201 | const onhover = vi.fn() 202 | const { container } = render(LinkedChart, { data, onhover }) 203 | 204 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect[tabindex]")) 205 | 206 | await fireEvent.mouseOver(rect) 207 | 208 | expect(onhover).toBeCalledWith({ 209 | eventElement: expect.any(SVGRectElement), 210 | index: 0, 211 | key: "0", 212 | linkedKey: expect.any(String), 213 | uid: expect.any(String), 214 | value: expect.any(Number), 215 | valueElement: undefined 216 | }) 217 | }) 218 | 219 | it("Should fire given onblur function when exiting hover of svg", async () => { 220 | const data = fakeData(5) 221 | const onblur = vi.fn() 222 | const { container } = render(LinkedChart, { data, onblur }) 223 | 224 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 225 | 226 | await fireEvent.mouseLeave(svg) 227 | 228 | expect(onblur).toBeCalledWith({ 229 | eventElement: expect.any(SVGElement), 230 | linkedKey: expect.any(String), 231 | uid: expect.any(String), 232 | valueElement: undefined 233 | }) 234 | }) 235 | 236 | it("Should fire given onclick function when clicking bars", async () => { 237 | const data = fakeData(5) 238 | const onclick = vi.fn() 239 | const { container } = render(LinkedChart, { data, onclick }) 240 | 241 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect[tabindex]")) 242 | 243 | await fireEvent.click(rect) 244 | 245 | expect(onclick).toBeCalledWith({ 246 | key: expect.any(String), 247 | index: expect.any(Number) 248 | }) 249 | }) 250 | 251 | it("Should use 0 as floor in bars by default", () => { 252 | const data = { "1": 50, "2": 100 } 253 | const { container } = render(LinkedChart, { data, height: 50 }) 254 | 255 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect")) 256 | 257 | expect(rect.getAttribute("height")).toBe("25") 258 | }) 259 | 260 | it("Should use scaleMin as floor in bars if given", () => { 261 | const data = { "1": 50, "2": 100 } 262 | const { container } = render(LinkedChart, { data, height: 50, scaleMin: 50 }) 263 | 264 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect")) 265 | 266 | expect(rect.getAttribute("height")).toBe("0") 267 | }) 268 | 269 | it("Should use max value as ceiling in bars by default", () => { 270 | const data = { "1": 25 } 271 | const { container } = render(LinkedChart, { data, height: 50 }) 272 | 273 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect")) 274 | 275 | expect(rect.getAttribute("height")).toBe("50") 276 | }) 277 | 278 | it("Should use scaleMax as ceiling in bars if given", () => { 279 | const data = { "1": 20 } 280 | const { container } = render(LinkedChart, { data, height: 50, scaleMax: 100 }) 281 | 282 | const rect = /** @type {SVGRectElement} */ (container.querySelector("rect")) 283 | 284 | expect(rect.getAttribute("height")).toBe("10") 285 | }) 286 | 287 | it("Should not preserve aspect ratio by default", () => { 288 | const { container } = render(LinkedChart, { data: {} }) 289 | 290 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 291 | 292 | expect(svg.getAttribute("preserveAspectRatio")).toBe("none") 293 | }) 294 | 295 | it("Should preserve aspect ratio when prop is given", () => { 296 | const { container } = render(LinkedChart, { data: {}, preserveAspectRatio: true }) 297 | 298 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 299 | 300 | expect(svg.getAttribute("preserveAspectRatio")).toBe("true") 301 | }) 302 | 303 | it("Should include any rest props on the svg", () => { 304 | const { container } = render(LinkedChart, { data: {}, "data-thing": "Data value", "aria-label": "Some label" }) 305 | 306 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 307 | 308 | expect(svg.getAttribute("data-thing")).toBe("Data value") 309 | expect(svg.getAttribute("aria-label")).toBe("Some label") 310 | }) 311 | 312 | it("Should set tabindex to given value", () => { 313 | const { container } = render(LinkedChart, { data: fakeData(30), tabindex: 0 }) 314 | 315 | const rects = /** @type {SVGRectElement[]} */ (Array.from(container.querySelectorAll("rect[tabindex]"))) 316 | 317 | expect(rects.every(rect => rect.getAttribute("tabindex") === "0")).toBeTruthy() 318 | }) 319 | 320 | it("Should add title element in each interactable bar when tabindex is 0", () => { 321 | const data = fakeData(30) 322 | const { container } = render(LinkedChart, { data, tabindex: 0 }) 323 | 324 | const rects = /** @type {SVGRectElement[]} */ (Array.from(container.querySelectorAll("rect[tabindex]"))) 325 | 326 | expect(rects.every(rect => rect.querySelector("title"))).toBeTruthy() 327 | expect(rects[0].querySelector("title")?.innerHTML).toBe(`${Object.keys(data)[0]}: ${Object.values(data)[0]}`) 328 | expect(rects[5].querySelector("title")?.innerHTML).toBe(`${Object.keys(data)[5]}: ${Object.values(data)[5]}`) 329 | }) 330 | 331 | it("Should not add title element in each interactable bar when tabindex is not set", () => { 332 | const data = fakeData(30) 333 | const { container } = render(LinkedChart, { data }) 334 | 335 | const rects = /** @type {SVGRectElement[]} */ (Array.from(container.querySelectorAll("rect[tabindex]"))) 336 | 337 | expect(rects.every(rect => !rect.querySelector("title"))).toBeTruthy() 338 | }) 339 | 340 | it("Should add title element in svg when title is given", () => { 341 | const { container } = render(LinkedChart, { data: {}, title: "Some title" }) 342 | 343 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 344 | 345 | expect(svg.getAttribute("aria-labelledby")).toBeTruthy() 346 | expect(svg.querySelector("title")?.innerHTML).toBe("Some title") 347 | }) 348 | 349 | it("Should not add title element in svg when no title is given", () => { 350 | const { container } = render(LinkedChart, { data: {} }) 351 | 352 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 353 | 354 | expect(svg.getAttribute("aria-labelledby")).not.toBeTruthy() 355 | expect(svg.querySelector("title")).not.toBeTruthy() 356 | }) 357 | 358 | it("Should add desc element in svg when description is given", () => { 359 | const { container } = render(LinkedChart, { data: {}, description: "Some description" }) 360 | 361 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 362 | 363 | expect(svg.querySelector("desc")?.innerHTML).toBe("Some description") 364 | }) 365 | 366 | it("Should not add desc element in svg when no description is given", () => { 367 | const { container } = render(LinkedChart, { data: {} }) 368 | 369 | const svg = /** @type {SVGElement} */ (container.querySelector("svg")) 370 | 371 | expect(svg.querySelector("desc")).not.toBeTruthy() 372 | }) 373 | }) 374 | -------------------------------------------------------------------------------- /src/lib/LinkedLabel.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if label} 12 | {transform(label)} 13 | {:else} 14 | 15 | {@html empty} 16 | {/if} 17 | -------------------------------------------------------------------------------- /src/lib/LinkedLabel.test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/svelte" 2 | import { afterEach, describe, expect, it } from "vitest" 3 | 4 | import LinkedLabel from "$lib/LinkedLabel.svelte" 5 | import { hoveringKey } from "$lib/stores/tinyLinkedCharts.js" 6 | 7 | describe("LinkedLabel.svelte", () => { 8 | afterEach(() => { 9 | hoveringKey.set({}) 10 | }) 11 | 12 | it("Should render label with set empty property", () => { 13 | const { getByText } = render(LinkedLabel, { empty: "test", linked: "link-1" }) 14 | 15 | expect(getByText("test")).toBeTruthy() 16 | }) 17 | 18 | it("Should render value if hoveringValue has given uid", () => { 19 | hoveringKey.set({ "link-1": "Some label" }) 20 | 21 | const { getByText, queryByText } = render(LinkedLabel, { empty: "test", linked: "link-1" }) 22 | 23 | expect(queryByText("test")).not.toBeTruthy() 24 | expect(getByText("Some label")).toBeTruthy() 25 | }) 26 | 27 | it("Should transform value with given function", () => { 28 | hoveringKey.set({ "link-1": "Some label" }) 29 | const { getByText } = render(LinkedLabel, { empty: "test", linked: "link-1", transform: (/** @type {string} */ label) => label + " thing" }) 30 | 31 | expect(getByText("Some label thing")).toBeTruthy() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/lib/LinkedValue.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | {#if uid in $hoveringValue && value !== null} 17 | {transform(value) || valueUndefined} 18 | {:else} 19 | 20 | {@html empty} 21 | {/if} 22 | -------------------------------------------------------------------------------- /src/lib/LinkedValue.test.js: -------------------------------------------------------------------------------- 1 | import { render } from "@testing-library/svelte" 2 | import { afterEach, describe, expect, it } from "vitest" 3 | 4 | import LinkedValue from "$lib/LinkedValue.svelte" 5 | import { hoveringValue } from "$lib/stores/tinyLinkedCharts.js" 6 | 7 | describe("LinkedValue.svelte", () => { 8 | afterEach(() => { 9 | hoveringValue.set({}) 10 | }) 11 | 12 | it("Should render value with set empty property", () => { 13 | const { getByText } = render(LinkedValue, { empty: "test", uid: "some-uid" }) 14 | 15 | expect(getByText("test")).toBeTruthy() 16 | }) 17 | 18 | it("Should render value if hoveringValue has given uid", () => { 19 | hoveringValue.set({ "some-uid": 50 }) 20 | const { getByText, queryByText } = render(LinkedValue, { empty: "test", uid: "some-uid" }) 21 | 22 | expect(queryByText("test")).not.toBeTruthy() 23 | expect(getByText(50)).toBeTruthy() 24 | }) 25 | 26 | it("Should transform value with given function", () => { 27 | hoveringValue.set({ "some-uid": 50 }) 28 | const { getByText } = render(LinkedValue, { empty: "test", uid: "some-uid", transform: (/** @type {string} */ value) => value + "%" }) 29 | 30 | expect(getByText("50%")).toBeTruthy() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /src/lib/index.js: -------------------------------------------------------------------------------- 1 | import LinkedChart from "./LinkedChart.svelte" 2 | import LinkedLabel from "./LinkedLabel.svelte" 3 | import LinkedValue from "./LinkedValue.svelte" 4 | 5 | export { LinkedChart, LinkedLabel, LinkedValue } 6 | -------------------------------------------------------------------------------- /src/lib/stores/tinyLinkedCharts.js: -------------------------------------------------------------------------------- 1 | import { writable } from "svelte/store" 2 | 3 | /** @type {import('svelte/store').Writable>} */ 4 | export const hoveringKey = writable({}) 5 | 6 | /** @type {import('svelte/store').Writable>} */ 7 | export const hoveringValue = writable({}) 8 | -------------------------------------------------------------------------------- /src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | export const prerender = true 2 | -------------------------------------------------------------------------------- /src/routes/+page.svelte: -------------------------------------------------------------------------------- 1 | 66 | 67 |
68 |
69 |

Tiny Linked Charts for Svelte

70 |
71 | 72 | 73 |
74 |

This is a library to display tiny bar charts. These charts are more so meant for graphic aids, rather than scientific representations. There's no axis labels, no extensive data visualisation, just bars.

75 | 76 |

Inspired by steamcharts.com

77 | 78 |

GitHub

79 | 80 |

Demo

81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 |
NameValue
A thing a + b).toLocaleString()} />
Another thing a + b).toLocaleString()} />
A third thing a + b).toLocaleString()} />
An incomplete thing a + b).toLocaleString()} />
A changing thing a + b).toLocaleString()} />
A varying thing i > 60 ? "#49da9a" : i > 30 ? "#f7d038" : "#e6261f")} linked="table" uid="table-array" /> a + b).toLocaleString()} />
A thing using lines a + b).toLocaleString()} />
135 | 136 |

Installation

137 | 138 |

Install using Yarn or NPM.

139 | 140 | 141 | yarn add svelte-tiny-linked-charts 142 | 143 | 144 | 145 | npm install --save svelte-tiny-linked-charts 146 | 147 | 148 |

If you are using Svelte 4, use version ^1.0.0. Version 2 and above is reserved for Svelte 5. The props between Svelte 4 and 5 are almost the same, but there are some breaking changes.

149 | 150 |

Include the chart in your app.

151 | 152 | 153 | <LinkedChart {data} /> 154 | 155 | 156 | 157 | import { 158 |
  LinkedChart, 159 |
  LinkedLabel, 160 |
  LinkedValue 161 |
} from "svelte-tiny-linked-charts" 162 |
163 |
164 | 165 |
166 |

167 | Supply your data in a simple key:value object: 168 |

169 | 170 | 171 | let data = {
172 |   "2005-01-01": 25,
173 |   "2005-01-02": 20,
174 |   "2005-01-03": 18,
175 |   "2005-01-04": 17,
176 |   "2005-01-05": 21
177 | } 178 |
179 | 180 | 181 | <LinkedChart {data} /> 182 | 183 | 184 |

Or if you prefer supply the labels and values separately:

185 | 186 | 187 | let labels = [
188 |   "2005-01-01",
189 |   "2005-01-02",
190 |   "2005-01-03",
191 |   "2005-01-04",
192 |   "2005-01-05"
193 | ] 194 |
195 | 196 | 197 | let values = [
198 |   25,
199 |   20,
200 |   18,
201 |   17,
202 |   21
203 | ] 204 |
205 | 206 | 207 | <LinkedChart {labels} {values} /> 208 | 209 |
210 | 211 |

Usage

212 | 213 |
214 |
215 | The chart in it's most basic form. 216 | 217 | 218 | <LinkedChart {data} /> 219 | 220 |
221 | 222 | 223 |
224 | 225 |
226 |
227 | You can link multiple charts together, hovering one will also highlight others. 228 | 229 | 230 | <LinkedChart {data} linked="link-1" />
231 | <LinkedChart {data} linked="link-1" />
232 | <LinkedChart {data} linked="link-1" />
233 | <LinkedChart {data} linked="link-1" /> 234 |
235 |
236 | 237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 | 245 |
246 |
247 | The highest value in the chart is automatically determined by the highest value in your data. To overwrite this use "scaleMax". 248 | 249 | 250 | <LinkedChart {data} scaleMax={100} />
251 | <LinkedChart {data} scaleMax={100} /> 252 |
253 |
254 | 255 |
256 |
257 |
258 |
259 |
260 | 261 |
262 |
263 | In some cases you might be working with very precise values in a specific range. By default the bar will always scale from 0. This can be overwritten using "scaleMin". 264 | 265 | 266 | <LinkedChart {data} scaleMin={30} scaleMax={31} />
267 | <LinkedChart {data} scaleMin={5000} scaleMax={5010} /> 268 |
269 |
270 | 271 |
272 |
273 |
274 |
275 |
276 | 277 |

Label

278 | 279 |
280 |
281 | You can optionally display a label, which will display the label of what you're currently hovering. 282 | 283 | 284 | <LinkedLabel linked="link-2" />
285 |
286 | <LinkedChart {data} linked="link-2" />
287 | <LinkedChart {data} linked="link-2" /> 288 |
289 |
290 | The label has no styling by default. 291 |
292 | 293 |
294 | 295 | 296 |
297 |
298 |
299 |
300 | 301 |
302 |
303 | You can enable the value you're hovering using "showValue". 304 | 305 | 306 | <LinkedChart {data} showValue /> 307 | 308 | 309 |
310 | This can be further enhanced with "valueDefault", "valuePrepend", and "valueAppend". 311 | 312 | 313 | <LinkedChart
314 |   {data}
315 |   showValue
316 |   valueDefault="Empty label"
317 |   valuePrepend="Thing:"
318 |   valueAppend="views" /> 319 |
320 |
321 | This value has no styling by default. 322 |
323 | 324 |
325 |
326 |
327 |
328 |
329 | 330 |
331 |
332 | The value can be positioned at the location of the hovered bar using "valuePosition". 333 | 334 | 335 | <LinkedChart
336 |   {data}
337 |   showValue
338 |   valuePosition="floating" /> 339 |
340 |
341 | You're expected to style this value further yourself. 342 |
343 | 344 |
345 |
346 |
347 |
348 |
349 |
350 | 351 |
352 | Alternatively you can show the value as a separate element wherever you like using the "LinkedValue" component. Use "uid" to link the chart and value together. 353 | 354 | 355 | <LinkedChart {data} uid="some-id" /> 356 |
357 | <LinkedValue uid="some-id" />
358 |
359 |
360 | This value has no styling by default. 361 | 362 |

363 | 364 |
365 | 366 | 367 | 368 |
369 | 370 |
371 | 372 | 373 | 374 |
375 | 376 |

The value can be transformed in order to append, prepend, or otherwise format the value. This is done using the transform prop.

377 | 378 | 379 | <LinkedValue
380 |   uid="some-id"
381 |   transform={(value) => value.toLocaleString() + "%"} /> 382 |
383 | 384 |
385 | 386 |
387 | 388 | 389 | value.toLocaleString() + "%"} /> 390 |
391 |
392 | 393 |

Styling

394 | 395 |
396 |
397 | The width of the bars is fixed by default, but can be set to grow to fill the chart. 398 | 399 | 400 | <LinkedChart data={...} grow /> 401 | 402 |
403 | 404 | 405 |
406 | 407 |
408 |
409 | To change the size of the bars set the "barMinWidth" property. 410 | 411 | 412 | <LinkedChart data={...} barMinWidth={2} />
413 | <LinkedChart data={...} barMinWidth={14} /> 414 |
415 |
416 | 417 |
418 |
419 |
420 |
421 |
422 | 423 |
424 |
425 | A minimum height can be set using the "barMinHeight" property. Bars will never be lower than this value, even if it's zero. 426 | 427 | 428 | <LinkedChart data={...} barMinHeight={0} />
429 | <LinkedChart data={...} barMinHeight={5} /> 430 |
431 |
432 | 433 |
434 |
435 |
436 |
437 |
438 | 439 |
440 |
441 | In some cases you may want to hide bars below a certain number. An empty space will be shown instead. For this we can use "hideBarBelow". We can use this in combination with "barMinHeight" to make sure tiny numbers still render, but 0 is not shown. 442 | 443 | 444 | <LinkedChart
445 |   data={...}
446 |   barMinHeight={2}
447 |   hideBarBelow={1} /> 448 |
449 |
450 | 451 |
452 |
453 |
454 |
455 | 456 |
457 |
458 | To always fill out the content, giving the bars a dynamic width, you can set both the "grow" and "barMinWidth" properties. 459 | 460 | 461 | <LinkedChart
462 |   data={...}
463 |   grow
464 |   barMinWidth={0} /> 465 |
466 |
467 | 468 |
469 |
470 |
471 |
472 |
473 | 474 |
475 |
476 | The charts can be resized to any size you like. It renders as an SVG, so they can easily be made responsive with some CSS. 477 | 478 | 479 | <LinkedChart
480 |   data={...}
481 |   width={250}
482 |   height={100} /> 483 |
484 | 485 | 486 | svg {
487 |   width: 100%;
488 |   height: auto;
489 | } 490 |
491 | 492 |
493 | or for a fixed height; 494 | 495 | 496 | svg {
497 |   width: 100%;
498 |   height: 50px;
499 | } 500 |
501 |
502 | 503 |
504 |
505 |
506 |
507 |
508 | 509 |
510 |
511 | The gap in between bars can also be adjusted. 512 | 513 | 514 | <LinkedChart {data} gap={10} />
515 | <LinkedChart {data} gap={0} /> 516 |
517 |
518 | 519 |
520 |
521 |
522 |
523 |
524 | 525 |
526 |
527 | When the bars do not fill the width of the graph they are aligned to the right by default. This can be set to be left aligned instead. 528 | 529 | 530 | <LinkedChart {data} align="left" /> 531 | 532 |
533 | 534 |
535 |
536 |
537 |
538 |
539 | 540 |
541 |
542 | The bars can be colored any way you wish. 543 | 544 | 545 | <LinkedChart fill="#ff00ff" />
546 | <LinkedChart fill="rgb(255, 255, 0)" />
547 | <LinkedChart fill="hsla(290, 55%, 50%, 1)" /> 548 |
549 |
550 | 551 |
552 |
553 |
554 |
555 |
556 |
557 |
558 |
559 |
560 |
561 |
562 | 563 |
564 |
565 | (From version 2.1.0)
566 | An array can be passed to color each bar individually. This supports the same values as the fill property above. 567 | Bars will be filled matching the index of the bar, falling back to the given fill when not given. 568 | For instance; In an array of [null, "red"], all items will use the given fill color, except for the second bar. 569 | 570 | 571 | <LinkedChart fillArray={data.map(i => i > 40 ? "green" : "red")} />
572 | <LinkedChart fillArray={data.map(i => `hsl(${i * 10}, 55%, 50%)`)} /> 573 |
574 |
575 | 576 |
577 |
i > 40 ? "#49da9a" : "#eb7532")} linked="link-array" />
578 |
`hsl(20, ${10 + i * 3}%, 50%)`)} linked="link-array" />
579 |
`hsl(${i * 10}, 55%, 50%)`)} linked="link-array" />
580 |
`hsl(${fillArrayHueOffset + i * 10}, 55%, 50%)`)} linked="link-array" />
581 |
582 |
583 |
584 | 585 |
586 |
587 | The opacity of faded out bars can be adjusted using "fadeOpacity". 588 | 589 | 590 | <LinkedChart {data} fadeOpacity={0.15} /> 591 | 592 |
593 | 594 | 595 |
596 | 597 |
598 |
599 | The hover effect can be disabled altogether using "hover". 600 | 601 | 602 | <LinkedChart {data} hover={false} /> 603 | 604 |
605 | 606 | 607 |
608 | 609 |
610 |
611 | Bars can be set to transition between states.
612 | Value is speed in milliseconds. 613 | 614 | 615 | <LinkedChart {data} transition={500} /> 616 | 617 |
618 | 619 | 620 |
621 | 622 |
623 |
624 |

Instead of bars you can also opt for a line-chart using "type=line". "lineColor" can be used to color the line, "fill" to color the points. This can have all of the bar properties as well.

625 | 626 | 627 | <LinkedChart {data} type="line" />
628 | <LinkedChart
629 |   {data}
630 |   type="line"
631 |   lineColor="#4355db"
632 |   fill="var(--text-color)" /> 633 |
634 |
635 | 636 |
637 |
638 |
639 |
640 |
641 |
642 |
643 | 644 |

Accessibility

645 | 646 |
647 |
648 | To improve accessibility you can set tabindex=0, allowing navigating to each data point using the keyboard. When this is passed, each bar will have a title element describing it's label and value (e.g. 2005-04-12: 76). 649 | 650 | 651 | <LinkedChart {data} tabindex="0" />
652 |
653 | 654 |

(From version 2.2.0)
Additionally, you can provide a title and description. Both are used to describe the chart. When the chart is interactive, you should consider explaining the interaction in the description.

655 | 656 | 657 | <LinkedChart {data} title="Monthly things chart" />
658 |
659 | 660 | 661 | <LinkedChart {data} description="A bar chart showing monthly things; A varying trend is shown with data points for each day." />
662 |
663 | 664 |

Providing a title and description is crucial for all users to be able to understand your chart when using tabindex=0.

665 |
666 | 667 |
668 |
669 |
670 |
671 | 672 |

Events

673 | 674 |
675 |
676 |

Several events are available to use the chart data on various user interactions or updates.

677 | 678 | 679 | <LinkedChart
680 |   onhover={(options) => console.log(options)}
681 |   onblur={(options) => console.log(options)}
682 |   onvalueupdate={(options) => console.log(options)} />
683 |
684 | 685 |

This could be used to construct your own value element that can be formatted as you wish. For example in this example the values are given as cents, but the value is formatted as dollars.

686 | 687 |
688 | { 691 | const element = /** @type {HTMLElement} */ (document.querySelector("[data-role='currency']")) 692 | element.innerHTML = (value || 0 / 100).toLocaleString("en-US", { style: "currency", currency: "USD" }) 693 | }} 694 | onblur={() => /** @type {HTMLElement} */ (document.querySelector("[data-role='currency']")).innerHTML = " "} /> 695 | 696 |   697 |
698 | 699 | 700 | <LinkedChart
701 |   onhover={({ value }) => {
702 |     const element = document.querySelector("[data-role='currency']")
703 |
704 |     element.innerHTML = (value || 0 / 100).toLocaleString("en-US", {
705 |       style: "currency", currency: "USD"
706 |     })
707 |   }}
708 |   onblur={() => document.querySelector("[data-role='currency']").innerHTML = " "} />
709 |
710 | <span data-role="currency"></span> 711 |
712 | 713 |
714 | 715 |

In this example we format the value element inside the chart directly to make use of "toLocaleString()" to format the number. Ideally you would supply the value already formatted to avoid having to do this, but that's not always possible.

716 | 717 |
718 | { 724 | if (valueElement) valueElement.innerText = (value || 0).toLocaleString() 725 | }} /> 726 |
727 | 728 | 729 | <LinkedChart
730 |   showValue
731 |   valuePosition="floating"
732 |   valuePrepend="Value: "
733 |   onvalueupdate={({ valueElement, value }) => {
734 |    if (valueElement) valueElement.innerText = (value || 0).toLocaleString()
735 |   }} /> 736 |
737 | 738 |
739 | 740 |

All events

741 | 742 |
743 | Property Description Return 744 | onhover
On hover of bars
uid, key, index, linkedKey, value, valueElement, eventElement 745 | onblur
On blur of the chart
uid, linkedKey, valueElement, eventElement 746 | onvalueupdate
Any time the value updates
value, uid, linkedKey, valueElement 747 |
748 |
749 |
750 | 751 |

Properties

752 | 753 |
754 |

This is a list of all configurable properties on the "LinkedChart" component.

755 | 756 |
757 | Property Default Description 758 | data {}
Data that will be displayed in the chart supplied in key:value object.
759 | labels []
Labels supplied separately, to be used together with "values" property.
760 | values []
Values supplied separately, to be used together with "labels" property.
761 | linked
Key to link this chart to other charts with the same key.
762 | uid
Unique ID to link this chart to a LinkedValue component with the same uid.
763 | height 40
Height of the chart in pixels.
764 | width 150
Width of the chart in pixels.
765 | barMinWidth 4
Width of the bars in the chart in pixels.
766 | barMinHeight 0
Minimum height of the bars in the chart in pixels.
767 | hideBarBelow 0
Bars below this value will be hidden, showing as 0 height.
768 | grow false
Whether or not the bar should grow to fill out the full width of the chart.
769 | align right
The side the bars should align to when they do not completely fill out the chart.
770 | gap 1
Gap between the bars in pixels.
771 | fill #ff3e00
Color of the bars, can be any valid CSS color.
772 | fillArray []
Array of colors for each individual bar.
773 | fadeOpacity 0.5
The opacity the faded out bars should display in.
774 | hover true
Boolean whether or not this chart can be hovered at all.
775 | transition 0
Transition the chart between different stats. Value is time in milliseconds.
776 | showValue false
Boolean whether or not a value will be shown.
777 | valueDefault " "
Default value when not hovering.
778 | valueUndefined 0
For when the hovering value returns undefined.
779 | valuePrepend
String to prepend the value.
780 | valueAppend
String to append to the value.
781 | valuePosition static
Can be set to "floating" to follow the position of the hover.
782 | scaleMax 0
Use this to overwrite the automatic scale set to the highest value in your array.
783 | scaleMax 0
Use this to overwrite the default value floor of 0.
784 | type bar
Can be set to "line" to display a line chart instead.
785 | lineColor fill
Color of the line if used with type="line".
786 | preserveAspectRatio false
Sets whether or not the SVG will preserve it's aspect ratio.
787 | tabindex -1
Sets the tabindex of each bar. When a tabindex of 0 is given, each bar will contain a title that describes the bar's label and value.
788 | title ""
Title that describes the chart for screen readers.
789 | description ""
Description that describes the chart for screen readers.
790 | onclick () => null
Function that executes on click and returns the key and index for the clicked data.
791 | onhover () => null
Function that executes on hover of each bar.
792 | onblur () => null
Function that executes when focus leaves the chart.
793 | onvalueupdate () => null
Function that executes when a value in the chart updates.
794 |
795 |
796 | 797 |
798 |

This is a list of all configurable properties on the "LinkedLabel" component.

799 | 800 |
801 | Property Default Description 802 | linked
Key to link this label to charts with the same key.
803 | empty &nbsp;
String that will be displayed when no bar is being hovered.
804 | transform (label) => label
Transform the given label to format it differently from how it was supplied.
805 |
806 |
807 | 808 |
809 |

This is a list of all configurable properties on the "LinkedValue" component.

810 | 811 |
812 | Property Default Description 813 | uid
Unique ID to link this value to a chart with the same uid.
814 | empty &nbsp;
String that will be displayed when no bar is being hovered.
815 | valueUndefined 0
For when the hovering value returns undefined.
816 | transform (value) => value
Transform the given value to format it differently from how it was supplied.
817 |
818 |
819 | 820 |
821 | Made by Mitchel Jager 822 |
823 |
824 | 825 | 826 | 827 | 1001 | -------------------------------------------------------------------------------- /src/test.setup.js: -------------------------------------------------------------------------------- 1 | import { cleanup } from "@testing-library/svelte" 2 | import { afterEach } from "vitest" 3 | 4 | afterEach(() => { 5 | cleanup() 6 | }) 7 | -------------------------------------------------------------------------------- /static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mitcheljager/svelte-tiny-linked-charts/0ab22600dab9c3e4e97b7453b95f929945761d2c/static/.nojekyll -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mitcheljager/svelte-tiny-linked-charts/0ab22600dab9c3e4e97b7453b95f929945761d2c/static/favicon.png -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapter from "@sveltejs/adapter-static" 2 | 3 | const dev = process.argv.includes("dev") 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | kit: { 8 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. 9 | // If your environment is not supported or you settled on a specific environment, switch out the adapter. 10 | // See https://kit.svelte.dev/docs/adapters for more information about adapters. 11 | adapter: adapter({ 12 | // default options are shown. On some platforms 13 | // these options are set automatically — see below 14 | pages: "build", 15 | assets: "build", 16 | fallback: undefined, 17 | precompress: false, 18 | strict: true, 19 | paths: { 20 | base: dev ? "" : process.env.BASE_PATH 21 | } 22 | }) 23 | } 24 | } 25 | 26 | export default config 27 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import path from "path" 2 | import { sveltekit } from "@sveltejs/kit/vite" 3 | import { defineConfig } from "vitest/config" 4 | 5 | export default defineConfig({ 6 | plugins: [sveltekit()], 7 | test: { 8 | environment: "happy-dom", 9 | include: ["src/**/*.{test,spec}.{js,ts}"], 10 | setupFiles: ["src/test.setup.js"] 11 | }, 12 | 13 | resolve: { 14 | conditions: process.env.VITEST ? ["browser"] : [], 15 | alias: { 16 | "$lib": path.resolve(__dirname, "./src/lib") 17 | } 18 | } 19 | }) 20 | --------------------------------------------------------------------------------